astreum 0.2.40__py3-none-any.whl → 0.2.42__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of astreum might be problematic. Click here for more details.

@@ -1,3 +1,5 @@
1
+ from .account import Account
2
+ from .accounts import Accounts
1
3
  from .block import Block
2
4
  from .chain import Chain
3
5
  from .fork import Fork
@@ -12,5 +14,7 @@ __all__ = [
12
14
  "Fork",
13
15
  "Receipt",
14
16
  "Transaction",
17
+ "Account",
18
+ "Accounts",
15
19
  "consensus_setup",
16
20
  ]
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, List, Optional, Tuple
5
+
6
+ from .._storage.atom import Atom, ZERO32
7
+ from .._storage.patricia import PatriciaTrie
8
+ from ..utils.integer import bytes_to_int, int_to_bytes
9
+
10
+
11
+ @dataclass
12
+ class Account:
13
+ balance: int
14
+ code: bytes
15
+ counter: int
16
+ data_hash: bytes
17
+ data: PatriciaTrie
18
+ hash: bytes = ZERO32
19
+ body_hash: bytes = ZERO32
20
+ atoms: List[Atom] = field(default_factory=list)
21
+
22
+ @classmethod
23
+ def create(cls, balance: int = 0, data_hash: bytes = ZERO32, code: bytes = ZERO32, counter: int = 0) -> "Account":
24
+ account = cls(
25
+ balance=int(balance),
26
+ code=bytes(code),
27
+ counter=int(counter),
28
+ data_hash=bytes(data_hash),
29
+ data=PatriciaTrie(root_hash=bytes(data_hash)),
30
+ )
31
+ account.to_atom()
32
+ return account
33
+
34
+ @classmethod
35
+ def from_atom(cls, node: Any, account_id: bytes) -> "Account":
36
+ storage_get = node._local_get
37
+
38
+ type_atom = storage_get(account_id)
39
+ if type_atom is None or type_atom.data != b"account":
40
+ raise ValueError("not an account (type mismatch)")
41
+
42
+ def _read_atom(atom_id: Optional[bytes]) -> Optional[Atom]:
43
+ if not atom_id or atom_id == ZERO32:
44
+ return None
45
+ return storage_get(atom_id)
46
+
47
+ balance_atom = _read_atom(type_atom.next)
48
+ if balance_atom is None:
49
+ raise ValueError("malformed account (balance missing)")
50
+
51
+ code_atom = _read_atom(balance_atom.next)
52
+ if code_atom is None:
53
+ raise ValueError("malformed account (code missing)")
54
+
55
+ counter_atom = _read_atom(code_atom.next)
56
+ if counter_atom is None:
57
+ raise ValueError("malformed account (counter missing)")
58
+
59
+ data_atom = _read_atom(counter_atom.next)
60
+ if data_atom is None:
61
+ raise ValueError("malformed account (data missing)")
62
+
63
+ account = cls.create(
64
+ balance=bytes_to_int(balance_atom.data),
65
+ data_hash=data_atom.data,
66
+ counter=bytes_to_int(counter_atom.data),
67
+ code=code_atom.data,
68
+ )
69
+ if account.hash != account_id:
70
+ raise ValueError("account hash mismatch while decoding")
71
+ return account
72
+
73
+ def to_atom(self) -> Tuple[bytes, List[Atom]]:
74
+ # Build a single forward chain: account -> balance -> code -> counter -> data.
75
+ data_atom = Atom.from_data(data=bytes(self.data_hash))
76
+ counter_atom = Atom.from_data(
77
+ data=int_to_bytes(self.counter),
78
+ next_hash=data_atom.object_id(),
79
+ )
80
+ code_atom = Atom.from_data(
81
+ data=bytes(self.code),
82
+ next_hash=counter_atom.object_id(),
83
+ )
84
+ balance_atom = Atom.from_data(
85
+ data=int_to_bytes(self.balance),
86
+ next_hash=code_atom.object_id(),
87
+ )
88
+ type_atom = Atom.from_data(data=b"account", next_hash=balance_atom.object_id())
89
+
90
+ atoms = [data_atom, counter_atom, code_atom, balance_atom, type_atom]
91
+ account_hash = type_atom.object_id()
92
+ self.hash = account_hash
93
+ self.body_hash = account_hash
94
+ self.atoms = atoms
95
+ return account_hash, list(atoms)
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from .._storage.patricia import PatriciaTrie
6
+ from .account import Account
7
+
8
+
9
+ class Accounts:
10
+ def __init__(
11
+ self,
12
+ root_hash: Optional[bytes] = None,
13
+ ) -> None:
14
+ self._trie = PatriciaTrie(root_hash=root_hash)
15
+ self._cache: Dict[bytes, Account] = {}
16
+
17
+ @property
18
+ def root_hash(self) -> Optional[bytes]:
19
+ return self._trie.root_hash
20
+
21
+ def get_account(self, address: bytes, node: Optional[Any] = None) -> Optional[Account]:
22
+ cached = self._cache.get(address)
23
+ if cached is not None:
24
+ return cached
25
+
26
+ if node is None:
27
+ raise ValueError("Accounts requires a node reference for trie access")
28
+
29
+ account_id: Optional[bytes] = self._trie.get(node, address)
30
+ if account_id is None:
31
+ return None
32
+
33
+ account = Account.from_atom(node, account_id)
34
+ self._cache[address] = account
35
+ return account
36
+
37
+ def set_account(self, address: bytes, account: Account) -> None:
38
+ self._cache[address] = account
@@ -1,5 +1,5 @@
1
1
 
