astreum 0.1.16__tar.gz → 0.1.17__tar.gz

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.

Files changed (67) hide show
  1. {astreum-0.1.16/src/astreum.egg-info → astreum-0.1.17}/PKG-INFO +1 -1
  2. {astreum-0.1.16 → astreum-0.1.17}/pyproject.toml +1 -1
  3. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/node/relay/__init__.py +3 -0
  4. astreum-0.1.17/src/astreum/node/storage/merkle.py +224 -0
  5. astreum-0.1.17/src/astreum/node/storage/patricia.py +289 -0
  6. astreum-0.1.17/src/astreum/node/validation/account.py +99 -0
  7. astreum-0.1.17/src/astreum/node/validation/block.py +30 -0
  8. astreum-0.1.17/src/astreum/node/validation/transaction.py +146 -0
  9. {astreum-0.1.16 → astreum-0.1.17/src/astreum.egg-info}/PKG-INFO +1 -1
  10. {astreum-0.1.16 → astreum-0.1.17}/src/astreum.egg-info/SOURCES.txt +6 -5
  11. astreum-0.1.16/src/astreum/node/storage/merkle.py +0 -734
  12. astreum-0.1.16/src/astreum/node/storage/trie.py +0 -146
  13. astreum-0.1.16/src/astreum/node/validation/account.py +0 -874
  14. astreum-0.1.16/src/astreum/node/validation/transaction.py +0 -90
  15. {astreum-0.1.16 → astreum-0.1.17}/LICENSE +0 -0
  16. {astreum-0.1.16 → astreum-0.1.17}/README.md +0 -0
  17. {astreum-0.1.16 → astreum-0.1.17}/setup.cfg +0 -0
  18. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/__init__.py +0 -0
  19. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/lispeum/__init__.py +0 -0
  20. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/lispeum/expression.py +0 -0
  21. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/lispeum/parser.py +0 -0
  22. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/lispeum/special/__init__.py +0 -0
  23. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/lispeum/special/definition.py +0 -0
  24. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/lispeum/special/list/__init__.py +0 -0
  25. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/lispeum/special/list/all.py +0 -0
  26. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/lispeum/special/list/any.py +0 -0
  27. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/lispeum/special/list/fold.py +0 -0
  28. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/lispeum/special/list/get.py +0 -0
  29. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/lispeum/special/list/insert.py +0 -0
  30. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/lispeum/special/list/map.py +0 -0
  31. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/lispeum/special/list/position.py +0 -0
  32. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/lispeum/special/list/remove.py +0 -0
  33. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/lispeum/special/number/__init__.py +0 -0
  34. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/lispeum/special/number/addition.py +0 -0
  35. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/lispeum/storage.py +0 -0
  36. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/lispeum/tokenizer.py +0 -0
  37. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/lispeum/utils.py +0 -0
  38. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/machine/__init__.py +0 -0
  39. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/machine/environment.py +0 -0
  40. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/machine/error.py +0 -0
  41. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/node/__init__.py +0 -0
  42. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/node/crypto/__init__.py +0 -0
  43. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/node/crypto/ed25519.py +0 -0
  44. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/node/crypto/x25519.py +0 -0
  45. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/node/relay/bucket.py +0 -0
  46. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/node/relay/envelope.py +0 -0
  47. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/node/relay/message.py +0 -0
  48. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/node/relay/peer.py +0 -0
  49. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/node/relay/route.py +0 -0
  50. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/node/storage/__init__.py +0 -0
  51. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/node/storage/storage.py +0 -0
  52. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/node/storage/utils.py +0 -0
  53. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/node/utils.py +0 -0
  54. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/node/validation/__init__.py +0 -0
  55. {astreum-0.1.16/src/astreum/node/validation/block → astreum-0.1.17/src/astreum/node/validation/_block}/__init__.py +0 -0
  56. {astreum-0.1.16/src/astreum/node/validation/block → astreum-0.1.17/src/astreum/node/validation/_block}/create.py +0 -0
  57. {astreum-0.1.16/src/astreum/node/validation/block → astreum-0.1.17/src/astreum/node/validation/_block}/model.py +0 -0
  58. {astreum-0.1.16/src/astreum/node/validation/block → astreum-0.1.17/src/astreum/node/validation/_block}/validate.py +0 -0
  59. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/node/validation/constants.py +0 -0
  60. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/node/validation/stake.py +0 -0
  61. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/node/validation/state.py +0 -0
  62. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/node/validation/vdf.py +0 -0
  63. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/utils/__init__.py +0 -0
  64. {astreum-0.1.16 → astreum-0.1.17}/src/astreum/utils/bytes_format.py +0 -0
  65. {astreum-0.1.16 → astreum-0.1.17}/src/astreum.egg-info/dependency_links.txt +0 -0
  66. {astreum-0.1.16 → astreum-0.1.17}/src/astreum.egg-info/requires.txt +0 -0
  67. {astreum-0.1.16 → astreum-0.1.17}/src/astreum.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.1.16
