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.

@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, Union
4
+
5
+ ByteLike = Union[bytes, bytearray, memoryview]
6
+
7
+
8
+ def int_to_bytes(value: Optional[int]) -> bytes:
9
+ """Convert an integer to a little-endian byte string with minimal length."""
10
+ if value is None:
11
+ return b""
12
+ value = int(value)
13
+ if value == 0:
14
+ return b"\x00"
15
+ length = (value.bit_length() + 7) // 8
16
+ return value.to_bytes(length, "little", signed=False)
17
+
18
+
19
+ def bytes_to_int(data: Optional[ByteLike]) -> int:
20
+ """Convert a little-endian byte string to an integer."""
21
+ if not data:
22
+ return 0
23
+ if isinstance(data, memoryview):
24
+ data = data.tobytes()
25
+ return int.from_bytes(bytes(data), "little", signed=False)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.2.40
3
+ Version: 0.2.42
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,7 +1,7 @@
1
1
  astreum/__init__.py,sha256=9tzA27B_eG5wRF1SAWJIV7xTmCcR1QFc123b_cvFOa4,345
2
2
  astreum/_node.py,sha256=3fpfULVs3MrPBR-ymlvPyuZMi8lv0a8JBnxPb2oFOTU,2214
3
3
  astreum/format.py,sha256=X4tG5GGPweNCE54bHYkLFiuLTbmpy5upO_s1Cef-MGA,2711
4
- astreum/node.py,sha256=SuVm1b0QWl1FpDUaLRH1fiFYnXCrPs6qYeUQlPDae8w,38358
4
+ astreum/node.py,sha256=MmlK3jaANTMB3ZAxR8IaSc82OS9meJmVawYIVURADbg,39689
5
5
  astreum/_communication/__init__.py,sha256=XJui0yOcfAur4HKt-8sSRlwB-MSU1rchkuOAY-nKDOE,207
6
6
  astreum/_communication/message.py,sha256=zNjUnt1roJRHMEoiLkcA7VK_TsHXok8PYEF7KTCqKnQ,3303
7
7
  astreum/_communication/peer.py,sha256=DT2vJOzeLyyOj7vTDQ1u1lF5Vc7Epj0Hhie-YfDrzfA,425
@@ -9,14 +9,20 @@ astreum/_communication/ping.py,sha256=u_DQTZJsbMdYiDDqjdZDsLaN5na2m9WZjVeEM3zq9_
9
9
  astreum/_communication/route.py,sha256=fbVXY0xF3O-k7dY9TuCsr6_XxD3m7Cb9ugVacQZ6GSk,2490
10
10
  astreum/_communication/setup.py,sha256=TzXpc8dajV31BJhHQMMcb5UdaFrxb1xlYSj58vc_5mg,9072
11
11
  astreum/_communication/util.py,sha256=bJ3td3naDzmCelAJQpLwiDMoRBkijQl9YLROjsWyOrI,1256
