diff --git a/ethereumj-core/src/main/java/org/ethereum/solidity/SolidityType.java b/ethereumj-core/src/main/java/org/ethereum/solidity/SolidityType.java index 86228abc39..c641d9f48a 100644 --- a/ethereumj-core/src/main/java/org/ethereum/solidity/SolidityType.java +++ b/ethereumj-core/src/main/java/org/ethereum/solidity/SolidityType.java @@ -32,6 +32,7 @@ import static org.ethereum.util.ByteUtil.toHexString; public abstract class SolidityType { + private final static int Int32Size = 32; protected String name; public SolidityType(String name) { @@ -56,7 +57,7 @@ public String getCanonicalName() { @JsonCreator public static SolidityType getType(String typeName) { - if (typeName.contains("[")) return ArrayType.getType(typeName); + if (typeName.endsWith("]")) return ArrayType.getType(typeName); if ("bool".equals(typeName)) return new BoolType(); if (typeName.startsWith("int")) return new IntType(typeName); if (typeName.startsWith("uint")) return new UnsignedIntType(typeName); @@ -86,7 +87,7 @@ public Object decode(byte[] encoded) { * which is effectively the int offset to dynamic data */ public int getFixedSize() { - return 32; + return Int32Size; } public boolean isDynamicType() { @@ -101,8 +102,8 @@ public String toString() { public static abstract class ArrayType extends SolidityType { public static ArrayType getType(String typeName) { - int idx1 = typeName.indexOf("["); - int idx2 = typeName.indexOf("]", idx1); + int idx1 = typeName.lastIndexOf("["); + int idx2 = typeName.lastIndexOf("]"); if (idx1 + 1 == idx2) { return new DynamicArrayType(typeName); } else { @@ -114,11 +115,7 @@ public static ArrayType getType(String typeName) { public ArrayType(String name) { super(name); - int idx = name.indexOf("["); - String st = name.substring(0, idx); - int idx2 = name.indexOf("]", idx); - String subDim = idx2 + 1 == name.length() ? "" : name.substring(idx2 + 1); - elementType = SolidityType.getType(st + subDim); + elementType = SolidityType.getType(name.substring(0, name.lastIndexOf("["))); } @Override @@ -135,23 +132,42 @@ public byte[] encode(Object value) { throw new RuntimeException("List value expected for type " + getName()); } } - - @Override - public String getCanonicalName() { - return getArrayCanonicalName(""); - } - String getArrayCanonicalName(String parentDimStr) { - String myDimStr = parentDimStr + getCanonicalDimension(); - if (getElementType() instanceof ArrayType) { - return ((ArrayType) getElementType()). - getArrayCanonicalName(myDimStr); + protected byte[] encodeTuple(List l) { + byte[][] elems; + if (elementType.isDynamicType()) { + elems = new byte[l.size() * 2][]; + int offset = l.size() * Int32Size; + for (int i = 0; i < l.size(); i++) { + elems[i] = IntType.encodeInt(offset); + byte[] encoded = elementType.encode(l.get(i)); + elems[l.size() + i] = encoded; + offset += Int32Size * ((encoded.length - 1) / Int32Size + 1); + } } else { - return getElementType().getCanonicalName() + myDimStr; + elems = new byte[l.size()][]; + for (int i = 0; i < l.size(); i++) { + elems[i] = elementType.encode(l.get(i)); + } } + return ByteUtil.merge(elems); + } + + public Object[] decodeTuple(byte[] encoded, int origOffset, int len) { + int offset = origOffset; + Object[] ret = new Object[len]; + + for (int i = 0; i < len; i++) { + if (elementType.isDynamicType()) { + ret[i] = elementType.decode(encoded, origOffset + IntType.decodeInt(encoded, offset).intValue()); + } else { + ret[i] = elementType.decode(encoded, offset); + } + offset += elementType.getFixedSize(); + } + return ret; } - protected abstract String getCanonicalDimension(); public SolidityType getElementType() { return elementType; @@ -165,52 +181,40 @@ public static class StaticArrayType extends ArrayType { public StaticArrayType(String name) { super(name); - int idx1 = name.indexOf("["); - int idx2 = name.indexOf("]", idx1); + int idx1 = name.lastIndexOf("["); + int idx2 = name.lastIndexOf("]"); String dim = name.substring(idx1 + 1, idx2); size = Integer.parseInt(dim); } @Override public String getCanonicalName() { - if (elementType instanceof ArrayType) { - String elementTypeName = elementType.getCanonicalName(); - int idx1 = elementTypeName.indexOf("["); - return elementTypeName.substring(0, idx1) + "[" + size + "]" + elementTypeName.substring(idx1); - } else { - return elementType.getCanonicalName() + "[" + size + "]"; - } - } - - @Override - protected String getCanonicalDimension() { - return "[" + size + "]"; + return getElementType().getCanonicalName() + "[" + size + "]"; } @Override public byte[] encodeList(List l) { if (l.size() != size) throw new RuntimeException("List size (" + l.size() + ") != " + size + " for type " + getName()); - byte[][] elems = new byte[size][]; - for (int i = 0; i < l.size(); i++) { - elems[i] = elementType.encode(l.get(i)); - } - return ByteUtil.merge(elems); + return encodeTuple(l); } @Override public Object[] decode(byte[] encoded, int offset) { - Object[] result = new Object[size]; - for (int i = 0; i < size; i++) { - result[i] = elementType.decode(encoded, offset + i * elementType.getFixedSize()); - } - - return result; + return decodeTuple(encoded, offset, size); } @Override public int getFixedSize() { - // return negative if elementType is dynamic - return elementType.getFixedSize() * size; + if (isDynamicType()) { + return Int32Size; + } else { + return elementType.getFixedSize() * size; + } + } + + @Override + public boolean isDynamicType() { + return getElementType().isDynamicType() && size > 0; } } @@ -221,60 +225,18 @@ public DynamicArrayType(String name) { @Override public String getCanonicalName() { - if (elementType instanceof ArrayType) { - String elementTypeName = elementType.getCanonicalName(); - int idx1 = elementTypeName.indexOf("["); - return elementTypeName.substring(0, idx1) + "[]" + elementTypeName.substring(idx1); - } else { - return elementType.getCanonicalName() + "[]"; - } - } - - @Override - protected String getCanonicalDimension() { - return "[]"; + return elementType.getCanonicalName() + "[]"; } @Override public byte[] encodeList(List l) { - byte[][] elems; - if (elementType.isDynamicType()) { - elems = new byte[l.size() * 2 + 1][]; - elems[0] = IntType.encodeInt(l.size()); - int offset = l.size() * 32; - for (int i = 0; i < l.size(); i++) { - elems[i + 1] = IntType.encodeInt(offset); - byte[] encoded = elementType.encode(l.get(i)); - elems[l.size() + i + 1] = encoded; - offset += 32 * ((encoded.length - 1) / 32 + 1); - } - } else { - elems = new byte[l.size() + 1][]; - elems[0] = IntType.encodeInt(l.size()); - - for (int i = 0; i < l.size(); i++) { - elems[i + 1] = elementType.encode(l.get(i)); - } - } - return ByteUtil.merge(elems); + return ByteUtil.merge(IntType.encodeInt(l.size()), encodeTuple(l)); } @Override public Object decode(byte[] encoded, int origOffset) { int len = IntType.decodeInt(encoded, origOffset).intValue(); - origOffset += 32; - int offset = origOffset; - Object[] ret = new Object[len]; - - for (int i = 0; i < len; i++) { - if (elementType.isDynamicType()) { - ret[i] = elementType.decode(encoded, origOffset + IntType.decodeInt(encoded, offset).intValue()); - } else { - ret[i] = elementType.decode(encoded, offset); - } - offset += elementType.getFixedSize(); - } - return ret; + return decodeTuple(encoded, origOffset + Int32Size, len); } @Override @@ -302,7 +264,7 @@ public byte[] encode(Object value) { } else { throw new RuntimeException("byte[] or String value is expected for type 'bytes'"); } - byte[] ret = new byte[((bb.length - 1) / 32 + 1) * 32]; // padding 32 bytes + byte[] ret = new byte[((bb.length - 1) / Int32Size + 1) * Int32Size]; // padding 32 bytes System.arraycopy(bb, 0, ret, 0, bb.length); return ByteUtil.merge(IntType.encodeInt(bb.length), ret); @@ -312,7 +274,7 @@ public byte[] encode(Object value) { public Object decode(byte[] encoded, int offset) { int len = IntType.decodeInt(encoded, offset).intValue(); if (len == 0) return new byte[0]; - offset += 32; + offset += Int32Size; return Arrays.copyOfRange(encoded, offset, offset + len); } @@ -350,14 +312,14 @@ public byte[] encode(Object value) { BigInteger bigInt = new BigInteger(value.toString()); return IntType.encodeInt(bigInt); } else if (value instanceof String) { - byte[] ret = new byte[32]; + byte[] ret = new byte[Int32Size]; byte[] bytes = ((String) value).getBytes(StandardCharsets.UTF_8); System.arraycopy(bytes, 0, ret, 0, bytes.length); return ret; } else if (value instanceof byte[]) { byte[] bytes = (byte[]) value; - byte[] ret = new byte[32]; - System.arraycopy(bytes, 0, ret, 32 - bytes.length, bytes.length); + byte[] ret = new byte[Int32Size]; + System.arraycopy(bytes, 0, ret, Int32Size - bytes.length, bytes.length); return ret; } @@ -370,7 +332,7 @@ public Object decode(byte[] encoded, int offset) { } public static byte[] decodeBytes32(byte[] encoded, int offset) { - return Arrays.copyOfRange(encoded, offset, offset + 32); + return Arrays.copyOfRange(encoded, offset, offset + Int32Size); } } @@ -443,13 +405,13 @@ public String getCanonicalName() { return super.getCanonicalName(); } public static BigInteger decodeInt(byte[] encoded, int offset) { - return new BigInteger(Arrays.copyOfRange(encoded, offset, offset + 32)); + return new BigInteger(Arrays.copyOfRange(encoded, offset, offset + Int32Size)); } public static byte[] encodeInt(int i) { return encodeInt(new BigInteger("" + i)); } public static byte[] encodeInt(BigInteger bigInt) { - return ByteUtil.bigIntegerToBytesSigned(bigInt, 32); + return ByteUtil.bigIntegerToBytesSigned(bigInt, Int32Size); } @Override public Object decode(byte[] encoded, int offset) { @@ -473,7 +435,7 @@ public String getCanonicalName() { return super.getCanonicalName(); } public static BigInteger decodeInt(byte[] encoded, int offset) { - return new BigInteger(1, Arrays.copyOfRange(encoded, offset, offset + 32)); + return new BigInteger(1, Arrays.copyOfRange(encoded, offset, offset + Int32Size)); } public static byte[] encodeInt(int i) { return encodeInt(new BigInteger("" + i)); @@ -482,7 +444,7 @@ public static byte[] encodeInt(BigInteger bigInt) { if (bigInt.signum() == -1) { throw new RuntimeException("Wrong value for uint type: " + bigInt); } - return ByteUtil.bigIntegerToBytes(bigInt, 32); + return ByteUtil.bigIntegerToBytes(bigInt, Int32Size); } @Override public byte[] encode(Object value) { diff --git a/ethereumj-core/src/test/java/org/ethereum/core/ABITest.java b/ethereumj-core/src/test/java/org/ethereum/core/ABITest.java index 03e59be128..dca533f767 100644 --- a/ethereumj-core/src/test/java/org/ethereum/core/ABITest.java +++ b/ethereumj-core/src/test/java/org/ethereum/core/ABITest.java @@ -19,12 +19,18 @@ import static org.ethereum.crypto.HashUtil.sha3; +import org.ethereum.solidity.SolidityType; +import org.ethereum.util.blockchain.SolidityCallResult; +import org.ethereum.util.blockchain.SolidityContract; +import org.ethereum.util.blockchain.StandaloneBlockchain; import org.junit.Assert; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.spongycastle.util.encoders.Hex; +import java.math.BigInteger; + /** * @author Anton Nashatyrev */ @@ -335,33 +341,33 @@ public void decodeWithUnknownFunctionTypeTest() { Assert.assertArrayEquals((Object[]) objects[1], strings); Assert.assertEquals(((Number) objects[2]).intValue(), 222); } - + @Test public void twoDimensionalArrayType_hasDimensionDefinitionInCorrectOrder() { - String funcJson = "{ \n" + - " 'constant':false,\n" + - " 'inputs':[ \n" + - " { \n" + - " 'name':'param1',\n" + - " 'type':'address[5][]'\n" + - " },\n" + - " { \n" + - " 'name':'param2',\n" + - " 'type':'uint256[6][2]'\n" + - " },\n" + - " { \n" + - " 'name':'param2',\n" + - " 'type':'uint256[][]'\n" + + String funcJson = "{ \n" + + " 'constant':false,\n" + + " 'inputs':[ \n" + + " { \n" + + " 'name':'param1',\n" + + " 'type':'address[5][]'\n" + + " },\n" + + " { \n" + + " 'name':'param2',\n" + + " 'type':'uint256[6][2]'\n" + " },\n" + - " { \n" + - " 'name':'param3',\n" + - " 'type':'uint256[][2]'\n" + + " { \n" + + " 'name':'param2',\n" + + " 'type':'uint256[][]'\n" + + " },\n" + + " { \n" + + " 'name':'param3',\n" + + " 'type':'uint256[][2]'\n" + " }\n" + - " ],\n" + - " 'name':'testTwoDimArray',\n" + - " 'outputs':[],\n" + - " 'payable':false,\n" + - " 'type':'function'\n" + + " ],\n" + + " 'name':'testTwoDimArray',\n" + + " 'outputs':[],\n" + + " 'payable':false,\n" + + " 'type':'function'\n" + "}"; funcJson = funcJson.replaceAll("'", "\""); CallTransaction.Function function = CallTransaction.Function.fromJsonInterface(funcJson); @@ -369,4 +375,215 @@ public void twoDimensionalArrayType_hasDimensionDefinitionInCorrectOrder() { String actual = function.toString(); Assert.assertEquals(expected, actual); } + + @Test + public void twoDimensionalArrayTypeAsParameter_isDecoded() { + String funcJson = "{ " + + " 'constant':false, " + + " 'inputs':[ " + + " { " + + " 'name':'orderAddresses', " + + " 'type':'address[5][]' " + + " }, " + + " { " + + " 'name':'orderValues', " + + " 'type':'uint256[6][]' " + + " }, " + + " { " + + " 'name':'fillTakerTokenAmounts', " + + " 'type':'uint256[]' " + + " }, " + + " { " + + " 'name':'v', " + + " 'type':'uint8[]' " + + " }, " + + " { " + + " 'name':'r', " + + " 'type':'bytes32[]' " + + " }, " + + " { " + + " 'name':'s', " + + " 'type':'bytes32[]' " + + " } " + + " ], " + + " 'name':'batchFillOrKillOrders', " + + " 'outputs':[], " + + " 'payable':false, " + + " 'type':'function' " + + " }"; + funcJson = funcJson.replaceAll("'", "\""); + CallTransaction.Function function = CallTransaction.Function.fromJsonInterface(funcJson); + + Object[] args = new Object[]{ + new byte[][][]{ + new byte[][]{ + Hex.decode("1b2a9cc5ea11c11b70908d75207b5b1f0ac4a839"), + Hex.decode("e697a9f14f182c5291287dbeb47d41773091f035"), + Hex.decode("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"), + Hex.decode("2d0ea9f9591205a642eb01826ba4fa019eb0efc6"), + Hex.decode("8124071f810d533ff63de61d0c98db99eeb99d64") + }, + new byte[][]{ + Hex.decode("1b2a9cc5ea11c11b70908d75207b5b1f0ac4a839"), + Hex.decode("e697a9f14f182c5291287dbeb47d41773091f035"), + Hex.decode("2d0ea9f9591205a642eb01826ba4fa019eb0efc6"), + Hex.decode("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"), + Hex.decode("8124071f810d533ff63de61d0c98db99eeb99d64") + } + }, new BigInteger[][]{ + new BigInteger[]{ + new BigInteger("15920000000000000000"), + new BigInteger("1592000000000000000000"), + BigInteger.valueOf(0), + BigInteger.valueOf(0), + BigInteger.valueOf(1537516391517L), + new BigInteger("88416929899962839058958574884878701761157019606353286750292520499350182621314") + }, + new BigInteger[]{ + new BigInteger("1642000000000000000000"), + new BigInteger("16420000000000002000"), + BigInteger.valueOf(0), + BigInteger.valueOf(0), + BigInteger.valueOf(1537517358153L), + new BigInteger("93513067008724755490443777049125356883124657581213787456489051336421643029820") + } + }, + new BigInteger[]{ + new BigInteger("14000000000000000000"), + new BigInteger("140000000000000017") + }, + new BigInteger[]{ + BigInteger.valueOf(27), + BigInteger.valueOf(28) + }, + new byte[][]{ + Hex.decode("9202d3602753ffdb469e9dbae74cbe7528c648f708334f7791acc6fe0ce8182b"), + Hex.decode("ef362daf1bc2c805797761ae93a6c46ed53d73483a2bcc5b499ab65a8ba7f16c") + }, + new byte[][]{ + Hex.decode("0b43ad3ff547ebf5089802a74e764692bdc092190438b31be34d1d79406a75ba"), + Hex.decode("4e87fcd4ead36423d5bbcc7f1b41616235a17ec1f053a66827281bab104b718b") + } + }; + byte[] bytes = function.encode(args); + + String input = "4f15078700000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003c00000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000048000000000000000000000000000000000000000000000000000000000000004e000000000000000000000000000000000000000000000000000000000000000020000000000000000000000001b2a9cc5ea11c11b70908d75207b5b1f0ac4a839000000000000000000000000e697a9f14f182c5291287dbeb47d41773091f035000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000002d0ea9f9591205a642eb01826ba4fa019eb0efc60000000000000000000000008124071f810d533ff63de61d0c98db99eeb99d640000000000000000000000001b2a9cc5ea11c11b70908d75207b5b1f0ac4a839000000000000000000000000e697a9f14f182c5291287dbeb47d41773091f0350000000000000000000000002d0ea9f9591205a642eb01826ba4fa019eb0efc6000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000008124071f810d533ff63de61d0c98db99eeb99d640000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000dcef33a6f83800000000000000000000000000000000000000000000000000564d702d38f5e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000165fb1e4c5dc37a357a19313983db8360977d0c3cfe274a9deb63fc1a994f3c290b2644f0820000000000000000000000000000000000000000000000590353dc4fa7680000000000000000000000000000000000000000000000000000e3df8f00cbea07d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000165fb2d0c49cebe85312f1f1cc97430bc1315aff0fbb0d7f1219c8759774672c9939e168d3c0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000c249fdd32778000000000000000000000000000000000000000000000000000001f161421c8e00110000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001b000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000029202d3602753ffdb469e9dbae74cbe7528c648f708334f7791acc6fe0ce8182bef362daf1bc2c805797761ae93a6c46ed53d73483a2bcc5b499ab65a8ba7f16c00000000000000000000000000000000000000000000000000000000000000020b43ad3ff547ebf5089802a74e764692bdc092190438b31be34d1d79406a75ba4e87fcd4ead36423d5bbcc7f1b41616235a17ec1f053a66827281bab104b718b"; + Assert.assertEquals(input, Hex.toHexString(bytes)); + + Object[] decode = function.decode(Hex.decode(input)); + Assert.assertArrayEquals(args, decode); + } + + @Test + public void staticArrayWithDynamicElements() { + // static array with dynamic elements is itself dynamic type + String funcJson = "{ " + + " 'constant':false, " + + " 'inputs':[{ " + + " 'name':'p1', " + + " 'type':'address[][2]' " + + " }]," + + " 'name':'f1', " + + " 'outputs':[], " + + " 'payable':false, " + + " 'type':'function' " + + "}"; + funcJson = funcJson.replaceAll("'", "\""); + + CallTransaction.Function function = CallTransaction.Function.fromJsonInterface(funcJson); + Assert.assertTrue(function.inputs[0].type instanceof SolidityType.StaticArrayType); + Assert.assertTrue(function.inputs[0].type.isDynamicType()); + + try { + function.encode((Object) new byte[][][]{ + new byte[][]{ + Hex.decode("1111111111111111111111111111111111111111"), + }} + ); + throw new RuntimeException("Exception should be thrown"); + } catch (Exception e) { + System.out.println("Expected exception: " + e); + } + + Object[] args = new Object[]{ + new byte[][][]{ + new byte[][]{ + Hex.decode("1111111111111111111111111111111111111111"), + Hex.decode("2222222222222222222222222222222222222222"), + Hex.decode("3333333333333333333333333333333333333333"), + }, + new byte[][]{ + Hex.decode("4444444444444444444444444444444444444444"), + } + } + }; + + byte[] bytes = function.encode(args); + String input = "7e5f5dc50000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000300000000000000000000000011111111111111111111111111111111111111110000000000000000000000002222222222222222222222222222222222222222000000000000000000000000333333333333333333333333333333333333333300000000000000000000000000000000000000000000000000000000000000010000000000000000000000004444444444444444444444444444444444444444"; + System.out.println(Hex.toHexString(bytes)); + + Assert.assertEquals(input, Hex.toHexString(bytes)); + Object[] decode = function.decode(bytes); + Assert.assertArrayEquals(args, decode); + } + + @Test + public void staticArrayWithDynamicElementsSolidity() { + String contract = + "pragma solidity ^0.4.3;\n" + + "pragma experimental ABIEncoderV2;\n"+ + "contract A {" + + " function call(uint[][2] arr) public returns (uint) {" + + " if (arr.length != 2) return 2;" + + " if (arr[0].length != 3) return 3;" + + " if (arr[1].length != 2) return 4;" + + " if (arr[0][0] != 10) return 5;" + + " if (arr[0][1] != 11) return 6;" + + " if (arr[0][2] != 12) return 7;" + + " if (arr[1][0] != 13) return 8;" + + " if (arr[1][1] != 14) return 9;" + + " return 1;" + + " }" + + " function ret() public returns (uint[][2]) {" + + " uint[][2] a1;" + + " a1[0] = [uint(3),uint(4),uint(5)];" + + " a1[1] = [uint(6),uint(7)];" + + " return a1;" + + " }" + + "}"; + + StandaloneBlockchain bc = new StandaloneBlockchain().withAutoblock(true); + SolidityContract a = bc.submitNewContract(contract); + SolidityCallResult res = a.callFunction("call", + (Object) new BigInteger[][]{ + new BigInteger[]{ + BigInteger.valueOf(10), + BigInteger.valueOf(11), + BigInteger.valueOf(12), + }, + new BigInteger[]{ + BigInteger.valueOf(13), + BigInteger.valueOf(14), + }, + } + ); + Assert.assertTrue(res.isSuccessful()); + Assert.assertEquals(BigInteger.valueOf(1), res.getReturnValue()); + + Object[] ret = a.callConstFunction("ret"); + Assert.assertArrayEquals( + new Object[] { + new BigInteger[][]{ + new BigInteger[]{ + BigInteger.valueOf(3), + BigInteger.valueOf(4), + BigInteger.valueOf(5), + }, + new BigInteger[]{ + BigInteger.valueOf(6), + BigInteger.valueOf(7), + }, + }}, ret); + } + }