/* * 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.jsontestsuite.suite; import org.ethereum.config.CommonConfig; import org.ethereum.config.SystemProperties; import org.ethereum.core.Block; import org.ethereum.core.BlockchainImpl; import org.ethereum.core.ImportResult; import org.ethereum.core.PendingStateImpl; import org.ethereum.core.Repository; import org.ethereum.datasource.inmem.HashMapDB; import org.ethereum.db.*; import org.ethereum.jsontestsuite.suite.builder.BlockBuilder; import org.ethereum.jsontestsuite.suite.builder.RepositoryBuilder; import org.ethereum.jsontestsuite.suite.model.BlockTck; import org.ethereum.jsontestsuite.suite.validators.BlockHeaderValidator; import org.ethereum.jsontestsuite.suite.validators.RepositoryValidator; import org.ethereum.listener.EthereumListenerAdapter; import org.ethereum.util.ByteUtil; import org.ethereum.validator.DependentBlockHeaderRuleAdapter; import org.ethereum.vm.DataWord; import org.ethereum.vm.LogInfo; import org.ethereum.vm.VM; import org.ethereum.vm.program.Program; import org.ethereum.vm.program.invoke.ProgramInvoke; import org.ethereum.vm.program.invoke.ProgramInvokeFactoryImpl; import org.ethereum.vm.program.invoke.ProgramInvokeImpl; import org.ethereum.vm.trace.ProgramTrace; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.spongycastle.util.encoders.Hex; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; import static org.ethereum.crypto.HashUtil.shortHash; import static org.ethereum.util.ByteUtil.EMPTY_BYTE_ARRAY; import static org.ethereum.vm.VMUtils.saveProgramTraceFile; /** * @author Roman Mandeleil * @since 02.07.2014 */ public class TestRunner { private Logger logger = LoggerFactory.getLogger("TCK-Test"); private ProgramTrace trace = null; private boolean setNewStateRoot; private String bestStateRoot; public List<String> runTestSuite(TestSuite testSuite) { Iterator<TestCase> testIterator = testSuite.iterator(); List<String> resultCollector = new ArrayList<>(); while (testIterator.hasNext()) { TestCase testCase = testIterator.next(); TestRunner runner = new TestRunner(); List<String> result = runner.runTestCase(testCase); resultCollector.addAll(result); } return resultCollector; } public List<String> runTestCase(BlockTestCase testCase) { /* 1 */ // Create genesis + init pre state Block genesis = BlockBuilder.build(testCase.getGenesisBlockHeader(), null, null); Repository repository = RepositoryBuilder.build(testCase.getPre()); IndexedBlockStore blockStore = new IndexedBlockStore(); blockStore.init(new HashMapDB<byte[]>(), new HashMapDB<byte[]>()); blockStore.saveBlock(genesis, genesis.getCumulativeDifficulty(), true); ProgramInvokeFactoryImpl programInvokeFactory = new ProgramInvokeFactoryImpl(); BlockchainImpl blockchain = new BlockchainImpl(blockStore, repository) .withParentBlockHeaderValidator(CommonConfig.getDefault().parentHeaderValidator()); blockchain.byTest = true; PendingStateImpl pendingState = new PendingStateImpl(new EthereumListenerAdapter(), blockchain); blockchain.setBestBlock(genesis); blockchain.setTotalDifficulty(genesis.getCumulativeDifficulty()); blockchain.setParentHeaderValidator(new DependentBlockHeaderRuleAdapter()); blockchain.setProgramInvokeFactory(programInvokeFactory); blockchain.setPendingState(pendingState); pendingState.setBlockchain(blockchain); /* 2 */ // Create block traffic list List<Block> blockTraffic = new ArrayList<>(); for (BlockTck blockTck : testCase.getBlocks()) { Block block = BlockBuilder.build(blockTck.getBlockHeader(), blockTck.getTransactions(), blockTck.getUncleHeaders()); setNewStateRoot = !((blockTck.getTransactions() == null) && (blockTck.getUncleHeaders() == null) && (blockTck.getBlockHeader() == null)); Block tBlock = null; try { byte[] rlp = Utils.parseData(blockTck.getRlp()); tBlock = new Block(rlp); ArrayList<String> outputSummary = BlockHeaderValidator.valid(tBlock.getHeader(), block.getHeader()); if (!outputSummary.isEmpty()){ for (String output : outputSummary) logger.error("{}", output); } blockTraffic.add(tBlock); } catch (Exception e) { System.out.println("*** Exception"); } } /* 3 */ // Inject blocks to the blockchain execution for (Block block : blockTraffic) { ImportResult importResult = blockchain.tryToConnect(block); logger.debug("{} ~ {} difficulty: {} ::: {}", block.getShortHash(), shortHash(block.getParentHash()), block.getCumulativeDifficulty(), importResult.toString()); } repository = blockchain.getRepository(); //Check state root matches last valid block List<String> results = new ArrayList<>(); String currRoot = Hex.toHexString(repository.getRoot()); byte[] bestHash = Hex.decode(testCase.getLastblockhash()); String finalRoot = Hex.toHexString(blockStore.getBlockByHash(bestHash).getStateRoot()); if (!finalRoot.equals(currRoot)){ String formattedString = String.format("Root hash doesn't match best: expected: %s current: %s", finalRoot, currRoot); results.add(formattedString); } Repository postRepository = RepositoryBuilder.build(testCase.getPostState()); List<String> repoResults = RepositoryValidator.valid(repository, postRepository); results.addAll(repoResults); return results; } public List<String> runTestCase(TestCase testCase) { logger.info("\n***"); logger.info(" Running test case: [" + testCase.getName() + "]"); logger.info("***\n"); List<String> results = new ArrayList<>(); logger.info("--------- PRE ---------"); IterableTestRepository testRepository = new IterableTestRepository(new RepositoryRoot(new HashMapDB<byte[]>())); testRepository.environmental = true; Repository repository = loadRepository(testRepository, testCase.getPre()); try { /* 2. Create ProgramInvoke - Env/Exec */ Env env = testCase.getEnv(); Exec exec = testCase.getExec(); Logs logs = testCase.getLogs(); byte[] address = exec.getAddress(); byte[] origin = exec.getOrigin(); byte[] caller = exec.getCaller(); byte[] balance = ByteUtil.bigIntegerToBytes(repository.getBalance(exec.getAddress())); byte[] gasPrice = exec.getGasPrice(); byte[] gas = exec.getGas(); byte[] callValue = exec.getValue(); byte[] msgData = exec.getData(); byte[] lastHash = env.getPreviousHash(); byte[] coinbase = env.getCurrentCoinbase(); long timestamp = ByteUtil.byteArrayToLong(env.getCurrentTimestamp()); long number = ByteUtil.byteArrayToLong(env.getCurrentNumber()); byte[] difficulty = env.getCurrentDifficulty(); byte[] gaslimit = env.getCurrentGasLimit(); // Origin and caller need to exist in order to be able to execute if (repository.getAccountState(origin) == null) repository.createAccount(origin); if (repository.getAccountState(caller) == null) repository.createAccount(caller); ProgramInvoke programInvoke = new ProgramInvokeImpl(address, origin, caller, balance, gasPrice, gas, callValue, msgData, lastHash, coinbase, timestamp, number, difficulty, gaslimit, repository, new BlockStoreDummy(), true); /* 3. Create Program - exec.code */ /* 4. run VM */ VM vm = new VM(); Program program = new Program(exec.getCode(), programInvoke); boolean vmDidThrowAnEception = false; RuntimeException e = null; try { while (!program.isStopped()) vm.step(program); } catch (RuntimeException ex) { vmDidThrowAnEception = true; e = ex; } String content = program.getTrace().asJsonString(true); saveProgramTraceFile(SystemProperties.getDefault(), testCase.getName(), content); if (testCase.getPost() == null) { if (!vmDidThrowAnEception) { String output = "VM was expected to throw an exception"; logger.info(output); results.add(output); } else logger.info("VM did throw an exception: " + e.toString()); } else { if (vmDidThrowAnEception) { String output = "VM threw an unexpected exception: " + e.toString(); logger.info(output, e); results.add(output); return results; } this.trace = program.getTrace(); logger.info("--------- POST --------"); /* 5. Assert Post values */ if (testCase.getPost() != null) { for (ByteArrayWrapper key : testCase.getPost().keySet()) { AccountState accountState = testCase.getPost().get(key); long expectedNonce = accountState.getNonceLong(); BigInteger expectedBalance = accountState.getBigIntegerBalance(); byte[] expectedCode = accountState.getCode(); boolean accountExist = (null != repository.getAccountState(key.getData())); if (!accountExist) { String output = String.format("The expected account does not exist. key: [ %s ]", Hex.toHexString(key.getData())); logger.info(output); results.add(output); continue; } long actualNonce = repository.getNonce(key.getData()).longValue(); BigInteger actualBalance = repository.getBalance(key.getData()); byte[] actualCode = repository.getCode(key.getData()); if (actualCode == null) actualCode = "".getBytes(); if (expectedNonce != actualNonce) { String output = String.format("The nonce result is different. key: [ %s ], expectedNonce: [ %d ] is actualNonce: [ %d ] ", Hex.toHexString(key.getData()), expectedNonce, actualNonce); logger.info(output); results.add(output); } if (!expectedBalance.equals(actualBalance)) { String output = String.format("The balance result is different. key: [ %s ], expectedBalance: [ %s ] is actualBalance: [ %s ] ", Hex.toHexString(key.getData()), expectedBalance.toString(), actualBalance.toString()); logger.info(output); results.add(output); } if (!Arrays.equals(expectedCode, actualCode)) { String output = String.format("The code result is different. account: [ %s ], expectedCode: [ %s ] is actualCode: [ %s ] ", Hex.toHexString(key.getData()), Hex.toHexString(expectedCode), Hex.toHexString(actualCode)); logger.info(output); results.add(output); } // assert storage Map<DataWord, DataWord> storage = accountState.getStorage(); for (DataWord storageKey : storage.keySet()) { byte[] expectedStValue = storage.get(storageKey).getData(); ContractDetails contractDetails = program.getStorage().getContractDetails(accountState.getAddress()); if (contractDetails == null) { String output = String.format("Storage raw doesn't exist: key [ %s ], expectedValue: [ %s ]", Hex.toHexString(storageKey.getData()), Hex.toHexString(expectedStValue) ); logger.info(output); results.add(output); continue; } Map<DataWord, DataWord> testStorage = contractDetails.getStorage(); DataWord actualValue = testStorage.get(new DataWord(storageKey.getData())); if (actualValue == null || !Arrays.equals(expectedStValue, actualValue.getData())) { String output = String.format("Storage value different: key [ %s ], expectedValue: [ %s ], actualValue: [ %s ]", Hex.toHexString(storageKey.getData()), Hex.toHexString(expectedStValue), actualValue == null ? "" : Hex.toHexString(actualValue.getNoLeadZeroesData())); logger.info(output); results.add(output); } } /* asset logs */ List<LogInfo> logResult = program.getResult().getLogInfoList(); Iterator<LogInfo> postLogs = logs.getIterator(); int i = 0; while (postLogs.hasNext()) { LogInfo expectedLogInfo = postLogs.next(); LogInfo foundLogInfo = null; if (logResult.size() > i) foundLogInfo = logResult.get(i); if (foundLogInfo == null) { String output = String.format("Expected log [ %s ]", expectedLogInfo.toString()); logger.info(output); results.add(output); } else { if (!Arrays.equals(expectedLogInfo.getAddress(), foundLogInfo.getAddress())) { String output = String.format("Expected address [ %s ], found [ %s ]", Hex.toHexString(expectedLogInfo.getAddress()), Hex.toHexString(foundLogInfo.getAddress())); logger.info(output); results.add(output); } if (!Arrays.equals(expectedLogInfo.getData(), foundLogInfo.getData())) { String output = String.format("Expected data [ %s ], found [ %s ]", Hex.toHexString(expectedLogInfo.getData()), Hex.toHexString(foundLogInfo.getData())); logger.info(output); results.add(output); } if (!expectedLogInfo.getBloom().equals(foundLogInfo.getBloom())) { String output = String.format("Expected bloom [ %s ], found [ %s ]", Hex.toHexString(expectedLogInfo.getBloom().getData()), Hex.toHexString(foundLogInfo.getBloom().getData())); logger.info(output); results.add(output); } if (expectedLogInfo.getTopics().size() != foundLogInfo.getTopics().size()) { String output = String.format("Expected number of topics [ %d ], found [ %d ]", expectedLogInfo.getTopics().size(), foundLogInfo.getTopics().size()); logger.info(output); results.add(output); } else { int j = 0; for (DataWord topic : expectedLogInfo.getTopics()) { byte[] foundTopic = foundLogInfo.getTopics().get(j).getData(); if (!Arrays.equals(topic.getData(), foundTopic)) { String output = String.format("Expected topic [ %s ], found [ %s ]", Hex.toHexString(topic.getData()), Hex.toHexString(foundTopic)); logger.info(output); results.add(output); } ++j; } } } ++i; } } } // TODO: assert that you have no extra accounts in the repository // TODO: -> basically the deleted by suicide should be deleted // TODO: -> and no unexpected created List<org.ethereum.vm.CallCreate> resultCallCreates = program.getResult().getCallCreateList(); // assert call creates for (int i = 0; i < testCase.getCallCreateList().size(); ++i) { org.ethereum.vm.CallCreate resultCallCreate = null; if (resultCallCreates != null && resultCallCreates.size() > i) { resultCallCreate = resultCallCreates.get(i); } CallCreate expectedCallCreate = testCase.getCallCreateList().get(i); if (resultCallCreate == null && expectedCallCreate != null) { String output = String.format("Missing call/create invoke: to: [ %s ], data: [ %s ], gas: [ %s ], value: [ %s ]", Hex.toHexString(expectedCallCreate.getDestination()), Hex.toHexString(expectedCallCreate.getData()), Hex.toHexString(expectedCallCreate.getGasLimit()), Hex.toHexString(expectedCallCreate.getValue())); logger.info(output); results.add(output); continue; } boolean assertDestination = Arrays.equals( expectedCallCreate.getDestination(), resultCallCreate.getDestination()); if (!assertDestination) { String output = String.format("Call/Create destination is different. Expected: [ %s ], result: [ %s ]", Hex.toHexString(expectedCallCreate.getDestination()), Hex.toHexString(resultCallCreate.getDestination())); logger.info(output); results.add(output); } boolean assertData = Arrays.equals( expectedCallCreate.getData(), resultCallCreate.getData()); if (!assertData) { String output = String.format("Call/Create data is different. Expected: [ %s ], result: [ %s ]", Hex.toHexString(expectedCallCreate.getData()), Hex.toHexString(resultCallCreate.getData())); logger.info(output); results.add(output); } boolean assertGasLimit = Arrays.equals( expectedCallCreate.getGasLimit(), resultCallCreate.getGasLimit()); if (!assertGasLimit) { String output = String.format("Call/Create gasLimit is different. Expected: [ %s ], result: [ %s ]", Hex.toHexString(expectedCallCreate.getGasLimit()), Hex.toHexString(resultCallCreate.getGasLimit())); logger.info(output); results.add(output); } boolean assertValue = Arrays.equals( expectedCallCreate.getValue(), resultCallCreate.getValue()); if (!assertValue) { String output = String.format("Call/Create value is different. Expected: [ %s ], result: [ %s ]", Hex.toHexString(expectedCallCreate.getValue()), Hex.toHexString(resultCallCreate.getValue())); logger.info(output); results.add(output); } } // assert out byte[] expectedHReturn = testCase.getOut(); byte[] actualHReturn = EMPTY_BYTE_ARRAY; if (program.getResult().getHReturn() != null) { actualHReturn = program.getResult().getHReturn(); } if (!Arrays.equals(expectedHReturn, actualHReturn)) { String output = String.format("HReturn is different. Expected hReturn: [ %s ], actual hReturn: [ %s ]", Hex.toHexString(expectedHReturn), Hex.toHexString(actualHReturn)); logger.info(output); results.add(output); } // assert gas BigInteger expectedGas = new BigInteger(1, testCase.getGas()); BigInteger actualGas = new BigInteger(1, gas).subtract(BigInteger.valueOf(program.getResult().getGasUsed())); if (!expectedGas.equals(actualGas)) { String output = String.format("Gas remaining is different. Expected gas remaining: [ %s ], actual gas remaining: [ %s ]", expectedGas.toString(), actualGas.toString()); logger.info(output); results.add(output); } /* * end of if(testCase.getPost().size() == 0) */ } return results; } finally { // repository.close(); } } public org.ethereum.core.Transaction createTransaction(Transaction tx) { byte[] nonceBytes = ByteUtil.longToBytes(tx.nonce); byte[] gasPriceBytes = ByteUtil.longToBytes(tx.gasPrice); byte[] gasBytes = tx.gasLimit; byte[] valueBytes = ByteUtil.longToBytes(tx.value); byte[] toAddr = tx.getTo(); byte[] data = tx.getData(); org.ethereum.core.Transaction transaction = new org.ethereum.core.Transaction( nonceBytes, gasPriceBytes, gasBytes, toAddr, valueBytes, data); return transaction; } public Repository loadRepository(Repository track, Map<ByteArrayWrapper, AccountState> pre) { /* 1. Store pre-exist accounts - Pre */ for (ByteArrayWrapper key : pre.keySet()) { AccountState accountState = pre.get(key); byte[] addr = key.getData(); track.addBalance(addr, new BigInteger(1, accountState.getBalance())); track.setNonce(key.getData(), new BigInteger(1, accountState.getNonce())); track.saveCode(addr, accountState.getCode()); for (DataWord storageKey : accountState.getStorage().keySet()) { track.addStorageRow(addr, storageKey, accountState.getStorage().get(storageKey)); } } return track; } public ProgramTrace getTrace() { return trace; } }