12
- astreum/_consensus/__init__.py,sha256=3RnLg5wdUCb4l7yFAB5fjxH6drGfpcjMx7XaHepXdY4,285
13
- astreum/_consensus/block.py,sha256=fnDOAAmHx2gaqA3XHuP-gBEXNadJyVjICkpv1-n_FVo,12037
14
- astreum/_consensus/chain.py,sha256=jx5O3tjLQZUl81XFAK-AI0Wh3WQ0NrHle3GrwyBXX2Q,2730
15
- astreum/_consensus/fork.py,sha256=29r4jyVihy0Gyt4JzCgEEDbrip_BkwQUtOU8JlypdpA,3753
16
- astreum/_consensus/genesis.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- astreum/_consensus/receipt.py,sha256=dm01AgOG7SFyrHBbNB48_81t_gUZnHCNE4Ps874P7_k,5663
18
- astreum/_consensus/setup.py,sha256=gpTbbHRAAzRhaQWNlw5NCiFXlOrUOMx5OkCJv_kY5Io,8073
19
- astreum/_consensus/transaction.py,sha256=qg1ahHslIXU7YSa-rUGd2zsrsIA5phgcNrRRsAvyHig,5770
12
+ astreum/_consensus/__init__.py,sha256=gOCpvnIeO17CGjUGr0odaKNvGEggmDRXfT5IuyrYtcM,376
13
+ astreum/_consensus/account.py,sha256=XCBWRapmFMrLCzz0wLtuuIaDRyNN9IKyOGc4dEl8W0s,3294
14
+ astreum/_consensus/accounts.py,sha256=zGq2BCMJPtD_lzcj4jqMzB2Foc3ptxXSPhc9zypB1T0,1106
15
+ astreum/_consensus/block.py,sha256=v6U2y8br9PVKGn0Q8fLVOtEODlXayPtEd_Wqprem_TU,12358
16
+ astreum/_consensus/chain.py,sha256=WwUeLrdg7uj--ZsUxse6xFlzO2QeQQggyKSL49KhfU0,2768
17
+ astreum/_consensus/fork.py,sha256=dK5DtT9hWCj_rQs6MS1c1bcGBbkgVTBPIOudYbqS9vw,3825
18
+ astreum/_consensus/genesis.py,sha256=oED7AHw0fPJgMu0OmhbbikJcbItw_bxVBVU8RlB2rJM,4766
19
+ astreum/_consensus/receipt.py,sha256=GPLspqVJHnyBr1cKmBoClsJyeEUecYmg3_acz4L4rRo,6031
20
+ astreum/_consensus/setup.py,sha256=agwqfOemdLqE0Z1zrPV2XHVmZ9sAxp8Bh4k6e2u-t8w,2385
21
+ astreum/_consensus/transaction.py,sha256=VRp06ExbjAebiPcTZEX7PH-zwjzy4CLcf5kclyooL1k,7475
22
+ astreum/_consensus/workers/__init__.py,sha256=bS5FjbevbIR5FHbVGnT4Jli17VIld_5auemRw4CaHFU,278
23
+ astreum/_consensus/workers/discovery.py,sha256=X1yjKGjLSApMJ9mgWbnc7N21ALD09khDf-in-M45Mis,1683
24
+ astreum/_consensus/workers/validation.py,sha256=WcHLOs2NLMTrJpErghBXWeW2aqtX3-xGPo-HcySdEZ8,4814
25
+ astreum/_consensus/workers/verify.py,sha256=uqmf2UICj_eBFeGDpGyzKQLtz9qeCbOtz5euwNqkZmw,1924
20
26
  astreum/_lispeum/__init__.py,sha256=LAy2Z-gBBQlByBHRGUKaaQrOB7QzFEEyGRtInmwTpfU,304
21
27
  astreum/_lispeum/environment.py,sha256=pJ0rjp9GoQxHhDiPIVei0jP7dZ_Pznso2O_tpp94-Ik,328
22
28
  astreum/_lispeum/expression.py,sha256=io8tbCer_1TJee77yRbcNI5q-DPFGa8xZiC80tGvRRQ,1063
@@ -26,34 +32,23 @@ astreum/_lispeum/meter.py,sha256=5q2PFW7_jmgKVM1-vwE4RRjMfPEthUA4iu1CwR-Axws,505
26
32
  astreum/_lispeum/parser.py,sha256=WOW3sSZWkIzPEy3fYTyl0lrtkMxHL9zRrNSYztPDemQ,2271
27
33
  astreum/_lispeum/tokenizer.py,sha256=P68uIj4aPKzjuCJ85jfzRi67QztpuXIOC1vvLQueBI4,552
28
34
  astreum/_storage/__init__.py,sha256=EmKZNAZmo3UVE3ekOOuckwFnBVjpa0Sy8Oxg72Lgdxc,53