2
- from typing import Callable, List, Optional, Tuple, TYPE_CHECKING
2
+ from typing import Any, Callable, List, Optional, Tuple, TYPE_CHECKING
3
3
 
4
4
  from .._storage.atom import Atom, ZERO32
5
5
 
@@ -181,7 +181,14 @@ class Block:
181
181
  return self.hash, atoms_acc
182
182
 
183
183
  @classmethod
184
- def from_atom(cls, storage_get: Callable[[bytes], Optional[Atom]], block_id: bytes) -> "Block":
184
+ def from_atom(cls, source: Any, block_id: bytes) -> "Block":
185
+ storage_get: Optional[Callable[[bytes], Optional[Atom]]]
186
+ if callable(source):
187
+ storage_get = source
188
+ else:
189
+ storage_get = getattr(source, "_local_get", None)
190
+ if not callable(storage_get):
191
+ raise TypeError("Block.from_atom requires a node with '_local_get' or a callable storage getter")
185
192
  # 1) Expect main list
186
193
  main_typ = storage_get(block_id)
187
194
  if main_typ is None or main_typ.data != b"list":
@@ -1,66 +1,66 @@
1
1
  # chain.py
2
- from typing import Callable, Dict, Optional
3
- from .block import Block
4
- from .._storage.atom import ZERO32, Atom
5
-
6
- class Chain:
7
- def __init__(self, head_block: Block):
8
- self.head_block = head_block
9
- self.validated_upto_block = None
10
- # Root (genesis) hash for this chain; set by validation setup when known
11
- self.root: Optional[bytes] = None
12
- # Fork position: the head hash of the default/current fork for this chain
13
- self.fork_position: Optional[bytes] = getattr(head_block, "hash", None)
14
- # Mark the first malicious block encountered during validation; None means not found
15
- self.malicious_block_hash: Optional[bytes] = None
16
-
17
- def validate(self, storage_get: Callable[[bytes], Atom]) -> Block:
18
- """Validate the chain from head to genesis and return the root block.
19
-
20
- Incorporates per-block validation (signature on body and timestamp
21
- monotonicity). Uses a simple cache to avoid duplicate Atom fetches and
22
- duplicate block decoding during the backward walk.
23
- """
24
- # Atom and Block caches for this validation pass
25
- atom_cache: Dict[bytes, Optional[Atom]] = {}
26
- block_cache: Dict[bytes, Block] = {}
27
-
28
- def get_cached(k: bytes) -> Optional[Atom]:
29
- if k in atom_cache:
30
- return atom_cache[k]
31
- a = storage_get(k)
32
- atom_cache[k] = a
33
- return a
34
-
35
- def load_block(bid: bytes) -> Block:
36
- if bid in block_cache:
37
- return block_cache[bid]
38
- b = Block.from_atom(get_cached, bid) # type: ignore[arg-type]
39
- block_cache[bid] = b
40
- return b
41
-
42
- blk = self.head_block
43
- # Ensure head is in cache if it has a hash
44
- if getattr(blk, "hash", None):
45
- block_cache[blk.hash] = blk # type: ignore[attr-defined]
46
-
47
- # Walk back, validating each block
48
- while True:
49
- # Validate current block (signature over body, timestamp rule)
50
- try:
51
- blk.validate(get_cached) # may decode previous but uses cached atoms
52
- except Exception:
53
- # record first failure point then propagate
54
- self.malicious_block_hash = getattr(blk, "hash", None)
55
- raise
56
-
57
- prev_hash = blk.previous_block_hash if hasattr(blk, "previous_block_hash") else ZERO32
58
- if prev_hash == ZERO32:
59
- break
60
- # Move to previous block using cache-aware loader
61
- prev_blk = load_block(prev_hash)
62
- blk.previous_block = prev_blk # cache the object for any downstream use
63
- blk = prev_blk
64
-
65
- self.validated_upto_block = blk
66
- return blk
2
+ from typing import Callable, Dict, Optional
3
+ from .block import Block
4
+ from .._storage.atom import ZERO32, Atom
5
+
6
+ class Chain:
7
+ def __init__(self, head_block: Block):
8
+ self.head_block = head_block
9
+ self.validated_upto_block = None
10
+ # Root (genesis) hash for this chain; set by validation setup when known
11
+ self.root: Optional[bytes] = None
12
+ # Fork position: the head hash of the default/current fork for this chain
13
+ self.fork_position: Optional[bytes] = getattr(head_block, "hash", None)
14
+ # Mark the first malicious block encountered during validation; None means not found
15
+ self.malicious_block_hash: Optional[bytes] = None
16
+
17
+ def validate(self, storage_get: Callable[[bytes], Atom]) -> Block:
18
+ """Validate the chain from head to genesis and return the root block.
19
+
20
+ Incorporates per-block validation (signature on body and timestamp
21
+ monotonicity). Uses a simple cache to avoid duplicate Atom fetches and
22
+ duplicate block decoding during the backward walk.
23
+ """
24
+ # Atom and Block caches for this validation pass
25
+ atom_cache: Dict[bytes, Optional[Atom]] = {}
26
+ block_cache: Dict[bytes, Block] = {}
27
+
28
+ def get_cached(k: bytes) -> Optional[Atom]:
29
+ if k in atom_cache:
30
+ return atom_cache[k]
31
+ a = storage_get(k)
32
+ atom_cache[k] = a
33
+ return a
34
+
35
+ def load_block(bid: bytes) -> Block:
36
+ if bid in block_cache:
37
+ return block_cache[bid]
38
+ b = Block.from_atom(get_cached, bid)
39
+ block_cache[bid] = b
40
+ return b
41
+
42
+ blk = self.head_block
43
+ # Ensure head is in cache if it has a hash
44
+ if getattr(blk, "hash", None):
45
+ block_cache[blk.hash] = blk # type: ignore[attr-defined]
46
+
47
+ # Walk back, validating each block
48
+ while True:
49
+ # Validate current block (signature over body, timestamp rule)
50
+ try:
51
+ blk.validate(get_cached) # may decode previous but uses cached atoms
52
+ except Exception:
53
+ # record first failure point then propagate
54
+ self.malicious_block_hash = getattr(blk, "hash", None)
55
+ raise
56
+
57
+ prev_hash = blk.previous_block_hash if hasattr(blk, "previous_block_hash") else ZERO32
58
+ if prev_hash == ZERO32:
59
+ break
60
+ # Move to previous block using cache-aware loader
61
+ prev_blk = load_block(prev_hash)
62
+ blk.previous_block = prev_blk # cache the object for any downstream use
63
+ blk = prev_blk
64
+
65
+ self.validated_upto_block = blk
66
+ return blk
@@ -1,100 +1,100 @@
1
- from __future__ import annotations
2
-
1
+ from __future__ import annotations
2
+
3
3
  from typing import Optional, Set, Any, Callable, Dict
