Skip to content

Commit 2ca1892

Browse files
authored
feat: rlp encoding (#146)
1 parent 2c493d4 commit 2ca1892

34 files changed

+824
-355
lines changed

crypto/enums/constants.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from enum import Enum
2+
3+
class Constants(Enum):
4+
ETHEREUM_RECOVERY_ID_OFFSET = 27
5+
EIP_1559_PREFIX = '02'

crypto/identity/private_key.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from binascii import hexlify
22
from hashlib import sha256
3-
43
from coincurve import PrivateKey as PvtKey
54

5+
from crypto.enums.constants import Constants
6+
67
class PrivateKey(object):
78
def __init__(self, private_key: str):
89
self.private_key = PvtKey.from_hex(private_key)
@@ -18,8 +19,21 @@ def sign(self, message: bytes) -> bytes:
1819
bytes: signature of the signed message
1920
"""
2021
signature = self.private_key.sign(message)
21-
22-
return hexlify(signature).decode()
22+
23+
return hexlify(signature)
24+
25+
def sign_compact(self, message: bytes) -> bytes:
26+
"""Sign a message with this private key object
27+
28+
Args:
29+
message (bytes): bytes data you want to sign
30+
31+
Returns:
32+
bytes: signature of the signed message
33+
"""
34+
der = self.private_key.sign_recoverable(message)
35+
36+
return bytes([der[64] + Constants.ETHEREUM_RECOVERY_ID_OFFSET.value]) + der[0:64]
2337

2438
def to_hex(self):
2539
"""Returns a private key in hex format
Lines changed: 43 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
1+
import re
2+
from binascii import unhexlify
3+
from typing import Optional
4+
from crypto.enums.constants import Constants
15
from crypto.transactions.types.abstract_transaction import AbstractTransaction
26
from crypto.transactions.types.transfer import Transfer
37
from crypto.transactions.types.evm_call import EvmCall
48
from crypto.transactions.types.vote import Vote
59
from crypto.transactions.types.unvote import Unvote
610
from crypto.transactions.types.validator_registration import ValidatorRegistration
711
from crypto.transactions.types.validator_resignation import ValidatorResignation
8-
from binascii import unhexlify, hexlify
912

10-
from binary.unsigned_integer.reader import (
11-
read_bit8,
12-
read_bit32,
13-
read_bit64,
14-
)
1513
from crypto.enums.abi_function import AbiFunction
1614
from crypto.utils.abi_decoder import AbiDecoder
15+
from crypto.utils.rlp_decoder import RlpDecoder
1716

1817
class Deserializer:
1918
SIGNATURE_SIZE = 64
@@ -23,77 +22,39 @@ def __init__(self, serialized: str):
2322
self.serialized = unhexlify(serialized) if isinstance(serialized, str) else serialized
2423
self.pointer = 0
2524

25+
self.encoded_rlp = '0x' + serialized[2:]
26+
2627
@staticmethod
2728
def new(serialized: str):
2829
return Deserializer(serialized)
2930

3031
def deserialize(self) -> AbstractTransaction:
31-
data = {}
32+
decoded_rlp = RlpDecoder.decode(self.encoded_rlp)
33+
34+
data = {
35+
'network': Deserializer.parse_number(decoded_rlp[0]),
36+
'nonce': Deserializer.parse_big_number(decoded_rlp[1]),
37+
'gasPrice': Deserializer.parse_number(decoded_rlp[3]),
38+
'gasLimit': Deserializer.parse_number(decoded_rlp[4]),
39+
'recipientAddress': Deserializer.parse_address(decoded_rlp[5]),
40+
'value': Deserializer.parse_big_number(decoded_rlp[6]),
41+
'data': Deserializer.parse_hex(decoded_rlp[7]),
42+
}
43+
44+
if len(decoded_rlp) == 12:
45+
data['v'] = Deserializer.parse_number(decoded_rlp[9]) + Constants.ETHEREUM_RECOVERY_ID_OFFSET.value
46+
data['r'] = Deserializer.parse_hex(decoded_rlp[10])
47+
data['s'] = Deserializer.parse_hex(decoded_rlp[11])
3248

33-
self.deserialize_common(data)
34-
self.deserialize_data(data)
3549
transaction = self.guess_transaction_from_data(data)
36-
self.deserialize_signatures(data)
3750

3851
transaction.data = data
3952
transaction.recover_sender()
4053

41-
transaction.data['id'] = transaction.hash(skip_signature=False).hex()
54+
transaction.data['id'] = transaction.get_id()
4255

4356
return transaction
4457

45-
def read_bytes(self, length: int) -> bytes:
46-
result = self.serialized[self.pointer:self.pointer + length]
47-
self.pointer += length
48-
return result
49-
50-
def deserialize_common(self, data: dict):
51-
data['network'] = read_bit8(self.serialized, self.pointer)
52-
self.pointer += 1
53-
54-
nonce = read_bit64(self.serialized, self.pointer)
55-
data['nonce'] = str(nonce)
56-
self.pointer += 8
57-
58-
gas_price = read_bit32(self.serialized, self.pointer)
59-
data['gasPrice'] = gas_price
60-
self.pointer += 4
61-
62-
gas_limit = read_bit32(self.serialized, self.pointer)
63-
data['gasLimit'] = gas_limit
64-
self.pointer += 4
65-
66-
data['value'] = '0'
67-
68-
def deserialize_data(self, data: dict):
69-
value = int.from_bytes(self.serialized[self.pointer:self.pointer + 32], byteorder='big')
70-
self.pointer += 32
71-
72-
data['value'] = str(value)
73-
74-
recipient_marker = read_bit8(self.serialized, self.pointer)
75-
self.pointer += 1
76-
77-
if recipient_marker == 1:
78-
recipient_address_bytes = self.read_bytes(20)
79-
recipient_address = '0x' + hexlify(recipient_address_bytes).decode()
80-
data['recipientAddress'] = recipient_address
81-
82-
payload_length = read_bit32(self.serialized, self.pointer)
83-
self.pointer += 4
84-
85-
payload_hex = ''
86-
if payload_length > 0:
87-
payload_bytes = self.read_bytes(payload_length)
88-
payload_hex = hexlify(payload_bytes).decode()
89-
90-
data['data'] = payload_hex
91-
92-
def deserialize_signatures(self, data: dict):
93-
signature_length = self.SIGNATURE_SIZE + self.RECOVERY_SIZE
94-
signature_bytes = self.read_bytes(signature_length)
95-
data['signature'] = hexlify(signature_bytes).decode()
96-
9758
def guess_transaction_from_data(self, data: dict) -> AbstractTransaction:
9859
if data['value'] != '0':
9960
return Transfer(data)
@@ -115,7 +76,7 @@ def guess_transaction_from_data(self, data: dict) -> AbstractTransaction:
11576
else:
11677
return EvmCall(data)
11778

118-
def decode_payload(self, data: dict) -> dict:
79+
def decode_payload(self, data: dict) -> Optional[dict]:
11980
payload = data.get('data', '')
12081

12182
if payload == '':
@@ -126,4 +87,21 @@ def decode_payload(self, data: dict) -> dict:
12687
return decoder.decode_function_data(payload)
12788
except Exception as e:
12889
print(f"Error decoding payload: {str(e)}")
129-
return None
90+
91+
return None
92+
93+
@staticmethod
94+
def parse_number(value: str) -> int:
95+
return 0 if value == '0x' else int(value, 16)
96+
97+
@staticmethod
98+
def parse_big_number(value: str) -> str:
99+
return str(Deserializer.parse_number(value))
100+
101+
@staticmethod
102+
def parse_hex(value: str) -> str:
103+
return re.sub(r'^0x', '', value)
104+
105+
@staticmethod
106+
def parse_address(value: str) -> Optional[str]:
107+
return None if value == '0x' else value

crypto/transactions/serializer.py

Lines changed: 3 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
1-
from binascii import unhexlify
21
from crypto.transactions.types.abstract_transaction import AbstractTransaction
3-
from crypto.configuration.network import get_network
4-
from binary.unsigned_integer.writer import (
5-
write_bit8,
6-
write_bit32,
7-
write_bit64,
8-
)
2+
from crypto.utils.transaction_utils import TransactionUtils
93

104
class Serializer:
115
def __init__(self, transaction: AbstractTransaction):
@@ -22,48 +16,6 @@ def get_bytes(transaction: AbstractTransaction, skip_signature: bool = False) ->
2216
return transaction.serialize(skip_signature=skip_signature)
2317

2418
def serialize(self, skip_signature: bool = False) -> bytes:
25-
bytes_data = bytes()
19+
transaction_hash = TransactionUtils.to_buffer(self.transaction.data, skip_signature=skip_signature).decode()
2620

27-
bytes_data += self.serialize_common()
28-
bytes_data += self.serialize_data()
29-
if not skip_signature:
30-
bytes_data += self.serialize_signatures()
31-
32-
return bytes_data
33-
34-
def serialize_common(self) -> bytes:
35-
bytes_data = bytes()
36-
network_version = self.transaction.data.get('network', get_network()['version'])
37-
bytes_data += write_bit8(int(network_version))
38-
bytes_data += write_bit64(int(self.transaction.data['nonce']))
39-
bytes_data += write_bit32(int(self.transaction.data['gasPrice']))
40-
bytes_data += write_bit32(int(self.transaction.data['gasLimit']))
41-
return bytes_data
42-
43-
def serialize_data(self) -> bytes:
44-
bytes_data = bytes()
45-
46-
bytes_data += int(self.transaction.data['value']).to_bytes(32, byteorder='big')
47-
48-
if 'recipientAddress' in self.transaction.data:
49-
bytes_data += write_bit8(1)
50-
recipient_address = self.transaction.data['recipientAddress']
51-
bytes_data += unhexlify(recipient_address.replace('0x', ''))
52-
else:
53-
bytes_data += write_bit8(0)
54-
55-
payload_hex = self.transaction.data.get('data', '')
56-
payload_length = len(payload_hex) // 2
57-
bytes_data += write_bit32(payload_length)
58-
59-
if payload_length > 0:
60-
bytes_data += unhexlify(payload_hex)
61-
62-
return bytes_data
63-
64-
def serialize_signatures(self) -> bytes:
65-
bytes_data = bytes()
66-
if 'signature' in self.transaction.data:
67-
bytes_data += unhexlify(self.transaction.data['signature'])
68-
69-
return bytes_data
21+
return bytes.fromhex(transaction_hash)

crypto/transactions/types/abstract_transaction.py

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
from typing import Optional
33

44
from crypto.configuration.network import get_network
5+
from crypto.enums.constants import Constants
56
from crypto.identity.address import address_from_public_key
67
from crypto.identity.private_key import PrivateKey
7-
from crypto.utils.transaction_hasher import TransactionHasher
8+
from crypto.utils.transaction_utils import TransactionUtils
89
from coincurve import PublicKey
910
from crypto.utils.abi_decoder import AbiDecoder
1011

@@ -19,7 +20,7 @@ def get_payload(self) -> str:
1920
def decode_payload(self, data: dict) -> Optional[dict]:
2021
if 'data' not in data or data['data'] == '':
2122
return None
22-
23+
2324
payload = data['data']
2425
decoder = AbiDecoder()
2526

@@ -31,51 +32,59 @@ def refresh_payload_data(self):
3132
self.data['data'] = self.get_payload().lstrip('0x')
3233

3334
def get_id(self) -> str:
34-
return self.hash(skip_signature=False).hex()
35+
return TransactionUtils.get_id(self.data)
3536

3637
def get_bytes(self, skip_signature: bool = False) -> bytes:
3738
from crypto.transactions.serializer import Serializer
39+
3840
return Serializer.get_bytes(self, skip_signature)
3941

4042
def sign(self, private_key: PrivateKey):
41-
hash_ = self.hash(skip_signature=True)
42-
signature_with_recid = private_key.private_key.sign_recoverable(hash_, hasher=None)
43-
signature_hex = signature_with_recid.hex()
44-
self.data['signature'] = signature_hex
43+
transaction_hash = TransactionUtils.to_buffer(self.data, skip_signature=True).decode()
44+
45+
message = bytes.fromhex(transaction_hash)
46+
47+
transaction_signature = private_key.sign_compact(message)
48+
49+
self.data['v'] = transaction_signature[0]
50+
self.data['r'] = transaction_signature[1:33].hex()
51+
self.data['s'] = transaction_signature[33:].hex()
52+
4553
return self
4654

4755
def get_public_key(self, compact_signature, hash_):
4856
public_key = PublicKey.from_signature_and_message(compact_signature, hash_, hasher=None)
57+
4958
return public_key
5059

5160
def recover_sender(self):
52-
signature_hex = self.data.get('signature')
53-
if not signature_hex:
54-
raise ValueError("No signature to recover from")
61+
signature_with_recid = self.get_signature()
62+
if not signature_with_recid:
63+
return False
5564

56-
signature_with_recid = bytes.fromhex(signature_hex)
57-
hash_ = self.hash(skip_signature=True)
65+
hash_ = bytes.fromhex(self.hash(skip_signature=True))
5866
public_key = self.get_public_key(signature_with_recid, hash_)
5967
self.data['senderPublicKey'] = public_key.format().hex()
6068
self.data['senderAddress'] = address_from_public_key(self.data['senderPublicKey'])
6169

6270
def verify(self) -> bool:
63-
signature_hex = self.data.get('signature')
64-
if not signature_hex:
71+
signature_with_recid = self.get_signature()
72+
if not signature_with_recid:
6573
return False
6674

67-
signature_with_recid = bytes.fromhex(signature_hex)
68-
hash_ = self.hash(skip_signature=True)
75+
hash_ = bytes.fromhex(self.hash(skip_signature=True))
6976
recovered_public_key = self.get_public_key(signature_with_recid, hash_)
7077
sender_public_key_hex = self.data.get('senderPublicKey')
7178
if not sender_public_key_hex:
7279
return False
7380

7481
sender_public_key_bytes = bytes.fromhex(sender_public_key_hex)
82+
7583
return recovered_public_key.format() == sender_public_key_bytes
7684

7785
def serialize(self, skip_signature: bool = False) -> bytes:
7886
from crypto.transactions.serializer import Serializer
87+
7988
return Serializer(self).serialize(skip_signature)
8089

8190
def to_dict(self) -> dict:
@@ -84,14 +93,15 @@ def to_dict(self) -> dict:
8493
def to_json(self) -> str:
8594
return json.dumps(self.to_dict())
8695

87-
def hash(self, skip_signature: bool) -> bytes:
88-
hash_data = self.data.copy()
89-
if skip_signature:
90-
hash_data['signature'] = None
91-
return TransactionHasher.to_hash(hash_data, skip_signature)
96+
def hash(self, skip_signature: bool) -> str:
97+
return TransactionUtils.to_hash(self.data, skip_signature=skip_signature)
9298

9399
def get_signature(self):
94-
signature_hex = self.data.get('signature')
95-
if signature_hex:
96-
return bytes.fromhex(signature_hex)
100+
recover_id = int(self.data.get('v', 0)) - Constants.ETHEREUM_RECOVERY_ID_OFFSET.value
101+
r = self.data.get('r')
102+
s = self.data.get('s')
103+
104+
if r and s:
105+
return bytes.fromhex(r) + bytes.fromhex(s) + bytes([recover_id])
106+
97107
return None

0 commit comments

Comments
 (0)