29
- astreum/_storage/atom.py,sha256=w0O_jw74nGhO0hgxFHk0RmkCIgHXHweBEOVULi7_dhQ,3652
35
+ astreum/_storage/atom.py,sha256=YFjvfMhniInch13iaKGpw4CCxxgyWonniryb-Rfse4A,4177
30
36
  astreum/_storage/patricia.py,sha256=Eh8AO2_J8WCai4kyuML_0Jb_zntgGlq-VPljnSjss10,14851
31
37
  astreum/crypto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
38
  astreum/crypto/ed25519.py,sha256=FRnvlN0kZlxn4j-sJKl-C9tqiz_0z4LZyXLj3KIj1TQ,1760
33
39
  astreum/crypto/quadratic_form.py,sha256=pJgbORey2NTWbQNhdyvrjy_6yjORudQ67jBz2ScHptg,4037
34
40
  astreum/crypto/wesolowski.py,sha256=SUgGXW3Id07dJtWzDcs4dluIhjqbRWQ8YWjn_mK78AQ,4092
35
41
  astreum/crypto/x25519.py,sha256=i29v4BmwKRcbz9E7NKqFDQyxzFtJUqN0St9jd7GS1uA,1137
36
- astreum/lispeum/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
- astreum/lispeum/environment.py,sha256=wolwt9psDl62scgjaVG0G59xlBs1AM4NPgryUbxzG_4,1220
38
- astreum/lispeum/expression.py,sha256=K8gFifDaHu394bs9qnpvP8tjeiymFGQpnDC_iW9nU4E,2379
39
- astreum/lispeum/parser.py,sha256=jQRzZYvBuSg8t_bxsbt1-WcHaR_LPveHNX7Qlxhaw-M,1165
40
- astreum/lispeum/tokenizer.py,sha256=J-I7MEd0r2ZoVqxvRPlu-Afe2ZdM0tKXXhf1R4SxYTo,1429
41
42
  astreum/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
- astreum/models/account.py,sha256=sHujGSwtV13rvOGJ5LZXuMrJ4F9XUdvyuWKz-zJ9lkE,2986
43
- astreum/models/accounts.py,sha256=aFSEWlq6zRf65-KGAdNGqEJyNVY3fpKhx8y1vU6sgSc,1164
44
- astreum/models/block.py,sha256=XNuzLaYKGX9Mv_naqWATfPskpWmeBh6FYzo4TGimLwg,18120
43
+ astreum/models/block.py,sha256=_dgas0zW9OLFV7C1Bo-laxa102klqpJrCAG3DnwQ51U,18092
45
44
  astreum/models/merkle.py,sha256=lvWJa9nmrBL0n_2h_uNqpB_9a5s5Hn1FceRLx0IZIVQ,6778
46
45
  astreum/models/patricia.py,sha256=ohmXrcaz7Ae561tyC4u4iPOkQPkKr8N0IWJek4upFIg,13392
47
- astreum/models/transaction.py,sha256=MkLL5YX18kIf9-O4LBaZ4eWjkXDAaYIrDcDehbDZoqg,3038
48
- astreum/relay/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
- astreum/relay/peer.py,sha256=94rNkHfsvYfq-ijLDR9QQkEJZ1meMr17HsaNYDBB8kg,398
50
- astreum/relay/route.py,sha256=enWT_1260LJq-L-zK-jtacQ8LbZGquNO9yj-9IglSXE,1232
51
- astreum/relay/setup.py,sha256=ynvGaJdlDtw_f5LLiow2Wo7IRzUjvgk8eSr1Sv4_zTg,2090
52
46
  astreum/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
47
  astreum/storage/object.py,sha256=knFlvw_tpcC4twSu1DGNpHX31wlANN8E5dgEqIfU--Q,2041
54
48
  astreum/storage/setup.py,sha256=1-9ztEFI_BvRDvAA0lAn4mFya8iq65THTArlj--M3Hg,626
