chainlib-eth

Ethereum implementation of the chainlib interface
Log | Files | Refs | README | LICENSE

commit 5b97b9e5f1968c10e81f11cbb7df70c01620c3a3
parent f9a715e968a5cbb047dbeea8fbc5a68c239c1997
Author: lash <dev@holbrook.no>
Date:   Wed, 29 Mar 2023 10:17:24 +0100

Implement tuple abi encoding support

Diffstat:
MCHANGELOG | 4++++
Mchainlib/eth/contract.py | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mrequirements.txt | 2+-
Msetup.cfg | 2+-
Mtests/TestContract.bin | 4++--
Mtests/TestContract.sol | 17+++++++++++++++++
Mtests/test_abi.py | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/test_event.py | 2+-
8 files changed, 203 insertions(+), 39 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG @@ -1,3 +1,7 @@ +- 0.4.18 + * Set a dirty bit on abi encoder when type added before contents generation + * Support for tuple abi encoding (decoding not implemented) + * Rename log decoder signature method to get_signature - 0.4.17 * Add dialect filter to info and block cli tools - 0.4.16 diff --git a/chainlib/eth/contract.py b/chainlib/eth/contract.py @@ -36,12 +36,17 @@ class ABIContractType(enum.Enum): STRING = 'string' BYTES = 'bytes' BOOLEAN = 'bool' + TUPLE = 'tuple' dynamic_contract_types = [ ABIContractType.STRING, ABIContractType.BYTES, ] +pointer_contract_types = [ + ABIContractType.TUPLE, + ] + dynamic_contract_types + class ABIContract: """Base class for Ethereum smart contract encoder @@ -49,6 +54,12 @@ class ABIContract: def __init__(self): self.types = [] self.contents = [] + self.dirty = False + + + def add_type(self, v): + self.types.append(v) + self.dirty = True class ABIMethodEncoder(ABIContract): @@ -79,9 +90,10 @@ class ABIMethodEncoder(ABIContract): :rtype: str :returns: Method signature """ + contents = '(' + ','.join(self.method_contents) + ')' if self.method_name == None: - return '' - return '{}({})'.format(self.method_name, ','.join(self.method_contents)) + return contents + return self.method_name + contents def typ(self, v): @@ -94,8 +106,8 @@ class ABIMethodEncoder(ABIContract): :raises AttributeError: Type set before method name :raises TypeError: Invalid type """ - if self.method_name == None: - raise AttributeError('method name must be set before adding types') + if isinstance(v, ABIContractEncoder): + return self.typ_tuple(v) if not isinstance(v, ABIContractType): raise TypeError('method type not valid; expected {}, got {}'.format(type(ABIContractType).__name__, type(v).__name__)) self.method_contents.append(v.value) @@ -109,10 +121,37 @@ class ABIMethodEncoder(ABIContract): self.__log_method() + def typ_tuple(self, v): + if not isinstance(v, ABIContractEncoder): + raise TypeError('tuple type not valid; expected {}, got {}'.format(type(ABIContractEncoder).__name__, type(v).__name__)) + r = v.get_method() + self.method_contents.append(r) + self.__log_method() + + def __log_method(self): logg.debug('method set to {}'.format(self.get_method())) + def get_signature(self): + """Generate topic signature from set topic. + + :rtype: str + :returns: Topic signature, in hex + """ + if self.method_name == None: + return '' + s = self.get_method() + return keccak256_string_to_hex(s) + + + def get_method_signature(self): + s = self.get_signature() + if s == '': + return s + return s[:8] + + class ABIContractDecoder(ABIContract): """Decode serialized ABI contract input data to corresponding python primitives. @@ -127,7 +166,9 @@ class ABIContractDecoder(ABIContract): """ if not isinstance(v, ABIContractType): raise TypeError('method type not valid; expected {}, got {}'.format(type(ABIContractType).__name__, type(v).__name__)) - self.types.append(v.value) + if v == ABIContractType.TUPLE: + raise NotImplementedError('sorry, tuple decoding not yet implemented') + self.add_type(v.value) self.__log_typ() @@ -286,17 +327,6 @@ class ABIContractLogDecoder(ABIMethodEncoder, ABIContractDecoder): """ self.method(event) - - def get_method_signature(self): - """Generate topic signature from set topic. - - :rtype: str - :returns: Topic signature, in hex - """ - s = self.get_method() - return keccak256_string_to_hex(s) - - def typ(self, v): """Add type to event argument array. @@ -304,7 +334,7 @@ class ABIContractLogDecoder(ABIMethodEncoder, ABIContractDecoder): :type v: chainlib.eth.contract.ABIContractType """ super(ABIContractLogDecoder, self).typ(v) - self.types.append(v.value) + self.add_type(v.value) @@ -319,12 +349,18 @@ class ABIContractLogDecoder(ABIMethodEncoder, ABIContractDecoder): :type data: str :raises ValueError: Topic of input does not match topic set in parser """ - t = self.get_method_signature() + t = self.get_signature() if topics[0] != t: raise ValueError('topic mismatch') for i in range(len(topics) - 1): self.contents.append(topics[i+1]) self.contents += data + + + # Backwards compatibility + def get_method_signature(self): + logg.warning('ABIContractLogDecoder.get_method_signature() is deprecated. Use ABIContractLogDecoder.get_signature() instead') + return self.get_signature() class ABIContractEncoder(ABIMethodEncoder): @@ -343,7 +379,7 @@ class ABIContractEncoder(ABIMethodEncoder): v = int(v) b = v.to_bytes(32, 'big') self.contents.append(b.hex()) - self.types.append(ABIContractType.UINT256) + self.add_type(ABIContractType.UINT256) self.__log_latest(v) @@ -365,7 +401,7 @@ class ABIContractEncoder(ABIMethodEncoder): b = v.to_bytes(int(bitsize / 8), 'big') self.contents.append(b.hex()) typ = getattr(ABIContractType, 'UINT' + str(bitsize)) - self.types.append(typ) + self.add_type(typ) self.__log_latest(v) @@ -395,7 +431,7 @@ class ABIContractEncoder(ABIMethodEncoder): :type v: str """ self.bytes_fixed(32, v, 20) - self.types.append(ABIContractType.ADDRESS) + self.add_type(ABIContractType.ADDRESS) self.__log_latest(v) @@ -406,7 +442,7 @@ class ABIContractEncoder(ABIMethodEncoder): :type v: str """ self.bytes_fixed(32, v) - self.types.append(ABIContractType.BYTES32) + self.add_type(ABIContractType.BYTES32) self.__log_latest(v) @@ -417,10 +453,18 @@ class ABIContractEncoder(ABIMethodEncoder): :type v: str """ self.bytes_fixed(4, v) - self.types.append(ABIContractType.BYTES4) + self.add_type(ABIContractType.BYTES4) self.__log_latest(v) + def tuple(self, v): + if type(v).__name__ != 'ABIContractEncoder': + raise ValueError('Type for tuple must be ABIContractEncoder') + r = v.get_contents() + self.bytes_fixed(int(len(r) / 2), r) + self.add_type(ABIContractType.TUPLE) + self.__log_latest(v) + def string(self, v): """Encode value to string and add to input value vector. @@ -445,12 +489,11 @@ class ABIContractEncoder(ABIMethodEncoder): if pad: contents += padlen * b'\x00' self.bytes_fixed(len(contents), contents) - self.types.append(ABIContractType.STRING) + self.add_type(ABIContractType.STRING) self.__log_latest(v) return contents - def bytes_fixed(self, mx, v, exact=0, enforce_word=False): """Add arbirary length byte data to value vector. @@ -490,14 +533,6 @@ class ABIContractEncoder(ABIMethodEncoder): raise ValueError('invalid input {}'.format(typ)) self.contents.append(v.ljust(64, '0')) - - def get_method_signature(self): - """Return abi encoded signature of currently set method. - """ - s = self.get_method() - if s == '': - return s - return keccak256_string_to_hex(s)[:8] def get_contents(self): @@ -511,7 +546,7 @@ class ABIContractEncoder(ABIMethodEncoder): l = len(self.types) pointer_cursor = 32 * l for i in range(l): - if self.types[i] in dynamic_contract_types: + if self.types[i] in pointer_contract_types: content_length = len(self.contents[i]) pointer_contents += self.contents[i] direct_contents += pointer_cursor.to_bytes(32, 'big').hex() @@ -524,6 +559,7 @@ class ABIContractEncoder(ABIMethodEncoder): if l > 64: l = 64 logg.debug('code word {} {}'.format(int(i / 64), s[i:i+64])) + self.dirty = False return s diff --git a/requirements.txt b/requirements.txt @@ -1,4 +1,4 @@ -funga-eth~=0.6.1 +funga-eth~=0.6.6 pysha3==1.0.2 hexathon~=0.1.7 websocket-client==0.57.0 diff --git a/setup.cfg b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = chainlib-eth -version = 0.4.17 +version = 0.4.18 description = Ethereum implementation of the chainlib interface author = Louis Holbrook author_email = dev@holbrook.no diff --git a/tests/TestContract.bin b/tests/TestContract.bin @@ -1 +1 @@ -608060405234801561001057600080fd5b50610260806100206000396000f3fe608060405234801561001057600080fd5b5060043610610048576000357c01000000000000000000000000000000000000000000000000000000009004806333aa24121461004d575b600080fd5b61006760048036038101906100629190610122565b61007d565b604051610074919061018b565b60405180910390f35b6000827fa8ed44a382304c8db2c9059e4de342080401fb8e9a71986396d595c869fa3792836040516100af91906101a6565b60405180910390a27f33c4ea6ccc21f1a14e9de7326edcc52c2b8a302ac875ae6b8fee2560eadd75ad836040516100e691906101c1565b60405180910390a16001905092915050565b600081359050610107816101fc565b92915050565b60008135905061011c81610213565b92915050565b6000806040838503121561013557600080fd5b60006101438582860161010d565b9250506020610154858286016100f8565b9150509250929050565b610167816101dc565b82525050565b610176816101e8565b82525050565b610185816101f2565b82525050565b60006020820190506101a0600083018461015e565b92915050565b60006020820190506101bb600083018461016d565b92915050565b60006020820190506101d6600083018461017c565b92915050565b60008115159050919050565b6000819050919050565b6000819050919050565b610205816101e8565b811461021057600080fd5b50565b61021c816101f2565b811461022757600080fd5b5056fea26469706673582212205c428504ca5a53a9250d76b287bec8ee4b13d5c7cb386a0f67cbf77ecb96a9ea64736f6c63430008040033 -\ No newline at end of file +608060405234801561001057600080fd5b50610695806100206000396000f3fe608060405234801561001057600080fd5b5060043610610053576000357c01000000000000000000000000000000000000000000000000000000009004806333aa2412146100585780638cd9051d14610088575b600080fd5b610072600480360381019061006d91906101c6565b6100b8565b60405161007f9190610221565b60405180910390f35b6100a2600480360381019061009d919061050e565b610133565b6040516100af91906105e9565b60405180910390f35b6000827fa8ed44a382304c8db2c9059e4de342080401fb8e9a71986396d595c869fa3792836040516100ea919061061a565b60405180910390a27f33c4ea6ccc21f1a14e9de7326edcc52c2b8a302ac875ae6b8fee2560eadd75ad836040516101219190610644565b60405180910390a16001905092915050565b6060826000015160000151905092915050565b6000604051905090565b600080fd5b600080fd5b6000819050919050565b61016d8161015a565b811461017857600080fd5b50565b60008135905061018a81610164565b92915050565b6000819050919050565b6101a381610190565b81146101ae57600080fd5b50565b6000813590506101c08161019a565b92915050565b600080604083850312156101dd576101dc610150565b5b60006101eb8582860161017b565b92505060206101fc858286016101b1565b9150509250929050565b60008115159050919050565b61021b81610206565b82525050565b60006020820190506102366000830184610212565b92915050565b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b61028a82610241565b810181811067ffffffffffffffff821117156102a9576102a8610252565b5b80604052505050565b60006102bc610146565b90506102c88282610281565b919050565b600080fd5b600080fd5b600080fd5b600067ffffffffffffffff8211156102f7576102f6610252565b5b61030082610241565b9050602081019050919050565b82818337600083830152505050565b600061032f61032a846102dc565b6102b2565b90508281526020810184848401111561034b5761034a6102d7565b5b61035684828561030d565b509392505050565b600082601f830112610373576103726102d2565b5b813561038384826020860161031c565b91505092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006103b78261038c565b9050919050565b6103c7816103ac565b81146103d257600080fd5b50565b6000813590506103e4816103be565b92915050565b600060408284031215610400576103ff61023c565b5b61040a60406102b2565b9050600082013567ffffffffffffffff81111561042a576104296102cd565b5b6104368482850161035e565b600083015250602061044a848285016103d5565b60208301525092915050565b60006060828403121561046c5761046b61023c565b5b61047660606102b2565b9050600082013567ffffffffffffffff811115610496576104956102cd565b5b6104a2848285016103ea565b600083015250602082013567ffffffffffffffff8111156104c6576104c56102cd565b5b6104d2848285016103ea565b602083015250604082013567ffffffffffffffff8111156104f6576104f56102cd565b5b6105028482850161035e565b60408301525092915050565b6000806040838503121561052557610524610150565b5b600083013567ffffffffffffffff81111561054357610542610155565b5b61054f85828601610456565b92505060206105608582860161017b565b9150509250929050565b600081519050919050565b600082825260208201905092915050565b60005b838110156105a4578082015181840152602081019050610589565b60008484015250505050565b60006105bb8261056a565b6105c58185610575565b93506105d5818560208601610586565b6105de81610241565b840191505092915050565b6000602082019050818103600083015261060381846105b0565b905092915050565b61061481610190565b82525050565b600060208201905061062f600083018461060b565b92915050565b61063e8161015a565b82525050565b60006020820190506106596000830184610635565b9291505056fea2646970667358221220297897985f0dab43c3088bbd085bb9a8fe99c5c13dbe0eee1bd95a0d303899be64736f6c63430008130033 +\ No newline at end of file diff --git a/tests/TestContract.sol b/tests/TestContract.sol @@ -5,9 +5,26 @@ contract TestEventContract { event TestEventOne(uint256 indexed _foo, bytes32 _bar); event TestEventTwo(uint256 _foo); + struct Person { + string uid; + address wallet; + } + + struct Mail { + Person from; + Person to; + string contents; + } + function foo(uint256 _foo, bytes32 _bar) public returns (bool) { emit TestEventOne(_foo, _bar); emit TestEventTwo(_foo); return true; } + + function foo(Mail memory _mail, uint256 _nonce) public pure returns(string memory) { + _mail; + _nonce; + return _mail.from.uid; + } } diff --git a/tests/test_abi.py b/tests/test_abi.py @@ -1,5 +1,7 @@ # standard imports import unittest +import logging +import os # local imports from chainlib.eth.contract import ( @@ -7,6 +9,9 @@ from chainlib.eth.contract import ( ABIContractType, ) +logging.basicConfig(level=logging.DEBUG) +logg = logging.getLogger() + class TestContract(unittest.TestCase): @@ -66,5 +71,107 @@ class TestContract(unittest.TestCase): self.assertEqual(e.get(), 'f31a6969000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000086465616462656566000000000000000000000000000000000000000000000000') + def test_abi_tuple(self): + e = ABIContractEncoder() + e.typ(ABIContractType.STRING) + e.typ(ABIContractType.BYTES32) + e.string('deadbeef') + e.bytes32('666f6f') + + ee = ABIContractEncoder() + ee.method('foo') + ee.typ(e) + ee.typ(ABIContractType.UINT256) + ee.tuple(e) + ee.uint256(42) + + self.assertEqual(ee.get_method(), 'foo((string,bytes32),uint256)') + r = ee.get() + self.assertEqual(r[:8], '7bab4ebd') + r = r[8:] + valid = [ + '0000000000000000000000000000000000000000000000000000000000000040', + '000000000000000000000000000000000000000000000000000000000000002a', + '0000000000000000000000000000000000000000000000000000000000000040', + '0000000000000000000000000000000000000000000000000000000000666f6f', + '0000000000000000000000000000000000000000000000000000000000000008', + '6465616462656566000000000000000000000000000000000000000000000000', + ] + + i = 0 + c = 0 + while c < len(r): + v = r[c:c+64] + logg.debug('check position {} {}'.format((i*32).to_bytes(2, byteorder='big').hex(), v)) + self.assertEqual(v, valid[i]) + c += 64 + i += 1 + + + def test_abi_tuple_embedded(self): + a = os.urandom(20) + ea = ABIContractEncoder() + ea.typ(ABIContractType.STRING) + ea.typ(ABIContractType.ADDRESS) + ea.string('foo@bar.com') + ea.address(a.hex()) + + b = os.urandom(20) + eb = ABIContractEncoder() + eb.typ(ABIContractType.STRING) + eb.typ(ABIContractType.ADDRESS) + eb.string('baz@xyzzy.org') + eb.address(b.hex()) + + ee = ABIContractEncoder() + ee.typ(ea) + ee.typ(eb) + ee.typ(ABIContractType.STRING) + ee.tuple(ea) + ee.tuple(eb) + ee.string('barbarbar') + + e = ABIContractEncoder() + e.method('foo') + e.typ(ee) + e.typ(ABIContractType.UINT256) + e.tuple(ee) + e.uint256(42) + + self.assertEqual(e.get_method(), 'foo(((string,address),(string,address),string),uint256)') + r = e.get() + print(r) + self.assertEqual(r[:8], '8cd9051d') + r = r[8:] + + valid = [ + '0000000000000000000000000000000000000000000000000000000000000040', + '000000000000000000000000000000000000000000000000000000000000002a', + '0000000000000000000000000000000000000000000000000000000000000060', + '00000000000000000000000000000000000000000000000000000000000000e0', + '0000000000000000000000000000000000000000000000000000000000000160', + '0000000000000000000000000000000000000000000000000000000000000040', + '000000000000000000000000' + a.hex(), + '000000000000000000000000000000000000000000000000000000000000000b', + '666f6f406261722e636f6d000000000000000000000000000000000000000000', + '0000000000000000000000000000000000000000000000000000000000000040', + '000000000000000000000000' + b.hex(), + '000000000000000000000000000000000000000000000000000000000000000d', + '62617a4078797a7a792e6f726700000000000000000000000000000000000000', + '0000000000000000000000000000000000000000000000000000000000000009', + '6261726261726261720000000000000000000000000000000000000000000000', + ] + + i = 0 + c = 0 + while c < len(r): + v = r[c:c+64] + logg.debug('check position {} {}'.format((i*32).to_bytes(2, byteorder='big').hex(), v)) + self.assertEqual(v, valid[i]) + c += 64 + i += 1 + + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_event.py b/tests/test_event.py @@ -19,7 +19,7 @@ class TestContractLog(EthTesterCase): dec.topic('TestEventOne') dec.typ(ABIContractType.UINT256) dec.typ(ABIContractType.BYTES32) - s = dec.get_method_signature() + s = dec.get_signature() n = 42 topics = [ s,