astreum 0.2.61__py3-none-any.whl → 0.3.1__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.
Files changed (75) hide show
  1. astreum/__init__.py +16 -7
  2. astreum/{_communication → communication}/__init__.py +3 -3
  3. astreum/communication/handlers/handshake.py +83 -0
  4. astreum/communication/handlers/ping.py +48 -0
  5. astreum/communication/handlers/storage_request.py +81 -0
  6. astreum/communication/models/__init__.py +0 -0
  7. astreum/{_communication → communication/models}/route.py +5 -5
  8. astreum/communication/setup.py +205 -0
  9. astreum/communication/start.py +38 -0
  10. astreum/consensus/__init__.py +20 -0
  11. astreum/consensus/genesis.py +66 -0
  12. astreum/consensus/models/__init__.py +0 -0
  13. astreum/consensus/models/account.py +84 -0
  14. astreum/consensus/models/accounts.py +72 -0
  15. astreum/consensus/models/block.py +364 -0
  16. astreum/{_consensus → consensus/models}/chain.py +7 -7
  17. astreum/{_consensus → consensus/models}/fork.py +8 -8
  18. astreum/consensus/models/receipt.py +98 -0
  19. astreum/{_consensus → consensus/models}/transaction.py +76 -78
  20. astreum/{_consensus → consensus}/setup.py +18 -50
  21. astreum/consensus/start.py +68 -0
  22. astreum/consensus/validator.py +95 -0
  23. astreum/{_consensus → consensus}/workers/discovery.py +20 -1
  24. astreum/consensus/workers/validation.py +291 -0
  25. astreum/{_consensus → consensus}/workers/verify.py +31 -2
  26. astreum/machine/__init__.py +20 -0
  27. astreum/machine/evaluations/__init__.py +0 -0
  28. astreum/{_lispeum → machine/evaluations}/high_evaluation.py +16 -15
  29. astreum/machine/evaluations/low_evaluation.py +281 -0
  30. astreum/machine/evaluations/script_evaluation.py +27 -0
  31. astreum/machine/models/__init__.py +0 -0
  32. astreum/machine/models/environment.py +31 -0
  33. astreum/{_lispeum → machine/models}/expression.py +36 -8
  34. astreum/machine/tokenizer.py +90 -0
  35. astreum/node.py +73 -781
  36. astreum/storage/__init__.py +7 -0
  37. astreum/storage/actions/get.py +69 -0
  38. astreum/storage/actions/set.py +132 -0
  39. astreum/{_storage → storage/models}/atom.py +55 -57
  40. astreum/{_storage/patricia.py → storage/models/trie.py} +227 -203
  41. astreum/storage/setup.py +44 -15
  42. {astreum-0.2.61.dist-info → astreum-0.3.1.dist-info}/METADATA +25 -24
  43. astreum-0.3.1.dist-info/RECORD +62 -0
  44. astreum/_communication/setup.py +0 -322
  45. astreum/_consensus/__init__.py +0 -20
  46. astreum/_consensus/account.py +0 -95
  47. astreum/_consensus/accounts.py +0 -38
  48. astreum/_consensus/block.py +0 -311
  49. astreum/_consensus/genesis.py +0 -72
  50. astreum/_consensus/receipt.py +0 -136
  51. astreum/_consensus/workers/validation.py +0 -125
  52. astreum/_lispeum/__init__.py +0 -16
  53. astreum/_lispeum/environment.py +0 -13
  54. astreum/_lispeum/low_evaluation.py +0 -123
  55. astreum/_lispeum/tokenizer.py +0 -22
  56. astreum/_node.py +0 -198
  57. astreum/_storage/__init__.py +0 -7
  58. astreum/_storage/setup.py +0 -35
  59. astreum/format.py +0 -75
  60. astreum/models/block.py +0 -441
  61. astreum/models/merkle.py +0 -205
  62. astreum/models/patricia.py +0 -393
  63. astreum/storage/object.py +0 -68
  64. astreum-0.2.61.dist-info/RECORD +0 -57
  65. /astreum/{models → communication/handlers}/__init__.py +0 -0
  66. /astreum/{_communication → communication/models}/message.py +0 -0
  67. /astreum/{_communication → communication/models}/peer.py +0 -0
  68. /astreum/{_communication → communication/models}/ping.py +0 -0
  69. /astreum/{_communication → communication}/util.py +0 -0
  70. /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
  71. /astreum/{_lispeum → machine/models}/meter.py +0 -0
  72. /astreum/{_lispeum → machine}/parser.py +0 -0
  73. {astreum-0.2.61.dist-info → astreum-0.3.1.dist-info}/WHEEL +0 -0
  74. {astreum-0.2.61.dist-info → astreum-0.3.1.dist-info}/licenses/LICENSE +0 -0
  75. {astreum-0.2.61.dist-info → astreum-0.3.1.dist-info}/top_level.txt +0 -0