55
- astreum-0.2.40.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
56
- astreum-0.2.40.dist-info/METADATA,sha256=ctn855yZQ8DLk5ZA-TsVvqSUCGU9viV6XzDF95vl1Hs,6181
57
- astreum-0.2.40.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
- astreum-0.2.40.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
59
- astreum-0.2.40.dist-info/RECORD,,
49
+ astreum/utils/integer.py,sha256=iQt-klWOYVghu_NOT341MmHbOle4FDT3by4PNKNXscg,736
50
+ astreum-0.2.42.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
51
+ astreum-0.2.42.dist-info/METADATA,sha256=xb_Bi75R1izIliHmHZQiJ93TxWPn6l9Z9EiN4IqVTAc,6181
52
+ astreum-0.2.42.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
+ astreum-0.2.42.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
54
+ astreum-0.2.42.dist-info/RECORD,,
File without changes
@@ -1,40 +0,0 @@
1
- from typing import Dict, Optional
2
- import uuid
3
-
4
- from astreum.lispeum.expression import Expr
5
-
6
-
7
- class Env:
8
- def __init__(
9
- self,
10
- data: Optional[Dict[str, Expr]] = None,
11
- parent_id: Optional[uuid.UUID] = None,
12
- max_exprs: Optional[int] = 8,
13
- ):
14
- self.data: Dict[str, Expr] = data if data is not None else {}
15
- self.parent_id: Optional[uuid.UUID] = parent_id
16
- self.max_exprs: Optional[int] = max_exprs
17
-
18
- def put(self, name: str, value: Expr) -> None:
19
- if (
20
- self.max_exprs is not None
21
- and name not in self.data
22
- and len(self.data) >= self.max_exprs
23
- ):
24
- raise RuntimeError(
25
- f"environment full: {len(self.data)} ≥ max_exprs={self.max_exprs}"
26
- )
27
- self.data[name] = value
28
-
29
- def get(self, name: str) -> Optional[Expr]:
30
- return self.data.get(name)
31
-
32
- def pop(self, name: str) -> Optional[Expr]:
33
- return self.data.pop(name, None)
34
-
35
- def __repr__(self) -> str:
36
- return (
37
- f"Env(size={len(self.data)}, "
38
- f"max_exprs={self.max_exprs}, "
39
- f"parent_id={self.parent_id})"
40
- )
@@ -1,86 +0,0 @@
1
-
2
- from typing import List, Optional, Union
3
-
4
-
5
- class Expr:
6
- class ListExpr:
7
- def __init__(self, elements: List['Expr']):
8
- self.elements = elements
9
-
10
- def __eq__(self, other):
11
- if not isinstance(other, Expr.ListExpr):
12
- return NotImplemented
13
- return self.elements == other.elements
14
-
15
- def __ne__(self, other):
16
- return not self.__eq__(other)
17
-
18
- @property
19
- def value(self):
20
- inner = " ".join(str(e) for e in self.elements)
21
- return f"({inner})"
22
-
23
-
24
- def __repr__(self):
25
- if not self.elements:
26
- return "()"
27
-
28
- inner = " ".join(str(e) for e in self.elements)
29
- return f"({inner})"
30
-
31
- def __iter__(self):
32
- return iter(self.elements)
33
-
34
- def __getitem__(self, index: Union[int, slice]):
35
- return self.elements[index]
36
-
37
- def __len__(self):
38
- return len(self.elements)
39
-
40
- class Symbol:
41
- def __init__(self, value: str):
42
- self.value = value
43
-
44
- def __repr__(self):
45
- return self.value
46
-
47
- class Integer:
48
- def __init__(self, value: int):
49
- self.value = value
50
-
51
- def __repr__(self):
52
- return str(self.value)
53
-
54
- class String:
55
- def __init__(self, value: str):
56
- self.value = value
57
-
58
- def __repr__(self):
59
- return f'"{self.value}"'
60
-
61
- class Boolean:
62
- def __init__(self, value: bool):
63
- self.value = value
64
-
65
- def __repr__(self):
66
- return "true" if self.value else "false"
67
-
68
- class Function:
69
- def __init__(self, params: List[str], body: 'Expr'):
70
- self.params = params
71
- self.body = body
72
-
73
- def __repr__(self):
74
- params_str = " ".join(self.params)
75
- body_str = str(self.body)
76
- return f"(fn ({params_str}) {body_str})"
77
-
78
- class Error:
79
- def __init__(self, message: str, origin: Optional['Expr'] = None):
80
- self.message = message
81
- self.origin = origin
82
-
83
- def __repr__(self):
84
- if self.origin is None:
85
- return f'(error "{self.message}")'
86
- return f'(error "{self.message}" in {self.origin})'
astreum/lispeum/parser.py DELETED
@@ -1,41 +0,0 @@
1
- from typing import List, Tuple
2
- from ..node import Expr
3
-
4
- class ParseError(Exception):
5
- pass
6
-
7
- def parse(tokens: List[str]) -> Tuple[Expr, List[str]]:
8
- if not tokens:
9
- raise ParseError("Unexpected end of input")
10
-
11
- first_token, *rest = tokens
12
-
13
- if first_token == '(':
14
- if not rest:
15
- raise ParseError("Expected token after '('")
16
-
17
- list_items = []
18
- inner_tokens = rest
19
-
20
- while inner_tokens:
21
- if inner_tokens[0] == ')':
22
- return Expr.ListExpr(list_items), inner_tokens[1:]
23
-
24
- expr, inner_tokens = parse(inner_tokens)
25
- list_items.append(expr)
26
-
27
- raise ParseError("Expected closing ')'")
28
-
29
- elif first_token == ')':
30
- raise ParseError("Unexpected closing parenthesis")
31
-
32
- elif first_token.startswith('"') and first_token.endswith('"'):
33
- string_content = first_token[1:-1]
34
- return Expr.String(string_content), rest
35
-
36
- else:
37
- try:
38
- number = int(first_token)
39
- return Expr.Integer(number), rest
40
- except ValueError:
41
- return Expr.Symbol(first_token), rest
@@ -1,52 +0,0 @@
1
-
2
-
3
- # Tokenizer function
4
- from typing import List
5
-
6
- class ParseError(Exception):
7
- pass
8
-
9
- def tokenize(input: str) -> List[str]:
10
- tokens = []
11
- current_token = ""
12
- in_string = False
13
- escape = False
14
-
15
- for char in input:
16
- if in_string:
17
- if escape:
18
- current_token += char
19
- escape = False
20
- elif char == '\\':
21
- escape = True
22
- elif char == '"':
23
- in_string = False
24
- tokens.append(f'"{current_token}"')
25
- current_token = ""
26
- else:
27
- current_token += char
28
- else:
29
- if char.isspace():
30
- if current_token:
31
- tokens.append(current_token)
32
- current_token = ""
33
- elif char == '(' or char == ')':
34
- if current_token:
35
- tokens.append(current_token)
36
- current_token = ""
37
- tokens.append(char)
38
- elif char == '"':
39
- if current_token:
40
- tokens.append(current_token)
41
- current_token = ""
42
- in_string = True
43
- else:
44
- current_token += char
45
-
46
- if in_string:
47
- raise ParseError("Unterminated string literal")
48
-
49
- if current_token:
50
- tokens.append(current_token)
51
-
52
- return tokens
astreum/models/account.py DELETED
@@ -1,91 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Optional, Callable
4
-
5
- from .merkle import MerkleTree
6
-
7
- _FIELD_ORDER = ["balance", "data", "nonce"]
8
- _INT_FIELDS = {"balance", "nonce"}
9
-
10
- def _int_to_min_bytes(i: int) -> bytes:
11
- length = (i.bit_length() + 7) // 8 or 1
12
- return i.to_bytes(length, "big")
13
-
14
- class Account:
15
- def __init__(
16
- self,
17
- body_hash: bytes,
18
- *,
19
- body_tree: Optional[MerkleTree] = None,
20
- get_node_fn: Optional[Callable[[bytes], Optional[bytes]]] = None,
21
- ) -> None:
22
- self._body_hash = body_hash
23
- self._body_tree = body_tree
24
- self._balance: Optional[int] = None
25
- self._data: Optional[bytes] = None
26
- self._nonce: Optional[int] = None
27
-
28
- if self._body_tree and get_node_fn:
29
- self._body_tree._node_get = get_node_fn
30
-
31
- @classmethod
32
- def create(
33
- cls,
34
- balance: int,
35
- data: bytes,
36
- nonce: int,
37
- ) -> Account:
38
- """Build an Account body from explicit fields in alphabetical order."""
39
- # prepare values dict
40
- values = {"balance": balance, "data": data, "nonce": nonce}
41
-
42
- # build leaves in alphabetical order
43
- leaves: list[bytes] = []
44
- for name in _FIELD_ORDER:
45
- v = values[name]
46
- if name in _INT_FIELDS:
47
- leaves.append(_int_to_min_bytes(v)) # type: ignore[arg-type]
48
- else:
49
- leaves.append(v)
50
-
51
- tree = MerkleTree.from_leaves(leaves)
52
- return cls(tree.root_hash, body_tree=tree)
53
-
54
- def body_hash(self) -> bytes:
55
- """Return the Merkle root of the account body."""
56
- return self._body_hash
57
-
58
- def _require_tree(self) -> MerkleTree:
59
- if not self._body_tree:
60
- raise ValueError("Body tree unavailable for this Account")
61
- return self._body_tree
62
-
63
- def balance(self) -> int:
64
- """Fetch & cache the `balance` field (leaf 0)."""
65
- if self._balance is not None:
66
- return self._balance
67
- raw = self._require_tree().get(0)
68
- if raw is None:
69
- raise ValueError("Merkle leaf 0 (balance) missing")
70
- self._balance = int.from_bytes(raw, "big")
71
- return self._balance
72
-
73
- def data(self) -> bytes:
74
- """Fetch & cache the `data` field (leaf 1)."""
75
- if self._data is not None:
76
- return self._data
77
- raw = self._require_tree().get(1)
78
- if raw is None:
79
- raise ValueError("Merkle leaf 1 (data) missing")
80
- self._data = raw
81
- return self._data
82
-
83
- def nonce(self) -> int:
84
- """Fetch & cache the `nonce` field (leaf 2)."""
85
- if self._nonce is not None:
86
- return self._nonce
87
- raw = self._require_tree().get(2)
88
- if raw is None:
89
- raise ValueError("Merkle leaf 2 (nonce) missing")
90
- self._nonce = int.from_bytes(raw, "big")
91
- return self._nonce
@@ -1,34 +0,0 @@
1
- from __future__ import annotations
2
- from typing import Dict, Optional, Callable
3
- from .patricia import PatriciaTrie
4
- from .account import Account
5
-
6
- class Accounts:
7
- def __init__(
8
- self,
9
- root_hash: Optional[bytes] = None,
10
- global_get_fn: Optional[Callable[[bytes], Optional[bytes]]] = None,
11
- ) -> None:
12
- self._global_get_fn = global_get_fn
13
- self._trie = PatriciaTrie(node_get=global_get_fn, root_hash=root_hash)
14
- self._cache: Dict[bytes, Account] = {}
15
-
16
- @property
17
- def root_hash(self) -> Optional[bytes]:
18
- return self._trie.root_hash
19
-
20
- def get_account(self, address: bytes) -> Optional[Account]:
21
- if address in self._cache:
22
- return self._cache[address]
23
-
24
- body_hash: Optional[bytes] = self._trie.get(address)
25
- if body_hash is None:
26
- return None
27
-
28
- acc = Account(body_hash, get_node_fn=self._global_get_fn)
29
- self._cache[address] = acc
30
- return acc
31
-
32
- def set_account(self, address: bytes, account: Account) -> None:
33
- self._cache[address] = account
34
- self._trie.put(address, account.body_hash())
@@ -1,106 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Dict, List, Optional, Union, Any, Callable
4
-
5
- from .merkle import MerkleTree
6
- from ..crypto import ed25519
7
-
8
- _FIELD_ORDER: List[str] = [
9
- "amount",
10
- "balance",
11
- "fee",
12
- "nonce",
13
- "recipient_pk",
14
- "sender_pk",
15
- ]
16
-
17
- _INT_FIELDS = {"amount", "balance", "fee", "nonce"}
18
-
19
- def _int_to_min_bytes(i: int) -> bytes:
20
- length = (i.bit_length() + 7) // 8 or 1
21
- return i.to_bytes(length, "big")
22
-
23
- class Transaction:
24
- # init
25
- def __init__(
26
- self,
27
- tx_hash: bytes,
28
- *,
29
- tree: Optional[MerkleTree] = None,
30
- global_get_fn: Optional[Callable[[bytes], Optional[bytes]]] = None,
31
- ) -> None:
32
- self._hash = tx_hash
33
- self._tree = tree
34
- self._field_cache: Dict[str, Union[int, bytes]] = {}
35
-
36
- if self._tree and global_get_fn:
37
- self._tree.global_get_fn = global_get_fn
38
-
39
- @classmethod
40
- def create(
41
- cls,
42
- *,
43
- amount: int,
44
- balance: int,
45
- fee: int,
46
- nonce: int,
47
- recipient_pk: bytes,
48
- sender_pk: bytes,
49
- ) -> "Transaction":
50
- vals: Dict[str, Any] = locals().copy()
51
- leaves = [
52
- vals[name] if isinstance(vals[name], bytes) else _int_to_min_bytes(vals[name])
53
- for name in _FIELD_ORDER
54
- ]
55
-
56
- tree = MerkleTree.from_leaves(leaves)
57
- return cls(tx_hash=tree.root_hash, tree=tree)
58
-
59
- @property
60
- def hash(self) -> bytes:
61
- return self._hash
62
-
63
- def _require_tree(self) -> MerkleTree:
64
- if not self._tree:
65
- raise ValueError("Merkle tree unavailable for this Transaction")
66
- return self._tree
67
-
68
- def _field(self, idx: int, name: str) -> Union[int, bytes]:
69
- if name in self._field_cache:
70
- return self._field_cache[name]
71
-
72
- raw = self._require_tree().get(idx)
73
- if raw is None:
74
- raise ValueError(f"Leaf {idx} (‘{name}’) missing from Merkle tree")
75
-
76
- value = int.from_bytes(raw, "big") if name in _INT_FIELDS else raw
77
- self._field_cache[name] = value
78
- return value
79
-
80
- def get_amount(self) -> int:
81
- return self._field(0, "amount")
82
-
83
- def get_balance(self) -> int:
84
- return self._field(1, "balance")
85
-
86
- def get_fee(self) -> int:
87
- return self._field(2, "fee")
88
-
89
- def get_nonce(self) -> int:
90
- return self._field(3, "nonce")
91
-
92
- def get_recipient_pk(self) -> bytes:
93
- return self._field(4, "recipient_pk")
94
-
95
- def get_sender_pk(self) -> bytes:
96
- return self._field(5, "sender_pk")
97
-
98
- def sign(self, priv: ed25519.Ed25519PrivateKey) -> bytes:
99
- return priv.sign(self.hash)
100
-
101
- def verify_signature(self, sig: bytes, sender_pk: bytes) -> bool:
102
- try:
103
- ed25519.Ed25519PublicKey.from_public_bytes(sender_pk).verify(sig, self.hash)
104
- return True
105
- except Exception:
106
- return False
astreum/relay/__init__.py DELETED
File without changes
astreum/relay/peer.py DELETED
@@ -1,9 +0,0 @@
1
- from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
2
- from datetime import datetime, timezone
3
-
4
- class Peer:
5
- shared_key: bytes
6
- timestamp: datetime
7
- def __init__(self, my_sec_key: X25519PrivateKey, peer_pub_key: X25519PublicKey):
8
- self.shared_key = my_sec_key.exchange(peer_pub_key)
9
- self.timestamp = datetime.now(timezone.utc)
astreum/relay/route.py DELETED
@@ -1,25 +0,0 @@
1
- from typing import Dict, List
2
- from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
3
-
4
- class Route:
5
- def __init__(self, relay_public_key: X25519PublicKey, bucket_size: int = 16):
6
- self.relay_public_key_bytes = relay_public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
7
- self.bucket_size = bucket_size
8
- self.buckets: Dict[int, List[X25519PublicKey]] = {
9
- i: [] for i in range(len(self.relay_public_key_bytes) * 8)
10
- }
11
- self.peers = {}
12
-
13
- @staticmethod
14
- def _matching_leading_bits(a: bytes, b: bytes) -> int:
15
- for byte_index, (ba, bb) in enumerate(zip(a, b)):
16
- diff = ba ^ bb
17
- if diff:
18
- return byte_index * 8 + (8 - diff.bit_length())
19
- return len(a) * 8
20
-
21
- def add_peer(self, peer_public_key: X25519PublicKey):
22
- peer_public_key_bytes = peer_public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
23
- bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
24
- if len(self.buckets[bucket_idx]) < self.bucket_size:
25
- self.buckets[bucket_idx].append(peer_public_key)
astreum/relay/setup.py DELETED
@@ -1,58 +0,0 @@
1
- import socket, threading
2
- from queue import Queue
3
- from typing import Tuple, Optional
4
- from cryptography.hazmat.primitives.asymmetric import ed25519
5
- from cryptography.hazmat.primitives.asymmetric.x25519 import (
6
- X25519PrivateKey,
7
- X25519PublicKey,
8
- )
9
- from yourproject.routes import Route
10
-
11
- def load_x25519(hex_key: Optional[str]) -> X25519PrivateKey:
12
- """DH key for relaying (always X25519)."""
13
- return
14
-
15
- def load_ed25519(hex_key: Optional[str]) -> Optional[ed25519.Ed25519PrivateKey]:
16
- """Signing key for validation (Ed25519), or None if absent."""
17
- return ed25519.Ed25519PrivateKey.from_private_bytes(bytes.fromhex(hex_key)) \
18
- if hex_key else None
19
-
20
- def make_routes(
21
- relay_pk: X25519PublicKey,
22
- val_sk: Optional[ed25519.Ed25519PrivateKey]
23
- ) -> Tuple[Route, Optional[Route]]:
24
- """Peer route (DH pubkey) + optional validation route (ed pubkey)."""
25
- peer_rt = Route(relay_pk)
26
- val_rt = Route(val_sk.public_key()) if val_sk else None
27
- return peer_rt, val_rt
28
-
29
- def setup_udp(
30
- bind_port: int,
31
- use_ipv6: bool
32
- ) -> Tuple[socket.socket, int, Queue, threading.Thread, threading.Thread]:
33
- fam = socket.AF_INET6 if use_ipv6 else socket.AF_INET
34
- sock = socket.socket(fam, socket.SOCK_DGRAM)
35
- if use_ipv6:
36
- sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
37
- sock.bind(("::" if use_ipv6 else "0.0.0.0", bind_port or 0))
38
- port = sock.getsockname()[1]
39
-
40
- q = Queue()
41
- pop = threading.Thread(target=lambda: None, daemon=True)
42
- proc = threading.Thread(target=lambda: None, daemon=True)
43
- pop.start(); proc.start()
44
- return sock, port, q, pop, proc
45
-
46
- def setup_outgoing(
47
- use_ipv6: bool
48
- ) -> Tuple[socket.socket, Queue, threading.Thread]:
49
- fam = socket.AF_INET6 if use_ipv6 else socket.AF_INET
50
- sock = socket.socket(fam, socket.SOCK_DGRAM)
51
- q = Queue()
52
- thr = threading.Thread(target=lambda: None, daemon=True)
53
- thr.start()
54
- return sock, q, thr
55
-
56
- def make_maps():
57
- """Empty lookup maps: peers and addresses."""
58
- return