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.
- alx_protocol-1.0.0/PKG-INFO +50 -0
- alx_protocol-1.0.0/README.md +38 -0
- alx_protocol-1.0.0/pyproject.toml +25 -0
- alx_protocol-1.0.0/setup.cfg +4 -0
- alx_protocol-1.0.0/src/alx_protocol/__init__.py +34 -0
- alx_protocol-1.0.0/src/alx_protocol/block.py +58 -0
- alx_protocol-1.0.0/src/alx_protocol/canonical.py +63 -0
- alx_protocol-1.0.0/src/alx_protocol/lineage.py +94 -0
- alx_protocol-1.0.0/src/alx_protocol/merkle.py +56 -0
- alx_protocol-1.0.0/src/alx_protocol/trace.py +97 -0
- alx_protocol-1.0.0/src/alx_protocol.egg-info/PKG-INFO +50 -0
- alx_protocol-1.0.0/src/alx_protocol.egg-info/SOURCES.txt +15 -0
- alx_protocol-1.0.0/src/alx_protocol.egg-info/dependency_links.txt +1 -0
- alx_protocol-1.0.0/src/alx_protocol.egg-info/requires.txt +4 -0
- alx_protocol-1.0.0/src/alx_protocol.egg-info/top_level.txt +1 -0
- alx_protocol-1.0.0/tests/test_smoke.py +30 -0
- alx_protocol-1.0.0/tests/test_vectors.py +84 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -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
|