From b017df080b328f2dd78cac3cdb56112c362c599b Mon Sep 17 00:00:00 2001 From: Roman Mandeleil Date: Fri, 9 Jan 2015 16:41:36 +0200 Subject: [PATCH] Implement pre-compiled contract In order to improve performance of certain VM functionality Ethereum introduce plug-in mechanism called pre-compiled contracts. Supported functionality: ecRecover - (addr: 01) - recover address out of hash, v, r, s - parameters. sha256 - (addr: 02) - calculate hash value with sha256 algorithm ripempd160 - (addr:03) - calculate hash value with repimpd algorithm --- .../ethereum/jsontestsuite/TestRunner.java | 5 +- .../main/java/org/ethereum/vm/DataWord.java | 8 ++ .../org/ethereum/vm/PrecompiledContracts.java | 131 ++++++++++++++++++ .../main/java/org/ethereum/vm/Program.java | 33 ++++- .../src/main/java/org/ethereum/vm/VM.java | 13 +- .../jsontestsuite/GitHubStateTest.java | 29 ++-- .../ethereum/vm/PrecompiledContractTest.java | 104 ++++++++++++++ 7 files changed, 310 insertions(+), 13 deletions(-) create mode 100644 ethereumj-core/src/main/java/org/ethereum/vm/PrecompiledContracts.java create mode 100644 ethereumj-core/src/test/java/test/ethereum/vm/PrecompiledContractTest.java diff --git a/ethereumj-core/src/main/java/org/ethereum/jsontestsuite/TestRunner.java b/ethereumj-core/src/main/java/org/ethereum/jsontestsuite/TestRunner.java index 2490e986..7bce5ad0 100644 --- a/ethereumj-core/src/main/java/org/ethereum/jsontestsuite/TestRunner.java +++ b/ethereumj-core/src/main/java/org/ethereum/jsontestsuite/TestRunner.java @@ -133,7 +133,10 @@ public class TestRunner { int postRepoSize = testCase.getPost().size(); if (postRepoSize > repoSize) { - results.add("ERROR: Post repository contains more accounts than executed repository "); + results.add("ERROR: Expected 'Post' repository contains more accounts than executed repository "); + + logger.info("Full address set: " + fullAddressSet); + } return results; diff --git a/ethereumj-core/src/main/java/org/ethereum/vm/DataWord.java b/ethereumj-core/src/main/java/org/ethereum/vm/DataWord.java index 34654202..f8dc9e44 100644 --- a/ethereumj-core/src/main/java/org/ethereum/vm/DataWord.java +++ b/ethereumj-core/src/main/java/org/ethereum/vm/DataWord.java @@ -45,6 +45,10 @@ public class DataWord implements Comparable { this.data = data.array(); } + public DataWord(String data){ + this(Hex.decode(data)); + } + public DataWord(byte[] data) { if (data == null) this.data = ByteUtil.EMPTY_BYTE_ARRAY; @@ -312,4 +316,8 @@ public class DataWord implements Comparable { if (firstNonZero == -1) return 0; return 31 - firstNonZero + 1; } + + public boolean isHex(String hex){ + return Hex.toHexString(data).equals(hex); + } } diff --git a/ethereumj-core/src/main/java/org/ethereum/vm/PrecompiledContracts.java b/ethereumj-core/src/main/java/org/ethereum/vm/PrecompiledContracts.java new file mode 100644 index 00000000..f31d6d85 --- /dev/null +++ b/ethereumj-core/src/main/java/org/ethereum/vm/PrecompiledContracts.java @@ -0,0 +1,131 @@ +package org.ethereum.vm; + +import org.ethereum.crypto.ECKey; +import org.ethereum.crypto.HashUtil; +import org.ethereum.util.ByteUtil; + +/** + * @author Roman Mandeleil + * Created on: 09/01/2015 08:05 + */ + +public class PrecompiledContracts { + + private static ECRecover ecRecover = new ECRecover(); + private static Sha256 sha256 = new Sha256(); + private static Ripempd160 ripempd160 = new Ripempd160(); + private static Identity identity = new Identity(); + + + public static PrecompiledContract getContractForAddress(DataWord address){ + + if (address == null) return identity; + if (address.isHex("0000000000000000000000000000000000000000000000000000000000000001")) return ecRecover; + if (address.isHex("0000000000000000000000000000000000000000000000000000000000000002")) return sha256; + if (address.isHex("0000000000000000000000000000000000000000000000000000000000000003")) return ripempd160; + if (address.isHex("0000000000000000000000000000000000000000000000000000000000000004")) return identity; + + return null; + } + + + public static abstract class PrecompiledContract{ + public abstract long getGasForData(byte[] data); + public abstract byte[] execute(byte[] data); + } + + public static class Identity extends PrecompiledContract{ + + public Identity() { + } + + @Override + public long getGasForData(byte[] data) { + if (data == null) return 1; + return 1 + (data.length + 31) / 32 * 1; + } + + @Override + public byte[] execute(byte[] data) { + return data; + } + } + + public static class Sha256 extends PrecompiledContract{ + + + @Override + public long getGasForData(byte[] data) { + if (data == null) return 50; + return 50 + (data.length + 31) / 32 * 50; + } + + @Override + public byte[] execute(byte[] data) { + + if (data == null) return HashUtil.sha256(ByteUtil.EMPTY_BYTE_ARRAY); + return HashUtil.sha256(data); + } + } + + + public static class Ripempd160 extends PrecompiledContract{ + + + @Override + public long getGasForData(byte[] data) { + if (data == null) return 50; + return 50 + (data.length + 31) / 32 * 50; + } + + @Override + public byte[] execute(byte[] data) { + + byte[] result = null; + if (data == null) result = HashUtil.ripemd160(ByteUtil.EMPTY_BYTE_ARRAY); + else result = HashUtil.ripemd160(data); + + return new DataWord(result).getData(); + } + } + + + public static class ECRecover extends PrecompiledContract{ + + @Override + public long getGasForData(byte[] data) { + return 500; + } + + @Override + public byte[] execute(byte[] data) { + + byte[] h = new byte[32]; + byte[] v = new byte[32]; + byte[] r = new byte[32]; + byte[] s = new byte[32]; + + DataWord out = null; + + try{ + System.arraycopy(data, 0, h, 0, 32); + System.arraycopy(data, 32, v, 0, 32); + System.arraycopy(data, 64, r, 0, 32); + System.arraycopy(data, 96, s, 0, 32); + + + ECKey.ECDSASignature signature = ECKey.ECDSASignature.fromComponents(r, s, v[31]); + + ECKey key = ECKey.signatureToKey(h, signature.toBase64()); + out = new DataWord(key.getAddress()); + } catch (Throwable any){} + + if (out == null) out = new DataWord(0); + + return out.getData(); + } + } + + + +} diff --git a/ethereumj-core/src/main/java/org/ethereum/vm/Program.java b/ethereumj-core/src/main/java/org/ethereum/vm/Program.java index acf16648..ccf9b511 100644 --- a/ethereumj-core/src/main/java/org/ethereum/vm/Program.java +++ b/ethereumj-core/src/main/java/org/ethereum/vm/Program.java @@ -6,6 +6,7 @@ import org.ethereum.db.ContractDetails; import org.ethereum.facade.Repository; import org.ethereum.util.ByteUtil; import org.ethereum.vm.MessageCall.MsgType; +import org.ethereum.vm.PrecompiledContracts.PrecompiledContract; import org.ethereum.vmtrace.Op; import org.ethereum.vmtrace.ProgramTrace; @@ -214,6 +215,14 @@ public class Program { public void memorySave(int addr, byte[] value) { memorySave(addr, value.length, value); } + + public void memoryExpand(DataWord outDataOffs, DataWord outDataSize){ + + int maxAddress = outDataOffs.intValue() + outDataSize.intValue(); + if (getMemSize() < maxAddress){ + memorySave(maxAddress, new byte[]{0}); + } + } /** * Allocates a piece of memory and stores value at given offset address @@ -420,7 +429,7 @@ public class Program { stackPushZero(); return; } - + byte[] data = memoryChunk(msg.getInDataOffs(), msg.getInDataSize()).array(); // FETCH THE SAVED STORAGE @@ -900,6 +909,28 @@ public class Program { if (!jumpdest.contains(nextPC)) throw new BadJumpDestinationException(); } + public void callToPrecompiledAddress(MessageCall msg, PrecompiledContract contract) { + + byte[] data = this.memoryChunk( msg.getInDataOffs(), msg.getInDataSize()).array(); + + this.result.getRepository().addBalance(this.getOwnerAddress().getLast20Bytes(), msg.getEndowment().value().negate()); + this.result.getRepository().addBalance(msg.getCodeAddress().getLast20Bytes(), msg.getEndowment().value()); + + long requiredGas = contract.getGasForData(data); + if (requiredGas > msg.getGas().longValue()){ + + this.spendGas(msg.getGas().longValue(), "call pre-compiled"); + this.stackPushZero(); + } else { + + this.spendGas(requiredGas, "call pre-compiled"); + byte[] out = contract.execute(data); + + this.memorySave( msg.getOutDataOffs().intValue(), out); + this.stackPushOne(); + } + } + public interface ProgramListener { public void output(String out); } 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 8ed4db8f..c5ff238a 100644 --- a/ethereumj-core/src/main/java/org/ethereum/vm/VM.java +++ b/ethereumj-core/src/main/java/org/ethereum/vm/VM.java @@ -1042,14 +1042,25 @@ public class VM { program.getGas().value(), program.invokeData.getCallDeep(), hint); } + + program.memoryExpand(outDataOffs, outDataSize); MessageCall msg = new MessageCall( op.equals(CALL) ? MsgType.CALL : MsgType.STATELESS, gas, codeAddress, value, inDataOffs, inDataSize, outDataOffs, outDataSize); - program.callToAddress(msg); + PrecompiledContracts.PrecompiledContract contract = + PrecompiledContracts.getContractForAddress(codeAddress); + + if (contract != null) + program.callToPrecompiledAddress(msg, contract); + else + program.callToAddress(msg); + + program.step(); + } break; case RETURN: { diff --git a/ethereumj-core/src/test/java/test/ethereum/jsontestsuite/GitHubStateTest.java b/ethereumj-core/src/test/java/test/ethereum/jsontestsuite/GitHubStateTest.java index c96c3c3c..9aa66891 100644 --- a/ethereumj-core/src/test/java/test/ethereum/jsontestsuite/GitHubStateTest.java +++ b/ethereumj-core/src/test/java/test/ethereum/jsontestsuite/GitHubStateTest.java @@ -19,7 +19,7 @@ public class GitHubStateTest { @Test public void stSingleTest() throws ParseException { String json = JSONReader.loadJSON("StateTests/stSystemOperationsTest.json"); - GitHubJSONTestSuite.runGitHubJsonStateTest(json, "CallToReturn1ForDynamicJump1"); + GitHubJSONTestSuite.runGitHubJsonStateTest(json, "CallRecursiveBombLog2"); } @Ignore @@ -27,9 +27,10 @@ public class GitHubStateTest { public void runWithExcludedTest() throws ParseException { Set excluded = new HashSet<>(); - excluded.add("CallToReturn1ForDynamicJump1"); + excluded.add("CallSha256_5"); - String json = JSONReader.loadJSON("StateTests/stSystemOperationsTest.json"); + + String json = JSONReader.loadJSON("StateTests/stPreCompiledContracts.json"); GitHubJSONTestSuite.runGitHubJsonStateTest(json, excluded); } @@ -41,12 +42,17 @@ public class GitHubStateTest { GitHubJSONTestSuite.runGitHubJsonStateTest(json); } - @Ignore - @Test + @Test // todo: fix: excluded test public void stInitCodeTest() throws ParseException { // [V] + Set excluded = new HashSet<>(); + excluded.add("NotEnoughCashContractCreation"); + excluded.add("CallContractToCreateContractOOG"); + excluded.add("CallContractToCreateContractNoCash"); + excluded.add("CallContractToCreateContractWhichWouldCreateContractInInitCode"); + String json = JSONReader.loadJSON("StateTests/stInitCodeTest.json"); - GitHubJSONTestSuite.runGitHubJsonStateTest(json); + GitHubJSONTestSuite.runGitHubJsonStateTest(json, excluded); } @Test @@ -56,7 +62,6 @@ public class GitHubStateTest { GitHubJSONTestSuite.runGitHubJsonStateTest(json); } - @Ignore @Test public void stPreCompiledContracts() throws ParseException { @@ -103,12 +108,16 @@ public class GitHubStateTest { } - @Ignore - @Test + @Test // todo: fix: excluded test public void stTransactionTest() throws ParseException { + Set excluded = new HashSet<>(); + excluded.add("EmptyTransaction"); + //todo: it goes OOG, because no gasLimit is given. So it does not change the state. + + String json = JSONReader.loadJSON("StateTests/stTransactionTest.json"); - GitHubJSONTestSuite.runGitHubJsonStateTest(json); + GitHubJSONTestSuite.runGitHubJsonStateTest(json, excluded); } diff --git a/ethereumj-core/src/test/java/test/ethereum/vm/PrecompiledContractTest.java b/ethereumj-core/src/test/java/test/ethereum/vm/PrecompiledContractTest.java new file mode 100644 index 00000000..5e23f8b7 --- /dev/null +++ b/ethereumj-core/src/test/java/test/ethereum/vm/PrecompiledContractTest.java @@ -0,0 +1,104 @@ +package test.ethereum.vm; + +import org.ethereum.util.ByteUtil; +import org.ethereum.vm.DataWord; +import org.ethereum.vm.PrecompiledContracts; +import org.ethereum.vm.PrecompiledContracts.PrecompiledContract; +import org.junit.Assert; +import org.junit.Test; +import org.spongycastle.util.encoders.Hex; + +import static org.junit.Assert.*; + +/** + * @author: Roman Mandeleil + * Created on: 09/01/2015 08:28 + */ + +public class PrecompiledContractTest { + + + + @Test + public void identityTest1(){ + + DataWord addr = new DataWord("0000000000000000000000000000000000000000000000000000000000000004"); + PrecompiledContract contract = PrecompiledContracts.getContractForAddress(addr); + byte[] data = Hex.decode("112233445566"); + byte[] expected = Hex.decode("112233445566"); + + byte[] result = contract.execute(data); + + assertArrayEquals(expected, result); + } + + + @Test + public void sha256Test1(){ + + DataWord addr = new DataWord("0000000000000000000000000000000000000000000000000000000000000002"); + PrecompiledContract contract = PrecompiledContracts.getContractForAddress(addr); + byte[] data = null; + String expected = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + + byte[] result = contract.execute(data); + + assertEquals(expected, Hex.toHexString(result)); + } + + @Test + public void sha256Test2(){ + + DataWord addr = new DataWord("0000000000000000000000000000000000000000000000000000000000000002"); + PrecompiledContract contract = PrecompiledContracts.getContractForAddress(addr); + byte[] data = ByteUtil.EMPTY_BYTE_ARRAY; + String expected = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + + byte[] result = contract.execute(data); + + assertEquals(expected, Hex.toHexString(result)); + } + + @Test + public void sha256Test3(){ + + DataWord addr = new DataWord("0000000000000000000000000000000000000000000000000000000000000002"); + PrecompiledContract contract = PrecompiledContracts.getContractForAddress(addr); + byte[] data = Hex.decode("112233"); + String expected = "49ee2bf93aac3b1fb4117e59095e07abe555c3383b38d608da37680a406096e8"; + + byte[] result = contract.execute(data); + + assertEquals(expected, Hex.toHexString(result)); + } + + + @Test + public void Ripempd160Test1(){ + + DataWord addr = new DataWord("0000000000000000000000000000000000000000000000000000000000000003"); + PrecompiledContract contract = PrecompiledContracts.getContractForAddress(addr); + byte[] data = Hex.decode("0000000000000000000000000000000000000000000000000000000000000001"); + String expected = "000000000000000000000000ae387fcfeb723c3f5964509af111cf5a67f30661"; + + byte[] result = contract.execute(data); + + assertEquals(expected, Hex.toHexString(result)); + } + + @Test + public void ecRecoverTest1(){ + + byte[] data = Hex.decode("18c547e4f7b0f325ad1e56f57e26c745b09a3e503d86e00e5255ff7f715d3d1c000000000000000000000000000000000000000000000000000000000000001c73b1693892219d736caba55bdb67216e485557ea6b6af75f37096c9aa6a5a75feeb940b1d03b21e36b0e47e79769f095fe2ab855bd91e3a38756b7d75a9c4549"); + DataWord addr = new DataWord("0000000000000000000000000000000000000000000000000000000000000001"); + PrecompiledContract contract = PrecompiledContracts.getContractForAddress(addr); + String expected = "000000000000000000000000ae387fcfeb723c3f5964509af111cf5a67f30661"; + + byte[] result = contract.execute(data); + + System.out.println(Hex.toHexString(result)); + + + } + +}