Skip to content

Commit 838ee25

Browse files
authored
feat: add support for vertex headers (#52)
1 parent 01c3c07 commit 838ee25

File tree

15 files changed

+506
-106
lines changed

15 files changed

+506
-106
lines changed

hathorlib/base_transaction.py

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from hathorlib.exceptions import InvalidOutputValue, WeightError
2020
from hathorlib.scripts import P2PKH, DataScript, MultiSig, parse_address_script
2121
from hathorlib.utils import int_to_bytes, unpack, unpack_len
22+
from hathorlib.vertex_parser import VertexParser
2223

2324
settings = HathorSettings()
2425

@@ -81,14 +82,14 @@ def _missing_(cls, value: Any) -> None:
8182

8283
def get_cls(self) -> Type['BaseTransaction']:
8384
from hathorlib import Block, TokenCreationTransaction, Transaction
84-
from hathorlib.nanocontracts.nanocontract import NanoContract
85+
from hathorlib.nanocontracts.nanocontract import DeprecatedNanoContract
8586
from hathorlib.nanocontracts.on_chain_blueprint import OnChainBlueprint
8687

8788
cls_map: Dict[TxVersion, Type[BaseTransaction]] = {
8889
TxVersion.REGULAR_BLOCK: Block,
8990
TxVersion.REGULAR_TRANSACTION: Transaction,
9091
TxVersion.TOKEN_CREATION_TRANSACTION: TokenCreationTransaction,
91-
TxVersion.NANO_CONTRACT: NanoContract,
92+
TxVersion.NANO_CONTRACT: DeprecatedNanoContract,
9293
TxVersion.ON_CHAIN_BLUEPRINT: OnChainBlueprint,
9394
}
9495

@@ -103,6 +104,10 @@ def get_cls(self) -> Type['BaseTransaction']:
103104
class BaseTransaction(ABC):
104105
"""Hathor base transaction"""
105106

107+
__slots__ = (
108+
'version', 'signal_bits', 'weight', 'timestamp', 'nonce', 'inputs', 'outputs', 'parents', 'hash', 'headers'
109+
)
110+
106111
# Even though nonce is serialized with different sizes for tx and blocks
107112
# the same size is used for hashes to enable mining algorithm compatibility
108113
SERIALIZATION_NONCE_SIZE: ClassVar[int]
@@ -116,6 +121,7 @@ class BaseTransaction(ABC):
116121
signal_bits: int
117122

118123
def __init__(self) -> None:
124+
from hathorlib.headers import VertexBaseHeader
119125
self.nonce: int = 0
120126
self.timestamp: int = 0
121127
self.signal_bits: int = 0
@@ -125,6 +131,7 @@ def __init__(self) -> None:
125131
self.outputs: List['TxOutput'] = []
126132
self.parents: List[bytes] = []
127133
self.hash: bytes = b''
134+
self.headers: list[VertexBaseHeader] = []
128135

129136
@property
130137
@abstractmethod
@@ -136,6 +143,10 @@ def is_block(self) -> bool:
136143
def is_transaction(self) -> bool:
137144
raise NotImplementedError
138145

146+
def is_nano_contract(self) -> bool:
147+
"""Return True if this transaction is a nano contract or not."""
148+
return False
149+
139150
def _get_formatted_fields_dict(self, short: bool = True) -> Dict[str, str]:
140151
""" Used internally on __repr__ and __str__, returns a dict of `field_name: formatted_value`.
141152
"""
@@ -186,6 +197,21 @@ def get_fields_from_struct(self, struct_bytes: bytes) -> bytes:
186197
buf = self.get_graph_fields_from_struct(buf)
187198
return buf
188199

200+
def get_header_from_bytes(self, buf: bytes) -> bytes:
201+
"""Parse bytes and return the next header in buffer."""
202+
if len(self.headers) >= self.get_maximum_number_of_headers():
203+
raise ValueError('too many headers')
204+
205+
header_type = buf[:1]
206+
header_class = VertexParser.get_header_parser(header_type)
207+
header, buf = header_class.deserialize(self, buf)
208+
self.headers.append(header)
209+
return buf
210+
211+
def get_maximum_number_of_headers(self) -> int:
212+
"""Return the maximum number of headers for this vertex."""
213+
return 1
214+
189215
@classmethod
190216
@abstractmethod
191217
def create_from_struct(cls, struct_bytes: bytes) -> 'BaseTransaction':
@@ -294,6 +320,10 @@ def get_graph_struct(self) -> bytes:
294320
struct_bytes += parent
295321
return struct_bytes
296322

323+
def get_headers_struct(self) -> bytes:
324+
"""Return the serialization of the headers only."""
325+
return b''.join(h.serialize() for h in self.headers)
326+
297327
def get_struct_without_nonce(self) -> bytes:
298328
"""Return a partial serialization of the transaction, without including the nonce field
299329
@@ -321,6 +351,7 @@ def get_struct(self) -> bytes:
321351
"""
322352
struct_bytes = self.get_struct_without_nonce()
323353
struct_bytes += self.get_struct_nonce()
354+
struct_bytes += self.get_headers_struct()
324355
return struct_bytes
325356

326357
def verify_pow(self, override_weight: Optional[float] = None) -> bool:
@@ -355,13 +386,22 @@ def get_graph_hash(self) -> bytes:
355386
graph_hash.update(self.get_graph_struct())
356387
return graph_hash.digest()
357388

358-
def get_header_without_nonce(self) -> bytes:
389+
def get_headers_hash(self) -> bytes:
390+
"""Return the sha256 of the headers of the transaction."""
391+
if not self.headers:
392+
return b''
393+
394+
h = hashlib.sha256()
395+
h.update(self.get_headers_struct())
396+
return h.digest()
397+
398+
def get_mining_header_without_nonce(self) -> bytes:
359399
"""Return the transaction header without the nonce
360400
361401
:return: transaction header without the nonce
362402
:rtype: bytes
363403
"""
364-
return self.get_funds_hash() + self.get_graph_hash()
404+
return self.get_funds_hash() + self.get_graph_hash() + self.get_headers_hash()
365405

366406
def calculate_hash1(self) -> HASH:
367407
"""Return the sha256 of the transaction without including the `nonce`
@@ -370,7 +410,7 @@ def calculate_hash1(self) -> HASH:
370410
:rtype: :py:class:`_hashlib.HASH`
371411
"""
372412
calculate_hash1 = hashlib.sha256()
373-
calculate_hash1.update(self.get_header_without_nonce())
413+
calculate_hash1.update(self.get_mining_header_without_nonce())
374414
return calculate_hash1
375415

376416
def calculate_hash2(self, part1: HASH) -> bytes:

hathorlib/block.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,15 @@ def create_from_struct(cls, struct_bytes: bytes) -> 'Block':
4242
blc = cls()
4343
buf = blc.get_fields_from_struct(struct_bytes)
4444

45-
blc.nonce = int.from_bytes(buf, byteorder='big')
46-
if len(buf) != cls.SERIALIZATION_NONCE_SIZE:
45+
if len(buf) < cls.SERIALIZATION_NONCE_SIZE:
4746
raise ValueError('Invalid sequence of bytes')
4847

48+
blc.nonce = int.from_bytes(buf[:cls.SERIALIZATION_NONCE_SIZE], byteorder='big')
49+
buf = buf[cls.SERIALIZATION_NONCE_SIZE:]
50+
51+
while buf:
52+
buf = blc.get_header_from_bytes(buf)
53+
4954
blc.hash = blc.calculate_hash()
5055

5156
return blc

hathorlib/headers/__init__.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Copyright 2023 Hathor Labs
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from hathorlib.headers.base import VertexBaseHeader
16+
from hathorlib.headers.nano_header import NC_INITIALIZE_METHOD, NanoHeader
17+
from hathorlib.headers.types import VertexHeaderId
18+
19+
__all__ = [
20+
'VertexBaseHeader',
21+
'VertexHeaderId',
22+
'NanoHeader',
23+
'NC_INITIALIZE_METHOD',
24+
]

hathorlib/headers/base.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright 2023 Hathor Labs
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from abc import ABC, abstractmethod
18+
from typing import TYPE_CHECKING
19+
20+
if TYPE_CHECKING:
21+
from hathorlib.base_transaction import BaseTransaction
22+
23+
24+
class VertexBaseHeader(ABC):
25+
@classmethod
26+
@abstractmethod
27+
def deserialize(cls, tx: BaseTransaction, buf: bytes) -> tuple[VertexBaseHeader, bytes]:
28+
"""Deserialize header from `buf` which starts with header id."""
29+
raise NotImplementedError
30+
31+
@abstractmethod
32+
def serialize(self) -> bytes:
33+
"""Serialize header with header id as prefix."""
34+
raise NotImplementedError
35+
36+
@abstractmethod
37+
def get_sighash_bytes(self) -> bytes:
38+
"""Return sighash bytes to check digital signatures."""
39+
raise NotImplementedError

hathorlib/headers/nano_header.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# Copyright 2023 Hathor Labs
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from collections import deque
18+
from dataclasses import dataclass
19+
from typing import TYPE_CHECKING
20+
21+
from hathorlib.headers.base import VertexBaseHeader
22+
from hathorlib.headers.types import VertexHeaderId
23+
from hathorlib.nanocontracts import DeprecatedNanoContract
24+
from hathorlib.nanocontracts.types import NCActionType
25+
from hathorlib.utils import int_to_bytes, unpack, unpack_len
26+
27+
if TYPE_CHECKING:
28+
from hathorlib.base_transaction import BaseTransaction
29+
30+
NC_VERSION = 1
31+
NC_INITIALIZE_METHOD = 'initialize'
32+
33+
34+
@dataclass(frozen=True)
35+
class NanoHeaderAction:
36+
type: NCActionType
37+
token_index: int
38+
amount: int
39+
40+
41+
@dataclass(frozen=True)
42+
class NanoHeader(VertexBaseHeader):
43+
tx: BaseTransaction
44+
45+
# nc_id equals to the blueprint_id when a Nano Contract is being created.
46+
# nc_id equals to the nanocontract_id when a method is being called.
47+
nc_id: bytes
48+
49+
# Name of the method to be called. When creating a new Nano Contract, it must be equal to 'initialize'.
50+
nc_method: str
51+
52+
# Serialized arguments to nc_method.
53+
nc_args_bytes: bytes
54+
55+
nc_actions: list[NanoHeaderAction]
56+
57+
# Pubkey and signature of the transaction owner / caller.
58+
nc_pubkey: bytes
59+
nc_signature: bytes
60+
61+
nc_version: int = NC_VERSION
62+
63+
@classmethod
64+
def _deserialize_action(cls, buf: bytes) -> tuple[NanoHeaderAction, bytes]:
65+
from hathorlib.base_transaction import bytes_to_output_value
66+
type_bytes, buf = buf[:1], buf[1:]
67+
action_type = NCActionType.from_bytes(type_bytes)
68+
(token_index,), buf = unpack('!B', buf)
69+
amount, buf = bytes_to_output_value(buf)
70+
return NanoHeaderAction(
71+
type=action_type,
72+
token_index=token_index,
73+
amount=amount,
74+
), buf
75+
76+
@classmethod
77+
def deserialize(cls, tx: BaseTransaction, buf: bytes) -> tuple[NanoHeader, bytes]:
78+
header_id, buf = buf[:1], buf[1:]
79+
assert header_id == VertexHeaderId.NANO_HEADER.value
80+
(nc_version,), buf = unpack('!B', buf)
81+
if nc_version != NC_VERSION:
82+
raise ValueError('unknown nanocontract version: {}'.format(nc_version))
83+
84+
nc_id, buf = unpack_len(32, buf)
85+
(nc_method_len,), buf = unpack('!B', buf)
86+
nc_method, buf = unpack_len(nc_method_len, buf)
87+
(nc_args_bytes_len,), buf = unpack('!H', buf)
88+
nc_args_bytes, buf = unpack_len(nc_args_bytes_len, buf)
89+
90+
nc_actions: list[NanoHeaderAction] = []
91+
if not isinstance(tx, DeprecatedNanoContract):
92+
(nc_actions_len,), buf = unpack('!B', buf)
93+
for _ in range(nc_actions_len):
94+
action, buf = cls._deserialize_action(buf)
95+
nc_actions.append(action)
96+
97+
(nc_pubkey_len,), buf = unpack('!B', buf)
98+
nc_pubkey, buf = unpack_len(nc_pubkey_len, buf)
99+
(nc_signature_len,), buf = unpack('!B', buf)
100+
nc_signature, buf = unpack_len(nc_signature_len, buf)
101+
102+
decoded_nc_method = nc_method.decode('ascii')
103+
104+
return cls(
105+
tx=tx,
106+
nc_version=nc_version,
107+
nc_id=nc_id,
108+
nc_method=decoded_nc_method,
109+
nc_args_bytes=nc_args_bytes,
110+
nc_actions=nc_actions,
111+
nc_pubkey=nc_pubkey,
112+
nc_signature=nc_signature,
113+
), bytes(buf)
114+
115+
def _serialize_action(self, action: NanoHeaderAction) -> bytes:
116+
from hathorlib.base_transaction import output_value_to_bytes
117+
ret = [
118+
action.type.to_bytes(),
119+
int_to_bytes(action.token_index, 1),
120+
output_value_to_bytes(action.amount),
121+
]
122+
return b''.join(ret)
123+
124+
def _serialize_without_header_id(self, *, skip_signature: bool) -> deque[bytes]:
125+
"""Serialize the header with the option to skip the signature."""
126+
encoded_method = self.nc_method.encode('ascii')
127+
128+
ret: deque[bytes] = deque()
129+
ret.append(int_to_bytes(NC_VERSION, 1))
130+
ret.append(self.nc_id)
131+
ret.append(int_to_bytes(len(encoded_method), 1))
132+
ret.append(encoded_method)
133+
ret.append(int_to_bytes(len(self.nc_args_bytes), 2))
134+
ret.append(self.nc_args_bytes)
135+
136+
if not isinstance(self.tx, DeprecatedNanoContract):
137+
ret.append(int_to_bytes(len(self.nc_actions), 1))
138+
for action in self.nc_actions:
139+
ret.append(self._serialize_action(action))
140+
141+
ret.append(int_to_bytes(len(self.nc_pubkey), 1))
142+
ret.append(self.nc_pubkey)
143+
if not skip_signature:
144+
ret.append(int_to_bytes(len(self.nc_signature), 1))
145+
ret.append(self.nc_signature)
146+
else:
147+
ret.append(int_to_bytes(0, 1))
148+
return ret
149+
150+
def serialize(self) -> bytes:
151+
ret = self._serialize_without_header_id(skip_signature=False)
152+
ret.appendleft(VertexHeaderId.NANO_HEADER.value)
153+
return b''.join(ret)
154+
155+
def get_sighash_bytes(self) -> bytes:
156+
ret = self._serialize_without_header_id(skip_signature=True)
157+
return b''.join(ret)

hathorlib/headers/types.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2023 Hathor Labs
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from enum import Enum, unique
16+
17+
18+
@unique
19+
class VertexHeaderId(Enum):
20+
NANO_HEADER = b'\x10'

0 commit comments

Comments
 (0)