astreum/__init__.py CHANGED
@@ -1,9 +1,18 @@
1
- """Lightweight package initializer to avoid circular imports during tests.
1
+
2
+ from astreum.consensus import Account, Accounts, Block, Chain, Fork, Receipt, Transaction
3
+ from astreum.machine import Env, Expr
4
+ from astreum.node import Node
2
5
 
3
- Exports are intentionally minimal; import submodules directly as needed:
4
- - Node, Expr, Env, tokenize, parse -> from astreum._node or astreum.lispeum
5
- - Validation types -> from astreum._validation
6
- - Storage types -> from astreum._storage
7
- """
8
6
 
9
- __all__: list[str] = []
7
+ __all__: list[str] = [
8
+ "Node",
9
+ "Env",
10
+ "Expr",
11
+ "Block",
12
+ "Chain",
13
+ "Fork",
14
+ "Receipt",
15
+ "Transaction",
16
+ "Account",
17
+ "Accounts",
18
+ ]
@@ -1,6 +1,6 @@
1
- from .message import Message
2
- from .peer import Peer
3
- from .route import Route
1
+ from .models.message import Message
2
+ from .models.peer import Peer
3
+ from .models.route import Route
4
4
  from .setup import communication_setup
5
5
 
6
6
  __all__ = [
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Sequence
4
+
5
+ from cryptography.hazmat.primitives import serialization
6
+
7
+ from ..models.peer import Peer
8
+ from ..models.message import Message
9
+
10
+ if TYPE_CHECKING:
11
+ from .... import Node
12
+
13
+
14
+ def handle_handshake(node: "Node", addr: Sequence[object], message: Message) -> bool:
15
+ """Handle incoming handshake messages.
16
+
17
+ Returns True if the outer loop should `continue`, False otherwise.
18
+ """
19
+ logger = node.logger
20
+
21
+ sender_key = message.sender
22
+ try:
23
+ sender_public_key_bytes = sender_key.public_bytes(
24
+ encoding=serialization.Encoding.Raw,
25
+ format=serialization.PublicFormat.Raw,
26
+ )
27
+ except Exception as exc:
28
+ logger.warning("Error extracting sender key bytes: %s", exc)
29
+ return True
30
+
31
+ try:
32
+ host, port = addr[0], int(addr[1])
33
+ except Exception:
34
+ return True
35
+ address_key = (host, port)
36
+
37
+ old_key_bytes = node.addresses.get(address_key)
38
+ node.addresses[address_key] = sender_public_key_bytes
39
+
40
+ if old_key_bytes is None:
41
+ try:
42
+ peer = Peer(node.relay_secret_key, sender_key)
43
+ except Exception:
44
+ return True
45
+ peer.address = address_key
46
+
47
+ node.peers[sender_public_key_bytes] = peer
48
+ node.peer_route.add_peer(sender_public_key_bytes, peer)
49
+
50
+ logger.info(
51
+ "Handshake accepted from %s:%s; peer added",
52
+ address_key[0],
53
+ address_key[1],
54
+ )
55
+ response = Message(handshake=True, sender=node.relay_public_key)
56
+ node.outgoing_queue.put((response.to_bytes(), address_key))
57
+ return True
58
+
59
+ if old_key_bytes == sender_public_key_bytes:
60
+ peer = node.peers.get(sender_public_key_bytes)
61
+ if peer is not None:
62
+ peer.address = address_key
63
+ return False
64
+
65
+ node.peers.pop(old_key_bytes, None)
66
+ try:
67
+ node.peer_route.remove_peer(old_key_bytes)
68
+ except Exception:
69
+ pass
70
+ try:
71
+ peer = Peer(node.relay_secret_key, sender_key)
72
+ except Exception:
73
+ return True
74
+ peer.address = address_key
75
+
76
+ node.peers[sender_public_key_bytes] = peer
77
+ node.peer_route.add_peer(sender_public_key_bytes, peer)
78
+ logger.info(
79
+ "Peer at %s:%s replaced due to key change",
80
+ address_key[0],
81
+ address_key[1],
82
+ )
83
+ return False
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import TYPE_CHECKING, Sequence
5
+
6
+ from ..models.ping import Ping
7
+
8
+ if TYPE_CHECKING:
9
+ from .... import Node
10
+
11
+
12
+ def handle_ping(node: "Node", addr: Sequence[object], payload: bytes) -> None:
13
+ """Update peer and validation state based on an incoming ping message."""
14
+ logger = node.logger
15
+ try:
16
+ host, port = addr[0], int(addr[1])
17
+ except Exception:
18
+ return
19
+
20
+ address_key = (host, port)
21
+ sender_public_key_bytes = node.addresses.get(address_key)
22
+ if sender_public_key_bytes is None:
23
+ return
24
+
25
+ peer = node.peers.get(sender_public_key_bytes)
26
+ if peer is None:
27
+ return
28
+
29
+ try:
30
+ ping = Ping.from_bytes(payload)
31
+ except Exception as exc:
32
+ logger.warning("Error decoding ping: %s", exc)
33
+ return
34
+
35
+ peer.timestamp = datetime.now(timezone.utc)
36
+ peer.latest_block = ping.latest_block
37
+
38
+ validation_route = node.validation_route
39
+ if validation_route is None:
40
+ return
41
+
42
+ try:
43
+ if ping.is_validator:
44
+ validation_route.add_peer(sender_public_key_bytes)
45
+ else:
46
+ validation_route.remove_peer(sender_public_key_bytes)
47
+ except Exception:
48
+ pass
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Sequence
4
+
5
+ from cryptography.hazmat.primitives import serialization
6
+
7
+ if TYPE_CHECKING:
8
+ from .... import Node
9
+ from ..models.message import Message
10
+
11
+
12
+ def handle_storage_request(node: "Node", addr: Sequence[object], message: "Message") -> None:
13
+ """Process incoming storage request payloads, forwarding if needed."""
14
+ logger = node.logger
15
+ payload = message.content
16
+ if len(payload) < 32:
17
+ return
18
+
19
+ atom_id = payload[:32]
20
+ provider_bytes = payload[32:]
21
+ if not provider_bytes:
22
+ return
23
+
24
+ try:
25
+ provider_str = provider_bytes.decode("utf-8")
26
+ except UnicodeDecodeError:
27
+ return
28
+
29
+ try:
30
+ host, port = addr[0], int(addr[1])
31
+ except Exception:
32
+ return
33
+ address_key = (host, port)
34
+ sender_key_bytes = node.addresses.get(address_key)
35
+ if sender_key_bytes is None:
36
+ return
37
+
38
+ try:
39
+ local_key_bytes = node.relay_public_key.public_bytes(
40
+ encoding=serialization.Encoding.Raw,
41
+ format=serialization.PublicFormat.Raw,
42
+ )
43
+ except Exception:
44
+ return
45
+
46
+ def xor_distance(target: bytes, key: bytes) -> int:
47
+ return int.from_bytes(
48
+ bytes(a ^ b for a, b in zip(target, key)),
49
+ byteorder="big",
50
+ signed=False,
51
+ )
52
+
53
+ self_distance = xor_distance(atom_id, local_key_bytes)
54
+
55
+ try:
56
+ closest_peer = node.peer_route.closest_peer_for_hash(atom_id)
57
+ except Exception:
58
+ closest_peer = None
59
+
60
+ if closest_peer is not None and closest_peer.public_key_bytes != sender_key_bytes:
61
+ closest_distance = xor_distance(atom_id, closest_peer.public_key_bytes)
62
+ if closest_distance < self_distance:
63
+ target_addr = closest_peer.address
64
+ if target_addr is not None and target_addr != addr:
65
+ try:
66
+ node.outgoing_queue.put((message.to_bytes(), target_addr))
67
+ except Exception:
68
+ return
69
+ logger.debug(
70
+ "Forwarded storage request for %s to %s",
71
+ atom_id.hex(),
72
+ target_addr,
73
+ )
74
+ return
75
+
76
+ node.storage_index[atom_id] = provider_str.strip()
77
+ logger.debug(
78
+ "Stored provider %s for atom %s",
79
+ provider_str.strip(),
80
+ atom_id.hex(),
81
+ )
File without changes
@@ -19,11 +19,11 @@ class Route:
19
19
  self.peers: Dict[bytes, Peer] = {}
20
20
 
21
21
  @staticmethod
22
- def _matching_leading_bits(a: bytes, b: bytes) -> int:
23
- for byte_index, (ba, bb) in enumerate(zip(a, b)):
24
- diff = ba ^ bb
25
- if diff:
26
- return byte_index * 8 + (8 - diff.bit_length())
22
+ def _matching_leading_bits(a: bytes, b: bytes) -> int:
23
+ for byte_index, (ba, bb) in enumerate(zip(a, b)):
24
+ diff = ba ^ bb
25
+ if diff:
26
+ return byte_index * 8 + (8 - diff.bit_length())
27
27
  return len(a) * 8
28
28
 
29
29
  def _normalize_peer_key(self, peer_public_key: PeerKey) -> bytes:
@@ -0,0 +1,205 @@
1
+ import socket, threading
2
+ from queue import Queue
3
+ from typing import Tuple, Optional
4
+ from cryptography.hazmat.primitives import serialization
5
+ from cryptography.hazmat.primitives.asymmetric import ed25519
6
+ from cryptography.hazmat.primitives.asymmetric.x25519 import (
7
+ X25519PrivateKey,
8
+ X25519PublicKey,
9
+ )
10
+
11
+ from typing import TYPE_CHECKING
12
+ if TYPE_CHECKING:
13
+ from .. import Node
14
+
15
+ from . import Route, Message
16
+ from .handlers.handshake import handle_handshake
17
+ from .handlers.ping import handle_ping
18
+ from .handlers.storage_request import handle_storage_request
19
+ from .models.message import MessageTopic
20
+ from .util import address_str_to_host_and_port
21
+ from ..utils.bytes import hex_to_bytes
22
+
23
+ def load_x25519(hex_key: Optional[str]) -> X25519PrivateKey:
24
+ """DH key for relaying (always X25519)."""
25
+ if hex_key:
26
+ return X25519PrivateKey.from_private_bytes(bytes.fromhex(hex_key))
27
+ return X25519PrivateKey.generate()
28
+
29
+ def load_ed25519(hex_key: Optional[str]) -> Optional[ed25519.Ed25519PrivateKey]:
30
+ """Signing key for validation (Ed25519), or None if absent."""
31
+ return ed25519.Ed25519PrivateKey.from_private_bytes(bytes.fromhex(hex_key)) \
32
+ if hex_key else None
33
+
34
+ def make_routes(
35
+ relay_pk: X25519PublicKey,
36
+ val_sk: Optional[ed25519.Ed25519PrivateKey]
37
+ ) -> Tuple[Route, Optional[Route]]:
38
+ """Peer route (DH pubkey) + optional validation route (ed pubkey)."""
39
+ peer_rt = Route(relay_pk)
40
+ val_rt = Route(val_sk.public_key()) if val_sk else None
41
+ return peer_rt, val_rt
42
+
43
+ def setup_outgoing(
44
+ use_ipv6: bool
45
+ ) -> Tuple[socket.socket, Queue, threading.Thread]:
46
+ fam = socket.AF_INET6 if use_ipv6 else socket.AF_INET
47
+ sock = socket.socket(fam, socket.SOCK_DGRAM)
48
+ q = Queue()
49
+ thr = threading.Thread(target=lambda: None, daemon=True)
50
+ thr.start()
51
+ return sock, q, thr
52
+
53
+ def make_maps():
54
+ """Empty lookup maps: peers and addresses."""
55
+ return
56
+
57
+
58
+ def process_incoming_messages(node: "Node") -> None:
59
+ """Process incoming messages (placeholder)."""
60
+ node_logger = node.logger
61
+ while True:
62
+ try:
63
+ data, addr = node.incoming_queue.get()
64
+ except Exception as exc:
65
+ node_logger.exception("Error taking from incoming queue")
66
+ continue
67
+
68
+ try:
69
+ message = Message.from_bytes(data)
70
+ except Exception as exc:
71
+ node_logger.warning("Error decoding message: %s", exc)
72
+ continue
73
+
74
+ if message.handshake:
75
+ if handle_handshake(node, addr, message):
76
+ continue
77
+
78
+ match message.topic:
79
+ case MessageTopic.PING:
80
+ handle_ping(node, addr, message.content)
81
+ case MessageTopic.OBJECT_REQUEST:
82
+ pass
83
+ case MessageTopic.OBJECT_RESPONSE:
84
+ pass
85
+ case MessageTopic.ROUTE_REQUEST:
86
+ pass
87
+ case MessageTopic.ROUTE_RESPONSE:
88
+ pass
89
+ case MessageTopic.TRANSACTION:
90
+ if node.validation_secret_key is None:
91
+ continue
92
+ node._validation_transaction_queue.put(message.content)
93
+
94
+ case MessageTopic.STORAGE_REQUEST:
95
+ handle_storage_request(node, addr, message)
96
+
97
+ case _:
98
+ continue
99
+
100
+
101
+ def populate_incoming_messages(node: "Node") -> None:
102
+ """Receive UDP packets and feed the incoming queue (placeholder)."""
103
+ node_logger = node.logger
104
+ while True:
105
+ try:
106
+ data, addr = node.incoming_socket.recvfrom(4096)
107
+ node.incoming_queue.put((data, addr))
108
+ except Exception as exc:
109
+ node_logger.warning("Error populating incoming queue: %s", exc)
110
+
111
+ def communication_setup(node: "Node", config: dict):
112
+ node.logger.info("Setting up node communication")
113
+ node.use_ipv6 = config.get('use_ipv6', False)
114
+
115
+ # key loading
116
+ node.relay_secret_key = load_x25519(config.get('relay_secret_key'))
117
+ node.validation_secret_key = load_ed25519(config.get('validation_secret_key'))
118
+
119
+ # derive pubs + routes
120
+ node.relay_public_key = node.relay_secret_key.public_key()
121
+ node.validation_public_key = (
122
+ node.validation_secret_key.public_key().public_bytes(
123
+ encoding=serialization.Encoding.Raw,
124
+ format=serialization.PublicFormat.Raw,
125
+ )
126
+ if node.validation_secret_key
127
+ else None
128
+ )
129
+ node.peer_route, node.validation_route = make_routes(
130
+ node.relay_public_key,
131
+ node.validation_secret_key
132
+ )
133
+
134
+ # sockets + queues + threads
135
+ incoming_port = config.get('incoming_port', 7373)
136
+ fam = socket.AF_INET6 if node.use_ipv6 else socket.AF_INET
137
+ node.incoming_socket = socket.socket(fam, socket.SOCK_DGRAM)
138
+ if node.use_ipv6:
139
+ node.incoming_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
140
+ node.incoming_socket.bind(("::" if node.use_ipv6 else "0.0.0.0", incoming_port or 0))
141
+ node.incoming_port = node.incoming_socket.getsockname()[1]
142
+ node.logger.info(
143
+ "Incoming UDP socket bound to %s:%s",
144
+ "::" if node.use_ipv6 else "0.0.0.0",
145
+ node.incoming_port,
146
+ )
147
+ node.incoming_queue = Queue()
148
+ node.incoming_populate_thread = threading.Thread(
149
+ target=populate_incoming_messages,
150
+ args=(node,),
151
+ daemon=True,
152
+ )
153
+ node.incoming_process_thread = threading.Thread(
154
+ target=process_incoming_messages,
155
+ args=(node,),
156
+ daemon=True,
157
+ )
158
+ node.incoming_populate_thread.start()
159
+ node.incoming_process_thread.start()
160
+
161
+ (node.outgoing_socket,
162
+ node.outgoing_queue,
163
+ node.outgoing_thread
164
+ ) = setup_outgoing(node.use_ipv6)
165
+
166
+ # other workers & maps
167
+ node.object_request_queue = Queue()
168
+ node.peer_manager_thread = threading.Thread(
169
+ target=node._relay_peer_manager,
170
+ daemon=True
171
+ )
172
+ node.peer_manager_thread.start()
173
+
174
+ node.peers, node.addresses = {}, {} # peers: Dict[bytes,Peer], addresses: Dict[(str,int),bytes]
175
+
176
+ latest_block_hex = config.get("latest_block_hash")
177
+ if latest_block_hex:
178
+ try:
179
+ node.latest_block_hash = hex_to_bytes(latest_block_hex, expected_length=32)
180
+ except Exception as exc:
181
+ node.logger.warning("Invalid latest_block_hash in config: %s", exc)
182
+ node.latest_block_hash = None
183
+ else:
184
+ node.latest_block_hash = None
185
+
186
+ # bootstrap pings
187
+ bootstrap_peers = config.get('bootstrap', [])
188
+ for addr in bootstrap_peers:
189
+ try:
190
+ host, port = address_str_to_host_and_port(addr) # type: ignore[arg-type]
191
+ except Exception as exc:
192
+ node.logger.warning("Invalid bootstrap address %s: %s", addr, exc)
193
+ continue
194
+
195
+ handshake_message = Message(handshake=True, sender=node.relay_public_key)
196
+
197
+ node.outgoing_queue.put((handshake_message.to_bytes(), (host, port)))
198
+ node.logger.info("Sent bootstrap handshake to %s:%s", host, port)
199
+
200
+ node.logger.info(
201
+ "Communication ready (incoming_port=%s, outgoing_socket_initialized=%s, bootstrap_count=%s)",
202
+ node.incoming_port,
203
+ node.outgoing_socket is not None,
204
+ len(bootstrap_peers),
205
+ )
@@ -0,0 +1,38 @@
1
+ def connect_to_network_and_verify(self):
2
+ """Initialize communication and consensus components, then load latest block state."""
3
+ node_logger = self.logger
4
+ node_logger.info("Starting communication and consensus setup")
5
+ try:
6
+ from astreum.communication import communication_setup # type: ignore
7
+ communication_setup(node=self, config=self.config)
8
+ node_logger.info("Communication setup completed")
9
+ except Exception:
10
+ node_logger.exception("Communication setup failed")
11
+
12
+ try:
13
+ from astreum.consensus import consensus_setup # type: ignore
14
+ consensus_setup(node=self, config=self.config)
15
+ node_logger.info("Consensus setup completed")
16
+ except Exception:
17
+ node_logger.exception("Consensus setup failed")
18
+
19
+ # Load latest_block_hash from config
20
+ self.latest_block_hash = getattr(self, "latest_block_hash", None)
21
+ self.latest_block = getattr(self, "latest_block", None)
22
+
23
+ latest_block_hex = self.config.get("latest_block_hash")
24
+ if latest_block_hex and self.latest_block_hash is None:
25
+ try:
26
+ from astreum.utils.bytes import hex_to_bytes
27
+ self.latest_block_hash = hex_to_bytes(latest_block_hex, expected_length=32)
28
+ node_logger.debug("Loaded latest_block_hash override from config")
29
+ except Exception as exc:
30
+ node_logger.error("Invalid latest_block_hash in config: %s", exc)
31
+
32
+ if self.latest_block_hash and self.latest_block is None:
33
+ try:
34
+ from astreum.consensus.models.block import Block
35
+ self.latest_block = Block.from_atom(self, self.latest_block_hash)
36
+ node_logger.info("Loaded latest block %s from storage", self.latest_block_hash.hex())
37
+ except Exception as exc:
38
+ node_logger.warning("Could not load latest block from storage: %s", exc)
@@ -0,0 +1,20 @@
1
+ from .models.account import Account
2
+ from .models.accounts import Accounts
3
+ from .models.block import Block
4
+ from .models.chain import Chain
5
+ from .models.fork import Fork
6
+ from .models.receipt import Receipt
7
+ from .models.transaction import Transaction
8
+ from .setup import consensus_setup
9
+
10
+
11
+ __all__ = [
12
+ "Block",
13
+ "Chain",
14
+ "Fork",
15
+ "Receipt",
16
+ "Transaction",
17
+ "Account",
18
+ "Accounts",
19
+ "consensus_setup",
20
+ ]
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, List
4
+
5
+ from .models.account import Account
6
+ from .models.accounts import Accounts
7
+ from .models.block import Block
8
+ from ..storage.models.atom import ZERO32
9
+ from ..storage.models.trie import Trie
10
+ from ..utils.integer import int_to_bytes
11
+
12
+ TREASURY_ADDRESS = b"\x01" * 32
13
+ BURN_ADDRESS = b"\x00" * 32
14
+
15
+
16
+ def create_genesis_block(
17
+ node: Any,
18
+ validator_public_key: bytes,
19
+ chain_id: int = 0,
20
+ ) -> Block:
21
+ validator_pk = bytes(validator_public_key)
22
+
23
+ if len(validator_pk) != 32:
24
+ raise ValueError("validator_public_key must be 32 bytes")
25
+
26
+ stake_trie = Trie()
27
+ stake_amount = int_to_bytes(1)
28
+ stake_trie.put(storage_node=node, key=validator_pk, value=stake_amount)
29
+ stake_root = stake_trie.root_hash or ZERO32
30
+
31
+ treasury_account = Account.create(balance=1, data_hash=stake_root, counter=0)
32
+ treasury_account.data = stake_trie
33
+ treasury_account.data_hash = stake_root
34
+ burn_account = Account.create(balance=0, data_hash=b"", counter=0)
35
+ validator_account = Account.create(balance=0, data_hash=b"", counter=0)
36
+
37
+ accounts = Accounts()
38
+ accounts.set_account(TREASURY_ADDRESS, treasury_account)
39
+ accounts.set_account(BURN_ADDRESS, burn_account)
40
+ accounts.set_account(validator_pk, validator_account)
41
+
42
+ accounts.update_trie(node)
43
+ accounts_root = accounts.root_hash
44
+ if accounts_root is None:
45
+ raise ValueError("genesis accounts trie is empty")
46
+
47
+ block = Block(
48
+ chain_id=chain_id,
49
+ previous_block_hash=ZERO32,
50
+ previous_block=None,
51
+ number=0,
52
+ timestamp=0,
53
+ accounts_hash=accounts_root,
54
+ transactions_total_fees=0,
55
+ transactions_hash=ZERO32,
56
+ receipts_hash=ZERO32,
57
+ delay_difficulty=0,
58
+ validator_public_key=validator_pk,
59
+ nonce=0,
60
+ signature=b"",
61
+ accounts=accounts,
62
+ transactions=[],
63
+ receipts=[],
64
+ )
65
+
66
+ return block
File without changes
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, List, Tuple
5
+
6
+ from ...storage.models.atom import Atom, ZERO32, AtomKind
7
+ from ...storage.models.trie import Trie
8
+ from ...utils.integer import bytes_to_int, int_to_bytes
9
+
10
+
11
+ @dataclass
12
+ class Account:
13
+ balance: int
14
+ code_hash: bytes
15
+ counter: int
16
+ data_hash: bytes
17
+ data: Trie
18
+ atom_hash: bytes = ZERO32
19
+ atoms: List[Atom] = field(default_factory=list)
20
+
21
+ @classmethod
22
+ def create(cls, balance: int = 0, data_hash: bytes = ZERO32, code_hash: bytes = ZERO32, counter: int = 0) -> "Account":
23
+ account = cls(
24
+ balance=int(balance),
25
+ code_hash=bytes(code_hash),
26
+ counter=int(counter),
27
+ data_hash=bytes(data_hash),
28
+ data=Trie(root_hash=bytes(data_hash)),
29
+ )
30
+ atom_hash, atoms = account.to_atom()
31
+ account.atom_hash = atom_hash
32
+ account.atoms = atoms
33
+ return account
34
+
35
+ @classmethod
36
+ def from_atom(cls, node: Any, root_id: bytes) -> "Account":
37
+
38
+ account_atoms = node.get_atom_list_from_storage(root_hash=root_id)
39
+
40
+ if account_atoms is None or len(account_atoms) != 5:
41
+ raise ValueError("malformed account atom list")
42
+
43
+ type_atom, balance_atom, code_atom, counter_atom, data_atom = account_atoms
44
+
45
+ if type_atom.data != b"account":
46
+ raise ValueError("not an account (type mismatch)")
47
+
48
+ account = cls.create(
49
+ balance=bytes_to_int(balance_atom.data),
50
+ data_hash=data_atom.data,
51
+ counter=bytes_to_int(counter_atom.data),
52
+ code_hash=code_atom.data,
53
+ )
54
+
55
+ return account
56
+
57
+ def to_atom(self) -> Tuple[bytes, List[Atom]]:
58
+ data_atom = Atom(
59
+ data=bytes(self.data_hash),
60
+ kind=AtomKind.LIST,
61
+ )
62
+ counter_atom = Atom(
63
+ data=int_to_bytes(self.counter),
64
+ next_id=data_atom.object_id(),
65
+ kind=AtomKind.BYTES,
66
+ )
67
+ code_atom = Atom(
68
+ data=bytes(self.code_hash),
69
+ next_id=counter_atom.object_id(),
70
+ kind=AtomKind.LIST,
71
+ )
72
+ balance_atom = Atom(
73
+ data=int_to_bytes(self.balance),
74
+ next_id=code_atom.object_id(),
75
+ kind=AtomKind.BYTES,
76
+ )
77
+ type_atom = Atom(
78
+ data=b"account",
79
+ next_id=balance_atom.object_id(),
80
+ kind=AtomKind.SYMBOL,
81
+ )
82
+
83
+ atoms = [data_atom, counter_atom, code_atom, balance_atom, type_atom]
84
+ return type_atom.object_id(), list(atoms)