diff --git a/ethereumj-core/src/main/java/org/ethereum/config/BlockchainConfig.java b/ethereumj-core/src/main/java/org/ethereum/config/BlockchainConfig.java index c889d08dab..25da34b505 100644 --- a/ethereumj-core/src/main/java/org/ethereum/config/BlockchainConfig.java +++ b/ethereumj-core/src/main/java/org/ethereum/config/BlockchainConfig.java @@ -127,4 +127,14 @@ String validateTransactionChanges(BlockStore blockStore, Block curBlock, Transac * EIP155: https://github.com/ethereum/EIPs/issues/155 */ Integer getChainId(); + + /** + * EIP206: https://github.com/ethereum/EIPs/pull/206 + */ + boolean eip206(); + + /** + * EIP211: https://github.com/ethereum/EIPs/pull/211 + */ + boolean eip211(); } diff --git a/ethereumj-core/src/main/java/org/ethereum/config/blockchain/AbstractConfig.java b/ethereumj-core/src/main/java/org/ethereum/config/blockchain/AbstractConfig.java index ad9eba8a90..a3adcc1a15 100644 --- a/ethereumj-core/src/main/java/org/ethereum/config/blockchain/AbstractConfig.java +++ b/ethereumj-core/src/main/java/org/ethereum/config/blockchain/AbstractConfig.java @@ -158,6 +158,16 @@ public Integer getChainId() { return null; } + @Override + public boolean eip206() { + return false; + } + + @Override + public boolean eip211() { + return false; + } + @Override public String toString() { return getClass().getSimpleName(); diff --git a/ethereumj-core/src/main/java/org/ethereum/config/blockchain/ByzantiumConfig.java b/ethereumj-core/src/main/java/org/ethereum/config/blockchain/ByzantiumConfig.java index 92d4fe8da3..ef1a5eb286 100644 --- a/ethereumj-core/src/main/java/org/ethereum/config/blockchain/ByzantiumConfig.java +++ b/ethereumj-core/src/main/java/org/ethereum/config/blockchain/ByzantiumConfig.java @@ -62,4 +62,14 @@ public BigInteger getCalcDifficultyMultiplier(BlockHeader curBlock, BlockHeader long unclesAdj = parent.hasUncles() ? 2 : 1; return BigInteger.valueOf(Math.max(unclesAdj - (curBlock.getTimestamp() - parent.getTimestamp()) / 9, -99)); } + + @Override + public boolean eip206() { + return true; + } + + @Override + public boolean eip211() { + return true; + } } diff --git a/ethereumj-core/src/main/java/org/ethereum/config/blockchain/Eip150HFConfig.java b/ethereumj-core/src/main/java/org/ethereum/config/blockchain/Eip150HFConfig.java index 7e963ff137..66edce22d8 100644 --- a/ethereumj-core/src/main/java/org/ethereum/config/blockchain/Eip150HFConfig.java +++ b/ethereumj-core/src/main/java/org/ethereum/config/blockchain/Eip150HFConfig.java @@ -146,4 +146,14 @@ public Constants getCommonConstants() { public Integer getChainId() { return null; } + + @Override + public boolean eip206() { + return false; + } + + @Override + public boolean eip211() { + return false; + } } diff --git a/ethereumj-core/src/main/java/org/ethereum/core/TransactionExecutor.java b/ethereumj-core/src/main/java/org/ethereum/core/TransactionExecutor.java index deb0095caa..8df8a7d8d0 100644 --- a/ethereumj-core/src/main/java/org/ethereum/core/TransactionExecutor.java +++ b/ethereumj-core/src/main/java/org/ethereum/core/TransactionExecutor.java @@ -300,7 +300,7 @@ public void go() { result = program.getResult(); m_endGas = toBI(tx.getGasLimit()).subtract(toBI(program.getResult().getGasUsed())); - if (tx.isContractCreation()) { + if (tx.isContractCreation() && !result.isRevert()) { int returnDataGasValue = getLength(program.getResult().getHReturn()) * blockchainConfig.getGasCost().getCREATE_DATA(); if (m_endGas.compareTo(BigInteger.valueOf(returnDataGasValue)) < 0) { @@ -331,19 +331,24 @@ public void go() { } - if (result.getException() != null) { + if (result.getException() != null || result.isRevert()) { result.getDeleteAccounts().clear(); result.getLogInfoList().clear(); result.resetFutureRefund(); + cacheTrack.rollback(); - throw result.getException(); + if (result.getException() != null) { + throw result.getException(); + } + } else { + touchedAccounts.addAll(result.getTouchedAccounts()); + cacheTrack.commit(); } - touchedAccounts.addAll(result.getTouchedAccounts()); + } else { + cacheTrack.commit(); } - cacheTrack.commit(); - } catch (Throwable e) { // TODO: catch whatever they will throw on you !!! diff --git a/ethereumj-core/src/main/java/org/ethereum/vm/OpCode.java b/ethereumj-core/src/main/java/org/ethereum/vm/OpCode.java index d6f68fd861..2df6964d2d 100644 --- a/ethereumj-core/src/main/java/org/ethereum/vm/OpCode.java +++ b/ethereumj-core/src/main/java/org/ethereum/vm/OpCode.java @@ -188,6 +188,10 @@ public enum OpCode { * environment to memory */ CODECOPY(0x39, 3, 0, VeryLowTier), // [len code_start mem_start CODECOPY] + + RETURNDATASIZE(0x3d, 0, 1, BaseTier), + + RETURNDATACOPY(0x3e, 3, 0, VeryLowTier), /** * (0x3a) Get price of gas in current * environment @@ -587,6 +591,12 @@ public enum OpCode { * also the Value parameter is omitted for this opCode */ DELEGATECALL(0xf4, 6, 1, SpecialTier), + /** + * (0xfd) The `REVERT` instruction will stop execution, roll back all state changes done so far + * and provide a pointer to a memory section, which can be interpreted as an error code or message. + * While doing so, it will not consume all the remaining gas. + */ + REVERT(0xfd, 2, 0, ZeroTier), /** * (0xff) Halt execution and register account for * later deletion diff --git a/ethereumj-core/src/main/java/org/ethereum/vm/VM.java b/ethereumj-core/src/main/java/org/ethereum/vm/VM.java index b0ac939654..a84ac62a2d 100644 --- a/ethereumj-core/src/main/java/org/ethereum/vm/VM.java +++ b/ethereumj-core/src/main/java/org/ethereum/vm/VM.java @@ -35,11 +35,7 @@ import static org.ethereum.crypto.HashUtil.sha3; import static org.ethereum.util.ByteUtil.EMPTY_BYTE_ARRAY; -import static org.ethereum.vm.OpCode.CALL; -import static org.ethereum.vm.OpCode.CALLCODE; -import static org.ethereum.vm.OpCode.CREATE; -import static org.ethereum.vm.OpCode.DELEGATECALL; -import static org.ethereum.vm.OpCode.PUSH1; +import static org.ethereum.vm.OpCode.*; /** * The Ethereum Virtual Machine (EVM) is responsible for initialization @@ -151,11 +147,25 @@ public void step(Program program) { if (op == null) { throw Program.Exception.invalidOpCode(program.getCurrentOp()); } - if (op == DELEGATECALL) { - // opcode since Homestead release only - if (!blockchainConfig.getConstants().hasDelegateCallOpcode()) { - throw Program.Exception.invalidOpCode(program.getCurrentOp()); - } + + switch (op) { + case DELEGATECALL: + if (!blockchainConfig.getConstants().hasDelegateCallOpcode()) { + // opcode since Homestead release only + throw Program.Exception.invalidOpCode(program.getCurrentOp()); + } + break; + case REVERT: + if (!blockchainConfig.eip206()) { + throw Program.Exception.invalidOpCode(program.getCurrentOp()); + } + break; + case RETURNDATACOPY: + case RETURNDATASIZE: + if (!blockchainConfig.eip211()) { + throw Program.Exception.invalidOpCode(program.getCurrentOp()); + } + break; } program.setLastOp(op.val()); @@ -236,6 +246,7 @@ else if (oldValue != null && newValue.isZero()) { gasCost += calcMemGas(gasCosts, oldMemSize, memNeeded(stack.peek(), new DataWord(32)), 0); break; case RETURN: + case REVERT: gasCost = gasCosts.getSTOP() + calcMemGas(gasCosts, oldMemSize, memNeeded(stack.peek(), stack.get(stack.size() - 2)), 0); break; @@ -246,6 +257,7 @@ else if (oldValue != null && newValue.isZero()) { gasCost += chunkUsed * gasCosts.getSHA3_WORD(); break; case CALLDATACOPY: + case RETURNDATACOPY: gasCost += calcMemGas(gasCosts, oldMemSize, memNeeded(stack.peek(), stack.get(stack.size() - 3)), stack.get(stack.size() - 3).longValueSafe()); @@ -774,6 +786,34 @@ else if (oldValue != null && newValue.isZero()) { program.step(); } break; + case RETURNDATASIZE: { + DataWord dataSize = program.getReturnDataBufferSize(); + + if (logger.isInfoEnabled()) + hint = "size: " + dataSize.value(); + + program.stackPush(dataSize); + program.step(); + } + break; + case RETURNDATACOPY: { + DataWord memOffsetData = program.stackPop(); + DataWord dataOffsetData = program.stackPop(); + DataWord lengthData = program.stackPop(); + + byte[] msgData = program.getReturnDataBufferData(dataOffsetData, lengthData); + + if (msgData == null) { + throw new Program.ReturnDataCopyIllegalBoundsException(dataOffsetData, lengthData, program.getReturnDataBufferSize().longValueSafe()); + } + + if (logger.isInfoEnabled()) + hint = "data: " + Hex.toHexString(msgData); + + program.memorySave(memOffsetData.intValueSafe(), msgData); + program.step(); + } + break; case CODESIZE: case EXTCODESIZE: { @@ -1190,7 +1230,8 @@ else if (oldValue != null && newValue.isZero()) { program.step(); } break; - case RETURN: { + case RETURN: + case REVERT: { DataWord offset = program.stackPop(); DataWord size = program.stackPop(); @@ -1204,6 +1245,10 @@ else if (oldValue != null && newValue.isZero()) { program.step(); program.stop(); + + if (op == REVERT) { + program.getResult().setRevert(); + } } break; case SUICIDE: { diff --git a/ethereumj-core/src/main/java/org/ethereum/vm/program/Program.java b/ethereumj-core/src/main/java/org/ethereum/vm/program/Program.java index 3d0d3972cc..4a05895f70 100644 --- a/ethereumj-core/src/main/java/org/ethereum/vm/program/Program.java +++ b/ethereumj-core/src/main/java/org/ethereum/vm/program/Program.java @@ -42,7 +42,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.spongycastle.util.encoders.Hex; -import org.springframework.beans.factory.annotation.Autowired; import java.io.ByteArrayOutputStream; import java.math.BigInteger; @@ -87,6 +86,7 @@ public class Program { private Stack stack; private Memory memory; private Storage storage; + private byte[] returnDataBuffer; private ProgramResult result = new ProgramResult(); private ProgramTrace trace = new ProgramTrace(); @@ -460,8 +460,9 @@ this, new DataWord(newAddress), getOwnerAddress(), value, gasLimit, newBalance, null, track, this.invoke.getBlockStore(), byTestingSuite()); ProgramResult result = ProgramResult.empty(); - if (isNotEmpty(programCode)) { + returnDataBuffer = null; // reset return buffer right before the call + if (isNotEmpty(programCode)) { VM vm = new VM(config); Program program = new Program(programCode, programInvoke, internalTx, config).withCommonConfig(commonConfig); vm.play(program); @@ -470,27 +471,7 @@ this, new DataWord(newAddress), getOwnerAddress(), value, gasLimit, getResult().merge(result); } - // 4. CREATE THE CONTRACT OUT OF RETURN - byte[] code = result.getHReturn(); - - long storageCost = getLength(code) * getBlockchainConfig().getGasCost().getCREATE_DATA(); - long afterSpend = programInvoke.getGas().longValue() - storageCost - result.getGasUsed(); - if (afterSpend < 0) { - if (!config.getBlockchainConfig().getConfigForBlock(getNumber().longValue()).getConstants().createEmptyContractOnOOG()) { - result.setException(Program.Exception.notEnoughSpendingGas("No gas to return just created contract", - storageCost, this)); - } else { - track.saveCode(newAddress, EMPTY_BYTE_ARRAY); - } - } else if (getLength(code) > blockchainConfig.getConstants().getMAX_CONTRACT_SZIE()) { - result.setException(Program.Exception.notEnoughSpendingGas("Contract size too large: " + getLength(result.getHReturn()), - storageCost, this)); - } else { - result.spendGas(storageCost); - track.saveCode(newAddress, code); - } - - if (result.getException() != null) { + if (result.getException() != null || result.isRevert()) { logger.debug("contract run halted by Exception: contract: [{}], exception: [{}]", Hex.toHexString(newAddress), result.getException()); @@ -500,14 +481,39 @@ this, new DataWord(newAddress), getOwnerAddress(), value, gasLimit, track.rollback(); stackPushZero(); - return; - } - if (!byTestingSuite()) - track.commit(); + if (result.getException() != null) { + return; + } else { + returnDataBuffer = result.getHReturn(); + } + } else { + // 4. CREATE THE CONTRACT OUT OF RETURN + byte[] code = result.getHReturn(); + + long storageCost = getLength(code) * getBlockchainConfig().getGasCost().getCREATE_DATA(); + long afterSpend = programInvoke.getGas().longValue() - storageCost - result.getGasUsed(); + if (afterSpend < 0) { + if (!config.getBlockchainConfig().getConfigForBlock(getNumber().longValue()).getConstants().createEmptyContractOnOOG()) { + result.setException(Program.Exception.notEnoughSpendingGas("No gas to return just created contract", + storageCost, this)); + } else { + track.saveCode(newAddress, EMPTY_BYTE_ARRAY); + } + } else if (getLength(code) > blockchainConfig.getConstants().getMAX_CONTRACT_SZIE()) { + result.setException(Program.Exception.notEnoughSpendingGas("Contract size too large: " + getLength(result.getHReturn()), + storageCost, this)); + } else { + result.spendGas(storageCost); + track.saveCode(newAddress, code); + } - // IN SUCCESS PUSH THE ADDRESS INTO THE STACK - stackPush(new DataWord(newAddress)); + if (!byTestingSuite()) + track.commit(); + + // IN SUCCESS PUSH THE ADDRESS INTO THE STACK + stackPush(new DataWord(newAddress)); + } // 5. REFUND THE REMAIN GAS long refundGas = gasLimit.longValue() - result.getGasUsed(); @@ -580,6 +586,8 @@ public void callToAddress(MessageCall msg) { ProgramResult result = null; if (isNotEmpty(programCode)) { + returnDataBuffer = null; // reset return buffer right before the call + ProgramInvoke programInvoke = programInvokeFactory.createProgramInvoke( this, new DataWord(contextAddress), msg.getType() == MsgType.DELEGATECALL ? getCallerAddress() : getOwnerAddress(), @@ -594,7 +602,7 @@ this, new DataWord(contextAddress), getTrace().merge(program.getTrace()); getResult().merge(result); - if (result.getException() != null) { + if (result.getException() != null || result.isRevert()) { logger.debug("contract run halted by Exception: contract: [{}], exception: [{}]", Hex.toHexString(contextAddress), result.getException()); @@ -604,12 +612,25 @@ this, new DataWord(contextAddress), track.rollback(); stackPushZero(); - return; - } else if (byTestingSuite()) { + + if (result.getException() != null) { + return; + } + } else { + // 4. THE FLAG OF SUCCESS IS ONE PUSHED INTO THE STACK + track.commit(); + stackPushOne(); + } + + if (byTestingSuite()) { logger.info("Testing run, skipping storage diff listener"); } else if (Arrays.equals(transaction.getReceiveAddress(), internalTx.getReceiveAddress())) { storageDiffListener.merge(program.getStorageDiff()); } + } else { + // 4. THE FLAG OF SUCCESS IS ONE PUSHED INTO THE STACK + track.commit(); + stackPushOne(); } // 3. APPLY RESULTS: result.getHReturn() into out_memory allocated @@ -619,11 +640,9 @@ this, new DataWord(contextAddress), int size = msg.getOutDataSize().intValue(); memorySaveLimited(offset, buffer, size); - } - // 4. THE FLAG OF SUCCESS IS ONE PUSHED INTO THE STACK - track.commit(); - stackPushOne(); + returnDataBuffer = buffer; + } // 5. REFUND THE REMAIN GAS if (result != null) { @@ -739,6 +758,20 @@ public byte[] getDataCopy(DataWord offset, DataWord length) { return invoke.getDataCopy(offset, length); } + public DataWord getReturnDataBufferSize() { + return new DataWord(getReturnDataBufferSizeI()); + } + + private int getReturnDataBufferSizeI() { + return returnDataBuffer == null ? 0 : returnDataBuffer.length; + } + + public byte[] getReturnDataBufferData(DataWord off, DataWord size) { + if ((long) off.intValueSafe() + size.intValueSafe() > getReturnDataBufferSizeI()) return null; + return returnDataBuffer == null ? new byte[0] : + Arrays.copyOfRange(returnDataBuffer, off.intValueSafe(), off.intValueSafe() + size.intValueSafe()); + } + public DataWord storageLoad(DataWord key) { DataWord ret = getStorage().getStorageValue(getOwnerAddress().getLast20Bytes(), key.clone()); return ret == null ? null : ret.clone(); @@ -1194,6 +1227,14 @@ public StackTooSmallException(String message, Object... args) { } } + @SuppressWarnings("serial") + public static class ReturnDataCopyIllegalBoundsException extends BytecodeExecutionException { + public ReturnDataCopyIllegalBoundsException(DataWord off, DataWord size, long returnDataSize) { + super(String.format("Illegal RETURNDATACOPY arguments: offset (%s) + size (%s) > RETURNDATASIZE (%d)", off, size, returnDataSize)); + } + } + + public static class Exception { public static OutOfGasException notEnoughOpGas(OpCode op, long opGas, long programGas) { diff --git a/ethereumj-core/src/main/java/org/ethereum/vm/program/ProgramResult.java b/ethereumj-core/src/main/java/org/ethereum/vm/program/ProgramResult.java index e7150630e5..a0e7ad1d75 100644 --- a/ethereumj-core/src/main/java/org/ethereum/vm/program/ProgramResult.java +++ b/ethereumj-core/src/main/java/org/ethereum/vm/program/ProgramResult.java @@ -40,6 +40,7 @@ public class ProgramResult { private long gasUsed; private byte[] hReturn = EMPTY_BYTE_ARRAY; private RuntimeException exception; + private boolean revert; private Set deleteAccounts; private ByteArraySet touchedAccounts = new ByteArraySet(); @@ -58,6 +59,14 @@ public void spendGas(long gas) { gasUsed += gas; } + public void setRevert() { + this.revert = true; + } + + public boolean isRevert() { + return revert; + } + public void refundGas(long gas) { gasUsed -= gas; } @@ -180,7 +189,7 @@ public void resetFutureRefund() { public void merge(ProgramResult another) { addInternalTransactions(another.getInternalTransactions()); - if (another.getException() == null) { + if (another.getException() == null && !isRevert()) { addDeleteAccounts(another.getDeleteAccounts()); addLogInfos(another.getLogInfoList()); addFutureRefund(another.getFutureRefund()); diff --git a/ethereumj-core/src/test/java/org/ethereum/jsontestsuite/GitHubJSONTestSuite.java b/ethereumj-core/src/test/java/org/ethereum/jsontestsuite/GitHubJSONTestSuite.java index cbc229e07b..4e544653cd 100644 --- a/ethereumj-core/src/test/java/org/ethereum/jsontestsuite/GitHubJSONTestSuite.java +++ b/ethereumj-core/src/test/java/org/ethereum/jsontestsuite/GitHubJSONTestSuite.java @@ -278,6 +278,7 @@ public BlockchainNetConfig getConfig() { case Homestead: return new HomesteadConfig(); case EIP150: return new Eip150HFConfig(new DaoHFConfig()); case EIP158: return new Eip160HFConfig(new DaoHFConfig()); + case Byzantium: return new ByzantiumConfig(new DaoHFConfig()); case FrontierToHomesteadAt5: return new BaseNetConfig() {{ add(0, new FrontierConfig()); diff --git a/ethereumj-core/src/test/java/org/ethereum/jsontestsuite/GitHubStateTest.java b/ethereumj-core/src/test/java/org/ethereum/jsontestsuite/GitHubStateTest.java index 0ca90eac00..95e370b183 100644 --- a/ethereumj-core/src/test/java/org/ethereum/jsontestsuite/GitHubStateTest.java +++ b/ethereumj-core/src/test/java/org/ethereum/jsontestsuite/GitHubStateTest.java @@ -35,7 +35,9 @@ public class GitHubStateTest { GitHubJSONTestSuite.Network.Frontier, GitHubJSONTestSuite.Network.Homestead, GitHubJSONTestSuite.Network.EIP150, - GitHubJSONTestSuite.Network.EIP158 + GitHubJSONTestSuite.Network.EIP158, +// GitHubJSONTestSuite.Network.Byzantium + }; static GeneralStateTestSuite suite; @@ -56,7 +58,7 @@ public void clean() { // it reduces impact on GitHub API public void stSingleTest() throws IOException { GeneralStateTestSuite.runSingle( - "stTransactionTest/zeroSigTransacrionCreate.json", commitSHA, GitHubJSONTestSuite.Network.Frontier); + "stRevertTest/RevertOpcodeInCreateReturns.json", commitSHA, GitHubJSONTestSuite.Network.Byzantium); } @Test