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
This commit is contained in:
Roman Mandeleil 2015-01-09 16:41:36 +02:00
parent 620f365205
commit b017df080b
7 changed files with 310 additions and 13 deletions

View File

@ -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;

View File

@ -45,6 +45,10 @@ public class DataWord implements Comparable<DataWord> {
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<DataWord> {
if (firstNonZero == -1) return 0;
return 31 - firstNonZero + 1;
}
public boolean isHex(String hex){
return Hex.toHexString(data).equals(hex);
}
}

View File

@ -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();
}
}
}

View File

@ -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;
@ -215,6 +216,14 @@ public class Program {
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
*
@ -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);
}

View File

@ -1043,13 +1043,24 @@ public class VM {
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);
PrecompiledContracts.PrecompiledContract contract =
PrecompiledContracts.getContractForAddress(codeAddress);
if (contract != null)
program.callToPrecompiledAddress(msg, contract);
else
program.callToAddress(msg);
program.step();
}
break;
case RETURN: {

View File

@ -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<String> 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<String> 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<String> 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);
}

View File

@ -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));
}
}