From 41456fd1ba6e8d2d9168ff8cda4a5ea93eeaed73 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Tue, 11 Feb 2025 20:50:50 +0000 Subject: [PATCH 01/20] feat: rlp encoding --- crypto/identity/private_key.py | 37 ++++-- .../types/abstract_transaction.py | 26 +++-- crypto/utils/rlp_decoder.py | 109 ++++++++++++++++++ crypto/utils/rlp_encoder.py | 91 +++++++++++++++ crypto/utils/transaction_hasher.py | 94 --------------- crypto/utils/transaction_utils.py | 79 +++++++++++++ 6 files changed, 326 insertions(+), 110 deletions(-) create mode 100644 crypto/utils/rlp_decoder.py create mode 100644 crypto/utils/rlp_encoder.py delete mode 100644 crypto/utils/transaction_hasher.py create mode 100644 crypto/utils/transaction_utils.py diff --git a/crypto/identity/private_key.py b/crypto/identity/private_key.py index d15d9ae1..369d0504 100644 --- a/crypto/identity/private_key.py +++ b/crypto/identity/private_key.py @@ -1,12 +1,18 @@ from binascii import hexlify from hashlib import sha256 -from coincurve import PrivateKey as PvtKey +from btclib.to_prv_key import PrvKey, int_from_prv_key +from btclib.to_pub_key import pub_keyinfo_from_key +from btclib.ecc import bms class PrivateKey(object): + private_key: PrvKey + private_key_raw: str + def __init__(self, private_key: str): - self.private_key = PvtKey.from_hex(private_key) - self.public_key = hexlify(self.private_key.public_key.format()).decode() + self.private_key_raw = private_key + self.private_key = int_from_prv_key(private_key) + self.public_key = hexlify(pub_keyinfo_from_key(self.private_key)[0]).decode() def sign(self, message: bytes) -> bytes: """Sign a message with this private key object @@ -17,9 +23,26 @@ def sign(self, message: bytes) -> bytes: Returns: bytes: signature of the signed message """ - signature = self.private_key.sign(message) - - return hexlify(signature).decode() + signature = bms.sign(message, self.private_key) + + return signature.serialize() + + def sign_compact(self, message: bytes) -> bms.Sig: + """Sign a message with this private key object + + Args: + message (bytes): bytes data you want to sign + + Returns: + bytes: signature of the signed message + """ + return bms.sign(message, self.private_key) + # wif, address = bms.gen_keys(self.private_key, compressed=True) + + # # print('WIF', wif) + # # print('address', address) + + # return bms.sign(message, wif, address) def to_hex(self): """Returns a private key in hex format @@ -27,7 +50,7 @@ def to_hex(self): Returns: str: private key in hex format """ - return self.private_key.to_hex() + return hexlify(self.private_key.to_bytes(32, 'big')).decode() @classmethod def from_passphrase(cls, passphrase: str): diff --git a/crypto/transactions/types/abstract_transaction.py b/crypto/transactions/types/abstract_transaction.py index d7506152..89c6c4d9 100644 --- a/crypto/transactions/types/abstract_transaction.py +++ b/crypto/transactions/types/abstract_transaction.py @@ -4,7 +4,7 @@ from crypto.configuration.network import get_network from crypto.identity.address import address_from_public_key from crypto.identity.private_key import PrivateKey -from crypto.utils.transaction_hasher import TransactionHasher +from crypto.utils.transaction_utils import TransactionUtils from coincurve import PublicKey from crypto.utils.abi_decoder import AbiDecoder @@ -19,7 +19,7 @@ def get_payload(self) -> str: def decode_payload(self, data: dict) -> Optional[dict]: if 'data' not in data or data['data'] == '': return None - + payload = data['data'] decoder = AbiDecoder() @@ -31,17 +31,24 @@ def refresh_payload_data(self): self.data['data'] = self.get_payload().lstrip('0x') def get_id(self) -> str: - return self.hash(skip_signature=False).hex() + return self.hash(skip_signature=False) def get_bytes(self, skip_signature: bool = False) -> bytes: from crypto.transactions.serializer import Serializer + return Serializer.get_bytes(self, skip_signature) def sign(self, private_key: PrivateKey): - hash_ = self.hash(skip_signature=True) - signature_with_recid = private_key.private_key.sign_recoverable(hash_, hasher=None) - signature_hex = signature_with_recid.hex() - self.data['signature'] = signature_hex + transaction_hash = self.hash(skip_signature=True) + + message = bytes.fromhex(transaction_hash) + + transaction_signature = private_key.sign_compact(message) + + self.data['v'] = transaction_signature.rf + self.data['r'] = transaction_signature.dsa_sig.r.to_bytes(32, 'big').hex() + self.data['s'] = transaction_signature.dsa_sig.s.to_bytes(32, 'big').hex() + return self def get_public_key(self, compact_signature, hash_): @@ -84,11 +91,12 @@ def to_dict(self) -> dict: def to_json(self) -> str: return json.dumps(self.to_dict()) - def hash(self, skip_signature: bool) -> bytes: + def hash(self, skip_signature: bool) -> str: hash_data = self.data.copy() if skip_signature: hash_data['signature'] = None - return TransactionHasher.to_hash(hash_data, skip_signature) + + return TransactionUtils.to_hash(hash_data, skip_signature) def get_signature(self): signature_hex = self.data.get('signature') diff --git a/crypto/utils/rlp_decoder.py b/crypto/utils/rlp_decoder.py new file mode 100644 index 00000000..a5f62a0b --- /dev/null +++ b/crypto/utils/rlp_decoder.py @@ -0,0 +1,109 @@ +import re + +class RlpDecoder: + @classmethod + def decode(cls, data: str): + bytes_data = cls.get_bytes(data, 'data') + decoded = cls._decode(bytes_data, 0) + + if decoded['consumed'] != len(bytes_data): + raise ValueError('unexpected junk after RLP payload') + + return decoded['result'] + + @staticmethod + def get_bytes(value: str, name: str = 'value') -> list: + if re.match(r'^0x(?:[0-9a-fA-F]{2})*$', value): + hex_value = value[2:] + length = len(hex_value) // 2 + bytes_data = [int(hex_value[i * 2:i * 2 + 2], 16) for i in range(length)] + return bytes_data + + raise ValueError(f'Invalid BytesLike value for "{name}": {value}') + + @staticmethod + def hexlify(data) -> str: + return '0x' + ''.join(f'{byte:02x}' for byte in data) + + @staticmethod + def hexlify_byte(value) -> str: + return f'0x{value & 0xff:02x}' + + @staticmethod + def unarrayify_integer(data, offset, length) -> int: + result = 0 + for i in range(length): + result = (result << 8) + data[offset + i] + return result + + @classmethod + def _decode_children(cls, data, offset, child_offset, length) -> dict: + result = [] + end = offset + 1 + length + + while child_offset < end: + decoded = cls._decode(data, child_offset) + result.append(decoded['result']) + child_offset += decoded['consumed'] + + if child_offset > end: + raise ValueError('child data too short or malformed') + + return { + 'consumed': 1 + length, + 'result': result, + } + + @classmethod + def _decode(cls, data, offset) -> dict[str, int | str]: + cls.check_offset(offset, data) + + prefix = data[offset] + + if prefix >= 0xf8: + length_length = prefix - 0xf7 + cls.check_offset(offset + length_length, data) + + length = cls.unarrayify_integer(data, offset + 1, length_length) + cls.check_offset(offset + 1 + length_length + length - 1, data) + + return cls._decode_children(data, offset, offset + 1 + length_length, length_length + length) + elif prefix >= 0xc0: + length = prefix - 0xc0 + if length > 0: + cls.check_offset(offset + 1 + length - 1, data) + + return cls._decode_children(data, offset, offset + 1, length) + elif prefix >= 0xb8: + length_length = prefix - 0xb7 + cls.check_offset(offset + length_length, data) + + length = cls.unarrayify_integer(data, offset + 1, length_length) + if length > 0: + cls.check_offset(offset + 1 + length_length + length - 1, data) + slice_data = data[offset + 1 + length_length:offset + 1 + length_length + length] + + return { + 'consumed': 1 + length_length + length, + 'result': cls.hexlify(slice_data), + } + elif prefix >= 0x80: + length = prefix - 0x80 + if length > 0: + cls.check_offset(offset + 1 + length - 1, data) + slice_data = data[offset + 1:offset + 1 + length] + + return { + 'consumed': 1 + length, + 'result': cls.hexlify(slice_data), + } + + return { + 'consumed': 1, + 'result': cls.hexlify_byte(prefix), + } + + @staticmethod + def check_offset(offset, data) -> None: + if offset > len(data): + raise ValueError('data short segment or out of range') diff --git a/crypto/utils/rlp_encoder.py b/crypto/utils/rlp_encoder.py new file mode 100644 index 00000000..21ed885d --- /dev/null +++ b/crypto/utils/rlp_encoder.py @@ -0,0 +1,91 @@ +from binascii import hexlify, unhexlify + + +class RlpEncoder: + @classmethod + def encode(cls, obj) -> str: + encoded = cls._encode(obj) + hex_str = '' + nibbles = '0123456789abcdef' + + for byte in encoded: + hex_str += nibbles[byte >> 4] + hex_str += nibbles[byte & 0x0f] + + return hex_str + + @classmethod + def _encode(cls, obj) -> list: + if isinstance(obj, list): + payload = [] + for child in obj: + payload.extend(cls._encode(child)) + + payload_length = len(payload) + if payload_length <= 55: + payload.insert(0, 0xc0 + payload_length) + + return payload + + length = cls.arrayify_integer(payload_length) + length.insert(0, 0xf7 + (len(length))) + + return length + payload + + data = cls.get_bytes(obj) + data_length = len(data) + if data_length == 1 and data[0] <= 0x7f: + return data + + if data_length <= 55: + data.insert(0, 0x80 + data_length) + + return data + + length = cls.arrayify_integer(len(data)) + length.insert(0, 0xb7 + len(length)) + + return length + data + + @staticmethod + def arrayify_integer(value) -> list: + result = [] + while value > 0: + result.insert(0, value & 0xff) + value >>= 8 + + return result + + @staticmethod + def get_bytes(value) -> list: + if isinstance(value, str) or isinstance(value, bytes): + if isinstance(value, str): + value = value.encode() + + if value.startswith(b'0x'): + hex_str = value[2:] + if hex_str == '': + return [] + + if len(hex_str) % 2 != 0: + hex_str = b'0' + hex_str + + return [int(hex_str[i:i+2], 16) for i in range(0, len(hex_str), 2)] + + return [char for char in value] + + if isinstance(value, int): + if value == 0: + return [] + + result = [] + while value > 0: + result.insert(0, value & 0xff) + value >>= 8 + + return result + + if isinstance(value, list): + return [v & 0xff for v in value] + + raise ValueError('invalid type', type(value)) diff --git a/crypto/utils/transaction_hasher.py b/crypto/utils/transaction_hasher.py deleted file mode 100644 index 23ba6beb..00000000 --- a/crypto/utils/transaction_hasher.py +++ /dev/null @@ -1,94 +0,0 @@ -import hashlib -from binascii import unhexlify - - -class TransactionHasher: - @staticmethod - def to_hash(transaction: dict, skip_signature: bool = False) -> bytes: - # Process recipientAddress - hex_address = transaction.get('recipientAddress', '').lstrip('0x') - # Pad with leading zero if necessary - if len(hex_address) % 2 != 0: - hex_address = '0' + hex_address - recipient_address = bytes.fromhex(hex_address.lower()) - - # Build the fields array - fields = [ - TransactionHasher.to_be_array(int(transaction['network'])), - TransactionHasher.to_be_array(int(transaction['nonce'])), - TransactionHasher.to_be_array(int(transaction['gasPrice'])), # maxPriorityFeePerGas - TransactionHasher.to_be_array(int(transaction['gasPrice'])), # maxFeePerGas - TransactionHasher.to_be_array(int(transaction['gasLimit'])), - recipient_address, - TransactionHasher.to_be_array(int(transaction['value'])), - bytes.fromhex(transaction.get('data', '').lstrip('0x')) if transaction.get('data') else b'', - [], # Access list is unused - ] - - if not skip_signature and 'signature' in transaction: - signature_buffer = bytes.fromhex(transaction['signature']) - r = signature_buffer[0:32] - s = signature_buffer[32:64] - v = signature_buffer[64] - fields.extend([ - TransactionHasher.to_be_array(v), - r, - s, - ]) - - eip1559_prefix = b'\x02' # Marker for Type 2 (EIP-1559) transaction - - encoded = TransactionHasher.encode_rlp(fields) - hash_input = eip1559_prefix + encoded - - # Use SHA256 for hashing - return hashlib.sha256(hash_input).digest() - - @staticmethod - def to_be_array(value): - if isinstance(value, int): - if value == 0: - return b'' # Empty bytes represent zero - else: - return value.to_bytes((value.bit_length() + 7) // 8, byteorder='big') - elif isinstance(value, bytes): - return value - else: - raise TypeError("Unsupported type for to_be_array") - - @staticmethod - def encode_length(length): - if length == 0: - return b'' - result = [] - while length > 0: - result.insert(0, length & 0xFF) - length >>= 8 - return bytes(result) - - @staticmethod - def encode_rlp(input_data): - if isinstance(input_data, bytes): - input_len = len(input_data) - if input_len == 1 and input_data[0] <= 0x7f: - return input_data - elif input_len <= 55: - return bytes([0x80 + input_len]) + input_data - else: - len_bytes = TransactionHasher.encode_length(input_len) - return bytes([0xb7 + len(len_bytes)]) + len_bytes + input_data - elif isinstance(input_data, list): - output = b''.join([TransactionHasher.encode_rlp(item) for item in input_data]) - output_len = len(output) - if output_len <= 55: - return bytes([0xc0 + output_len]) + output - else: - len_bytes = TransactionHasher.encode_length(output_len) - return bytes([0xf7 + len(len_bytes)]) + len_bytes + output - elif isinstance(input_data, int): - return TransactionHasher.encode_rlp(TransactionHasher.to_be_array(input_data)) - elif input_data is None: - return TransactionHasher.encode_rlp(b'') - else: - # Handle other types by converting to bytes - return TransactionHasher.encode_rlp(str(input_data).encode('utf-8')) diff --git a/crypto/utils/transaction_utils.py b/crypto/utils/transaction_utils.py new file mode 100644 index 00000000..899fcd4e --- /dev/null +++ b/crypto/utils/transaction_utils.py @@ -0,0 +1,79 @@ +from binascii import hexlify, unhexlify +import hashlib + +from crypto.utils.rlp_encoder import RlpEncoder + +class TransactionUtils: + EIP1559_PREFIX = '02' + + @classmethod + def to_buffer(cls, transaction: dict, skip_signature: bool = False) -> bytes: + # Process recipientAddress + hex_address = transaction.get('recipientAddress', '').lstrip('0x') + + # Pad with leading zero if necessary + if len(hex_address) % 2 != 0: + hex_address = '0' + hex_address + + recipient_address = bytes.fromhex(hex_address.lower()) + + # Build the fields array + fields = [ + cls.to_be_array(int(transaction['network'])), + cls.to_be_array(int(transaction.get('nonce', 0))), + cls.to_be_array(0), + cls.to_be_array(int(transaction['gasPrice'])), + cls.to_be_array(int(transaction['gasLimit'])), + recipient_address, + cls.to_be_array(int(transaction.get('value', 0))), + bytes.fromhex(transaction.get('data', '').lstrip('0x')) if transaction.get('data') else b'', + [], + ] + + if not skip_signature and 'signature' in transaction: + signature_buffer = bytes.fromhex(transaction['signature']) + r = signature_buffer[0:32] + s = signature_buffer[32:64] + v = signature_buffer[64] + fields.extend([ + cls.to_be_array(v), + r, + s, + ]) + + # print('FIELDS', fields, len(fields)) + # print('FIELDS', [f.hex() for f in fields]) + + # FIELDS [b'1e', b'01', b'', b'05', b'5208', b'6f0182a0cc707b055322ccf6d4cb6a5aff1aeb22', b'05f5e100', b''] + + encoded = RlpEncoder.encode(fields) + + # print('ENCODED', encoded) + # \xf8C\x821e\x8201\x80\x8205\x845208\xa86f0182a0cc707b055322ccf6d4cb6a5aff1aeb22\x8805f5e100\x80\xc0 + # print('ACTUAL 0xe31e018005825208946f0182a0cc707b055322ccf6d4cb6a5aff1aeb228405f5e10080c0') + + hash_input = cls.EIP1559_PREFIX + encoded + + print('HASH_INPUT', hash_input) + print('EXPECTED 02e31e018005825208946f0182a0cc707b055322ccf6d4cb6a5aff1aeb228405f5e10080c0') + print('MATCH', hash_input == '02e31e018005825208946f0182a0cc707b055322ccf6d4cb6a5aff1aeb228405f5e10080c0') + + # Use SHA256 for hashing + return hash_input.encode() + + @classmethod + def to_hash(cls, transaction: dict, skip_signature: bool = False) -> str: + return hashlib.sha256(unhexlify(cls.to_buffer(transaction, skip_signature))).hexdigest() + + @staticmethod + def to_be_array(value): + if isinstance(value, int): + if value == 0: + return b'' + else: + return value.to_bytes((value.bit_length() + 7) // 8, byteorder='big') + + if isinstance(value, bytes): + return value + + raise TypeError("Unsupported type for to_be_array") From b7cb1f239f5f1a03cd20c9a1c2553ba4a003ae25 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Tue, 11 Feb 2025 20:51:03 +0000 Subject: [PATCH 02/20] test --- tests/fixtures/evm-sign.json | 14 ++++---- tests/fixtures/multipayment-empty.json | 18 ++++++++++ tests/fixtures/multipayment-single.json | 18 ++++++++++ tests/fixtures/multipayment.json | 19 +++++++++++ tests/fixtures/transfer-0.json | 18 ++++++++++ tests/fixtures/transfer-large-amount.json | 18 ++++++++++ tests/fixtures/transfer.json | 14 ++++---- tests/fixtures/unvote.json | 20 ++++++----- tests/fixtures/username-registration.json | 18 ++++++++++ tests/fixtures/username-resignation.json | 17 ++++++++++ tests/fixtures/validator-registration.json | 20 ++++++----- tests/fixtures/validator-resignation.json | 20 ++++++----- tests/fixtures/vote.json | 20 ++++++----- tests/identity/conftest.py | 13 ++++++++ tests/identity/test_private_key.py | 33 +++++++++++++++++++ .../builder/test_transfer_builder.py | 28 +++++++++++----- 16 files changed, 251 insertions(+), 57 deletions(-) create mode 100644 tests/fixtures/multipayment-empty.json create mode 100644 tests/fixtures/multipayment-single.json create mode 100644 tests/fixtures/multipayment.json create mode 100644 tests/fixtures/transfer-0.json create mode 100644 tests/fixtures/transfer-large-amount.json create mode 100644 tests/fixtures/username-registration.json create mode 100644 tests/fixtures/username-resignation.json diff --git a/tests/fixtures/evm-sign.json b/tests/fixtures/evm-sign.json index 600ffa2d..a171db39 100644 --- a/tests/fixtures/evm-sign.json +++ b/tests/fixtures/evm-sign.json @@ -1,16 +1,18 @@ { "data": { "network": 30, - "nonce": "13", + "nonce": "1", "gasPrice": 5, "gasLimit": 1000000, "value": "0", - "recipientAddress": "0xE536720791A7DaDBeBdBCD8c8546fb0791a11901", - "data": "a9059cbb00000000000000000000000027fa7caffaae77ddb9ab232fdbda56d5e5af2393000000000000000000000000000000000000000000000000016345785d8a0000", - "signature": "ba30f9042519079895c7408b0e92046c3f20680e0a9294e38ab3cfdd19b26cd4036fe2a80644abb922f1ad7cd682811a83c20120a8030df47b244a3bc44f4dbd00", + "recipientAddress": "0xe536720791a7dadbebdbcd8c8546fb0791a11901", + "data": "a9059cbb00000000000000000000000027fa7caffaae77ddb9ab232fdbda56d5e5af23930000000000000000000000000000000000000000000000000000000000000064", "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", "senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", - "id": "3935ff0fe84ea6ac42fc889ed7cda4f97ddd11fd2d1c31e9201f14866acb6edc" + "id": "2cfa9d51e71f014f8054881052e41dc61267587f9dba04c59a31cc881f8ce35b", + "v": 27, + "r": "a56cf78a7203927af0c8216fdbc804182a0788569e460067efec519b6f7b2e55", + "s": "52ce951a20c7406a4a34707557f02e5a2af1451cc4e216645778c4ee0391a4cd" }, - "serialized": "1e0d000000000000000500000040420f00000000000000000000000000000000000000000000000000000000000000000001e536720791a7dadbebdbcd8c8546fb0791a1190144000000a9059cbb00000000000000000000000027fa7caffaae77ddb9ab232fdbda56d5e5af2393000000000000000000000000000000000000000000000000016345785d8a0000ba30f9042519079895c7408b0e92046c3f20680e0a9294e38ab3cfdd19b26cd4036fe2a80644abb922f1ad7cd682811a83c20120a8030df47b244a3bc44f4dbd00" + "serialized": "02f8a81e018005830f424094e536720791a7dadbebdbcd8c8546fb0791a1190180b844a9059cbb00000000000000000000000027fa7caffaae77ddb9ab232fdbda56d5e5af23930000000000000000000000000000000000000000000000000000000000000064c080a0a56cf78a7203927af0c8216fdbc804182a0788569e460067efec519b6f7b2e55a052ce951a20c7406a4a34707557f02e5a2af1451cc4e216645778c4ee0391a4cd" } diff --git a/tests/fixtures/multipayment-empty.json b/tests/fixtures/multipayment-empty.json new file mode 100644 index 00000000..7a235183 --- /dev/null +++ b/tests/fixtures/multipayment-empty.json @@ -0,0 +1,18 @@ +{ + "data": { + "network": 30, + "nonce": "1", + "gasPrice": 5, + "gasLimit": 1000000, + "value": "0", + "recipientAddress": "0x83769BeEB7e5405ef0B7dc3C66C43E3a51A6d27f", + "data": "084ce7080000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", + "senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", + "id": "d816a0f8b51eb618c4497aeddcc9d4596f94fefc39de306282f50596c9e1c071", + "v": 28, + "r": "bfab2a6634cfbf8bc802d0259dea575b7618131e8de9e29ca7df1950e68ce8eb", + "s": "5e7eeb4c2f973a8397f38b68520123798b07b4f1bf251bbaaacafb36e8e05a55" + }, + "serialized": "02f8e81e018005830f42409483769beeb7e5405ef0b7dc3c66c43e3a51a6d27f80b884084ce7080000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c001a0bfab2a6634cfbf8bc802d0259dea575b7618131e8de9e29ca7df1950e68ce8eba05e7eeb4c2f973a8397f38b68520123798b07b4f1bf251bbaaacafb36e8e05a55" +} diff --git a/tests/fixtures/multipayment-single.json b/tests/fixtures/multipayment-single.json new file mode 100644 index 00000000..b2e6098b --- /dev/null +++ b/tests/fixtures/multipayment-single.json @@ -0,0 +1,18 @@ +{ + "data": { + "network": 30, + "nonce": "1", + "gasPrice": 5, + "gasLimit": 1000000, + "value": "100000000", + "recipientAddress": "0x83769BeEB7e5405ef0B7dc3C66C43E3a51A6d27f", + "data": "084ce7080000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008233f6df6449d7655f4643d2e752dc8d2283fad500000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000005f5e100", + "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", + "senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", + "id": "d3775117889fce2397bded88564427dda03c31f2b85a839567f98ae776e3ff4c", + "v": 28, + "r": "c00a8f6a658c1dda434bceb708c74d62d086ec143939c2373010284221d25995", + "s": "43f6e7e5988f37c4336638c1b6a0cf4aa6caeefd5bda6937aa03464bdf44536c" + }, + "serialized": "02f9012c1e018005830f42409483769beeb7e5405ef0b7dc3c66c43e3a51a6d27f8405f5e100b8c4084ce7080000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008233f6df6449d7655f4643d2e752dc8d2283fad500000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000005f5e100c001a0c00a8f6a658c1dda434bceb708c74d62d086ec143939c2373010284221d25995a043f6e7e5988f37c4336638c1b6a0cf4aa6caeefd5bda6937aa03464bdf44536c" +} diff --git a/tests/fixtures/multipayment.json b/tests/fixtures/multipayment.json new file mode 100644 index 00000000..20e781c9 --- /dev/null +++ b/tests/fixtures/multipayment.json @@ -0,0 +1,19 @@ +{ + "data": { + "network": 30, + "nonce": "1", + "gasPrice": 5, + "gasLimit": 1000000, + "valaue": "3000000000000000000", + "value": "300000000", + "recipientAddress": "0x83769BeEB7e5405ef0B7dc3C66C43E3a51A6d27f", + "data": "084ce708000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000008233f6df6449d7655f4643d2e752dc8d2283fad50000000000000000000000008233f6df6449d7655f4643d2e752dc8d2283fad500000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000000000000000000000000000000000000bebc200", + "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", + "senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", + "id": "8ef542e888c41642297bf3e4cb0f58cfe3a07e38adcb74cd2f121b5653c4db9e", + "v": 27, + "r": "39e6a6fb8c41c33d1c56c6b0f8c15f977582f9fcf626dbc84b82526db201c8dc", + "s": "285f0849073a3952511bdc34682668272799eacf3cd7556a9800f46d57e557ea" + }, + "serialized": "02f9016d1e018005830f42409483769beeb7e5405ef0b7dc3c66c43e3a51a6d27f8411e1a300b90104084ce708000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000008233f6df6449d7655f4643d2e752dc8d2283fad50000000000000000000000008233f6df6449d7655f4643d2e752dc8d2283fad500000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000000000000000000000000000000000000bebc200c080a039e6a6fb8c41c33d1c56c6b0f8c15f977582f9fcf626dbc84b82526db201c8dca0285f0849073a3952511bdc34682668272799eacf3cd7556a9800f46d57e557ea" +} diff --git a/tests/fixtures/transfer-0.json b/tests/fixtures/transfer-0.json new file mode 100644 index 00000000..8d69083d --- /dev/null +++ b/tests/fixtures/transfer-0.json @@ -0,0 +1,18 @@ +{ + "data": { + "network": 30, + "nonce": "123", + "gasPrice": 5, + "gasLimit": 21000, + "recipientAddress": "0xb693449adda7efc015d87944eae8b7c37eb1690a", + "value": "0", + "data": "", + "v": 28, + "r": "f7bae93b75f06c39600f123ca6c805a38636db9643eac29d20d21c6bf0d2eca2", + "s": "17265ff237bff3fe0620c53ff1615c4b8ad9136fe3d61dca38562c60dc5fcae2", + "senderPublicKey": "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", + "senderAddress": "0x41aD2bc63A2059f9b623533d87fe99887D794847", + "id": "7d6ad61dabbc9907484080100096c9c7c6364c130ca003f9dd730901c112437f" + }, + "serialized": "02f8621e7b800582520894b693449adda7efc015d87944eae8b7c37eb1690a8080c001a0f7bae93b75f06c39600f123ca6c805a38636db9643eac29d20d21c6bf0d2eca2a017265ff237bff3fe0620c53ff1615c4b8ad9136fe3d61dca38562c60dc5fcae2" +} diff --git a/tests/fixtures/transfer-large-amount.json b/tests/fixtures/transfer-large-amount.json new file mode 100644 index 00000000..0fcee465 --- /dev/null +++ b/tests/fixtures/transfer-large-amount.json @@ -0,0 +1,18 @@ +{ + "data": { + "network": 30, + "nonce": "17", + "gasPrice": 5, + "gasLimit": 21000, + "recipientAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", + "value": "10000000000000000000", + "data": "", + "v": 27, + "r": "c741f8ccf811e7080216b71e0cbcad1138a4f187b08c749c5e8780568f1ff4bc", + "s": "530341bef473fa92db1fe96922758ed010ba8eb80f2e527c9441771fa4c5368b", + "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", + "senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", + "id": "fb9e521867f484cae4b59585d55ac55a99a8c6adfa0cf8b8b50d242115cbb40d" + }, + "serialized": "02f86a1e118005825208946f0182a0cc707b055322ccf6d4cb6a5aff1aeb22888ac7230489e8000080c080a0c741f8ccf811e7080216b71e0cbcad1138a4f187b08c749c5e8780568f1ff4bca0530341bef473fa92db1fe96922758ed010ba8eb80f2e527c9441771fa4c5368b" +} diff --git a/tests/fixtures/transfer.json b/tests/fixtures/transfer.json index 4c577082..24a2b1aa 100644 --- a/tests/fixtures/transfer.json +++ b/tests/fixtures/transfer.json @@ -1,16 +1,18 @@ { "data": { "network": 30, - "nonce": "12", + "nonce": "1", "gasPrice": 5, "gasLimit": 21000, - "value": "10000000000000000000", - "recipientAddress": "0x07Ac3E438719be72a9e2591bB6015F10E8Af2468", + "recipientAddress": "0x6f0182a0cc707b055322ccf6d4cb6a5aff1aeb22", + "value": "100000000", "data": "", - "signature": "b3bc84c8caf1b75c18a78dde87df9f555161003d341eafad659ab672501185e413a26284c3c95056809c7d440c4ffab26179c538864c4d14534ebd5a961852bf01", + "v": 27, + "r": "0567c4def813a66e03fa1cd499a27c6922698a67e25e0b38458d8f4bb0e581fc", + "s": "25fcdf9d110b82bde15bae4a49118deb83cfc3ec1656c2a29286d7836d328abe", "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", "senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", - "id": "b5d7b17d30da123d9eebc8bb6012c1a4e950e1dad2b080404bb052c30b8a8b2e" + "id": "0b8a05781cc5ea130573f33330a739f9a17f7d889bcfab519b09ee5ccc7f359d" }, - "serialized": "1e0c0000000000000005000000085200000000000000000000000000000000000000000000000000008ac7230489e800000107ac3e438719be72a9e2591bb6015f10e8af246800000000b3bc84c8caf1b75c18a78dde87df9f555161003d341eafad659ab672501185e413a26284c3c95056809c7d440c4ffab26179c538864c4d14534ebd5a961852bf01" + "serialized": "02f8661e018005825208946f0182a0cc707b055322ccf6d4cb6a5aff1aeb228405f5e10080c080a00567c4def813a66e03fa1cd499a27c6922698a67e25e0b38458d8f4bb0e581fca025fcdf9d110b82bde15bae4a49118deb83cfc3ec1656c2a29286d7836d328abe" } diff --git a/tests/fixtures/unvote.json b/tests/fixtures/unvote.json index d5e3a7cf..5dccb90f 100644 --- a/tests/fixtures/unvote.json +++ b/tests/fixtures/unvote.json @@ -1,16 +1,18 @@ { "data": { - "network": 30, - "nonce": "13", "gasPrice": 5, - "gasLimit": 200000, - "value": "0", - "recipientAddress": "0x522B3294E6d06aA25Ad0f1B8891242E335D3B459", - "data": "3174b689", - "signature": "d7534ec92c06a8547d0f2b3d3259dff5b0b17f8673d68dff9af023009c9c450e24205cb5f4fd6165d71c8b3ba3e9f741d1853110d44bd1e798e87f1a5d6a89c501", + "network": 30, + "id": "4f43cc9b97433bfc86f3aaa55dbedfad1f15c76b527ec68f7a80fc15105d2e43", + "gasLimit": 1000000, + "nonce": "1", "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", "senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", - "id": "92ca281a6699a4eb08e8e5c4a644c216026f6c6d3560611c50cab54d1300b690" + "recipientAddress": "0x535b3d7a252fa034ed71f0c53ec0c6f784cb64e1", + "value": "0", + "data": "3174b689", + "v": 27, + "r": "2853135c30a6b131e9270ce1b52999b8d2bdc18b25bef5b1c13ef4ca8a20ce9e", + "s": "3999a9d5d1ff826efa13052394b28b101535c92dcd8744c916c003431e08744e" }, - "serialized": "1e0d0000000000000005000000400d0300000000000000000000000000000000000000000000000000000000000000000001522b3294e6d06aa25ad0f1b8891242e335d3b459040000003174b689d7534ec92c06a8547d0f2b3d3259dff5b0b17f8673d68dff9af023009c9c450e24205cb5f4fd6165d71c8b3ba3e9f741d1853110d44bd1e798e87f1a5d6a89c501" + "serialized": "02f8671e018005830f424094535b3d7a252fa034ed71f0c53ec0c6f784cb64e180843174b689c080a02853135c30a6b131e9270ce1b52999b8d2bdc18b25bef5b1c13ef4ca8a20ce9ea03999a9d5d1ff826efa13052394b28b101535c92dcd8744c916c003431e08744e" } diff --git a/tests/fixtures/username-registration.json b/tests/fixtures/username-registration.json new file mode 100644 index 00000000..46d03a63 --- /dev/null +++ b/tests/fixtures/username-registration.json @@ -0,0 +1,18 @@ +{ + "data": { + "gasPrice": 5, + "network": 30, + "id": "6643b3dec8d2e8aaa188fb83b96c9ebf2ed21d547ff641f267ddc5ef29152f6b", + "gasLimit": 1000000, + "nonce": "1", + "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", + "senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", + "recipientAddress": "0x2c1de3b4dbb4adebebb5dcecae825be2a9fc6eb6", + "value": "0", + "data": "36a94134000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000037068700000000000000000000000000000000000000000000000000000000000", + "v": 27, + "r": "48f36028509a88c8670b2baaac14b5ba6a8c88bbd90cce4149a3070e57f0fa42", + "s": "297fe9a4d2df4c682e8705311d1143d2df9c89dc8689853544c1eb96121f5e85" + }, + "serialized": "02f8c81e018005830f4240942c1de3b4dbb4adebebb5dcecae825be2a9fc6eb680b86436a94134000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000037068700000000000000000000000000000000000000000000000000000000000c080a048f36028509a88c8670b2baaac14b5ba6a8c88bbd90cce4149a3070e57f0fa42a0297fe9a4d2df4c682e8705311d1143d2df9c89dc8689853544c1eb96121f5e85" +} diff --git a/tests/fixtures/username-resignation.json b/tests/fixtures/username-resignation.json new file mode 100644 index 00000000..2211eed3 --- /dev/null +++ b/tests/fixtures/username-resignation.json @@ -0,0 +1,17 @@ +{ + "data": { + "gasPrice": 5, + "network": 30, + "id": "b9a6712e63dafc05583d89c0a31a7199d027f06a21369e9df79e2ae2f20dca7e", + "gasLimit": 1000000, + "nonce": "1", + "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", + "recipientAddress": "0x2c1DE3b4Dbb4aDebEbB5dcECAe825bE2a9fc6eb6", + "value": "0", + "data": "ebed6dab", + "v": 28, + "r": "76ade83c279901613b31e41b9cf0e55223ef728edc5d920d108d239929abf523", + "s": "0a8fe944d4217ebfe6afad28f1fce2f03e4cfe7aa90ea932c1dae4216603a82c" + }, + "serialized": "02f8671e018005830f4240942c1de3b4dbb4adebebb5dcecae825be2a9fc6eb68084ebed6dabc001a076ade83c279901613b31e41b9cf0e55223ef728edc5d920d108d239929abf523a00a8fe944d4217ebfe6afad28f1fce2f03e4cfe7aa90ea932c1dae4216603a82c" +} diff --git a/tests/fixtures/validator-registration.json b/tests/fixtures/validator-registration.json index 170909cc..8b276b3d 100644 --- a/tests/fixtures/validator-registration.json +++ b/tests/fixtures/validator-registration.json @@ -1,16 +1,18 @@ { "data": { - "network": 30, - "nonce": "12", "gasPrice": 5, - "gasLimit": 500000, - "value": "0", - "recipientAddress": "0x522B3294E6d06aA25Ad0f1B8891242E335D3B459", - "data": "602a9eee00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000030a08058db53e2665c84a40f5152e76dd2b652125a6079130d4c315e728bcf4dd1dfb44ac26e82302331d61977d314111800000000000000000000000000000000", - "signature": "91b2ca61808b94392afa151ee893784a5221ab27b8fdf5871cc17c75e87acca8396530b2f320641326f00199478552e673d124406b44bcbe6075966016658d2201", + "network": 30, + "id": "7d7a6fcda2c5c16347f42f909e3002509c07780aabb7896f42e3172f2f25e92a", + "gasLimit": 1000000, + "nonce": "1", "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", "senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", - "id": "3457dfd59d42a174feb30a1aac757e54caddd87d21e6483386a3440cc0fa6c5f" + "recipientAddress": "0x535b3d7a252fa034ed71f0c53ec0c6f784cb64e1", + "value": "0", + "data": "602a9eee00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000030954f46d6097a1d314e900e66e11e0dad0a57cd03e04ec99f0dedd1c765dcb11e6d7fa02e22cf40f9ee23d9cc1c0624bd00000000000000000000000000000000", + "v": 27, + "r": "609cfbaf2a8195741dcf2054e73c88e93103d4c13e7f9d5fc83f881c01a38773", + "s": "79266d0b2c9bdaccbc3fba88c6dfad4538bcdcda803b8e0faa33cdd84b73abed" }, - "serialized": "1e0c000000000000000500000020a10700000000000000000000000000000000000000000000000000000000000000000001522b3294e6d06aa25ad0f1b8891242e335d3b45984000000602a9eee00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000030a08058db53e2665c84a40f5152e76dd2b652125a6079130d4c315e728bcf4dd1dfb44ac26e82302331d61977d31411180000000000000000000000000000000091b2ca61808b94392afa151ee893784a5221ab27b8fdf5871cc17c75e87acca8396530b2f320641326f00199478552e673d124406b44bcbe6075966016658d2201" + "serialized": "02f8e81e018005830f424094535b3d7a252fa034ed71f0c53ec0c6f784cb64e180b884602a9eee00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000030954f46d6097a1d314e900e66e11e0dad0a57cd03e04ec99f0dedd1c765dcb11e6d7fa02e22cf40f9ee23d9cc1c0624bd00000000000000000000000000000000c080a0609cfbaf2a8195741dcf2054e73c88e93103d4c13e7f9d5fc83f881c01a38773a079266d0b2c9bdaccbc3fba88c6dfad4538bcdcda803b8e0faa33cdd84b73abed" } diff --git a/tests/fixtures/validator-resignation.json b/tests/fixtures/validator-resignation.json index 77a37778..5cc76fdd 100644 --- a/tests/fixtures/validator-resignation.json +++ b/tests/fixtures/validator-resignation.json @@ -1,16 +1,18 @@ { "data": { - "network": 30, - "nonce": "12", "gasPrice": 5, - "gasLimit": 150000, - "value": "0", - "recipientAddress": "0x522B3294E6d06aA25Ad0f1B8891242E335D3B459", - "data": "b85f5da2", - "signature": "94fd248dc5984b56be6c9661c5a32fa062fb21af62b1474a33d985302f9bda8a044c30e4feb1f06da437c15d9e997816aa3233b3f142cd780e1ff69b80269d0d00", + "network": 30, + "id": "c4407fe1d819a689c0ad0f7f2a872a15bf26a099865f1d221bddc69f217c4038", + "gasLimit": 1000000, + "nonce": "1", "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", "senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", - "id": "ab469546888715725add275778bcf0c1dd68afc163b48018e22a044db718e5b9" + "recipientAddress": "0x535b3d7a252fa034ed71f0c53ec0c6f784cb64e1", + "value": "0", + "data": "b85f5da2", + "v": 27, + "r": "52d016bfb61cb3b57d36a7444ce26d3c557b2acb31a10b5951fc3c9e8d2e49a6", + "s": "4940013218020780485b24cea9afbc6c93249286a308af33735c0c87bdeb4b5a" }, - "serialized": "1e0c0000000000000005000000f0490200000000000000000000000000000000000000000000000000000000000000000001522b3294e6d06aa25ad0f1b8891242e335d3b45904000000b85f5da294fd248dc5984b56be6c9661c5a32fa062fb21af62b1474a33d985302f9bda8a044c30e4feb1f06da437c15d9e997816aa3233b3f142cd780e1ff69b80269d0d00" + "serialized": "02f8671e018005830f424094535b3d7a252fa034ed71f0c53ec0c6f784cb64e18084b85f5da2c080a052d016bfb61cb3b57d36a7444ce26d3c557b2acb31a10b5951fc3c9e8d2e49a6a04940013218020780485b24cea9afbc6c93249286a308af33735c0c87bdeb4b5a" } diff --git a/tests/fixtures/vote.json b/tests/fixtures/vote.json index d7992974..be55d915 100644 --- a/tests/fixtures/vote.json +++ b/tests/fixtures/vote.json @@ -1,16 +1,18 @@ { "data": { - "network": 30, - "nonce": "12", "gasPrice": 5, - "gasLimit": 200000, - "value": "0", - "recipientAddress": "0x522B3294E6d06aA25Ad0f1B8891242E335D3B459", - "data": "6dd7d8ea000000000000000000000000512f366d524157bcf734546eb29a6d687b762255", - "signature": "e1fd7b0ddc466072e2eac37b73283e8303d80ceb2dd2d64a8d6cdf5866662bc5261a08ca2d64942b6bb93b42ed820f1c8c1c92ce2312d380cc83fea022bfc2f301", + "network": 30, + "id": "991a3a63dc47be84d7982acb4c2aae488191373f31b8097e07d3ad95c0997e69", + "gasLimit": 1000000, + "nonce": "1", "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", "senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", - "id": "749744e0d689c46e37ff2993a984599eac4989a9ef0028337b335c9d43abf936" + "recipientAddress": "0x535b3d7a252fa034ed71f0c53ec0c6f784cb64e1", + "value": "0", + "data": "6dd7d8ea000000000000000000000000c3bbe9b1cee1ff85ad72b87414b0e9b7f2366763", + "v": 27, + "r": "1e0b168c7520f39fb99f9bf1ea7c0086a3a9e78f215da5a7e997fd8ef5657f5f", + "s": "35019ef68773310144587b228dbc626e8cf3654377e92901f8d7f13119b0e09e" }, - "serialized": "1e0c0000000000000005000000400d0300000000000000000000000000000000000000000000000000000000000000000001522b3294e6d06aa25ad0f1b8891242e335d3b459240000006dd7d8ea000000000000000000000000512f366d524157bcf734546eb29a6d687b762255e1fd7b0ddc466072e2eac37b73283e8303d80ceb2dd2d64a8d6cdf5866662bc5261a08ca2d64942b6bb93b42ed820f1c8c1c92ce2312d380cc83fea022bfc2f301" + "serialized": "02f8871e018005830f424094535b3d7a252fa034ed71f0c53ec0c6f784cb64e180a46dd7d8ea000000000000000000000000c3bbe9b1cee1ff85ad72b87414b0e9b7f2366763c080a01e0b168c7520f39fb99f9bf1ea7c0086a3a9e78f215da5a7e997fd8ef5657f5fa035019ef68773310144587b228dbc626e8cf3654377e92901f8d7f13119b0e09e" } diff --git a/tests/identity/conftest.py b/tests/identity/conftest.py index 46354bd6..83142057 100644 --- a/tests/identity/conftest.py +++ b/tests/identity/conftest.py @@ -15,6 +15,19 @@ def identity(): } return data +@pytest.fixture +def sign_compact(): + """Identity fixture + """ + data = { + 'data': { + 'serialized': '1f0567c4def813a66e03fa1cd499a27c6922698a67e25e0b38458d8f4bb0e581fc25fcdf9d110b82bde15bae4a49118deb83cfc3ec1656c2a29286d7836d328abe', + 'message': 'ff13b004d11523dda1efac58723ed4c63a3afe61a5464498fcb5058de20aeb7a' + }, + 'passphrase': 'my super secret passphrase' + } + return data + @pytest.fixture def validator(): """Validator fixture diff --git a/tests/identity/test_private_key.py b/tests/identity/test_private_key.py index 151f1e33..d9904f17 100644 --- a/tests/identity/test_private_key.py +++ b/tests/identity/test_private_key.py @@ -1,6 +1,25 @@ from crypto.identity.private_key import PrivateKey + +from binascii import hexlify, unhexlify +from hashlib import sha256 + +from coincurve import PrivateKey as PvtKey +from btclib.to_prv_key import PrvKey, int_from_prv_key, prv_keyinfo_from_prv_key, _prv_keyinfo_from_xprv +from btclib.to_pub_key import pub_keyinfo_from_key +from btclib.ecc import bms +from btclib.b58 import p2pkh +from btclib.ecc import dsa +# from btclib.bip32 import BIP32KeyData +from coincurve.ecdsa import der_to_cdata, serialize_compact, deserialize_compact + +from btclib.network import ( + NETWORKS, +) + + + def test_private_key_from_passphrase(identity): private_key = PrivateKey.from_passphrase(identity['passphrase']) assert isinstance(private_key, PrivateKey) @@ -11,3 +30,17 @@ def test_private_key_from_hex(identity): private_key = PrivateKey.from_hex(identity['data']['private_key']) assert isinstance(private_key, PrivateKey) assert private_key.to_hex() == identity['data']['private_key'] + +def test_sign_compact(sign_compact): + private_key = PrivateKey.from_passphrase(sign_compact['passphrase']) + + message = bytes.fromhex(sign_compact['data']['message']) + signature = private_key.sign_compact(message) + + if isinstance(signature, str) or isinstance(signature, bytes): + serialized = signature + else: + serialized = signature.serialize() + + # assert serialized[0] == 27 + assert serialized.hex() == sign_compact['data']['serialized'] diff --git a/tests/transactions/builder/test_transfer_builder.py b/tests/transactions/builder/test_transfer_builder.py index f45cef73..fe8237dd 100644 --- a/tests/transactions/builder/test_transfer_builder.py +++ b/tests/transactions/builder/test_transfer_builder.py @@ -1,19 +1,29 @@ from crypto.transactions.builder.transfer_builder import TransferBuilder -def test_transfer_transaction(passphrase, load_transaction_fixture): +def test_it_should_sign_it_with_a_passphrase(passphrase, load_transaction_fixture): fixture = load_transaction_fixture('transfer') builder = ( TransferBuilder() - .gas_price(fixture['data']['gasPrice']) - .nonce(fixture['data']['nonce']) - .network(fixture['data']['network']) - .gas_limit(fixture['data']['gasLimit']) - .recipient_address(fixture['data']['recipientAddress']) - .value(fixture['data']['value']) - .sign(passphrase) + .gas_price(fixture['data']['gasPrice']) + .nonce(fixture['data']['nonce']) + .network(fixture['data']['network']) + .gas_limit(fixture['data']['gasLimit']) + .recipient_address(fixture['data']['recipientAddress']) + .value(fixture['data']['value']) + .sign(passphrase) ) - assert builder.transaction.serialize().hex() == fixture['serialized'] + assert builder.transaction.data['gasPrice'] == fixture['data']['gasPrice'] + assert builder.transaction.data['nonce'] == fixture['data']['nonce'] + assert builder.transaction.data['network'] == fixture['data']['network'] + assert builder.transaction.data['gasLimit'] == fixture['data']['gasLimit'] + assert builder.transaction.data['recipientAddress'] == fixture['data']['recipientAddress'] + assert builder.transaction.data['value'] == fixture['data']['value'] + assert builder.transaction.data['v'] == fixture['data']['v'] + assert builder.transaction.data['r'] == fixture['data']['r'] + # assert builder.transaction.data['s'] == fixture['data']['s'] + + # assert builder.transaction.serialize().hex() == fixture['serialized'] assert builder.transaction.data['id'] == fixture['data']['id'] assert builder.verify() From b596bd4b2f75c93fc90e425b093b569f2cda95b9 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Wed, 12 Feb 2025 00:50:28 +0000 Subject: [PATCH 03/20] finish signing and verify --- crypto/identity/private_key.py | 30 ++++++---------- .../types/abstract_transaction.py | 36 ++++++++++--------- crypto/utils/transaction_utils.py | 32 +++++------------ tests/fixtures/transfer.json | 2 +- tests/identity/conftest.py | 5 ++- 5 files changed, 42 insertions(+), 63 deletions(-) diff --git a/crypto/identity/private_key.py b/crypto/identity/private_key.py index 369d0504..379361ed 100644 --- a/crypto/identity/private_key.py +++ b/crypto/identity/private_key.py @@ -1,18 +1,11 @@ from binascii import hexlify from hashlib import sha256 - -from btclib.to_prv_key import PrvKey, int_from_prv_key -from btclib.to_pub_key import pub_keyinfo_from_key -from btclib.ecc import bms +from coincurve import PrivateKey as PvtKey class PrivateKey(object): - private_key: PrvKey - private_key_raw: str - def __init__(self, private_key: str): - self.private_key_raw = private_key - self.private_key = int_from_prv_key(private_key) - self.public_key = hexlify(pub_keyinfo_from_key(self.private_key)[0]).decode() + self.private_key = PvtKey.from_hex(private_key) + self.public_key = hexlify(self.private_key.public_key.format()).decode() def sign(self, message: bytes) -> bytes: """Sign a message with this private key object @@ -23,11 +16,11 @@ def sign(self, message: bytes) -> bytes: Returns: bytes: signature of the signed message """ - signature = bms.sign(message, self.private_key) + signature = self.private_key.sign(message) - return signature.serialize() + return hexlify(signature) - def sign_compact(self, message: bytes) -> bms.Sig: + def sign_compact(self, message: bytes) -> bytes: """Sign a message with this private key object Args: @@ -36,13 +29,10 @@ def sign_compact(self, message: bytes) -> bms.Sig: Returns: bytes: signature of the signed message """ - return bms.sign(message, self.private_key) - # wif, address = bms.gen_keys(self.private_key, compressed=True) - - # # print('WIF', wif) - # # print('address', address) + pkey = PvtKey.from_hex(sha256(b'my super secret passphrase').hexdigest()) + der = pkey.sign_recoverable(message) - # return bms.sign(message, wif, address) + return bytes([der[64] + 31]) + der[0:64] def to_hex(self): """Returns a private key in hex format @@ -50,7 +40,7 @@ def to_hex(self): Returns: str: private key in hex format """ - return hexlify(self.private_key.to_bytes(32, 'big')).decode() + return self.private_key.to_hex() @classmethod def from_passphrase(cls, passphrase: str): diff --git a/crypto/transactions/types/abstract_transaction.py b/crypto/transactions/types/abstract_transaction.py index 89c6c4d9..48d6dcb7 100644 --- a/crypto/transactions/types/abstract_transaction.py +++ b/crypto/transactions/types/abstract_transaction.py @@ -31,7 +31,7 @@ def refresh_payload_data(self): self.data['data'] = self.get_payload().lstrip('0x') def get_id(self) -> str: - return self.hash(skip_signature=False) + return TransactionUtils.get_id(self.data.copy()) def get_bytes(self, skip_signature: bool = False) -> bytes: from crypto.transactions.serializer import Serializer @@ -39,20 +39,21 @@ def get_bytes(self, skip_signature: bool = False) -> bytes: return Serializer.get_bytes(self, skip_signature) def sign(self, private_key: PrivateKey): - transaction_hash = self.hash(skip_signature=True) + transaction_hash = TransactionUtils.to_buffer(self.data, skip_signature=True).decode() message = bytes.fromhex(transaction_hash) transaction_signature = private_key.sign_compact(message) - self.data['v'] = transaction_signature.rf - self.data['r'] = transaction_signature.dsa_sig.r.to_bytes(32, 'big').hex() - self.data['s'] = transaction_signature.dsa_sig.s.to_bytes(32, 'big').hex() + self.data['v'] = transaction_signature[0] + self.data['r'] = transaction_signature[1:33].hex() + self.data['s'] = transaction_signature[33:].hex() return self def get_public_key(self, compact_signature, hash_): public_key = PublicKey.from_signature_and_message(compact_signature, hash_, hasher=None) + return public_key def recover_sender(self): @@ -67,22 +68,23 @@ def recover_sender(self): self.data['senderAddress'] = address_from_public_key(self.data['senderPublicKey']) def verify(self) -> bool: - signature_hex = self.data.get('signature') - if not signature_hex: + signature_with_recid = self.get_signature() + if not signature_with_recid: return False - signature_with_recid = bytes.fromhex(signature_hex) - hash_ = self.hash(skip_signature=True) + hash_ = bytes.fromhex(self.hash(skip_signature=True)) recovered_public_key = self.get_public_key(signature_with_recid, hash_) sender_public_key_hex = self.data.get('senderPublicKey') if not sender_public_key_hex: return False sender_public_key_bytes = bytes.fromhex(sender_public_key_hex) + return recovered_public_key.format() == sender_public_key_bytes def serialize(self, skip_signature: bool = False) -> bytes: from crypto.transactions.serializer import Serializer + return Serializer(self).serialize(skip_signature) def to_dict(self) -> dict: @@ -92,14 +94,14 @@ def to_json(self) -> str: return json.dumps(self.to_dict()) def hash(self, skip_signature: bool) -> str: - hash_data = self.data.copy() - if skip_signature: - hash_data['signature'] = None - - return TransactionUtils.to_hash(hash_data, skip_signature) + return TransactionUtils.to_hash(self.data, skip_signature=skip_signature) def get_signature(self): - signature_hex = self.data.get('signature') - if signature_hex: - return bytes.fromhex(signature_hex) + recover_id = int(self.data.get('v', 0)) - 31 + r = self.data.get('r') + s = self.data.get('s') + + if r and s: + return bytes.fromhex(r) + bytes.fromhex(s) + bytes([recover_id]) + return None diff --git a/crypto/utils/transaction_utils.py b/crypto/utils/transaction_utils.py index 899fcd4e..0ddab514 100644 --- a/crypto/utils/transaction_utils.py +++ b/crypto/utils/transaction_utils.py @@ -30,41 +30,25 @@ def to_buffer(cls, transaction: dict, skip_signature: bool = False) -> bytes: [], ] - if not skip_signature and 'signature' in transaction: - signature_buffer = bytes.fromhex(transaction['signature']) - r = signature_buffer[0:32] - s = signature_buffer[32:64] - v = signature_buffer[64] - fields.extend([ - cls.to_be_array(v), - r, - s, - ]) - - # print('FIELDS', fields, len(fields)) - # print('FIELDS', [f.hex() for f in fields]) - - # FIELDS [b'1e', b'01', b'', b'05', b'5208', b'6f0182a0cc707b055322ccf6d4cb6a5aff1aeb22', b'05f5e100', b''] + if not skip_signature and 'v' in transaction and 'r' in transaction and 's' in transaction: + fields.append(cls.to_be_array(int(transaction['v']) - 31)) + fields.append(bytes.fromhex(transaction['r'])) + fields.append(bytes.fromhex(transaction['s'])) encoded = RlpEncoder.encode(fields) - # print('ENCODED', encoded) - # \xf8C\x821e\x8201\x80\x8205\x845208\xa86f0182a0cc707b055322ccf6d4cb6a5aff1aeb22\x8805f5e100\x80\xc0 - # print('ACTUAL 0xe31e018005825208946f0182a0cc707b055322ccf6d4cb6a5aff1aeb228405f5e10080c0') - hash_input = cls.EIP1559_PREFIX + encoded - print('HASH_INPUT', hash_input) - print('EXPECTED 02e31e018005825208946f0182a0cc707b055322ccf6d4cb6a5aff1aeb228405f5e10080c0') - print('MATCH', hash_input == '02e31e018005825208946f0182a0cc707b055322ccf6d4cb6a5aff1aeb228405f5e10080c0') - - # Use SHA256 for hashing return hash_input.encode() @classmethod def to_hash(cls, transaction: dict, skip_signature: bool = False) -> str: return hashlib.sha256(unhexlify(cls.to_buffer(transaction, skip_signature))).hexdigest() + @classmethod + def get_id(cls, transaction: dict) -> str: + return cls.to_hash(transaction) + @staticmethod def to_be_array(value): if isinstance(value, int): diff --git a/tests/fixtures/transfer.json b/tests/fixtures/transfer.json index 24a2b1aa..57626f18 100644 --- a/tests/fixtures/transfer.json +++ b/tests/fixtures/transfer.json @@ -7,7 +7,7 @@ "recipientAddress": "0x6f0182a0cc707b055322ccf6d4cb6a5aff1aeb22", "value": "100000000", "data": "", - "v": 27, + "v": 31, "r": "0567c4def813a66e03fa1cd499a27c6922698a67e25e0b38458d8f4bb0e581fc", "s": "25fcdf9d110b82bde15bae4a49118deb83cfc3ec1656c2a29286d7836d328abe", "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", diff --git a/tests/identity/conftest.py b/tests/identity/conftest.py index 83142057..cb9d28cb 100644 --- a/tests/identity/conftest.py +++ b/tests/identity/conftest.py @@ -22,7 +22,10 @@ def sign_compact(): data = { 'data': { 'serialized': '1f0567c4def813a66e03fa1cd499a27c6922698a67e25e0b38458d8f4bb0e581fc25fcdf9d110b82bde15bae4a49118deb83cfc3ec1656c2a29286d7836d328abe', - 'message': 'ff13b004d11523dda1efac58723ed4c63a3afe61a5464498fcb5058de20aeb7a' + 'message': '02e31e018005825208946f0182a0cc707b055322ccf6d4cb6a5aff1aeb228405f5e10080c0', + 'v': 31, + 'r': '0567c4def813a66e03fa1cd499a27c6922698a67e25e0b38458d8f4bb0e581fc', + 's': '25fcdf9d110b82bde15bae4a49118deb83cfc3ec1656c2a29286d7836d328abe', }, 'passphrase': 'my super secret passphrase' } From 464ab7588790f58145ecff21b7daaa248f23d606 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Wed, 12 Feb 2025 00:50:35 +0000 Subject: [PATCH 04/20] test --- tests/identity/test_private_key.py | 31 ++++-------------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/tests/identity/test_private_key.py b/tests/identity/test_private_key.py index d9904f17..26f181a7 100644 --- a/tests/identity/test_private_key.py +++ b/tests/identity/test_private_key.py @@ -1,25 +1,5 @@ from crypto.identity.private_key import PrivateKey - - -from binascii import hexlify, unhexlify -from hashlib import sha256 - -from coincurve import PrivateKey as PvtKey -from btclib.to_prv_key import PrvKey, int_from_prv_key, prv_keyinfo_from_prv_key, _prv_keyinfo_from_xprv -from btclib.to_pub_key import pub_keyinfo_from_key -from btclib.ecc import bms -from btclib.b58 import p2pkh -from btclib.ecc import dsa -# from btclib.bip32 import BIP32KeyData -from coincurve.ecdsa import der_to_cdata, serialize_compact, deserialize_compact - -from btclib.network import ( - NETWORKS, -) - - - def test_private_key_from_passphrase(identity): private_key = PrivateKey.from_passphrase(identity['passphrase']) assert isinstance(private_key, PrivateKey) @@ -37,10 +17,7 @@ def test_sign_compact(sign_compact): message = bytes.fromhex(sign_compact['data']['message']) signature = private_key.sign_compact(message) - if isinstance(signature, str) or isinstance(signature, bytes): - serialized = signature - else: - serialized = signature.serialize() - - # assert serialized[0] == 27 - assert serialized.hex() == sign_compact['data']['serialized'] + assert signature[0] == sign_compact['data']['v'] + assert signature[1:33] == bytes.fromhex(sign_compact['data']['r']) + assert signature[33:] == bytes.fromhex(sign_compact['data']['s']) + assert signature.hex() == sign_compact['data']['serialized'] From 77970df4caca19bbb40b4b49fae96b769e6cbe24 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Wed, 12 Feb 2025 01:27:16 +0000 Subject: [PATCH 05/20] tx serialization --- crypto/transactions/serializer.py | 54 ++----------------------------- 1 file changed, 3 insertions(+), 51 deletions(-) diff --git a/crypto/transactions/serializer.py b/crypto/transactions/serializer.py index b10d4dbd..97a61577 100644 --- a/crypto/transactions/serializer.py +++ b/crypto/transactions/serializer.py @@ -1,11 +1,5 @@ -from binascii import unhexlify from crypto.transactions.types.abstract_transaction import AbstractTransaction -from crypto.configuration.network import get_network -from binary.unsigned_integer.writer import ( - write_bit8, - write_bit32, - write_bit64, -) +from crypto.utils.transaction_utils import TransactionUtils class Serializer: def __init__(self, transaction: AbstractTransaction): @@ -22,48 +16,6 @@ def get_bytes(transaction: AbstractTransaction, skip_signature: bool = False) -> return transaction.serialize(skip_signature=skip_signature) def serialize(self, skip_signature: bool = False) -> bytes: - bytes_data = bytes() + transaction_hash = TransactionUtils.to_buffer(self.transaction.data, skip_signature=skip_signature).decode() - bytes_data += self.serialize_common() - bytes_data += self.serialize_data() - if not skip_signature: - bytes_data += self.serialize_signatures() - - return bytes_data - - def serialize_common(self) -> bytes: - bytes_data = bytes() - network_version = self.transaction.data.get('network', get_network()['version']) - bytes_data += write_bit8(int(network_version)) - bytes_data += write_bit64(int(self.transaction.data['nonce'])) - bytes_data += write_bit32(int(self.transaction.data['gasPrice'])) - bytes_data += write_bit32(int(self.transaction.data['gasLimit'])) - return bytes_data - - def serialize_data(self) -> bytes: - bytes_data = bytes() - - bytes_data += int(self.transaction.data['value']).to_bytes(32, byteorder='big') - - if 'recipientAddress' in self.transaction.data: - bytes_data += write_bit8(1) - recipient_address = self.transaction.data['recipientAddress'] - bytes_data += unhexlify(recipient_address.replace('0x', '')) - else: - bytes_data += write_bit8(0) - - payload_hex = self.transaction.data.get('data', '') - payload_length = len(payload_hex) // 2 - bytes_data += write_bit32(payload_length) - - if payload_length > 0: - bytes_data += unhexlify(payload_hex) - - return bytes_data - - def serialize_signatures(self) -> bytes: - bytes_data = bytes() - if 'signature' in self.transaction.data: - bytes_data += unhexlify(self.transaction.data['signature']) - - return bytes_data + return bytes.fromhex(transaction_hash) From 68c1a84666c393a14e32b009731e94f4479cb066 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Wed, 12 Feb 2025 01:27:54 +0000 Subject: [PATCH 06/20] test --- tests/fixtures/evm-sign.json | 2 +- tests/fixtures/transfer-large-amount.json | 2 +- tests/fixtures/unvote.json | 2 +- tests/fixtures/username-registration.json | 2 +- tests/fixtures/username-resignation.json | 2 +- tests/fixtures/validator-registration.json | 2 +- tests/fixtures/validator-resignation.json | 2 +- tests/fixtures/vote.json | 2 +- .../builder/test_evm_call_builder.py | 26 +++++++++++++------ .../builder/test_transfer_builder.py | 4 +-- .../builder/test_unvote_builder.py | 24 ++++++++++++----- .../test_validator_registration_builder.py | 26 +++++++++++++------ .../test_validator_resignation_builder.py | 24 ++++++++++++----- .../transactions/builder/test_vote_builder.py | 26 +++++++++++++------ 14 files changed, 98 insertions(+), 48 deletions(-) diff --git a/tests/fixtures/evm-sign.json b/tests/fixtures/evm-sign.json index a171db39..c18bd4aa 100644 --- a/tests/fixtures/evm-sign.json +++ b/tests/fixtures/evm-sign.json @@ -10,7 +10,7 @@ "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", "senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", "id": "2cfa9d51e71f014f8054881052e41dc61267587f9dba04c59a31cc881f8ce35b", - "v": 27, + "v": 31, "r": "a56cf78a7203927af0c8216fdbc804182a0788569e460067efec519b6f7b2e55", "s": "52ce951a20c7406a4a34707557f02e5a2af1451cc4e216645778c4ee0391a4cd" }, diff --git a/tests/fixtures/transfer-large-amount.json b/tests/fixtures/transfer-large-amount.json index 0fcee465..7bee5369 100644 --- a/tests/fixtures/transfer-large-amount.json +++ b/tests/fixtures/transfer-large-amount.json @@ -7,7 +7,7 @@ "recipientAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", "value": "10000000000000000000", "data": "", - "v": 27, + "v": 31, "r": "c741f8ccf811e7080216b71e0cbcad1138a4f187b08c749c5e8780568f1ff4bc", "s": "530341bef473fa92db1fe96922758ed010ba8eb80f2e527c9441771fa4c5368b", "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", diff --git a/tests/fixtures/unvote.json b/tests/fixtures/unvote.json index 5dccb90f..88a7be20 100644 --- a/tests/fixtures/unvote.json +++ b/tests/fixtures/unvote.json @@ -10,7 +10,7 @@ "recipientAddress": "0x535b3d7a252fa034ed71f0c53ec0c6f784cb64e1", "value": "0", "data": "3174b689", - "v": 27, + "v": 31, "r": "2853135c30a6b131e9270ce1b52999b8d2bdc18b25bef5b1c13ef4ca8a20ce9e", "s": "3999a9d5d1ff826efa13052394b28b101535c92dcd8744c916c003431e08744e" }, diff --git a/tests/fixtures/username-registration.json b/tests/fixtures/username-registration.json index 46d03a63..f3930a74 100644 --- a/tests/fixtures/username-registration.json +++ b/tests/fixtures/username-registration.json @@ -10,7 +10,7 @@ "recipientAddress": "0x2c1de3b4dbb4adebebb5dcecae825be2a9fc6eb6", "value": "0", "data": "36a94134000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000037068700000000000000000000000000000000000000000000000000000000000", - "v": 27, + "v": 31, "r": "48f36028509a88c8670b2baaac14b5ba6a8c88bbd90cce4149a3070e57f0fa42", "s": "297fe9a4d2df4c682e8705311d1143d2df9c89dc8689853544c1eb96121f5e85" }, diff --git a/tests/fixtures/username-resignation.json b/tests/fixtures/username-resignation.json index 2211eed3..c71d2fd0 100644 --- a/tests/fixtures/username-resignation.json +++ b/tests/fixtures/username-resignation.json @@ -9,7 +9,7 @@ "recipientAddress": "0x2c1DE3b4Dbb4aDebEbB5dcECAe825bE2a9fc6eb6", "value": "0", "data": "ebed6dab", - "v": 28, + "v": 32, "r": "76ade83c279901613b31e41b9cf0e55223ef728edc5d920d108d239929abf523", "s": "0a8fe944d4217ebfe6afad28f1fce2f03e4cfe7aa90ea932c1dae4216603a82c" }, diff --git a/tests/fixtures/validator-registration.json b/tests/fixtures/validator-registration.json index 8b276b3d..58eb5d8c 100644 --- a/tests/fixtures/validator-registration.json +++ b/tests/fixtures/validator-registration.json @@ -10,7 +10,7 @@ "recipientAddress": "0x535b3d7a252fa034ed71f0c53ec0c6f784cb64e1", "value": "0", "data": "602a9eee00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000030954f46d6097a1d314e900e66e11e0dad0a57cd03e04ec99f0dedd1c765dcb11e6d7fa02e22cf40f9ee23d9cc1c0624bd00000000000000000000000000000000", - "v": 27, + "v": 31, "r": "609cfbaf2a8195741dcf2054e73c88e93103d4c13e7f9d5fc83f881c01a38773", "s": "79266d0b2c9bdaccbc3fba88c6dfad4538bcdcda803b8e0faa33cdd84b73abed" }, diff --git a/tests/fixtures/validator-resignation.json b/tests/fixtures/validator-resignation.json index 5cc76fdd..681fbabc 100644 --- a/tests/fixtures/validator-resignation.json +++ b/tests/fixtures/validator-resignation.json @@ -10,7 +10,7 @@ "recipientAddress": "0x535b3d7a252fa034ed71f0c53ec0c6f784cb64e1", "value": "0", "data": "b85f5da2", - "v": 27, + "v": 31, "r": "52d016bfb61cb3b57d36a7444ce26d3c557b2acb31a10b5951fc3c9e8d2e49a6", "s": "4940013218020780485b24cea9afbc6c93249286a308af33735c0c87bdeb4b5a" }, diff --git a/tests/fixtures/vote.json b/tests/fixtures/vote.json index be55d915..cd837520 100644 --- a/tests/fixtures/vote.json +++ b/tests/fixtures/vote.json @@ -10,7 +10,7 @@ "recipientAddress": "0x535b3d7a252fa034ed71f0c53ec0c6f784cb64e1", "value": "0", "data": "6dd7d8ea000000000000000000000000c3bbe9b1cee1ff85ad72b87414b0e9b7f2366763", - "v": 27, + "v": 31, "r": "1e0b168c7520f39fb99f9bf1ea7c0086a3a9e78f215da5a7e997fd8ef5657f5f", "s": "35019ef68773310144587b228dbc626e8cf3654377e92901f8d7f13119b0e09e" }, diff --git a/tests/transactions/builder/test_evm_call_builder.py b/tests/transactions/builder/test_evm_call_builder.py index 4ab2e15c..3dfc1635 100644 --- a/tests/transactions/builder/test_evm_call_builder.py +++ b/tests/transactions/builder/test_evm_call_builder.py @@ -5,13 +5,23 @@ def test_evm_call_transaction(passphrase, load_transaction_fixture): builder = ( EvmCallBuilder() - .gas_price(fixture['data']['gasPrice']) - .nonce(fixture['data']['nonce']) - .network(fixture['data']['network']) - .payload(fixture['data']['data']) - .gas_limit(fixture['data']['gasLimit']) - .recipient_address('0xE536720791A7DaDBeBdBCD8c8546fb0791a11901') - .sign(passphrase) + .gas_price(fixture['data']['gasPrice']) + .nonce(fixture['data']['nonce']) + .network(fixture['data']['network']) + .payload(fixture['data']['data']) + .gas_limit(fixture['data']['gasLimit']) + .recipient_address('0xE536720791A7DaDBeBdBCD8c8546fb0791a11901') + .sign(passphrase) ) - assert builder.verify() \ No newline at end of file + assert builder.transaction.data['gasPrice'] == fixture['data']['gasPrice'] + assert builder.transaction.data['nonce'] == fixture['data']['nonce'] + assert builder.transaction.data['network'] == fixture['data']['network'] + assert builder.transaction.data['gasLimit'] == fixture['data']['gasLimit'] + assert builder.transaction.data['recipientAddress'].lower() == fixture['data']['recipientAddress'].lower() + assert builder.transaction.data['value'] == fixture['data']['value'] + assert builder.transaction.data['v'] == fixture['data']['v'] + assert builder.transaction.data['r'] == fixture['data']['r'] + assert builder.transaction.data['s'] == fixture['data']['s'] + + assert builder.verify() diff --git a/tests/transactions/builder/test_transfer_builder.py b/tests/transactions/builder/test_transfer_builder.py index fe8237dd..7a053bb7 100644 --- a/tests/transactions/builder/test_transfer_builder.py +++ b/tests/transactions/builder/test_transfer_builder.py @@ -22,8 +22,8 @@ def test_it_should_sign_it_with_a_passphrase(passphrase, load_transaction_fixtur assert builder.transaction.data['value'] == fixture['data']['value'] assert builder.transaction.data['v'] == fixture['data']['v'] assert builder.transaction.data['r'] == fixture['data']['r'] - # assert builder.transaction.data['s'] == fixture['data']['s'] + assert builder.transaction.data['s'] == fixture['data']['s'] - # assert builder.transaction.serialize().hex() == fixture['serialized'] + assert builder.transaction.serialize().hex() == fixture['serialized'] assert builder.transaction.data['id'] == fixture['data']['id'] assert builder.verify() diff --git a/tests/transactions/builder/test_unvote_builder.py b/tests/transactions/builder/test_unvote_builder.py index 1a286ca8..48896d73 100644 --- a/tests/transactions/builder/test_unvote_builder.py +++ b/tests/transactions/builder/test_unvote_builder.py @@ -5,14 +5,24 @@ def test_unvote_transaction(passphrase, load_transaction_fixture): builder = ( UnvoteBuilder() - .gas_price(fixture['data']['gasPrice']) - .nonce(fixture['data']['nonce']) - .network(fixture['data']['network']) - .gas_limit(fixture['data']['gasLimit']) - .recipient_address(fixture['data']['recipientAddress']) - .sign(passphrase) + .gas_price(fixture['data']['gasPrice']) + .nonce(fixture['data']['nonce']) + .network(fixture['data']['network']) + .gas_limit(fixture['data']['gasLimit']) + .recipient_address(fixture['data']['recipientAddress']) + .sign(passphrase) ) + assert builder.transaction.data['gasPrice'] == fixture['data']['gasPrice'] + assert builder.transaction.data['nonce'] == fixture['data']['nonce'] + assert builder.transaction.data['network'] == fixture['data']['network'] + assert builder.transaction.data['gasLimit'] == fixture['data']['gasLimit'] + assert builder.transaction.data['recipientAddress'] == fixture['data']['recipientAddress'] + assert builder.transaction.data['value'] == fixture['data']['value'] + assert builder.transaction.data['v'] == fixture['data']['v'] + assert builder.transaction.data['r'] == fixture['data']['r'] + assert builder.transaction.data['s'] == fixture['data']['s'] + assert builder.transaction.serialize().hex() == fixture['serialized'] assert builder.transaction.data['id'] == fixture['data']['id'] - assert builder.verify() \ No newline at end of file + assert builder.verify() diff --git a/tests/transactions/builder/test_validator_registration_builder.py b/tests/transactions/builder/test_validator_registration_builder.py index 874cfe22..284cd43a 100644 --- a/tests/transactions/builder/test_validator_registration_builder.py +++ b/tests/transactions/builder/test_validator_registration_builder.py @@ -5,15 +5,25 @@ def test_validator_registration_transaction(passphrase, load_transaction_fixture builder = ( ValidatorRegistrationBuilder() - .gas_price(fixture['data']['gasPrice']) - .nonce(fixture['data']['nonce']) - .network(fixture['data']['network']) - .gas_limit(fixture['data']['gasLimit']) - .validator_public_key('a08058db53e2665c84a40f5152e76dd2b652125a6079130d4c315e728bcf4dd1dfb44ac26e82302331d61977d3141118') - .recipient_address(fixture['data']['recipientAddress']) - .sign(passphrase) + .gas_price(fixture['data']['gasPrice']) + .nonce(fixture['data']['nonce']) + .network(fixture['data']['network']) + .gas_limit(fixture['data']['gasLimit']) + .validator_public_key('954f46d6097a1d314e900e66e11e0dad0a57cd03e04ec99f0dedd1c765dcb11e6d7fa02e22cf40f9ee23d9cc1c0624bd') + .recipient_address(fixture['data']['recipientAddress']) + .sign(passphrase) ) + assert builder.transaction.data['gasPrice'] == fixture['data']['gasPrice'] + assert builder.transaction.data['nonce'] == fixture['data']['nonce'] + assert builder.transaction.data['network'] == fixture['data']['network'] + assert builder.transaction.data['gasLimit'] == fixture['data']['gasLimit'] + assert builder.transaction.data['recipientAddress'] == fixture['data']['recipientAddress'] + assert builder.transaction.data['value'] == fixture['data']['value'] + assert builder.transaction.data['v'] == fixture['data']['v'] + assert builder.transaction.data['r'] == fixture['data']['r'] + assert builder.transaction.data['s'] == fixture['data']['s'] + assert builder.transaction.serialize().hex() == fixture['serialized'] assert builder.transaction.data['id'] == fixture['data']['id'] - assert builder.verify() \ No newline at end of file + assert builder.verify() diff --git a/tests/transactions/builder/test_validator_resignation_builder.py b/tests/transactions/builder/test_validator_resignation_builder.py index e1adfaf1..d51340a2 100644 --- a/tests/transactions/builder/test_validator_resignation_builder.py +++ b/tests/transactions/builder/test_validator_resignation_builder.py @@ -5,14 +5,24 @@ def test_validator_resignation_transaction(passphrase, load_transaction_fixture) builder = ( ValidatorResignationBuilder() - .gas_price(fixture['data']['gasPrice']) - .nonce(fixture['data']['nonce']) - .network(fixture['data']['network']) - .gas_limit(fixture['data']['gasLimit']) - .recipient_address(fixture['data']['recipientAddress']) - .sign(passphrase) + .gas_price(fixture['data']['gasPrice']) + .nonce(fixture['data']['nonce']) + .network(fixture['data']['network']) + .gas_limit(fixture['data']['gasLimit']) + .recipient_address(fixture['data']['recipientAddress']) + .sign(passphrase) ) + assert builder.transaction.data['gasPrice'] == fixture['data']['gasPrice'] + assert builder.transaction.data['nonce'] == fixture['data']['nonce'] + assert builder.transaction.data['network'] == fixture['data']['network'] + assert builder.transaction.data['gasLimit'] == fixture['data']['gasLimit'] + assert builder.transaction.data['recipientAddress'] == fixture['data']['recipientAddress'] + assert builder.transaction.data['value'] == fixture['data']['value'] + assert builder.transaction.data['v'] == fixture['data']['v'] + assert builder.transaction.data['r'] == fixture['data']['r'] + assert builder.transaction.data['s'] == fixture['data']['s'] + assert builder.transaction.serialize().hex() == fixture['serialized'] assert builder.transaction.data['id'] == fixture['data']['id'] - assert builder.verify() \ No newline at end of file + assert builder.verify() diff --git a/tests/transactions/builder/test_vote_builder.py b/tests/transactions/builder/test_vote_builder.py index a8a6f8f9..a94161f3 100644 --- a/tests/transactions/builder/test_vote_builder.py +++ b/tests/transactions/builder/test_vote_builder.py @@ -5,15 +5,25 @@ def test_vote_transaction(passphrase, load_transaction_fixture): builder = ( VoteBuilder() - .gas_price(fixture['data']['gasPrice']) - .nonce(fixture['data']['nonce']) - .network(fixture['data']['network']) - .gas_limit(fixture['data']['gasLimit']) - .recipient_address(fixture['data']['recipientAddress']) - .vote('0x512F366D524157BcF734546eB29a6d687B762255') # Example vote address - .sign(passphrase) + .gas_price(fixture['data']['gasPrice']) + .nonce(fixture['data']['nonce']) + .network(fixture['data']['network']) + .gas_limit(fixture['data']['gasLimit']) + .recipient_address(fixture['data']['recipientAddress']) + .vote('0xC3bBE9B1CeE1ff85Ad72b87414B0E9B7F2366763') # Example vote address + .sign(passphrase) ) + assert builder.transaction.data['gasPrice'] == fixture['data']['gasPrice'] + assert builder.transaction.data['nonce'] == fixture['data']['nonce'] + assert builder.transaction.data['network'] == fixture['data']['network'] + assert builder.transaction.data['gasLimit'] == fixture['data']['gasLimit'] + assert builder.transaction.data['recipientAddress'] == fixture['data']['recipientAddress'] + assert builder.transaction.data['value'] == fixture['data']['value'] + assert builder.transaction.data['v'] == fixture['data']['v'] + assert builder.transaction.data['r'] == fixture['data']['r'] + assert builder.transaction.data['s'] == fixture['data']['s'] + assert builder.transaction.serialize().hex() == fixture['serialized'] assert builder.transaction.data['id'] == fixture['data']['id'] - assert builder.verify() \ No newline at end of file + assert builder.verify() From c654e0d9be1e1a4b912e6a46384aad85da910e8c Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:24:09 +0000 Subject: [PATCH 07/20] updates to deserialize & rlp decoding --- crypto/transactions/deserializer.py | 112 ++++++++++++---------------- crypto/utils/rlp_decoder.py | 16 +++- 2 files changed, 61 insertions(+), 67 deletions(-) diff --git a/crypto/transactions/deserializer.py b/crypto/transactions/deserializer.py index 416a5c40..1c2cf2e8 100644 --- a/crypto/transactions/deserializer.py +++ b/crypto/transactions/deserializer.py @@ -1,3 +1,4 @@ +import re from crypto.transactions.types.abstract_transaction import AbstractTransaction from crypto.transactions.types.transfer import Transfer from crypto.transactions.types.evm_call import EvmCall @@ -5,94 +6,60 @@ from crypto.transactions.types.unvote import Unvote from crypto.transactions.types.validator_registration import ValidatorRegistration from crypto.transactions.types.validator_resignation import ValidatorResignation -from binascii import unhexlify, hexlify +from binascii import unhexlify -from binary.unsigned_integer.reader import ( - read_bit8, - read_bit32, - read_bit64, -) from crypto.enums.abi_function import AbiFunction from crypto.utils.abi_decoder import AbiDecoder +from crypto.utils.rlp_decoder import RlpDecoder class Deserializer: SIGNATURE_SIZE = 64 RECOVERY_SIZE = 1 + EIP1559_PREFIX = '02' def __init__(self, serialized: str): self.serialized = unhexlify(serialized) if isinstance(serialized, str) else serialized self.pointer = 0 + self.encoded_rlp = '0x' + serialized[2:] + @staticmethod def new(serialized: str): return Deserializer(serialized) def deserialize(self) -> AbstractTransaction: - data = {} - - self.deserialize_common(data) - self.deserialize_data(data) - transaction = self.guess_transaction_from_data(data) - self.deserialize_signatures(data) - - transaction.data = data - transaction.recover_sender() - - transaction.data['id'] = transaction.hash(skip_signature=False).hex() - - return transaction - - def read_bytes(self, length: int) -> bytes: - result = self.serialized[self.pointer:self.pointer + length] - self.pointer += length - return result - - def deserialize_common(self, data: dict): - data['network'] = read_bit8(self.serialized, self.pointer) - self.pointer += 1 - - nonce = read_bit64(self.serialized, self.pointer) - data['nonce'] = str(nonce) - self.pointer += 8 - - gas_price = read_bit32(self.serialized, self.pointer) - data['gasPrice'] = gas_price - self.pointer += 4 + print('ENCODED_RLP:', self.encoded_rlp) - gas_limit = read_bit32(self.serialized, self.pointer) - data['gasLimit'] = gas_limit - self.pointer += 4 + decoded_rlp = RlpDecoder.decode(self.encoded_rlp) - data['value'] = '0' + print('DECODED_RLP:', decoded_rlp) - def deserialize_data(self, data: dict): - value = int.from_bytes(self.serialized[self.pointer:self.pointer + 32], byteorder='big') - self.pointer += 32 - - data['value'] = str(value) + data = { + 'network': Deserializer.parse_number(decoded_rlp[0]), + 'nonce': Deserializer.parse_big_number(decoded_rlp[1]), + 'gasPrice': Deserializer.parse_number(decoded_rlp[3]), + 'gasLimit': Deserializer.parse_number(decoded_rlp[4]), + 'recipientAddress': Deserializer.parse_address(decoded_rlp[5]), + 'value': Deserializer.parse_big_number(decoded_rlp[6]), + 'data': Deserializer.parse_hex(decoded_rlp[7]), + } - recipient_marker = read_bit8(self.serialized, self.pointer) - self.pointer += 1 + if len(decoded_rlp) == 12: + data['v'] = Deserializer.parse_number(decoded_rlp[9]) + 31 + data['r'] = Deserializer.parse_hex(decoded_rlp[10]) + data['s'] = Deserializer.parse_hex(decoded_rlp[11]) - if recipient_marker == 1: - recipient_address_bytes = self.read_bytes(20) - recipient_address = '0x' + hexlify(recipient_address_bytes).decode() - data['recipientAddress'] = recipient_address + transaction = self.guess_transaction_from_data(data) - payload_length = read_bit32(self.serialized, self.pointer) - self.pointer += 4 + serialized_hex = self.EIP1559_PREFIX + self.encoded_rlp + # transaction.serialized = serialized_hex - payload_hex = '' - if payload_length > 0: - payload_bytes = self.read_bytes(payload_length) - payload_hex = hexlify(payload_bytes).decode() + transaction.data = data + transaction.recover_sender() - data['data'] = payload_hex + transaction.data['id'] = transaction.get_id() - def deserialize_signatures(self, data: dict): - signature_length = self.SIGNATURE_SIZE + self.RECOVERY_SIZE - signature_bytes = self.read_bytes(signature_length) - data['signature'] = hexlify(signature_bytes).decode() + return transaction def guess_transaction_from_data(self, data: dict) -> AbstractTransaction: if data['value'] != '0': @@ -115,7 +82,7 @@ def guess_transaction_from_data(self, data: dict) -> AbstractTransaction: else: return EvmCall(data) - def decode_payload(self, data: dict) -> dict: + def decode_payload(self, data: dict) -> dict | None: payload = data.get('data', '') if payload == '': @@ -126,4 +93,21 @@ def decode_payload(self, data: dict) -> dict: return decoder.decode_function_data(payload) except Exception as e: print(f"Error decoding payload: {str(e)}") - return None \ No newline at end of file + + return None + + @staticmethod + def parse_number(value: str) -> int: + return 0 if value == '0x' else int(value, 16) + + @staticmethod + def parse_big_number(value: str) -> str: + return str(Deserializer.parse_number(value)) + + @staticmethod + def parse_hex(value: str) -> str: + return re.sub(r'^0x', '', value) + + @staticmethod + def parse_address(value: str) -> str | None: + return None if value == '0x' else value diff --git a/crypto/utils/rlp_decoder.py b/crypto/utils/rlp_decoder.py index a5f62a0b..c7a4a6ad 100644 --- a/crypto/utils/rlp_decoder.py +++ b/crypto/utils/rlp_decoder.py @@ -1,8 +1,13 @@ import re +from typing import TypedDict + +class DecodedType(TypedDict): + consumed: int + result: str | list class RlpDecoder: @classmethod - def decode(cls, data: str): + def decode(cls, data: str) -> str | list: bytes_data = cls.get_bytes(data, 'data') decoded = cls._decode(bytes_data, 0) @@ -17,6 +22,7 @@ def get_bytes(value: str, name: str = 'value') -> list: hex_value = value[2:] length = len(hex_value) // 2 bytes_data = [int(hex_value[i * 2:i * 2 + 2], 16) for i in range(length)] + return bytes_data raise ValueError(f'Invalid BytesLike value for "{name}": {value}') @@ -34,10 +40,11 @@ def unarrayify_integer(data, offset, length) -> int: result = 0 for i in range(length): result = (result << 8) + data[offset + i] + return result @classmethod - def _decode_children(cls, data, offset, child_offset, length) -> dict: + def _decode_children(cls, data, offset, child_offset, length) -> DecodedType: result = [] end = offset + 1 + length @@ -55,7 +62,7 @@ def _decode_children(cls, data, offset, child_offset, length) -> dict: } @classmethod - def _decode(cls, data, offset) -> dict[str, int | str]: + def _decode(cls, data, offset) -> DecodedType: cls.check_offset(offset, data) prefix = data[offset] @@ -68,12 +75,14 @@ def _decode(cls, data, offset) -> dict[str, int | str]: cls.check_offset(offset + 1 + length_length + length - 1, data) return cls._decode_children(data, offset, offset + 1 + length_length, length_length + length) + elif prefix >= 0xc0: length = prefix - 0xc0 if length > 0: cls.check_offset(offset + 1 + length - 1, data) return cls._decode_children(data, offset, offset + 1, length) + elif prefix >= 0xb8: length_length = prefix - 0xb7 cls.check_offset(offset + length_length, data) @@ -87,6 +96,7 @@ def _decode(cls, data, offset) -> dict[str, int | str]: 'consumed': 1 + length_length + length, 'result': cls.hexlify(slice_data), } + elif prefix >= 0x80: length = prefix - 0x80 if length > 0: From b18007c1c8bf87029454e52884140bce1f9d8a37 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:24:29 +0000 Subject: [PATCH 08/20] update recover sender to use updated hash --- crypto/transactions/types/abstract_transaction.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crypto/transactions/types/abstract_transaction.py b/crypto/transactions/types/abstract_transaction.py index 48d6dcb7..9f621dd3 100644 --- a/crypto/transactions/types/abstract_transaction.py +++ b/crypto/transactions/types/abstract_transaction.py @@ -57,12 +57,11 @@ def get_public_key(self, compact_signature, hash_): return public_key def recover_sender(self): - signature_hex = self.data.get('signature') - if not signature_hex: - raise ValueError("No signature to recover from") + signature_with_recid = self.get_signature() + if not signature_with_recid: + return False - signature_with_recid = bytes.fromhex(signature_hex) - hash_ = self.hash(skip_signature=True) + hash_ = bytes.fromhex(self.hash(skip_signature=True)) public_key = self.get_public_key(signature_with_recid, hash_) self.data['senderPublicKey'] = public_key.format().hex() self.data['senderAddress'] = address_from_public_key(self.data['senderPublicKey']) From d5fabf310708db7b46e037a73a2aea2d1ada97b0 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:27:16 +0000 Subject: [PATCH 09/20] remove debug --- crypto/transactions/deserializer.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/crypto/transactions/deserializer.py b/crypto/transactions/deserializer.py index 1c2cf2e8..6a81a2bf 100644 --- a/crypto/transactions/deserializer.py +++ b/crypto/transactions/deserializer.py @@ -28,12 +28,8 @@ def new(serialized: str): return Deserializer(serialized) def deserialize(self) -> AbstractTransaction: - print('ENCODED_RLP:', self.encoded_rlp) - decoded_rlp = RlpDecoder.decode(self.encoded_rlp) - print('DECODED_RLP:', decoded_rlp) - data = { 'network': Deserializer.parse_number(decoded_rlp[0]), 'nonce': Deserializer.parse_big_number(decoded_rlp[1]), @@ -51,9 +47,6 @@ def deserialize(self) -> AbstractTransaction: transaction = self.guess_transaction_from_data(data) - serialized_hex = self.EIP1559_PREFIX + self.encoded_rlp - # transaction.serialized = serialized_hex - transaction.data = data transaction.recover_sender() From 9b4052e475971c2ee086f07d24e9f608c7882b61 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:27:23 +0000 Subject: [PATCH 10/20] update tests --- tests/transactions/test_deserializer.py | 28 ++++++++++++------------- tests/transactions/test_transaction.py | 17 ++++++++++----- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/tests/transactions/test_deserializer.py b/tests/transactions/test_deserializer.py index 50357cd2..3da00119 100644 --- a/tests/transactions/test_deserializer.py +++ b/tests/transactions/test_deserializer.py @@ -16,33 +16,33 @@ def assert_deserialized(fixture, keys): def test_deserialize_transfer(load_transaction_fixture): fixture = load_transaction_fixture('transfer') - transaction = assert_deserialized(fixture, ['id', 'nonce', 'gasPrice', 'gasLimit', 'value', 'signature']) - + transaction = assert_deserialized(fixture, ['id', 'nonce', 'gasPrice', 'gasLimit', 'value', 'v', 'r', 's']) + assert isinstance(transaction, Transfer) - assert transaction.data['value'] == '10000000000000000000' + assert transaction.data['value'] == '100000000' def test_deserialize_vote(load_transaction_fixture): fixture = load_transaction_fixture('vote') - transaction = assert_deserialized(fixture, ['id', 'nonce', 'gasPrice', 'gasLimit', 'signature']) - + transaction = assert_deserialized(fixture, ['id', 'nonce', 'gasPrice', 'gasLimit', 'v', 'r', 's']) + assert isinstance(transaction, Vote) - assert transaction.data['vote'] == '0x512F366D524157BcF734546eB29a6d687B762255' - assert transaction.data['id'] == '749744e0d689c46e37ff2993a984599eac4989a9ef0028337b335c9d43abf936' + assert transaction.data['vote'].lower() == '0xc3bbe9b1cee1ff85ad72b87414b0e9b7f2366763' + assert transaction.data['id'] == '991a3a63dc47be84d7982acb4c2aae488191373f31b8097e07d3ad95c0997e69' def test_deserialize_unvote(load_transaction_fixture): fixture = load_transaction_fixture('unvote') - transaction = assert_deserialized(fixture, ['id', 'nonce', 'gasPrice', 'gasLimit', 'signature']) - + transaction = assert_deserialized(fixture, ['id', 'nonce', 'gasPrice', 'gasLimit', 'v', 'r', 's']) + assert isinstance(transaction, Unvote) def test_deserialize_validator_registration(load_transaction_fixture): fixture = load_transaction_fixture('validator-registration') - transaction = assert_deserialized(fixture, ['id', 'nonce', 'gasPrice', 'gasLimit', 'signature']) - + transaction = assert_deserialized(fixture, ['id', 'nonce', 'gasPrice', 'gasLimit', 'v', 'r', 's']) + assert isinstance(transaction, ValidatorRegistration) def test_deserialize_validator_resignation(load_transaction_fixture): fixture = load_transaction_fixture('validator-resignation') - transaction = assert_deserialized(fixture, ['id', 'nonce', 'gasPrice', 'gasLimit', 'signature']) - - assert isinstance(transaction, ValidatorResignation) \ No newline at end of file + transaction = assert_deserialized(fixture, ['id', 'nonce', 'gasPrice', 'gasLimit', 'v', 'r', 's']) + + assert isinstance(transaction, ValidatorResignation) diff --git a/tests/transactions/test_transaction.py b/tests/transactions/test_transaction.py index 1b316f35..8b1a5f65 100644 --- a/tests/transactions/test_transaction.py +++ b/tests/transactions/test_transaction.py @@ -1,6 +1,5 @@ from crypto.identity.private_key import PrivateKey from crypto.transactions.deserializer import Deserializer -from crypto.transactions.types.abstract_transaction import AbstractTransaction def test_compute_id_of_transaction(load_transaction_fixture): transaction = Deserializer.new(load_transaction_fixture('transfer')['serialized']).deserialize() @@ -10,11 +9,19 @@ def test_sign_transaction_with_passphrase(load_transaction_fixture): private_key = PrivateKey.from_passphrase('this is a top secret passphrase') transaction = Deserializer.new(load_transaction_fixture('transfer')['serialized']).deserialize() - transaction.data['signature'] = '' - - assert 'signature' not in transaction.data or transaction.data['signature'] == '' + transaction.data['v'] = '' + transaction.data['r'] = '' + transaction.data['s'] = '' + + assert 'v' not in transaction.data or transaction.data['v'] == '' + assert 'r' not in transaction.data or transaction.data['r'] == '' + assert 's' not in transaction.data or transaction.data['s'] == '' + transaction.sign(private_key) - assert transaction.data['signature'] != '' + + assert transaction.data['v'] != '' + assert transaction.data['r'] != '' + assert transaction.data['s'] != '' def test_verify_transaction(load_transaction_fixture): transaction = Deserializer.new(load_transaction_fixture('transfer')['serialized']).deserialize(); From 3e60ef52359b24edf93fc15c10071a044d9db1c1 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:51:32 +0000 Subject: [PATCH 11/20] adjust typehints --- crypto/utils/rlp_decoder.py | 14 +++++++------- crypto/utils/rlp_encoder.py | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/crypto/utils/rlp_decoder.py b/crypto/utils/rlp_decoder.py index c7a4a6ad..d9c22a02 100644 --- a/crypto/utils/rlp_decoder.py +++ b/crypto/utils/rlp_decoder.py @@ -17,7 +17,7 @@ def decode(cls, data: str) -> str | list: return decoded['result'] @staticmethod - def get_bytes(value: str, name: str = 'value') -> list: + def get_bytes(value: str, name: str = 'value') -> list[int]: if re.match(r'^0x(?:[0-9a-fA-F]{2})*$', value): hex_value = value[2:] length = len(hex_value) // 2 @@ -28,15 +28,15 @@ def get_bytes(value: str, name: str = 'value') -> list: raise ValueError(f'Invalid BytesLike value for "{name}": {value}') @staticmethod - def hexlify(data) -> str: + def hexlify(data: list[int]) -> str: return '0x' + ''.join(f'{byte:02x}' for byte in data) @staticmethod - def hexlify_byte(value) -> str: + def hexlify_byte(value: int) -> str: return f'0x{value & 0xff:02x}' @staticmethod - def unarrayify_integer(data, offset, length) -> int: + def unarrayify_integer(data: list[int], offset: int, length: int) -> int: result = 0 for i in range(length): result = (result << 8) + data[offset + i] @@ -44,7 +44,7 @@ def unarrayify_integer(data, offset, length) -> int: return result @classmethod - def _decode_children(cls, data, offset, child_offset, length) -> DecodedType: + def _decode_children(cls, data: list[int], offset: int, child_offset: int, length: int) -> DecodedType: result = [] end = offset + 1 + length @@ -62,7 +62,7 @@ def _decode_children(cls, data, offset, child_offset, length) -> DecodedType: } @classmethod - def _decode(cls, data, offset) -> DecodedType: + def _decode(cls, data: list[int], offset: int) -> DecodedType: cls.check_offset(offset, data) prefix = data[offset] @@ -114,6 +114,6 @@ def _decode(cls, data, offset) -> DecodedType: } @staticmethod - def check_offset(offset, data) -> None: + def check_offset(offset: int, data: list[int]) -> None: if offset > len(data): raise ValueError('data short segment or out of range') diff --git a/crypto/utils/rlp_encoder.py b/crypto/utils/rlp_encoder.py index 21ed885d..70b4d72d 100644 --- a/crypto/utils/rlp_encoder.py +++ b/crypto/utils/rlp_encoder.py @@ -1,10 +1,10 @@ from binascii import hexlify, unhexlify - +from typing import Union class RlpEncoder: @classmethod - def encode(cls, obj) -> str: - encoded = cls._encode(obj) + def encode(cls, data: Union[str, bytes, int, list]) -> str: + encoded = cls._encode(data) hex_str = '' nibbles = '0123456789abcdef' @@ -15,10 +15,10 @@ def encode(cls, obj) -> str: return hex_str @classmethod - def _encode(cls, obj) -> list: - if isinstance(obj, list): + def _encode(cls, data: Union[str, bytes, int, list]) -> list: + if isinstance(data, list): payload = [] - for child in obj: + for child in data: payload.extend(cls._encode(child)) payload_length = len(payload) @@ -32,7 +32,7 @@ def _encode(cls, obj) -> list: return length + payload - data = cls.get_bytes(obj) + data = cls.get_bytes(data) data_length = len(data) if data_length == 1 and data[0] <= 0x7f: return data @@ -57,7 +57,7 @@ def arrayify_integer(value) -> list: return result @staticmethod - def get_bytes(value) -> list: + def get_bytes(value: Union[str, bytes, int, list]) -> list: if isinstance(value, str) or isinstance(value, bytes): if isinstance(value, str): value = value.encode() From 8aefce3946d1c777f4ec62bc913562504629b70a Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:51:40 +0000 Subject: [PATCH 12/20] additional deserializer tests --- tests/transactions/test_deserializer.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/transactions/test_deserializer.py b/tests/transactions/test_deserializer.py index 3da00119..4c4c44ba 100644 --- a/tests/transactions/test_deserializer.py +++ b/tests/transactions/test_deserializer.py @@ -46,3 +46,27 @@ def test_deserialize_validator_resignation(load_transaction_fixture): transaction = assert_deserialized(fixture, ['id', 'nonce', 'gasPrice', 'gasLimit', 'v', 'r', 's']) assert isinstance(transaction, ValidatorResignation) + +def test_parse_number(): + assert Deserializer.parse_number('0x01') == 1 + assert Deserializer.parse_number('0x0100') == 256 + assert Deserializer.parse_number('0x010000') == 65536 + assert Deserializer.parse_number('0x') == 0 + +def test_parse_big_number(): + assert Deserializer.parse_big_number('0x01') == '1' + assert Deserializer.parse_big_number('0x0100') == '256' + assert Deserializer.parse_big_number('0x010000') == '65536' + assert Deserializer.parse_big_number('0x') == '0' + assert Deserializer.parse_big_number('0x52B7D2DCC80CD2E4000000') == '100000000000000000000000000' + +def test_parse_hex(): + assert Deserializer.parse_hex('0x01') == '01' + assert Deserializer.parse_hex('0x0100') == '0100' + assert Deserializer.parse_hex('0x010000') == '010000' + assert Deserializer.parse_hex('0x') == '' + assert Deserializer.parse_hex('0x52B7D2DCC80CD2E4000000') == '52B7D2DCC80CD2E4000000' + +def test_parse_address(): + assert Deserializer.parse_address('0x52B7D2DCC80CD2E4000000') == '0x52B7D2DCC80CD2E4000000' + assert Deserializer.parse_address('0x') == None From 0a792fa4f66d8c4e56ee57ee046e1fb115774c47 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:51:45 +0000 Subject: [PATCH 13/20] test --- tests/utils/test_rpl_decoder.py | 43 +++++++++++++++++++++++++++++++++ tests/utils/test_rpl_encoder.py | 31 ++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 tests/utils/test_rpl_decoder.py create mode 100644 tests/utils/test_rpl_encoder.py diff --git a/tests/utils/test_rpl_decoder.py b/tests/utils/test_rpl_decoder.py new file mode 100644 index 00000000..c0098dd9 --- /dev/null +++ b/tests/utils/test_rpl_decoder.py @@ -0,0 +1,43 @@ +from crypto.transactions.deserializer import Deserializer +from crypto.utils.rlp_decoder import RlpDecoder +from crypto.utils.transaction_utils import TransactionUtils + + +def test_decode_function_call(load_transaction_fixture): + fixture = load_transaction_fixture('transfer') + + decoded_rlp = RlpDecoder.decode('0x' + fixture['serialized'][2:]) + + assert len(decoded_rlp) == 12 + assert decoded_rlp[0] == '0x1e' + assert decoded_rlp[1] == '0x01' + assert decoded_rlp[2] == '0x' + assert decoded_rlp[3] == '0x05' + assert decoded_rlp[4] == '0x5208' + assert decoded_rlp[5] == '0x6f0182a0cc707b055322ccf6d4cb6a5aff1aeb22' + assert decoded_rlp[6] == '0x05f5e100' + assert decoded_rlp[7] == '0x' + assert decoded_rlp[8] == [] + assert decoded_rlp[9] == '0x' + assert decoded_rlp[10] == '0x0567c4def813a66e03fa1cd499a27c6922698a67e25e0b38458d8f4bb0e581fc' + assert decoded_rlp[11] == '0x25fcdf9d110b82bde15bae4a49118deb83cfc3ec1656c2a29286d7836d328abe' + +def test_decoding_str(): + decoded = RlpDecoder.decode('0x8774657374696e67') + + assert decoded == '0x74657374696e67' + +def test_decoding_bytes(): + decoded = RlpDecoder.decode('0x8774657374696e67') + + assert decoded == '0x74657374696e67' + +def test_decoding_list(): + decoded = RlpDecoder.decode('0xc88774657374696e67') + + assert decoded == ['0x74657374696e67'] + +def test_decoding_int(): + decoded = RlpDecoder.decode('0x86313233343536') + + assert decoded == '0x313233343536' diff --git a/tests/utils/test_rpl_encoder.py b/tests/utils/test_rpl_encoder.py new file mode 100644 index 00000000..ca73437f --- /dev/null +++ b/tests/utils/test_rpl_encoder.py @@ -0,0 +1,31 @@ +from crypto.utils.rlp_encoder import RlpEncoder +from crypto.utils.transaction_utils import TransactionUtils + + +def test_encode_function_call(load_transaction_fixture): + fixture = load_transaction_fixture('transfer') + + # Calls RlpEncoder.encode() with the given transaction + transaction_hash = TransactionUtils.to_buffer(fixture['data']) + + assert transaction_hash.decode() == fixture['serialized'] + +def test_encoding_str(): + encoded = RlpEncoder.encode('testing') + + assert encoded == '8774657374696e67' + +def test_encoding_bytes(): + encoded = RlpEncoder.encode(b'testing') + + assert encoded == '8774657374696e67' + +def test_encoding_list(): + encoded = RlpEncoder.encode(['testing']) + + assert encoded == 'c88774657374696e67' + +def test_encoding_int(): + encoded = RlpEncoder.encode('123456') + + assert encoded == '86313233343536' From 9e5fba820518e5cb469fe0afba197a17fb63f2a9 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Wed, 12 Feb 2025 18:05:33 +0000 Subject: [PATCH 14/20] use union --- crypto/utils/rlp_decoder.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crypto/utils/rlp_decoder.py b/crypto/utils/rlp_decoder.py index d9c22a02..352b2473 100644 --- a/crypto/utils/rlp_decoder.py +++ b/crypto/utils/rlp_decoder.py @@ -1,13 +1,13 @@ import re -from typing import TypedDict +from typing import TypedDict, Union class DecodedType(TypedDict): consumed: int - result: str | list + result: Union[str, list] class RlpDecoder: @classmethod - def decode(cls, data: str) -> str | list: + def decode(cls, data: str) -> Union[str, list]: bytes_data = cls.get_bytes(data, 'data') decoded = cls._decode(bytes_data, 0) From 71c71117de954f7337bbdb4cd193a46d76328457 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Wed, 12 Feb 2025 18:07:43 +0000 Subject: [PATCH 15/20] optional typing --- crypto/transactions/deserializer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crypto/transactions/deserializer.py b/crypto/transactions/deserializer.py index 6a81a2bf..4875daa7 100644 --- a/crypto/transactions/deserializer.py +++ b/crypto/transactions/deserializer.py @@ -1,4 +1,5 @@ import re +from typing import Optional from crypto.transactions.types.abstract_transaction import AbstractTransaction from crypto.transactions.types.transfer import Transfer from crypto.transactions.types.evm_call import EvmCall @@ -75,7 +76,7 @@ def guess_transaction_from_data(self, data: dict) -> AbstractTransaction: else: return EvmCall(data) - def decode_payload(self, data: dict) -> dict | None: + def decode_payload(self, data: dict) -> Optional[dict]: payload = data.get('data', '') if payload == '': @@ -102,5 +103,5 @@ def parse_hex(value: str) -> str: return re.sub(r'^0x', '', value) @staticmethod - def parse_address(value: str) -> str | None: + def parse_address(value: str) -> Optional[str]: return None if value == '0x' else value From e03f1893dcd10ccbe20744e3c23a70b546db399e Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Wed, 12 Feb 2025 23:47:09 +0000 Subject: [PATCH 16/20] tidy up --- crypto/transactions/deserializer.py | 2 +- crypto/utils/rlp_decoder.py | 55 ++++++++++++++------------ crypto/utils/rlp_encoder.py | 24 +++++------ crypto/utils/transaction_utils.py | 2 +- tests/transactions/test_transaction.py | 8 ++-- tests/utils/test_rpl_decoder.py | 2 - 6 files changed, 48 insertions(+), 45 deletions(-) diff --git a/crypto/transactions/deserializer.py b/crypto/transactions/deserializer.py index 4875daa7..6ca72ddf 100644 --- a/crypto/transactions/deserializer.py +++ b/crypto/transactions/deserializer.py @@ -1,4 +1,5 @@ import re +from binascii import unhexlify from typing import Optional from crypto.transactions.types.abstract_transaction import AbstractTransaction from crypto.transactions.types.transfer import Transfer @@ -7,7 +8,6 @@ from crypto.transactions.types.unvote import Unvote from crypto.transactions.types.validator_registration import ValidatorRegistration from crypto.transactions.types.validator_resignation import ValidatorResignation -from binascii import unhexlify from crypto.enums.abi_function import AbiFunction from crypto.utils.abi_decoder import AbiDecoder diff --git a/crypto/utils/rlp_decoder.py b/crypto/utils/rlp_decoder.py index 352b2473..a42fe729 100644 --- a/crypto/utils/rlp_decoder.py +++ b/crypto/utils/rlp_decoder.py @@ -8,8 +8,11 @@ class DecodedType(TypedDict): class RlpDecoder: @classmethod def decode(cls, data: str) -> Union[str, list]: - bytes_data = cls.get_bytes(data, 'data') - decoded = cls._decode(bytes_data, 0) + """ + Decode RLP data from a hex string. + """ + bytes_data = cls.__get_bytes(data) + decoded = cls.__decode(bytes_data, 0) if decoded['consumed'] != len(bytes_data): raise ValueError('unexpected junk after RLP payload') @@ -17,7 +20,7 @@ def decode(cls, data: str) -> Union[str, list]: return decoded['result'] @staticmethod - def get_bytes(value: str, name: str = 'value') -> list[int]: + def __get_bytes(value: str) -> list[int]: if re.match(r'^0x(?:[0-9a-fA-F]{2})*$', value): hex_value = value[2:] length = len(hex_value) // 2 @@ -25,18 +28,18 @@ def get_bytes(value: str, name: str = 'value') -> list[int]: return bytes_data - raise ValueError(f'Invalid BytesLike value for "{name}": {value}') + raise ValueError(f'Invalid BytesLike value: {value}') @staticmethod - def hexlify(data: list[int]) -> str: + def __hexlify(data: list[int]) -> str: return '0x' + ''.join(f'{byte:02x}' for byte in data) @staticmethod - def hexlify_byte(value: int) -> str: + def __hexlify_byte(value: int) -> str: return f'0x{value & 0xff:02x}' @staticmethod - def unarrayify_integer(data: list[int], offset: int, length: int) -> int: + def __unarrayify_integer(data: list[int], offset: int, length: int) -> int: result = 0 for i in range(length): result = (result << 8) + data[offset + i] @@ -44,12 +47,12 @@ def unarrayify_integer(data: list[int], offset: int, length: int) -> int: return result @classmethod - def _decode_children(cls, data: list[int], offset: int, child_offset: int, length: int) -> DecodedType: + def __decode_children(cls, data: list[int], offset: int, child_offset: int, length: int) -> DecodedType: result = [] end = offset + 1 + length while child_offset < end: - decoded = cls._decode(data, child_offset) + decoded = cls.__decode(data, child_offset) result.append(decoded['result']) child_offset += decoded['consumed'] @@ -62,58 +65,58 @@ def _decode_children(cls, data: list[int], offset: int, child_offset: int, lengt } @classmethod - def _decode(cls, data: list[int], offset: int) -> DecodedType: - cls.check_offset(offset, data) + def __decode(cls, data: list[int], offset: int) -> DecodedType: + cls.__check_offset(offset, data) prefix = data[offset] if prefix >= 0xf8: length_length = prefix - 0xf7 - cls.check_offset(offset + length_length, data) + cls.__check_offset(offset + length_length, data) - length = cls.unarrayify_integer(data, offset + 1, length_length) - cls.check_offset(offset + 1 + length_length + length - 1, data) + length = cls.__unarrayify_integer(data, offset + 1, length_length) + cls.__check_offset(offset + 1 + length_length + length - 1, data) - return cls._decode_children(data, offset, offset + 1 + length_length, length_length + length) + return cls.__decode_children(data, offset, offset + 1 + length_length, length_length + length) elif prefix >= 0xc0: length = prefix - 0xc0 if length > 0: - cls.check_offset(offset + 1 + length - 1, data) + cls.__check_offset(offset + 1 + length - 1, data) - return cls._decode_children(data, offset, offset + 1, length) + return cls.__decode_children(data, offset, offset + 1, length) elif prefix >= 0xb8: length_length = prefix - 0xb7 - cls.check_offset(offset + length_length, data) + cls.__check_offset(offset + length_length, data) - length = cls.unarrayify_integer(data, offset + 1, length_length) + length = cls.__unarrayify_integer(data, offset + 1, length_length) if length > 0: - cls.check_offset(offset + 1 + length_length + length - 1, data) + cls.__check_offset(offset + 1 + length_length + length - 1, data) slice_data = data[offset + 1 + length_length:offset + 1 + length_length + length] return { 'consumed': 1 + length_length + length, - 'result': cls.hexlify(slice_data), + 'result': cls.__hexlify(slice_data), } elif prefix >= 0x80: length = prefix - 0x80 if length > 0: - cls.check_offset(offset + 1 + length - 1, data) + cls.__check_offset(offset + 1 + length - 1, data) slice_data = data[offset + 1:offset + 1 + length] return { 'consumed': 1 + length, - 'result': cls.hexlify(slice_data), + 'result': cls.__hexlify(slice_data), } return { 'consumed': 1, - 'result': cls.hexlify_byte(prefix), + 'result': cls.__hexlify_byte(prefix), } - @staticmethod - def check_offset(offset: int, data: list[int]) -> None: + @classmethod + def __check_offset(cls, offset: int, data: list[int]) -> None: if offset > len(data): raise ValueError('data short segment or out of range') diff --git a/crypto/utils/rlp_encoder.py b/crypto/utils/rlp_encoder.py index 70b4d72d..e5e63e85 100644 --- a/crypto/utils/rlp_encoder.py +++ b/crypto/utils/rlp_encoder.py @@ -1,10 +1,12 @@ -from binascii import hexlify, unhexlify from typing import Union class RlpEncoder: @classmethod def encode(cls, data: Union[str, bytes, int, list]) -> str: - encoded = cls._encode(data) + """ + Encodes the given data into RLP format. + """ + encoded = cls.__encode(data) hex_str = '' nibbles = '0123456789abcdef' @@ -15,11 +17,11 @@ def encode(cls, data: Union[str, bytes, int, list]) -> str: return hex_str @classmethod - def _encode(cls, data: Union[str, bytes, int, list]) -> list: + def __encode(cls, data: Union[str, bytes, int, list]) -> list: if isinstance(data, list): payload = [] for child in data: - payload.extend(cls._encode(child)) + payload.extend(cls.__encode(child)) payload_length = len(payload) if payload_length <= 55: @@ -27,12 +29,12 @@ def _encode(cls, data: Union[str, bytes, int, list]) -> list: return payload - length = cls.arrayify_integer(payload_length) + length = cls.__arrayify_integer(payload_length) length.insert(0, 0xf7 + (len(length))) return length + payload - data = cls.get_bytes(data) + data = cls.__get_bytes(data) data_length = len(data) if data_length == 1 and data[0] <= 0x7f: return data @@ -42,13 +44,13 @@ def _encode(cls, data: Union[str, bytes, int, list]) -> list: return data - length = cls.arrayify_integer(len(data)) + length = cls.__arrayify_integer(len(data)) length.insert(0, 0xb7 + len(length)) return length + data - @staticmethod - def arrayify_integer(value) -> list: + @classmethod + def __arrayify_integer(cls, value: int) -> list: result = [] while value > 0: result.insert(0, value & 0xff) @@ -56,8 +58,8 @@ def arrayify_integer(value) -> list: return result - @staticmethod - def get_bytes(value: Union[str, bytes, int, list]) -> list: + @classmethod + def __get_bytes(cls, value: Union[str, bytes, int, list]) -> list: if isinstance(value, str) or isinstance(value, bytes): if isinstance(value, str): value = value.encode() diff --git a/crypto/utils/transaction_utils.py b/crypto/utils/transaction_utils.py index 0ddab514..46904105 100644 --- a/crypto/utils/transaction_utils.py +++ b/crypto/utils/transaction_utils.py @@ -1,4 +1,4 @@ -from binascii import hexlify, unhexlify +from binascii import unhexlify import hashlib from crypto.utils.rlp_encoder import RlpEncoder diff --git a/tests/transactions/test_transaction.py b/tests/transactions/test_transaction.py index 8b1a5f65..477348ac 100644 --- a/tests/transactions/test_transaction.py +++ b/tests/transactions/test_transaction.py @@ -24,20 +24,20 @@ def test_sign_transaction_with_passphrase(load_transaction_fixture): assert transaction.data['s'] != '' def test_verify_transaction(load_transaction_fixture): - transaction = Deserializer.new(load_transaction_fixture('transfer')['serialized']).deserialize(); + transaction = Deserializer.new(load_transaction_fixture('transfer')['serialized']).deserialize() assert transaction.verify() def test_transaction_to_bytes(load_transaction_fixture): - transaction = Deserializer.new(load_transaction_fixture('transfer')['serialized']).deserialize(); + transaction = Deserializer.new(load_transaction_fixture('transfer')['serialized']).deserialize() actual = transaction.get_bytes() assert isinstance(actual, bytes) def test_transaction_to_array(load_transaction_fixture): - transaction = Deserializer.new(load_transaction_fixture('transfer')['serialized']).deserialize(); + transaction = Deserializer.new(load_transaction_fixture('transfer')['serialized']).deserialize() actual = transaction.to_dict() assert isinstance(actual, dict) def test_transaction_to_json(load_transaction_fixture): - transaction = Deserializer.new(load_transaction_fixture('transfer')['serialized']).deserialize(); + transaction = Deserializer.new(load_transaction_fixture('transfer')['serialized']).deserialize() actual = transaction.to_json() assert isinstance(actual, str) diff --git a/tests/utils/test_rpl_decoder.py b/tests/utils/test_rpl_decoder.py index c0098dd9..e896c426 100644 --- a/tests/utils/test_rpl_decoder.py +++ b/tests/utils/test_rpl_decoder.py @@ -1,6 +1,4 @@ -from crypto.transactions.deserializer import Deserializer from crypto.utils.rlp_decoder import RlpDecoder -from crypto.utils.transaction_utils import TransactionUtils def test_decode_function_call(load_transaction_fixture): From 7ebd0bf0ffc8844c57c1e88f839c834e4b2e4c8f Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Thu, 13 Feb 2025 12:53:00 +0000 Subject: [PATCH 17/20] use constants for recovery id offset --- crypto/enums/constants.py | 5 +++++ crypto/identity/private_key.py | 4 +++- crypto/transactions/deserializer.py | 4 ++-- crypto/transactions/types/abstract_transaction.py | 3 ++- crypto/utils/transaction_utils.py | 7 +++---- 5 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 crypto/enums/constants.py diff --git a/crypto/enums/constants.py b/crypto/enums/constants.py new file mode 100644 index 00000000..53ed6e28 --- /dev/null +++ b/crypto/enums/constants.py @@ -0,0 +1,5 @@ +from enum import Enum + +class Constants(Enum): + ETHEREUM_RECOVERY_ID_OFFSET = 27 + EIP_1559_PREFIX = '02' diff --git a/crypto/identity/private_key.py b/crypto/identity/private_key.py index 379361ed..1b3ca963 100644 --- a/crypto/identity/private_key.py +++ b/crypto/identity/private_key.py @@ -2,6 +2,8 @@ from hashlib import sha256 from coincurve import PrivateKey as PvtKey +from crypto.enums.constants import Constants + class PrivateKey(object): def __init__(self, private_key: str): self.private_key = PvtKey.from_hex(private_key) @@ -32,7 +34,7 @@ def sign_compact(self, message: bytes) -> bytes: pkey = PvtKey.from_hex(sha256(b'my super secret passphrase').hexdigest()) der = pkey.sign_recoverable(message) - return bytes([der[64] + 31]) + der[0:64] + return bytes([der[64] + Constants.ETHEREUM_RECOVERY_ID_OFFSET.value]) + der[0:64] def to_hex(self): """Returns a private key in hex format diff --git a/crypto/transactions/deserializer.py b/crypto/transactions/deserializer.py index 6ca72ddf..ab21002b 100644 --- a/crypto/transactions/deserializer.py +++ b/crypto/transactions/deserializer.py @@ -1,6 +1,7 @@ import re from binascii import unhexlify from typing import Optional +from crypto.enums.constants import Constants from crypto.transactions.types.abstract_transaction import AbstractTransaction from crypto.transactions.types.transfer import Transfer from crypto.transactions.types.evm_call import EvmCall @@ -16,7 +17,6 @@ class Deserializer: SIGNATURE_SIZE = 64 RECOVERY_SIZE = 1 - EIP1559_PREFIX = '02' def __init__(self, serialized: str): self.serialized = unhexlify(serialized) if isinstance(serialized, str) else serialized @@ -42,7 +42,7 @@ def deserialize(self) -> AbstractTransaction: } if len(decoded_rlp) == 12: - data['v'] = Deserializer.parse_number(decoded_rlp[9]) + 31 + data['v'] = Deserializer.parse_number(decoded_rlp[9]) + Constants.ETHEREUM_RECOVERY_ID_OFFSET.value data['r'] = Deserializer.parse_hex(decoded_rlp[10]) data['s'] = Deserializer.parse_hex(decoded_rlp[11]) diff --git a/crypto/transactions/types/abstract_transaction.py b/crypto/transactions/types/abstract_transaction.py index 9f621dd3..c5186155 100644 --- a/crypto/transactions/types/abstract_transaction.py +++ b/crypto/transactions/types/abstract_transaction.py @@ -2,6 +2,7 @@ from typing import Optional from crypto.configuration.network import get_network +from crypto.enums.constants import Constants from crypto.identity.address import address_from_public_key from crypto.identity.private_key import PrivateKey from crypto.utils.transaction_utils import TransactionUtils @@ -96,7 +97,7 @@ def hash(self, skip_signature: bool) -> str: return TransactionUtils.to_hash(self.data, skip_signature=skip_signature) def get_signature(self): - recover_id = int(self.data.get('v', 0)) - 31 + recover_id = int(self.data.get('v', 0)) - Constants.ETHEREUM_RECOVERY_ID_OFFSET.value r = self.data.get('r') s = self.data.get('s') diff --git a/crypto/utils/transaction_utils.py b/crypto/utils/transaction_utils.py index 46904105..3cfbda5b 100644 --- a/crypto/utils/transaction_utils.py +++ b/crypto/utils/transaction_utils.py @@ -1,11 +1,10 @@ from binascii import unhexlify import hashlib +from crypto.enums.constants import Constants from crypto.utils.rlp_encoder import RlpEncoder class TransactionUtils: - EIP1559_PREFIX = '02' - @classmethod def to_buffer(cls, transaction: dict, skip_signature: bool = False) -> bytes: # Process recipientAddress @@ -31,13 +30,13 @@ def to_buffer(cls, transaction: dict, skip_signature: bool = False) -> bytes: ] if not skip_signature and 'v' in transaction and 'r' in transaction and 's' in transaction: - fields.append(cls.to_be_array(int(transaction['v']) - 31)) + fields.append(cls.to_be_array(int(transaction['v']) - Constants.ETHEREUM_RECOVERY_ID_OFFSET.value)) fields.append(bytes.fromhex(transaction['r'])) fields.append(bytes.fromhex(transaction['s'])) encoded = RlpEncoder.encode(fields) - hash_input = cls.EIP1559_PREFIX + encoded + hash_input = Constants.EIP_1559_PREFIX.value + encoded return hash_input.encode() From 9b5f1859200b36b030d6739728cdf91836fd3c81 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Thu, 13 Feb 2025 12:53:28 +0000 Subject: [PATCH 18/20] revert fixtures to 27/28 --- tests/fixtures/evm-sign.json | 2 +- tests/fixtures/transfer-large-amount.json | 2 +- tests/fixtures/transfer.json | 2 +- tests/fixtures/unvote.json | 2 +- tests/fixtures/username-registration.json | 2 +- tests/fixtures/username-resignation.json | 2 +- tests/fixtures/validator-registration.json | 2 +- tests/fixtures/validator-resignation.json | 2 +- tests/fixtures/vote.json | 2 +- tests/identity/conftest.py | 4 ++-- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/fixtures/evm-sign.json b/tests/fixtures/evm-sign.json index c18bd4aa..a171db39 100644 --- a/tests/fixtures/evm-sign.json +++ b/tests/fixtures/evm-sign.json @@ -10,7 +10,7 @@ "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", "senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", "id": "2cfa9d51e71f014f8054881052e41dc61267587f9dba04c59a31cc881f8ce35b", - "v": 31, + "v": 27, "r": "a56cf78a7203927af0c8216fdbc804182a0788569e460067efec519b6f7b2e55", "s": "52ce951a20c7406a4a34707557f02e5a2af1451cc4e216645778c4ee0391a4cd" }, diff --git a/tests/fixtures/transfer-large-amount.json b/tests/fixtures/transfer-large-amount.json index 7bee5369..0fcee465 100644 --- a/tests/fixtures/transfer-large-amount.json +++ b/tests/fixtures/transfer-large-amount.json @@ -7,7 +7,7 @@ "recipientAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", "value": "10000000000000000000", "data": "", - "v": 31, + "v": 27, "r": "c741f8ccf811e7080216b71e0cbcad1138a4f187b08c749c5e8780568f1ff4bc", "s": "530341bef473fa92db1fe96922758ed010ba8eb80f2e527c9441771fa4c5368b", "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", diff --git a/tests/fixtures/transfer.json b/tests/fixtures/transfer.json index 57626f18..24a2b1aa 100644 --- a/tests/fixtures/transfer.json +++ b/tests/fixtures/transfer.json @@ -7,7 +7,7 @@ "recipientAddress": "0x6f0182a0cc707b055322ccf6d4cb6a5aff1aeb22", "value": "100000000", "data": "", - "v": 31, + "v": 27, "r": "0567c4def813a66e03fa1cd499a27c6922698a67e25e0b38458d8f4bb0e581fc", "s": "25fcdf9d110b82bde15bae4a49118deb83cfc3ec1656c2a29286d7836d328abe", "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", diff --git a/tests/fixtures/unvote.json b/tests/fixtures/unvote.json index 88a7be20..5dccb90f 100644 --- a/tests/fixtures/unvote.json +++ b/tests/fixtures/unvote.json @@ -10,7 +10,7 @@ "recipientAddress": "0x535b3d7a252fa034ed71f0c53ec0c6f784cb64e1", "value": "0", "data": "3174b689", - "v": 31, + "v": 27, "r": "2853135c30a6b131e9270ce1b52999b8d2bdc18b25bef5b1c13ef4ca8a20ce9e", "s": "3999a9d5d1ff826efa13052394b28b101535c92dcd8744c916c003431e08744e" }, diff --git a/tests/fixtures/username-registration.json b/tests/fixtures/username-registration.json index f3930a74..46d03a63 100644 --- a/tests/fixtures/username-registration.json +++ b/tests/fixtures/username-registration.json @@ -10,7 +10,7 @@ "recipientAddress": "0x2c1de3b4dbb4adebebb5dcecae825be2a9fc6eb6", "value": "0", "data": "36a94134000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000037068700000000000000000000000000000000000000000000000000000000000", - "v": 31, + "v": 27, "r": "48f36028509a88c8670b2baaac14b5ba6a8c88bbd90cce4149a3070e57f0fa42", "s": "297fe9a4d2df4c682e8705311d1143d2df9c89dc8689853544c1eb96121f5e85" }, diff --git a/tests/fixtures/username-resignation.json b/tests/fixtures/username-resignation.json index c71d2fd0..2211eed3 100644 --- a/tests/fixtures/username-resignation.json +++ b/tests/fixtures/username-resignation.json @@ -9,7 +9,7 @@ "recipientAddress": "0x2c1DE3b4Dbb4aDebEbB5dcECAe825bE2a9fc6eb6", "value": "0", "data": "ebed6dab", - "v": 32, + "v": 28, "r": "76ade83c279901613b31e41b9cf0e55223ef728edc5d920d108d239929abf523", "s": "0a8fe944d4217ebfe6afad28f1fce2f03e4cfe7aa90ea932c1dae4216603a82c" }, diff --git a/tests/fixtures/validator-registration.json b/tests/fixtures/validator-registration.json index 58eb5d8c..8b276b3d 100644 --- a/tests/fixtures/validator-registration.json +++ b/tests/fixtures/validator-registration.json @@ -10,7 +10,7 @@ "recipientAddress": "0x535b3d7a252fa034ed71f0c53ec0c6f784cb64e1", "value": "0", "data": "602a9eee00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000030954f46d6097a1d314e900e66e11e0dad0a57cd03e04ec99f0dedd1c765dcb11e6d7fa02e22cf40f9ee23d9cc1c0624bd00000000000000000000000000000000", - "v": 31, + "v": 27, "r": "609cfbaf2a8195741dcf2054e73c88e93103d4c13e7f9d5fc83f881c01a38773", "s": "79266d0b2c9bdaccbc3fba88c6dfad4538bcdcda803b8e0faa33cdd84b73abed" }, diff --git a/tests/fixtures/validator-resignation.json b/tests/fixtures/validator-resignation.json index 681fbabc..5cc76fdd 100644 --- a/tests/fixtures/validator-resignation.json +++ b/tests/fixtures/validator-resignation.json @@ -10,7 +10,7 @@ "recipientAddress": "0x535b3d7a252fa034ed71f0c53ec0c6f784cb64e1", "value": "0", "data": "b85f5da2", - "v": 31, + "v": 27, "r": "52d016bfb61cb3b57d36a7444ce26d3c557b2acb31a10b5951fc3c9e8d2e49a6", "s": "4940013218020780485b24cea9afbc6c93249286a308af33735c0c87bdeb4b5a" }, diff --git a/tests/fixtures/vote.json b/tests/fixtures/vote.json index cd837520..be55d915 100644 --- a/tests/fixtures/vote.json +++ b/tests/fixtures/vote.json @@ -10,7 +10,7 @@ "recipientAddress": "0x535b3d7a252fa034ed71f0c53ec0c6f784cb64e1", "value": "0", "data": "6dd7d8ea000000000000000000000000c3bbe9b1cee1ff85ad72b87414b0e9b7f2366763", - "v": 31, + "v": 27, "r": "1e0b168c7520f39fb99f9bf1ea7c0086a3a9e78f215da5a7e997fd8ef5657f5f", "s": "35019ef68773310144587b228dbc626e8cf3654377e92901f8d7f13119b0e09e" }, diff --git a/tests/identity/conftest.py b/tests/identity/conftest.py index cb9d28cb..faa90bb6 100644 --- a/tests/identity/conftest.py +++ b/tests/identity/conftest.py @@ -21,9 +21,9 @@ def sign_compact(): """ data = { 'data': { - 'serialized': '1f0567c4def813a66e03fa1cd499a27c6922698a67e25e0b38458d8f4bb0e581fc25fcdf9d110b82bde15bae4a49118deb83cfc3ec1656c2a29286d7836d328abe', + 'serialized': '1b0567c4def813a66e03fa1cd499a27c6922698a67e25e0b38458d8f4bb0e581fc25fcdf9d110b82bde15bae4a49118deb83cfc3ec1656c2a29286d7836d328abe', 'message': '02e31e018005825208946f0182a0cc707b055322ccf6d4cb6a5aff1aeb228405f5e10080c0', - 'v': 31, + 'v': 27, 'r': '0567c4def813a66e03fa1cd499a27c6922698a67e25e0b38458d8f4bb0e581fc', 's': '25fcdf9d110b82bde15bae4a49118deb83cfc3ec1656c2a29286d7836d328abe', }, From b9ecf9f28a9fdf218b4159489b6651ce29063932 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Thu, 13 Feb 2025 12:53:39 +0000 Subject: [PATCH 19/20] no need to copy data when getting id --- crypto/transactions/types/abstract_transaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto/transactions/types/abstract_transaction.py b/crypto/transactions/types/abstract_transaction.py index c5186155..d66ed034 100644 --- a/crypto/transactions/types/abstract_transaction.py +++ b/crypto/transactions/types/abstract_transaction.py @@ -32,7 +32,7 @@ def refresh_payload_data(self): self.data['data'] = self.get_payload().lstrip('0x') def get_id(self) -> str: - return TransactionUtils.get_id(self.data.copy()) + return TransactionUtils.get_id(self.data) def get_bytes(self, skip_signature: bool = False) -> bytes: from crypto.transactions.serializer import Serializer From 6be369e1b72007991c6666fb10865db436386d15 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Thu, 13 Feb 2025 14:07:41 +0000 Subject: [PATCH 20/20] remove debug --- crypto/identity/private_key.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crypto/identity/private_key.py b/crypto/identity/private_key.py index 1b3ca963..6ff93a8f 100644 --- a/crypto/identity/private_key.py +++ b/crypto/identity/private_key.py @@ -31,8 +31,7 @@ def sign_compact(self, message: bytes) -> bytes: Returns: bytes: signature of the signed message """ - pkey = PvtKey.from_hex(sha256(b'my super secret passphrase').hexdigest()) - der = pkey.sign_recoverable(message) + der = self.private_key.sign_recoverable(message) return bytes([der[64] + Constants.ETHEREUM_RECOVERY_ID_OFFSET.value]) + der[0:64]