4
- from .block import Block
5
- from .._storage.atom import ZERO32, Atom
6
-
7
-
8
- class Fork:
9
- """A branch head within a Chain (same root).
10
-
11
- - head: current tip block id (bytes)
12
- - peers: identifiers (e.g., peer pubkey objects) following this head
13
- - root: genesis block id for this chain (optional)
14
- - validated_upto: earliest verified ancestor (optional)
15
- - chain_fork_position: the chain's fork anchor relevant to this fork
16
- """
17
-
18
- def __init__(
19
- self,
20
- head: bytes,
21
- ) -> None:
22
- self.head: bytes = head
23
- self.peers: Set[Any] = set()
24
- self.root: Optional[bytes] = None
25
- self.validated_upto: Optional[bytes] = None
26
- self.chain_fork_position: Optional[bytes] = None
27
- # Mark the first block found malicious during validation; None means not found
28
- self.malicious_block_hash: Optional[bytes] = None
29
-
30
- def add_peer(self, peer_id: Any) -> None:
31
- self.peers.add(peer_id)
32
-
33
- def remove_peer(self, peer_id: Any) -> None:
34
- self.peers.discard(peer_id)
35
-
36
- def validate(
37
- self,
38
- storage_get: Callable[[bytes], Optional[object]],
39
- stop_heads: Optional[Set[bytes]] = None,
40
- ) -> bool:
41
- """Validate only up to the chain fork position, not genesis.
42
-
43
- Returns True if self.head descends from self.chain_fork_position (or if
44
- chain_fork_position is None/equals head), and updates validated_upto to
45
- that anchor. If stop_heads is provided, returns True early if ancestry
46
- reaches any of those heads, setting validated_upto to the matched head.
47
- Returns False if ancestry cannot be confirmed.
48
- """
49
- if self.chain_fork_position is None or self.chain_fork_position == self.head:
50
- self.validated_upto = self.head
51
- return True
52
- # Caches to avoid double fetching/decoding
53
- atom_cache: Dict[bytes, Optional[Atom]] = {}
54
- block_cache: Dict[bytes, Block] = {}
55
-
56
- def get_cached(k: bytes) -> Optional[Atom]:
57
- if k in atom_cache:
58
- return atom_cache[k]
59
- a = storage_get(k) # type: ignore[call-arg]
60
- atom_cache[k] = a # may be None if missing
61
- return a
62
-
63
- def load_block(bid: bytes) -> Optional[Block]:
64
- if bid in block_cache:
65
- return block_cache[bid]
66
- try:
67
- b = Block.from_atom(get_cached, bid) # type: ignore[arg-type]
68
- except Exception:
69
- return None
70
- block_cache[bid] = b
71
- return b
72
-
73
- blk = load_block(self.head)
74
- if blk is None:
75
- # Missing head data: unverifiable, not malicious
76
- return False
77
- # Walk up to fork anchor, validating each block signature + timestamp
78
- while True:
79
- try:
80
- blk.validate(get_cached) # type: ignore[arg-type]
81
- except Exception:
82
- # mark the first failure point
83
- self.malicious_block_hash = blk.hash
84
- return False
85
-
86
- # Early-exit if we met another known fork head
87
- if stop_heads and blk.hash in stop_heads:
88
- self.validated_upto = blk.hash
89
- return True
90
-
91
- if blk.hash == self.chain_fork_position:
92
- self.validated_upto = blk.hash
93
- return True
94
-
95
- prev_hash = blk.previous_block_hash if hasattr(blk, "previous_block_hash") else ZERO32
96
- nxt = load_block(prev_hash)
97
- if nxt is None:
98
- return False
99
- blk.previous_block = nxt # cache for future use
100
- blk = nxt
4
+ from .block import Block
5
+ from .._storage.atom import ZERO32, Atom
6
+
7
+
8
+ class Fork:
9
+ """A branch head within a Chain (same root).
10
+
11
+ - head: current tip block id (bytes)
12
+ - peers: identifiers (e.g., peer pubkey objects) following this head
13
+ - root: genesis block id for this chain (optional)
14
+ - validated_upto: earliest verified ancestor (optional)
15
+ - chain_fork_position: the chain's fork anchor relevant to this fork
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ head: bytes,
21
+ ) -> None:
22
+ self.head: bytes = head
23
+ self.peers: Set[Any] = set()
24
+ self.root: Optional[bytes] = None
25
+ self.validated_upto: Optional[bytes] = None
26
+ self.chain_fork_position: Optional[bytes] = None
27
+ # Mark the first block found malicious during validation; None means not found
28
+ self.malicious_block_hash: Optional[bytes] = None
29
+
30
+ def add_peer(self, peer_id: Any) -> None:
31
+ self.peers.add(peer_id)
32
+
33
+ def remove_peer(self, peer_id: Any) -> None:
34
+ self.peers.discard(peer_id)
35
+
36
+ def validate(
37
+ self,
38
+ storage_get: Callable[[bytes], Optional[object]],
39
+ stop_heads: Optional[Set[bytes]] = None,
40
+ ) -> bool:
41
+ """Validate only up to the chain fork position, not genesis.
42
+
43
+ Returns True if self.head descends from self.chain_fork_position (or if
44
+ chain_fork_position is None/equals head), and updates validated_upto to
45
+ that anchor. If stop_heads is provided, returns True early if ancestry
46
+ reaches any of those heads, setting validated_upto to the matched head.
47
+ Returns False if ancestry cannot be confirmed.
48
+ """
49
+ if self.chain_fork_position is None or self.chain_fork_position == self.head:
50
+ self.validated_upto = self.head
51
+ return True
52
+ # Caches to avoid double fetching/decoding
53
+ atom_cache: Dict[bytes, Optional[Atom]] = {}
54
+ block_cache: Dict[bytes, Block] = {}
55
+
56
+ def get_cached(k: bytes) -> Optional[Atom]:
57
+ if k in atom_cache:
58
+ return atom_cache[k]
59
+ a = storage_get(k) # type: ignore[call-arg]
60
+ atom_cache[k] = a # may be None if missing
61
+ return a
62
+
63
+ def load_block(bid: bytes) -> Optional[Block]:
64
+ if bid in block_cache:
65
+ return block_cache[bid]
66
+ try:
67
+ b = Block.from_atom(get_cached, bid)
68
+ except Exception:
69
+ return None
70
+ block_cache[bid] = b
71
+ return b
72
+
73
+ blk = load_block(self.head)
74
+ if blk is None:
75
+ # Missing head data: unverifiable, not malicious
76
+ return False
77
+ # Walk up to fork anchor, validating each block signature + timestamp
78
+ while True:
79
+ try:
80
+ blk.validate(get_cached) # type: ignore[arg-type]
81
+ except Exception:
82
+ # mark the first failure point
83
+ self.malicious_block_hash = blk.hash
84
+ return False
85
+
86
+ # Early-exit if we met another known fork head
87
+ if stop_heads and blk.hash in stop_heads:
88
+ self.validated_upto = blk.hash
89
+ return True
90
+
91
+ if blk.hash == self.chain_fork_position:
92
+ self.validated_upto = blk.hash
93
+ return True
94
+
95
+ prev_hash = blk.previous_block_hash if hasattr(blk, "previous_block_hash") else ZERO32
96
+ nxt = load_block(prev_hash)
97
+ if nxt is None:
98
+ return False
99
+ blk.previous_block = nxt # cache for future use
100
+ blk = nxt
@@ -0,0 +1,141 @@
1
+
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Iterable, List, Optional, Tuple
5
+
6
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
7
+
8
+ from .account import Account
9
+ from .block import Block
10
+ from .._storage.atom import Atom, ZERO32
11
+ from .._storage.patricia import PatriciaTrie, PatriciaNode
12
+
13
+ TREASURY_ADDRESS = b"\x01" * 32
14
+ BURN_ADDRESS = b"\x00" * 32
15
+
16
+
17
+ def _int_to_be_bytes(value: int) -> bytes:
18
+ if value < 0:
19
+ raise ValueError("integer fields in genesis must be non-negative")
20
+ if value == 0:
21
+ return b"\x00"
22
+ length = (value.bit_length() + 7) // 8
23
+ return value.to_bytes(length, "big")
24
+
25
+
26
+ def _make_list(child_ids: List[bytes]) -> Tuple[bytes, List[Atom]]:
27
+ next_hash = ZERO32
28
+ chain: List[Atom] = []
29
+ for child_id in reversed(child_ids):
30
+ elem = Atom.from_data(data=child_id, next_hash=next_hash)
31
+ next_hash = elem.object_id()
32
+ chain.append(elem)
33
+ chain.reverse()
34
+
35
+ value_atom = Atom.from_data(
36
+ data=len(child_ids).to_bytes(8, "little"),
37
+ next_hash=next_hash,
38
+ )
39
+ type_atom = Atom.from_data(data=b"list", next_hash=value_atom.object_id())
40
+ atoms = chain + [value_atom, type_atom]
41
+ return type_atom.object_id(), atoms
42
+
43
+
44
+ def _store_atoms(node: Any, atoms: Iterable[Atom]) -> None:
45
+ setter = getattr(node, "_local_set", None)
46
+ if not callable(setter):
47
+ raise TypeError("node must expose '_local_set(object_id, atom)'")
48
+ for atom in atoms:
49
+ setter(atom.object_id(), atom)
50
+
51
+
52
+ def _persist_trie(trie: PatriciaTrie, node: Any) -> None:
53
+ for patricia_node in trie.nodes.values():
54
+ _, atoms = patricia_node.to_atoms()
55
+ _store_atoms(node, atoms)
56
+
57
+
58
+ if not hasattr(PatriciaNode, "to_bytes"):
59
+ def _patricia_node_to_bytes(self: PatriciaNode) -> bytes: # type: ignore[no-redef]
60
+ fields = [
61
+ bytes([self.key_len]) + self.key,
62
+ self.child_0 or ZERO32,
63
+ self.child_1 or ZERO32,
64
+ self.value or b"",
65
+ ]
66
+ encoded: List[bytes] = []
67
+ for field in fields:
68
+ encoded.append(len(field).to_bytes(4, "big"))
69
+ encoded.append(field)
70
+ return b"".join(encoded)
71
+
72
+ PatriciaNode.to_bytes = _patricia_node_to_bytes # type: ignore[attr-defined]
73
+
74
+
75
+ def create_genesis_block(node: Any, validator_public_key: bytes, validator_secret_key: bytes) -> Block:
76
+ validator_pk = bytes(validator_public_key)
77
+
78
+ if len(validator_pk) != 32:
79
+ raise ValueError("validator_public_key must be 32 bytes")
80
+
81
+ # 1. Stake trie with single validator stake of 1 (encoded on 32 bytes).
82
+ stake_trie = PatriciaTrie()
83
+ stake_amount = (1).to_bytes(32, "big")
84
+ stake_trie.put(node, validator_pk, stake_amount)
85
+ _persist_trie(stake_trie, node)
86
+ stake_root = stake_trie.root_hash or ZERO32
87
+
88
+ # 2. Account trie with treasury, burn, and validator accounts.
89
+ accounts_trie = PatriciaTrie()
90
+
91
+ treasury_account = Account.create(balance=1, data=stake_root, counter=0)
92
+ treasury_account_id, treasury_atoms = treasury_account.to_atom()
93
+ _store_atoms(node, treasury_atoms)
94
+ accounts_trie.put(node, TREASURY_ADDRESS, treasury_account_id)
95
+
96
+ burn_account = Account.create(balance=0, data=b"", counter=0)
97
+ burn_account_id, burn_atoms = burn_account.to_atom()
98
+ _store_atoms(node, burn_atoms)
99
+ accounts_trie.put(node, BURN_ADDRESS, burn_account_id)
100
+
101
+ validator_account = Account.create(balance=0, data=b"", counter=0)
102
+ validator_account_id, validator_atoms = validator_account.to_atom()
103
+ _store_atoms(node, validator_atoms)
104
+ accounts_trie.put(node, validator_pk, validator_account_id)
105
+
106
+ _persist_trie(accounts_trie, node)
107
+
108
+ accounts_root = accounts_trie.root_hash
109
+ if accounts_root is None:
110
+ raise ValueError("genesis accounts trie is empty")
111
+
112
+ # 3. Assemble block metadata.
113
+ block = Block()
114
+ block.previous_block_hash = ZERO32
115
+ block.number = 0
116
+ block.timestamp = 0
117
+ block.accounts_hash = accounts_root
118
+ block.accounts = accounts_trie
119
+ block.transactions_total_fees = 0
120
+ block.transactions_hash = ZERO32
121
+ block.receipts_hash = ZERO32
122
+ block.delay_difficulty = 0
123
+ block.delay_output = b""
124
+ block.validator_public_key = validator_pk
125
+ block.transactions = []
126
+ block.receipts = []
127
+
128
+ # 4. Sign the block body with the validator secret key.
129
+ block.signature = b""
130
+ block.to_atom()
131
+
132
+ if block.body_hash is None:
133
+ raise ValueError("failed to materialise genesis block body")
134
+
135
+ secret = Ed25519PrivateKey.from_private_bytes(validator_secret_key)
136
+ block.signature = secret.sign(block.body_hash)
137
+ block_hash, block_atoms = block.to_atom()
138
+ _store_atoms(node, block_atoms)
139
+
140
+ block.hash = block_hash
141
+ return block
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from dataclasses import dataclass
3
+ from dataclasses import dataclass, field
4
4
  from typing import Callable, List, Optional, Tuple
5
5
 
6
6
  from .._storage.atom import Atom, ZERO32
@@ -77,6 +77,8 @@ class Receipt:
77
77
  cost: int = 0
78
78
  logs: bytes = b""
79
79
  status: int = 0
80
+ hash: bytes = ZERO32
81
+ atoms: List[Atom] = field(default_factory=list)
80
82
 
81
83
  def to_atom(self) -> Tuple[bytes, List[Atom]]:
82
84
  """Serialise the receipt into Atom storage."""
@@ -109,6 +111,13 @@ class Receipt:
109
111
 
110
112
  return top_list_id, atoms
111
113
 
114
+ def atomize(self) -> Tuple[bytes, List[Atom]]:
115
+ """Generate atoms for this receipt and cache them."""
116
+ receipt_id, atoms = self.to_atom()
117
+ self.hash = receipt_id
118
+ self.atoms = atoms
119
+ return receipt_id, atoms
120
+
112
121
  @classmethod
113
122
  def from_atom(
114
123
  cls,
@@ -164,4 +173,5 @@ class Receipt:
164
173
  cost=_be_bytes_to_int(cost_bytes),
165
174
  logs=logs_bytes,
166
175
  status=status_value,
176
+ hash=bytes(receipt_id),
167
177
  )