3
+ Version: 0.1.17
4
4
  Summary: Python library to interact with the Astreum blockchain and its Lispeum virtual machine.
5
5
  Author-email: "Roy R. O. Okello" <roy@stelar.xyz>
6
6
  Project-URL: Homepage, https://github.com/astreum/lib
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "astreum"
3
- version = "0.1.16"
3
+ version = "0.1.17"
4
4
  authors = [
5
5
  { name="Roy R. O. Okello", email="roy@stelar.xyz" },
6
6
  ]
@@ -96,6 +96,9 @@ class Relay:
96
96
  # Initialize route table with our node ID
97
97
  self.route_table = RouteTable(self)
98
98
 
99
+ # Initialize storage index
100
+ self.storage_index: Dict[bytes, bytes] = {}
101
+
99
102
  # Start worker threads
100
103
  self._start_workers()
101
104
 
@@ -0,0 +1,224 @@
1
+ import blake3
2
+ from .storage import Storage
3
+ import astreum.utils.bytes_format as bytes_format bytes_format.decode, bytes_format.encode
4
+
5
+
6
+
7
+ class MerkleTree:
8
+ def __init__(self, storage: Storage, root_hash: bytes = None, leaves: list[bytes] = None):
9
+ """
10
+ Initialize a Merkle tree from an existing root hash or by constructing a new tree from leaf data.
11
+
12
+ If a list of leaf data is provided, the tree will be built from the bottom up,
13
+ every node will be stored in the provided storage, and the computed root hash
14
+ will be used as the tree's identifier.
15
+
16
+ :param storage: A Storage instance used for storing and retrieving tree nodes.
17
+ :param root_hash: An optional existing root hash of a Merkle tree.
18
+ :param leaves: An optional list of leaf data (each as bytes). If provided, a new tree is built.
19
+ :raises ValueError: If neither root_hash nor leaves is provided.
20
+ """
21
+ self.storage = storage
22
+ if leaves is not None:
23
+ self.root_hash = self.build_tree_from_leaves(leaves)
24
+ elif root_hash is not None:
25
+ self.root_hash = root_hash
26
+ else:
27
+ raise ValueError("Either root_hash or leaves must be provided.")
28
+
29
+ def build_tree_from_leaves(self, leaves: list[bytes]) -> bytes:
30
+ """
31
+ Construct a Merkle tree from a list of leaf data and store each node in storage.
32
+
33
+ Each leaf data entry is wrapped in a MerkleNode (with leaf=True) and stored.
34
+ Then, nodes are paired (duplicating the last node if needed when the count is odd)
35
+ to form parent nodes. For each parent node, the data is the concatenation of its
36
+ two child hashes. This process repeats until a single root hash remains.
37
+
38
+ :param leaves: A list of bytes objects, each representing leaf data.
39
+ :return: The computed root hash of the newly built tree.
40
+ """
41
+ # Create leaf nodes and store them.
42
+ current_level = []
43
+ for leaf_data in leaves:
44
+ leaf_node = MerkleNode(True, leaf_data)
45
+ leaf_hash = leaf_node.hash()
46
+ self.storage.put(leaf_hash, leaf_node.to_bytes())
47
+ current_level.append(leaf_hash)
48
+
49
+ # Build the tree upward until one node remains.
50
+ while len(current_level) > 1:
51
+ next_level = []
52
+ # If an odd number of nodes, duplicate the last node.
53
+ if len(current_level) % 2 == 1:
54
+ current_level.append(current_level[-1])
55
+ for i in range(0, len(current_level), 2):
56
+ left_hash = current_level[i]
57
+ right_hash = current_level[i + 1]
58
+ # Create a parent node from the concatenated child hashes.
59
+ parent_node = MerkleNode(False, left_hash + right_hash)
60
+ parent_hash = parent_node.hash()
61
+ self.storage.put(parent_hash, parent_node.to_bytes())
62
+ next_level.append(parent_hash)
63
+ current_level = next_level
64
+
65
+ # The remaining hash is the root of the tree.
66
+ return current_level[0]
67
+
68
+ def get(self, index: int, level: int = 0) -> bytes:
69
+ """
70
+ Retrieve the data stored in the leaf at a given index.
71
+
72
+ The method traverses the tree from the root, using the binary representation
73
+ of the index to choose which branch to follow at each level. It assumes that
74
+ non-leaf nodes store two child hashes concatenated together (each 32 bytes).
75
+
76
+ :param index: The index of the leaf to retrieve. The bits of this number determine the path.
77
+ :param level: The current tree level (used internally for recursion).
78
+ :return: The data stored in the target leaf node, or None if not found.
79
+ """
80
+ current_node = MerkleNode.from_storage(self.storage, self.root_hash)
81
+ if current_node is None:
82
+ return None
83
+
84
+ # If at a leaf node, return its data.
85
+ if current_node.leaf:
86
+ return current_node.data
87
+
88
+ # For non-leaf nodes, extract the left and right child hashes.
89
+ left_hash = current_node.data[:32]
90
+ right_hash = current_node.data[32:64]
91
+
92
+ # Use the bit at position `level` in the index to select the branch:
93
+ # 0 selects the left branch, 1 selects the right branch.
94
+ bit = (index >> level) & 1
95
+ next_hash = left_hash if bit == 0 else right_hash
96
+
97
+ # Recursively traverse the tree.
98
+ return MerkleTree(self.storage, root_hash=next_hash).get(index, level + 1)
99
+
100
+ def set(self, index: int, new_data: bytes) -> None:
101
+ """
102
+ Update the leaf at the specified index with new data, rebuilding all affected nodes.
103
+
104
+ The update process recursively creates new nodes for the branch from the updated leaf
105
+ back to the root. At each step, the old node is removed from storage and replaced with
106
+ a new node that reflects the updated hash.
107
+
108
+ :param index: The index of the leaf node to update.
109
+ :param new_data: The new data (as bytes) to store in the leaf.
110
+ """
111
+ self.root_hash = self._update(self.root_hash, index, 0, new_data)
112
+
113
+ def _update(self, node_hash: bytes, index: int, level: int, new_data: bytes) -> bytes:
114
+ """
115
+ Recursive helper function to update a node on the path to the target leaf.
116
+
117
+ For a leaf node, a new node is created with the updated data.
118
+ For an internal node, the correct branch (determined by the index and level) is updated,
119
+ and a new parent node is constructed from the updated child hash and the unchanged sibling hash.
120
+
121
+ :param node_hash: The hash of the current node to update.
122
+ :param index: The target leaf index whose path is being updated.
123
+ :param level: The current depth in the tree.
124
+ :param new_data: The new data to set at the target leaf.
125
+ :return: The hash of the newly constructed node replacing the current node.
126
+ :raises Exception: If the node is not found in storage.
127
+ """
128
+ current_node = MerkleNode.from_storage(self.storage, node_hash)
129
+ if current_node is None:
130
+ raise Exception("Node not found in storage")
131
+
132
+ if current_node.leaf:
133
+ # At the leaf, create a new node with updated data.
134
+ new_leaf = MerkleNode(True, new_data)
135
+ new_hash = new_leaf.hash()
136
+ self.storage.put(new_hash, new_leaf.to_bytes())
137
+ self.storage.delete(node_hash) # Remove the outdated node.
138
+ return new_hash
139
+ else:
140
+ # For non-leaf nodes, update the correct branch.
141
+ left_hash = current_node.data[:32]
142
+ right_hash = current_node.data[32:64]
143
+ bit = (index >> level) & 1
144
+
145
+ if bit == 0:
146
+ new_left_hash = self._update(left_hash, index, level + 1, new_data)
147
+ new_right_hash = right_hash
148
+ else:
149
+ new_left_hash = left_hash
150
+ new_right_hash = self._update(right_hash, index, level + 1, new_data)
151
+
152
+ # Create a new parent node with updated child hashes.
153
+ updated_node_data = new_left_hash + new_right_hash
154
+ new_node = MerkleNode(False, updated_node_data)
155
+ new_node_hash = new_node.hash()
156
+ self.storage.put(new_node_hash, new_node.to_bytes())
157
+ self.storage.delete(node_hash) # Remove the outdated parent node.
158
+ return new_node_hash
159
+
160
+
161
+ class MerkleNode:
162
+ def __init__(self, leaf: bool, data: bytes):
163
+ """
164
+ Initialize a Merkle node.
165
+
166
+ For a leaf node, `data` is the actual content to be stored.
167
+ For an internal node, `data` should be the concatenation of the two child hashes.
168
+
169
+ :param leaf: A boolean flag indicating whether this node is a leaf node (True) or an internal node (False).
170
+ :param data: The node's data. For leaves, the stored data; for internal nodes, concatenated child hashes.
171
+ """
172
+ self.leaf = leaf
173
+ self.data = data
174
+ self._hash = None # Cached hash value to avoid recomputation.
175
+
176
+ @classmethod
177
+ def from_bytes(cls, data: bytes) -> 'MerkleNode':
178
+ """
179
+ Deserialize a MerkleNode from its byte representation.
180
+
181
+ The input bytes are expected to be in the Astreum format, containing a leaf flag and node data.
182
+
183
+ :param data: The serialized node data.
184
+ :return: A new MerkleNode instance.
185
+ """
186
+ leaf_flag, node_data = bytes_format.decode(data)
187
+ return cls(True if leaf_flag == 1 else False, node_data)
188
+
189
+ @classmethod
190
+ def from_storage(cls, storage: Storage, hash_value: bytes) -> 'MerkleNode' or None:
191
+ """
192
+ Retrieve and deserialize a MerkleNode from storage using its hash.
193
+
194
+ :param storage: The Storage instance used to retrieve the node.
195
+ :param hash_value: The hash key under which the node is stored.
196
+ :return: A MerkleNode instance if found, otherwise None.
197
+ """
198
+ node_bytes = storage.get(hash_value)
199
+ if node_bytes is None:
200
+ return None
201
+ return cls.from_bytes(node_bytes)
202
+
203
+ def to_bytes(self) -> bytes:
204
+ """
205
+ Serialize the MerkleNode into bytes using the Astreum format.
206
+
207
+ The format encodes a list containing the leaf flag and the node data.
208
+
209
+ :return: The serialized bytes representing the node.
210
+ """
211
+ return bytes_format.encode([1 if self.leaf else 0, self.data])
212
+
213
+ def hash(self) -> bytes:
214
+ """
215
+ Compute (or retrieve a cached) hash of the node using the Blake3 algorithm.
216
+
217
+ For leaf nodes, the hash is computed over the actual data.
218
+ For internal nodes, the hash is computed over the concatenated child hashes.
219
+
220
+ :return: The Blake3 digest of the node's data.
221
+ """
222
+ if self._hash is None:
223
+ self._hash = blake3.blake3(self.data).digest()
224
+ return self._hash
@@ -0,0 +1,289 @@
1
+ import blake3
2
+ from typing import Optional, List
3
+ from .storage import Storage
4
+ import astreum.utils.bytes_format as bytes_format bytes_format.decode, bytes_format.encode
5
+
6
+
7
+ def common_prefix_length(a: bytes, b: bytes) -> int:
8
+ """Return the number of common prefix bytes between a and b."""
9
+ i = 0
10
+ while i < len(a) and i < len(b) and a[i] == b[i]:
11
+ i += 1
12
+ return i
13
+
14
+
15
+ class PatriciaNode:
16
+ def __init__(self, key: bytes, value: Optional[bytes], children: Optional[List[bytes]] = None):
17
+ """
18
+ Initialize a Patricia node.
19
+
20
+ :param key: A compressed part of the key.
21
+ :param value: The stored value (if this node represents a complete key) or None.
22
+ :param children: A list of child node hashes (bytes). The children are ordered by the first
23
+ byte of the child's key.
24
+ """
25
+ self.key = key
26
+ self.value = value
27
+ self.children = children if children is not None else []
28
+ self._hash: Optional[bytes] = None
29
+
30
+ @classmethod
31
+ def from_bytes(cls, data: bytes) -> 'PatriciaNode':
32
+ """
33
+ Deserialize a PatriciaNode from its byte representation.
34
+
35
+ Expected format: [key, value, children]
36
+ where children is a list of child node hashes (bytes).
37
+ """
38
+ decoded = bytes_format.decode(data)
39
+ key, value, children = decoded
40
+ return cls(key, value, children)
41
+
42
+ @classmethod
43
+ def from_storage(cls, storage: Storage, hash_value: bytes) -> Optional['PatriciaNode']:
44
+ """
45
+ Retrieve and deserialize a PatriciaNode from storage using its hash.
46
+
47
+ :param storage: The Storage instance used to retrieve the node.
48
+ :param hash_value: The hash key under which the node is stored.
49
+ :return: A PatriciaNode instance if found, otherwise None.
50
+ """
51
+ node_bytes = storage.get(hash_value)
52
+ if node_bytes is None:
53
+ return None
54
+ return cls.from_bytes(node_bytes)
55
+
56
+ def to_bytes(self) -> bytes:
57
+ """
58
+ Serialize the PatriciaNode into bytes using the Astreum format.
59
+
60
+ Structure: [key, value, children]
61
+ """
62
+ return bytes_format.encode([self.key, self.value, self.children])
63
+
64
+ def hash(self) -> bytes:
65
+ """
66
+ Compute (or retrieve a cached) Blake3 hash over the node's serialized bytes.
67
+ """
68
+ if self._hash is None:
69
+ self._hash = blake3.blake3(self.to_bytes()).digest()
70
+ return self._hash
71
+
72
+ def invalidate_hash(self) -> None:
73
+ """Clear the cached hash so that it is recomputed on next use."""
74
+ self._hash = None
75
+
76
+
77
+ class PatriciaTrie:
78
+ def __init__(self, storage: Storage, root_hash: Optional[bytes] = None):
79
+ """
80
+ Initialize a Patricia Trie.
81
+
82
+ :param storage: A Storage instance for persisting nodes.
83
+ :param root_hash: Optionally, an existing root hash. If None, the trie is empty.
84
+ """
85
+ self.storage = storage
86
+ self.root_hash = root_hash
87
+
88
+ def get(self, key: bytes) -> Optional[bytes]:
89
+ """
90
+ Retrieve the value associated with the given key.
91
+
92
+ :param key: The key (as bytes) to search for.
93
+ :return: The stored value if found, otherwise None.
94
+ """
95
+ if self.root_hash is None:
96
+ return None
97
+ return self._get(self.root_hash, key)
98
+
99
+ def _get(self, node_hash: bytes, key: bytes) -> Optional[bytes]:
100
+ node = PatriciaNode.from_storage(self.storage, node_hash)
101
+ if node is None:
102
+ return None
103
+
104
+ cp_len = common_prefix_length(key, node.key)
105
+ # If node.key completely matches the beginning of key...
106
+ if cp_len == len(node.key):
107
+ if cp_len == len(key):
108
+ return node.value
109
+ remainder = key[cp_len:]
110
+ branch = remainder[0]
111
+ child_hash = self._find_child(node.children, branch)
112
+ if child_hash is None:
113
+ return None
114
+ return self._get(child_hash, remainder)
115
+ return None
116
+
117
+ def put(self, key: bytes, value: bytes) -> None:
118
+ """
119
+ Insert or update the key with the provided value.
120
+
121
+ :param key: The key (as bytes) to insert.
122
+ :param value: The value (as bytes) to associate with the key.
123
+ """
124
+ if self.root_hash is None:
125
+ new_node = PatriciaNode(key, value, [])
126
+ new_hash = new_node.hash()
127
+ self.storage.put(new_hash, new_node.to_bytes())
128
+ self.root_hash = new_hash
129
+ else:
130
+ self.root_hash = self._put(self.root_hash, key, value)
131
+
132
+ def _put(self, node_hash: bytes, key: bytes, value: bytes) -> bytes:
133
+ """
134
+ Recursive helper for inserting or updating a key.
135
+
136
+ Returns the new hash for the node that replaces the current node.
137
+ """
138
+ node = PatriciaNode.from_storage(self.storage, node_hash)
139
+ if node is None:
140
+ # Node missing: create a new leaf.
141
+ new_node = PatriciaNode(key, value, [])
142
+ new_hash = new_node.hash()
143
+ self.storage.put(new_hash, new_node.to_bytes())
144
+ return new_hash
145
+
146
+ cp_len = common_prefix_length(key, node.key)
147
+ len_node_key = len(node.key)
148
+ len_key = len(key)
149
+
150
+ # Case 1: Exact match: update the value.
151
+ if cp_len == len_node_key and cp_len == len_key:
152
+ node.value = value
153
+ node.invalidate_hash()
154
+ new_hash = node.hash()
155
+ self.storage.put(new_hash, node.to_bytes())
156
+ self.storage.delete(node_hash)
157
+ return new_hash
158
+
159
+ # Case 2: Node key is a prefix of key (descend to child).
160
+ if cp_len == len_node_key:
161
+ remainder = key[cp_len:]
162
+ branch = remainder[0]
163
+ child_hash = self._find_child(node.children, branch)
164
+ if child_hash is not None:
165
+ new_child_hash = self._put(child_hash, remainder, value)
166
+ # Update the child pointer in the list.
167
+ idx = self._find_child_index(node.children, branch)
168
+ if idx is None:
169
+ raise Exception("Child not found during update.")
170
+ node.children[idx] = new_child_hash
171
+ else:
172
+ # Create a new leaf for the remainder.
173
+ new_leaf = PatriciaNode(remainder, value, [])
174
+ new_leaf_hash = new_leaf.hash()
175
+ self.storage.put(new_leaf_hash, new_leaf.to_bytes())
176
+ self._insert_child(node.children, new_leaf_hash)
177
+ node.invalidate_hash()
178
+ new_hash = node.hash()
179
+ self.storage.put(new_hash, node.to_bytes())
180
+ self.storage.delete(node_hash)
181
+ return new_hash
182
+
183
+ # Case 3: Key is a prefix of node.key (split node).
184
+ if cp_len == len_key and cp_len < len_node_key:
185
+ old_suffix = node.key[cp_len:]
186
+ node.key = old_suffix # update node to hold only the suffix
187
+ node.invalidate_hash()
188
+
189
+ # Create a new branch node with the key as prefix and value.
190
+ branch_node = PatriciaNode(key, value, [])
191
+ # The existing node becomes a child of the branch.
192
+ self._insert_child(branch_node.children, node.hash())
193
+ branch_hash = branch_node.hash()
194
+ self.storage.put(branch_hash, branch_node.to_bytes())
195
+ self.storage.put(node.hash(), node.to_bytes())
196
+ self.storage.delete(node_hash)
197
+ return branch_hash
198
+
199
+ # Case 4: Partial common prefix (split into a branch with two children).
200
+ if cp_len < len_node_key and cp_len < len_key:
201
+ common_prefix = key[:cp_len]
202
+ old_suffix = node.key[cp_len:]
203
+ new_suffix = key[cp_len:]
204
+ branch_node = PatriciaNode(common_prefix, None, [])
205
+
206
+ # Adjust the existing node.
207
+ node.key = old_suffix
208
+ node.invalidate_hash()
209
+ self._insert_child(branch_node.children, node.hash())
210
+
211
+ # Create a new leaf for the new key’s remaining portion.
212
+ new_leaf = PatriciaNode(new_suffix, value, [])
213
+ new_leaf_hash = new_leaf.hash()
214
+ self.storage.put(new_leaf_hash, new_leaf.to_bytes())
215
+ self._insert_child(branch_node.children, new_leaf_hash)
216
+
217
+ branch_hash = branch_node.hash()
218
+ self.storage.put(branch_hash, branch_node.to_bytes())
219
+ self.storage.put(node.hash(), node.to_bytes())
220
+ self.storage.delete(node_hash)
221
+ return branch_hash
222
+
223
+ raise Exception("Unhandled case in PatriciaTrie.put")
224
+
225
+ def _find_child(self, children: List[bytes], branch: int) -> Optional[bytes]:
226
+ """
227
+ Perform a binary search over the ordered children list to find the child hash whose
228
+ branch (first byte of its key) equals the target branch.
229
+ """
230
+ lo = 0
231
+ hi = len(children)
232
+ while lo < hi:
233
+ mid = (lo + hi) // 2
234
+ child_hash = children[mid]
235
+ child_node = PatriciaNode.from_storage(self.storage, child_hash)
236
+ if child_node is None or not child_node.key:
237
+ raise Exception("Child node missing or has empty key.")
238
+ child_branch = child_node.key[0]
239
+ if child_branch == branch:
240
+ return child_hash
241
+ elif child_branch < branch:
242
+ lo = mid + 1
243
+ else:
244
+ hi = mid
245
+ return None
246
+
247
+ def _find_child_index(self, children: List[bytes], branch: int) -> Optional[int]:
248
+ """
249
+ Similar to _find_child but returns the index in the list.
250
+ """
251
+ lo = 0
252
+ hi = len(children)
253
+ while lo < hi:
254
+ mid = (lo + hi) // 2
255
+ child_hash = children[mid]
256
+ child_node = PatriciaNode.from_storage(self.storage, child_hash)
257
+ if child_node is None or not child_node.key:
258
+ raise Exception("Child node missing or has empty key.")
259
+ child_branch = child_node.key[0]
260
+ if child_branch == branch:
261
+ return mid
262
+ elif child_branch < branch:
263
+ lo = mid + 1
264
+ else:
265
+ hi = mid
266
+ return None
267
+
268
+ def _insert_child(self, children: List[bytes], new_child_hash: bytes) -> None:
269
+ """
270
+ Insert a new child hash into the ordered children list.
271
+ """
272
+ new_child_node = PatriciaNode.from_storage(self.storage, new_child_hash)
273
+ if new_child_node is None or not new_child_node.key:
274
+ raise Exception("New child node missing or has empty key.")
275
+ new_branch = new_child_node.key[0]
276
+ lo = 0
277
+ hi = len(children)
278
+ while lo < hi:
279
+ mid = (lo + hi) // 2
280
+ child_hash = children[mid]
281
+ child_node = PatriciaNode.from_storage(self.storage, child_hash)
282
+ if child_node is None or not child_node.key:
283
+ raise Exception("Child node missing or has empty key.")
284
+ child_branch = child_node.key[0]
285
+ if child_branch < new_branch:
286
+ lo = mid + 1
287
+ else:
288
+ hi = mid
289
+ children.insert(lo, new_child_hash)
@@ -0,0 +1,99 @@
1
+ from typing import Optional
2
+ from ..storage.patricia import PatriciaTrie
3
+
4
+ import astreum.utils.bytes_format as bytes_format
5
+ class Account:
6
+ def __init__(self, public_key: bytes, balance: int, code: bytes, counter: int, data: bytes, secret_key: Optional[bytes] = None):
7
+ """
8
+ Initialize an Account.
9
+
10
+ :param public_key: The public key used as the account identifier (used as trie key).
11
+ :param balance: The account balance.
12
+ :param code: The associated code (for example, smart contract code).
13
+ :param counter: A transaction counter (nonce).
14
+ :param data: Additional account data.
15
+ :param secret_key: (Optional) The account’s secret key.
16
+ """
17
+ self.public_key = public_key
18
+ self.secret_key = secret_key # Optional private key.
19
+ self.balance = balance
20
+ self.code = code
21
+ self.counter = counter
22
+ self.data = data
23
+
24
+ @classmethod
25
+ def from_bytes(cls, public_key: bytes, data: bytes, secret_key: Optional[bytes] = None) -> 'Account':
26
+ """
27
+ Deserialize an Account from its byte representation.
28
+
29
+ Expected format: [balance, code, counter, data]
30
+
31
+ The public_key (and optional secret_key) must be provided separately.
32
+ """
33
+ decoded = bytes_format.decode(data)
34
+ balance, code, counter, account_data = decoded
35
+ return cls(public_key, balance, code, counter, account_data, secret_key=secret_key)
36
+
37
+ def to_bytes(self) -> bytes:
38
+ """
39
+ Serialize the Account into bytes.
40
+
41
+ Format: [balance, code, counter, data]
42
+ """
43
+ return bytes_format.encode([
44
+ self.balance,
45
+ self.code,
46
+ self.counter,
47
+ self.data
48
+ ])
49
+
50
+
51
+ class Accounts(PatriciaTrie):
52
+ """
53
+ Accounts is a PatriciaTrie storing Account objects.
54
+
55
+ The trie key is the account’s public key (bytes), while the value is the account data
56
+ (serialized with balance, code, counter, and data only).
57
+
58
+ You can instantiate Accounts with a storage instance and, if available, a root hash
59
+ representing an existing trie.
60
+ """
61
+ def get_account(self, public_key: bytes) -> Optional[Account]:
62
+ """
63
+ Retrieve an account using its public key.
64
+
65
+ :param public_key: The public key (bytes) of the account.
66
+ :return: The Account instance if found, else None.
67
+ """
68
+ raw = self.get(public_key)
69
+ if raw is None:
70
+ return None
71
+ return Account.from_bytes(public_key, raw)
72
+
73
+ def put_account(self, account: Account) -> None:
74
+ """
75
+ Insert or update an account in the trie.
76
+
77
+ :param account: The Account instance to store.
78
+ """
79
+ self.put(account.public_key, account.to_bytes())
80
+
81
+ def get_account_from_storage(public_key: bytes, accounts: Accounts, storage: Storage) -> Optional[Account]:
82
+ # 1. get account details hash
83
+ # 2. resolve storage objects to get account details
84
+ # 3. return account
85
+ return None
86
+
87
+ # Example of instantiation:
88
+ #
89
+ # Assuming you have an instance of Storage (e.g., from your storage module), you can create
90
+ # an Accounts instance either as an empty trie or using an existing root hash:
91
+ #
92
+ # storage = Storage()
93
+ #
94
+ # # To instantiate an empty Accounts trie:
95
+ # accounts = Accounts(storage)
96
+ #
97
+ # # To instantiate with an existing trie root hash:
98
+ # existing_root_hash = b'...' # This would be loaded from persistent storage.
99
+ # accounts = Accounts(storage, root_hash=existing_root_hash)
@@ -0,0 +1,30 @@
1
+ class Block:
2
+ @classmethod
3
+ def from_bytes(cls, validator) -> 'Block':
4
+ """
5
+ Deserialize an Account from its byte representation.
6
+
7
+ Expected format: [balance, code, counter, data]
8
+
9
+ The public_key (and optional secret_key) must be provided separately.
10
+ """
11
+ decoded = bytes_format.decode(data)
12
+ balance, code, counter, account_data = decoded
13
+ return cls(public_key, balance, code, counter, account_data, secret_key=secret_key)
14
+
15
+ def to_bytes(self) -> bytes:
16
+ """
17
+ Serialize the Account into bytes.
18
+
19
+ Format: [balance, code, counter, data]
20
+ """
21
+ return bytes_format.encode([
22
+ self.balance,
23
+ self.code,
24
+ self.counter,
25
+ self.data
26
+ ])
27
+
28
+ class Chain:
29
+ def __init__(self, latest_block: Block):
30
+ self.latest_block = latest_block