alx-protocol 1.0.0__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.
@@ -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,38 @@
1
+ <p align="center">
2
+ <img src="../../assets/xandrlabs/xandrlabs_logo3.png" alt="Xandr Labs" width="160"/>
3
+ </p>
4
+ <p align="center"><strong><em>Infrastructure compounds. Block by block.</em></strong></p><p align="center">https://xandrlabs.ai</p>
5
+
6
+ ---
7
+
8
+ # ALX Protocol - Python Implementation
9
+
10
+ This implementation tracks parity with the TypeScript SDK in `reference-implementations/typescript`.
11
+
12
+ ## Authority
13
+
14
+ - Canonical specification: [`protocol/spec.md`](../../protocol/spec.md)
15
+ - JSON Schemas: [`protocol/schemas/`](../../protocol/schemas/)
16
+ - Test vectors: [`protocol/test-vectors/`](../../protocol/test-vectors/)
17
+ - Companion docs: [`docs/`](../../docs/)
18
+
19
+ ## Layout
20
+
21
+ - `pyproject.toml` - package `alx-protocol`, `src/alx_protocol/`
22
+ - `tests/` - smoke tests (`pytest`)
23
+
24
+ ```bash
25
+ cd reference-implementations/python
26
+ python -m venv .venv
27
+ .venv\Scripts\activate # Windows
28
+ pip install -e ".[dev]"
29
+ pytest
30
+ # or vector replay only (same entrypoint as CI):
31
+ python verify_vectors.py
32
+ ```
33
+
34
+ Next steps: virtual registry, signing (EIP-712), and broader API parity with the TypeScript reference implementation.
35
+
36
+ ## Reference
37
+
38
+ Compare behavior with [`reference-implementations/typescript`](../../reference-implementations/typescript/), especially `src/canonicalization.js` and `src/__tests__/conformance-vectors.test.js`.
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "alx-protocol"
7
+ version = "1.0.0"
8
+ description = "ALX Protocol — Python implementation"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "ALX Protocol contributors" }]
13
+ dependencies = [
14
+ "eth-hash[pycryptodome]>=0.5.0",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ dev = ["pytest>=7.0"]
19
+
20
+ [tool.setuptools.packages.find]
21
+ where = ["src"]
22
+
23
+ [tool.pytest.ini_options]
24
+ testpaths = ["tests"]
25
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ ]
@@ -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
@@ -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()
@@ -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,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/alx_protocol/__init__.py
4
+ src/alx_protocol/block.py
5
+ src/alx_protocol/canonical.py
6
+ src/alx_protocol/lineage.py
7
+ src/alx_protocol/merkle.py
8
+ src/alx_protocol/trace.py
9
+ src/alx_protocol.egg-info/PKG-INFO
10
+ src/alx_protocol.egg-info/SOURCES.txt
11
+ src/alx_protocol.egg-info/dependency_links.txt
12
+ src/alx_protocol.egg-info/requires.txt
13
+ src/alx_protocol.egg-info/top_level.txt
14
+ tests/test_smoke.py
15
+ tests/test_vectors.py
@@ -0,0 +1,4 @@
1
+ eth-hash[pycryptodome]>=0.5.0
2
+
3
+ [dev]
4
+ pytest>=7.0
@@ -0,0 +1 @@
1
+ alx_protocol
@@ -0,0 +1,30 @@
1
+ import alx_protocol
2
+
3
+
4
+ def test_version():
5
+ assert alx_protocol.__version__ == "1.0.0"
6
+
7
+
8
+ def test_block_creation():
9
+ block = alx_protocol.create_block("hello", [])
10
+ assert block["blockHash"].startswith("0x")
11
+ assert len(block["blockHash"]) == 66
12
+ assert block["content"] == "hello"
13
+ assert block["parentHashes"] == []
14
+
15
+
16
+ def test_block_validation():
17
+ block = alx_protocol.create_block("test", [])
18
+ result = alx_protocol.validate_block(block)
19
+ assert result["ok"] is True
20
+
21
+
22
+ def test_determinism():
23
+ a = alx_protocol.derive_block_hash("same", [])
24
+ b = alx_protocol.derive_block_hash("same", [])
25
+ assert a == b
26
+
27
+
28
+ def test_canonicalize():
29
+ assert alx_protocol.canonicalize(None) == "null"
30
+ assert alx_protocol.canonicalize({"b": 1, "a": 2}) == '{"a":2,"b":1}'
@@ -0,0 +1,84 @@
1
+ """ALX Protocol test vectors — Python implementation.
2
+
3
+ Loads canonical vectors from protocol/test-vectors/*.json and runs
4
+ language-specific inline tests for edge cases JSON cannot represent.
5
+ """
6
+
7
+ import json
8
+ from pathlib import Path
9
+
10
+ from alx_protocol import derive_block_hash, create_block, validate_block, canonicalize
11
+
12
+ VECTORS_DIR = Path(__file__).resolve().parents[3] / "protocol" / "test-vectors"
13
+
14
+
15
+ def load_vectors(filename: str) -> dict:
16
+ return json.loads((VECTORS_DIR / filename).read_text())
17
+
18
+
19
+ # ════════════��═══════════════════════════════════════════════════════════════
20
+ # DATA-DRIVEN: protocol/test-vectors/canonicalization.json
21
+ # ════════════════════════════════════════════════════════════════════════════
22
+
23
+
24
+ class TestCanonicalizationVectors:
25
+ """Load and replay canonicalization vectors from JSON data files."""
26
+
27
+ def test_vectors_from_json(self):
28
+ suite = load_vectors("canonicalization.json")
29
+ for v in suite["vectors"]:
30
+ # Skip C4 (negative zero — Python has no -0 literal) and C10 (undefined — N/A in Python)
31
+ if v["id"] in ("C4", "C10"):
32
+ continue
33
+ result = canonicalize(v["input"])
34
+ assert result == v["expected"], f"VECTOR {v['id']}: {v['description']} — got {result!r}"
35
+
36
+
37
+ # ═════════════���══════════════════════════════���═══════════════════════════════
38
+ # INLINE TESTS (language-specific edge cases + original vectors)
39
+ # ═══════════════════════════════��═════════════════════════════════���══════════
40
+
41
+
42
+ def test_null_canonicalizes_to_null():
43
+ assert canonicalize(None) == "null"
44
+
45
+
46
+ def test_keys_sorted():
47
+ assert canonicalize({"b": 1, "a": 2}) == '{"a":2,"b":1}'
48
+
49
+
50
+ def test_array_order_preserved():
51
+ assert canonicalize([3, 1, 2]) == "[3,1,2]"
52
+
53
+
54
+ def test_same_content_same_hash():
55
+ a = derive_block_hash("hello", [])
56
+ b = derive_block_hash("hello", [])
57
+ assert a == b
58
+
59
+
60
+ def test_different_content_different_hash():
61
+ a = derive_block_hash("hello", [])
62
+ b = derive_block_hash("world", [])
63
+ assert a != b
64
+
65
+
66
+ def test_parent_order_irrelevant():
67
+ p1 = "0x" + "a" * 64
68
+ p2 = "0x" + "b" * 64
69
+ a = derive_block_hash("test", [p1, p2])
70
+ b = derive_block_hash("test", [p2, p1])
71
+ assert a == b
72
+
73
+
74
+ def test_block_validates():
75
+ block = create_block("test", [])
76
+ result = validate_block(block)
77
+ assert result["ok"] is True
78
+
79
+
80
+ def test_tampered_block_fails():
81
+ block = create_block("test", [])
82
+ block["content"] = "tampered"
83
+ result = validate_block(block)
84
+ assert result["ok"] is False