alx-protocol 1.0.0__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.
@@ -0,0 +1,34 @@
1
+ """
2
+ ALX Protocol — Python implementation.
3
+
4
+ Deterministic infrastructure for verifiable content,
5
+ interoperable context, and traceable attribution.
6
+
7
+ One primitive: Block = { blockHash, contentHash, parentHashes, content }
8
+ Four operations: canonicalize, hash, validate, trace
9
+ """
10
+
11
+ from alx_protocol.canonical import canonicalize
12
+ from alx_protocol.block import (
13
+ derive_block_hash,
14
+ derive_content_hash,
15
+ create_block,
16
+ validate_block,
17
+ normalize_hash,
18
+ normalize_hashes,
19
+ )
20
+ from alx_protocol.lineage import validate_lineage, validate_graph, build_graph
21
+ from alx_protocol.trace import trace_attribution
22
+ from alx_protocol.merkle import build_merkle_tree, get_merkle_proof, verify_merkle_proof
23
+
24
+ __version__ = "1.0.0"
25
+
26
+ __all__ = [
27
+ "__version__",
28
+ "canonicalize",
29
+ "derive_block_hash", "derive_content_hash", "create_block", "validate_block",
30
+ "normalize_hash", "normalize_hashes",
31
+ "validate_lineage", "validate_graph", "build_graph",
32
+ "trace_attribution",
33
+ "build_merkle_tree", "get_merkle_proof", "verify_merkle_proof",
34
+ ]
alx_protocol/block.py ADDED
@@ -0,0 +1,58 @@
1
+ """
2
+ ALX Protocol — Block primitive.
3
+
4
+ Block = { blockHash, contentHash, parentHashes, content }
5
+
6
+ blockHash = keccak256(canonicalize({ content, parentHashes }))
7
+ contentHash = keccak256(canonicalize(content))
8
+ """
9
+
10
+ import re
11
+ from typing import Any, Dict, List, Optional
12
+ from eth_hash.auto import keccak
13
+ from alx_protocol.canonical import canonicalize
14
+
15
+ HASH_PATTERN = re.compile(r"^0x[a-f0-9]{64}$")
16
+
17
+
18
+ def derive_block_hash(content: Any, parent_hashes: Optional[List[str]] = None) -> str:
19
+ parents = normalize_hashes(parent_hashes or [])
20
+ canonical = canonicalize({"content": content, "parentHashes": parents})
21
+ return "0x" + keccak(canonical.encode("utf-8")).hex()
22
+
23
+
24
+ def derive_content_hash(content: Any) -> str:
25
+ canonical = canonicalize(content if content is not None else None)
26
+ return "0x" + keccak(canonical.encode("utf-8")).hex()
27
+
28
+
29
+ def create_block(content: Any, parent_hashes: Optional[List[str]] = None) -> Dict[str, Any]:
30
+ parents = normalize_hashes(parent_hashes or [])
31
+ return {
32
+ "blockHash": derive_block_hash(content, parents),
33
+ "contentHash": derive_content_hash(content),
34
+ "parentHashes": parents,
35
+ "content": content,
36
+ }
37
+
38
+
39
+ def validate_block(block: Dict[str, Any]) -> Dict[str, Any]:
40
+ expected = derive_block_hash(block.get("content"), block.get("parentHashes", []))
41
+ actual = normalize_hash(block["blockHash"])
42
+ return {"ok": expected == actual, "expected": expected, "actual": actual}
43
+
44
+
45
+ def normalize_hash(h: str) -> str:
46
+ s = str(h).strip().lower()
47
+ if not HASH_PATTERN.match(s):
48
+ raise ValueError(f"invalid hash: {h}")
49
+ return s
50
+
51
+
52
+ def normalize_hashes(hashes: List[str]) -> List[str]:
53
+ result = set()
54
+ for h in hashes:
55
+ s = str(h).strip().lower()
56
+ if HASH_PATTERN.match(s):
57
+ result.add(s)
58
+ return sorted(result)
@@ -0,0 +1,63 @@
1
+ """
2
+ ALX Protocol — Canonicalization.
3
+
4
+ Deterministic JSON serialization. Layer 1 of the protocol.
5
+
6
+ Rules:
7
+ - Objects: keys sorted lexicographically, undefined/None values excluded
8
+ - Arrays: order-preserving (not sorted)
9
+ - null/None: canonicalizes to "null"
10
+ - Numbers: -0 → 0, safe integer range enforced, non-finite rejected
11
+ - Strings: JSON-escaped
12
+ - Depth limit: 128
13
+ """
14
+
15
+ import json
16
+ import math
17
+ from typing import Any
18
+
19
+ MAX_DEPTH = 128
20
+ MAX_SAFE_INTEGER = 2**53 - 1
21
+ MIN_SAFE_INTEGER = -(2**53 - 1)
22
+
23
+
24
+ def canonicalize(value: Any, _depth: int = 0) -> str:
25
+ if _depth > MAX_DEPTH:
26
+ raise ValueError(f"Canonicalization depth exceeded {MAX_DEPTH}")
27
+
28
+ if value is None:
29
+ return "null"
30
+
31
+ if isinstance(value, bool):
32
+ return "true" if value else "false"
33
+
34
+ if isinstance(value, int) and not isinstance(value, bool):
35
+ if value > MAX_SAFE_INTEGER or value < MIN_SAFE_INTEGER:
36
+ raise ValueError(f"Integer out of safe range: {value}")
37
+ return str(value)
38
+
39
+ if isinstance(value, float):
40
+ if not math.isfinite(value):
41
+ raise ValueError(f"Non-finite number not allowed: {value}")
42
+ if value == 0.0 and math.copysign(1.0, value) == -1.0:
43
+ return "0" # -0 → 0
44
+ if value == int(value) and MIN_SAFE_INTEGER <= value <= MAX_SAFE_INTEGER:
45
+ return str(int(value))
46
+ return json.dumps(value)
47
+
48
+ if isinstance(value, str):
49
+ return json.dumps(value, ensure_ascii=False)
50
+
51
+ if isinstance(value, list):
52
+ return "[" + ",".join(canonicalize(v, _depth + 1) for v in value) + "]"
53
+
54
+ if isinstance(value, dict):
55
+ keys = sorted(k for k in value.keys() if value[k] is not None or True)
56
+ # Exclude keys whose values are the Python equivalent of JS undefined
57
+ # In Python, we don't have undefined — None maps to JSON null and is kept.
58
+ parts = []
59
+ for k in keys:
60
+ parts.append(json.dumps(str(k), ensure_ascii=False) + ":" + canonicalize(value[k], _depth + 1))
61
+ return "{" + ",".join(parts) + "}"
62
+
63
+ raise TypeError(f"Unsupported type for canonicalization: {type(value).__name__}")
@@ -0,0 +1,94 @@
1
+ """ALX Protocol — Lineage validation."""
2
+
3
+ from typing import Dict, List, Optional, Set
4
+
5
+
6
+ def validate_lineage(
7
+ block_hash: str,
8
+ parent_hashes: List[str],
9
+ known_hashes: Optional[Set[str]] = None,
10
+ ) -> dict:
11
+ h = block_hash.strip().lower()
12
+ raw = [p.strip().lower() for p in parent_hashes]
13
+ unique = sorted(set(raw))
14
+
15
+ seen = set()
16
+ duplicates = []
17
+ for p in raw:
18
+ if p in seen:
19
+ duplicates.append(p)
20
+ seen.add(p)
21
+
22
+ self_ref = h in unique
23
+ known = known_hashes or set()
24
+ missing = sorted(p for p in unique if known and p not in known)
25
+
26
+ return {
27
+ "ok": not duplicates and not missing and not self_ref,
28
+ "blockHash": h,
29
+ "parents": unique,
30
+ "duplicates": sorted(set(duplicates)),
31
+ "missing": missing,
32
+ "selfRef": self_ref,
33
+ "cycle": self_ref,
34
+ }
35
+
36
+
37
+ def validate_graph(graph: Dict[str, List[str]]) -> dict:
38
+ all_hashes = set(graph.keys())
39
+ cycle_nodes = _detect_all_cycles(graph)
40
+ issues = []
41
+ edges = 0
42
+ roots = []
43
+
44
+ for h, parents in graph.items():
45
+ edges += len(parents)
46
+ if not parents:
47
+ roots.append(h)
48
+ result = validate_lineage(h, parents, all_hashes)
49
+ if h in cycle_nodes:
50
+ result["cycle"] = True
51
+ result["ok"] = False
52
+ if not result["ok"]:
53
+ issues.append(result)
54
+
55
+ return {
56
+ "ok": len(issues) == 0,
57
+ "blocks": len(graph),
58
+ "edges": edges,
59
+ "roots": sorted(roots),
60
+ "issues": issues,
61
+ }
62
+
63
+
64
+ def build_graph(blocks: list) -> Dict[str, List[str]]:
65
+ g = {}
66
+ for b in blocks:
67
+ h = b["blockHash"].strip().lower()
68
+ parents = sorted(p.strip().lower() for p in b.get("parentHashes", []))
69
+ g[h] = parents
70
+ return g
71
+
72
+
73
+ def _detect_all_cycles(graph: Dict[str, List[str]]) -> Set[str]:
74
+ visiting = set()
75
+ visited = set()
76
+ in_cycle = set()
77
+
78
+ def visit(node):
79
+ if node in visited:
80
+ return False
81
+ if node in visiting:
82
+ in_cycle.add(node)
83
+ return True
84
+ visiting.add(node)
85
+ for parent in graph.get(node, []):
86
+ if visit(parent):
87
+ in_cycle.add(node)
88
+ visiting.discard(node)
89
+ visited.add(node)
90
+ return False
91
+
92
+ for node in graph:
93
+ visit(node)
94
+ return in_cycle
alx_protocol/merkle.py ADDED
@@ -0,0 +1,56 @@
1
+ """ALX Protocol — Sorted-pair Merkle tree for checkpoint proofs."""
2
+
3
+ from typing import List, Optional, Dict
4
+ from eth_hash.auto import keccak
5
+
6
+
7
+ def _hash_pair(a: str, b: str) -> str:
8
+ a_bytes = bytes.fromhex(a[2:])
9
+ b_bytes = bytes.fromhex(b[2:])
10
+ if a.lower() < b.lower():
11
+ concat = a_bytes + b_bytes
12
+ else:
13
+ concat = b_bytes + a_bytes
14
+ return "0x" + keccak(concat).hex()
15
+
16
+
17
+ def build_merkle_tree(leaves: List[str]) -> dict:
18
+ if not leaves:
19
+ raise ValueError("leaves must be a non-empty list")
20
+ layer = sorted(h.lower() for h in leaves)
21
+ layers = [list(layer)]
22
+ while len(layer) > 1:
23
+ next_layer = []
24
+ for i in range(0, len(layer), 2):
25
+ if i + 1 < len(layer):
26
+ next_layer.append(_hash_pair(layer[i], layer[i + 1]))
27
+ else:
28
+ next_layer.append(layer[i])
29
+ layer = next_layer
30
+ layers.append(list(layer))
31
+ return {"root": layer[0], "layers": layers}
32
+
33
+
34
+ def get_merkle_proof(leaves: List[str], leaf: str) -> Optional[Dict]:
35
+ tree = build_merkle_tree(leaves)
36
+ target = leaf.lower()
37
+ try:
38
+ index = tree["layers"][0].index(target)
39
+ except ValueError:
40
+ return None
41
+ proof = []
42
+ idx = index
43
+ for i in range(len(tree["layers"]) - 1):
44
+ layer_len = len(tree["layers"][i])
45
+ sibling = idx + 1 if idx % 2 == 0 else idx - 1
46
+ if sibling < layer_len:
47
+ proof.append(tree["layers"][i][sibling])
48
+ idx //= 2
49
+ return {"proof": proof, "root": tree["root"], "leaf": target, "index": index}
50
+
51
+
52
+ def verify_merkle_proof(root: str, leaf: str, proof: List[str]) -> bool:
53
+ h = leaf.lower()
54
+ for sibling in proof:
55
+ h = _hash_pair(h, sibling)
56
+ return h == root.lower()
alx_protocol/trace.py ADDED
@@ -0,0 +1,97 @@
1
+ """ALX Protocol — Attribution trace resolution."""
2
+
3
+ from collections import deque
4
+ from typing import Dict, List
5
+
6
+
7
+ def trace_attribution(root_hash: str, graph: Dict[str, List[str]]) -> dict:
8
+ root = root_hash.strip().lower()
9
+ if not root:
10
+ raise ValueError("rootHash is required")
11
+
12
+ # Cycle pre-check via DFS
13
+ cycle_detected = False
14
+ visiting = set()
15
+ visited_cycle = set()
16
+
17
+ def check_cycle(node):
18
+ nonlocal cycle_detected
19
+ if node in visited_cycle:
20
+ return
21
+ if node in visiting:
22
+ cycle_detected = True
23
+ return
24
+ visiting.add(node)
25
+ for p in graph.get(node, []):
26
+ check_cycle(p.strip().lower())
27
+ visiting.discard(node)
28
+ visited_cycle.add(node)
29
+
30
+ check_cycle(root)
31
+
32
+ # BFS discovery
33
+ node_data = {} # hash -> {min_depth, max_depth, parent_count}
34
+ edge_set = set()
35
+ edge_list = []
36
+ child_counts = {}
37
+ global_max_depth = 0
38
+
39
+ queue = deque([(root, 0)])
40
+ while queue:
41
+ h, depth = queue.popleft()
42
+ if h in node_data:
43
+ node_data[h]["min_depth"] = min(node_data[h]["min_depth"], depth)
44
+ node_data[h]["max_depth"] = max(node_data[h]["max_depth"], depth)
45
+ if depth > global_max_depth:
46
+ global_max_depth = depth
47
+ continue
48
+
49
+ parents = [p.strip().lower() for p in graph.get(h, [])]
50
+ node_data[h] = {"min_depth": depth, "max_depth": depth, "parent_count": len(parents)}
51
+ if depth > global_max_depth:
52
+ global_max_depth = depth
53
+
54
+ for p in parents:
55
+ edge_key = f"{h}\0{p}"
56
+ if edge_key not in edge_set:
57
+ edge_set.add(edge_key)
58
+ edge_list.append({"from": h, "to": p})
59
+ child_counts[p] = child_counts.get(p, 0) + 1
60
+ queue.append((p, depth + 1))
61
+
62
+ # Topological path count propagation
63
+ path_counts = {root: 1}
64
+ sorted_hashes = sorted(node_data.keys(), key=lambda x: (node_data[x]["min_depth"], x))
65
+ for h in sorted_hashes:
66
+ my_paths = path_counts.get(h, 0)
67
+ for p in [x.strip().lower() for x in graph.get(h, [])]:
68
+ if p in node_data:
69
+ path_counts[p] = path_counts.get(p, 0) + my_paths
70
+
71
+ # Build output
72
+ nodes = []
73
+ leaves = []
74
+ for h, data in node_data.items():
75
+ nodes.append({
76
+ "hash": h,
77
+ "minDepth": data["min_depth"],
78
+ "maxDepth": data["max_depth"],
79
+ "pathCount": path_counts.get(h, 0),
80
+ "parentCount": data["parent_count"],
81
+ "childCount": child_counts.get(h, 0),
82
+ })
83
+ if data["parent_count"] == 0:
84
+ leaves.append(h)
85
+
86
+ nodes.sort(key=lambda n: (n["minDepth"], n["hash"]))
87
+ edge_list.sort(key=lambda e: (e["from"], e["to"]))
88
+ leaves.sort()
89
+
90
+ return {
91
+ "root": root,
92
+ "nodes": nodes,
93
+ "edges": edge_list,
94
+ "leaves": leaves,
95
+ "maxDepth": global_max_depth,
96
+ "cycle": cycle_detected,
97
+ }
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: alx-protocol
3
+ Version: 1.0.0
4
+ Summary: ALX Protocol — Python implementation
5
+ Author: ALX Protocol contributors
6
+ License: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: eth-hash[pycryptodome]>=0.5.0
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=7.0; extra == "dev"
12
+
13
+ <p align="center">
14
+ <img src="../../assets/xandrlabs/xandrlabs_logo3.png" alt="Xandr Labs" width="160"/>
15
+ </p>
16
+ <p align="center"><strong><em>Infrastructure compounds. Block by block.</em></strong></p><p align="center">https://xandrlabs.ai</p>
17
+
18
+ ---
19
+
20
+ # ALX Protocol - Python Implementation
21
+
22
+ This implementation tracks parity with the TypeScript SDK in `reference-implementations/typescript`.
23
+
24
+ ## Authority
25
+
26
+ - Canonical specification: [`protocol/spec.md`](../../protocol/spec.md)
27
+ - JSON Schemas: [`protocol/schemas/`](../../protocol/schemas/)
28
+ - Test vectors: [`protocol/test-vectors/`](../../protocol/test-vectors/)
29
+ - Companion docs: [`docs/`](../../docs/)
30
+
31
+ ## Layout
32
+
33
+ - `pyproject.toml` - package `alx-protocol`, `src/alx_protocol/`
34
+ - `tests/` - smoke tests (`pytest`)
35
+
36
+ ```bash
37
+ cd reference-implementations/python
38
+ python -m venv .venv
39
+ .venv\Scripts\activate # Windows
40
+ pip install -e ".[dev]"
41
+ pytest
42
+ # or vector replay only (same entrypoint as CI):
43
+ python verify_vectors.py
44
+ ```
45
+
46
+ Next steps: virtual registry, signing (EIP-712), and broader API parity with the TypeScript reference implementation.
47
+
48
+ ## Reference
49
+
50
+ Compare behavior with [`reference-implementations/typescript`](../../reference-implementations/typescript/), especially `src/canonicalization.js` and `src/__tests__/conformance-vectors.test.js`.
@@ -0,0 +1,10 @@
1
+ alx_protocol/__init__.py,sha256=fOSqi9GRDHsIRGRRfYsPKRJeza-IEI5xKKmASOzR094,1093
2
+ alx_protocol/block.py,sha256=jhOWS2wQELvVTYTJrgjVfOPssXNOp0r0Rf2U2Z3fL78,1901
3
+ alx_protocol/canonical.py,sha256=VPe3xvQ4DtvCBnQUinHg5QlNZt0Zl6b6o6u-KliE7wY,2220
4
+ alx_protocol/lineage.py,sha256=BmgQUR_EdgR4roojnb5CVJ2vOuBhdhCGHwXH63kAVwA,2455
5
+ alx_protocol/merkle.py,sha256=KuAdNdEXi_CSZRqJnIRMrHX1PPxpUiFYPoq62QIFetY,1813
6
+ alx_protocol/trace.py,sha256=C84I5VWnl6Mh-svy4jKfASLCVveNLaxMzOcjdF3VF_c,3122
7
+ alx_protocol-1.0.0.dist-info/METADATA,sha256=Dzq3BKAlxF_UxS2vAHXZE-RXTGwErKa_CzTgaj9XKvs,1680
8
+ alx_protocol-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ alx_protocol-1.0.0.dist-info/top_level.txt,sha256=8AtWpigdDJn0YputhbBmsCs-quwEO5W7xaTZH3hrc0Y,13
10
+ alx_protocol-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ alx_protocol