137 lines
5.4 KiB
Python
Executable File
137 lines
5.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
External SHA-256 oracle для composite hash compositions Montana M4.
|
||
|
||
Назначение: Pass 25 (Independent Oracle / Differential Check) per CRITIC.md.
|
||
Текущие unit tests cross-check `domain::X` constants против литералов
|
||
`b"mt-X"` через SAME Rust SHA-256 implementation — circular validation.
|
||
Этот script даёт computation от Python `hashlib.sha256` (CPython OpenSSL
|
||
binding) — независимая reference для cross-impl conformance.
|
||
|
||
Запуск:
|
||
python3 scripts/oracle_python_sha256.py
|
||
|
||
Вывод: hex digests для каждого M4 composite hash на binding test inputs.
|
||
Tests в crates/mt-lottery/tests/external_oracle.rs + аналог mt-entry
|
||
читают этот вывод и assertion (либо hardcoded test fixture обновлён).
|
||
|
||
Все формулы соответствуют spec через mt-codec::domain registry +
|
||
NUL byte separator pattern (mt-crypto::hash):
|
||
SHA-256(domain || 0x00 || part0 || part1 || ...)
|
||
"""
|
||
|
||
import hashlib
|
||
import sys
|
||
|
||
|
||
def hash_with_domain(domain: bytes, *parts: bytes) -> bytes:
|
||
"""Эквивалент mt-crypto::hash(domain, &[parts]):
|
||
SHA-256(domain || 0x00 || parts[0] || parts[1] || ...).
|
||
"""
|
||
h = hashlib.sha256()
|
||
h.update(domain)
|
||
h.update(b"\x00")
|
||
for p in parts:
|
||
h.update(p)
|
||
return h.digest()
|
||
|
||
|
||
def fixed_block(byte_value: int, length: int) -> bytes:
|
||
"""Helper — fixed byte repeated. Matches тесты Rust (е.g., [0x11; 32])."""
|
||
return bytes([byte_value]) * length
|
||
|
||
|
||
def run() -> dict:
|
||
"""Compute oracle outputs для всех M4 composite hashes на binding inputs.
|
||
|
||
Inputs соответствуют mt-lottery / mt-entry / mt-consensus determinism
|
||
invariants test vectors — каждый test использует те же byte patterns,
|
||
Python oracle output должен match Rust output байт-в-байт.
|
||
"""
|
||
# Common test inputs (matching crates/mt-lottery/tests/determinism_invariants.rs
|
||
# и crates/mt-entry/tests/determinism_invariants.rs)
|
||
t_r = fixed_block(0x11, 32)
|
||
cba = fixed_block(0x22, 32)
|
||
node_id = fixed_block(0x33, 32)
|
||
pubkey = fixed_block(0x33, 1952) # PUBLIC_KEY_SIZE
|
||
|
||
results = {}
|
||
|
||
# --- mt-lottery::compute_endpoint ---
|
||
# spec: SHA-256("mt-lottery" || NUL || t_r || cba || node_id || w_le)
|
||
# window_index = 7, u32 LE = 07 00 00 00
|
||
w_le = (7).to_bytes(4, "little")
|
||
results["compute_endpoint(t_r=11..,cba=22..,node_id=33..,w=7)"] = (
|
||
hash_with_domain(b"mt-lottery", t_r, cba, node_id, w_le).hex()
|
||
)
|
||
|
||
# --- mt-entry::candidate_vdf_init ---
|
||
# spec: SHA-256("mt-candidate-vdf-init" || NUL || timechain || cba || node_id)
|
||
results["candidate_vdf_init(t_r=11..,cba=22..,node_id=33..)"] = (
|
||
hash_with_domain(b"mt-candidate-vdf-init", t_r, cba, node_id).hex()
|
||
)
|
||
|
||
# --- mt-entry::selection_sort_key ---
|
||
# spec: SHA-256("mt-selection" || NUL || timechain || cba || node_id)
|
||
results["selection_sort_key(t_r=11..,cba=22..,node_id=33..)"] = (
|
||
hash_with_domain(b"mt-selection", t_r, cba, node_id).hex()
|
||
)
|
||
|
||
# --- mt-entry::nr_sort_key ---
|
||
# spec: SHA-256("mt-nodereg-sort" || NUL || timechain || cba || node_pubkey)
|
||
# node_pubkey = [0x33; 1952] (PUBLIC_KEY_SIZE)
|
||
results["nr_sort_key(t_r=11..,cba=22..,pubkey=33..*1952)"] = (
|
||
hash_with_domain(b"mt-nodereg-sort", t_r, cba, pubkey).hex()
|
||
)
|
||
|
||
# --- Distinct domain separators check ---
|
||
# Three different domains для same (t_r, cba, node_id) input — must produce
|
||
# 3 different outputs (NUL pattern + domain isolation).
|
||
sel = hash_with_domain(b"mt-selection", t_r, cba, node_id)
|
||
vdf = hash_with_domain(b"mt-candidate-vdf-init", t_r, cba, node_id)
|
||
nr = hash_with_domain(b"mt-nodereg-sort", t_r, cba, pubkey)
|
||
distinct = (sel != vdf) and (vdf != nr) and (sel != nr)
|
||
results["distinct_domains_for_three_compositions"] = "PASS" if distinct else "FAIL"
|
||
|
||
# --- compute_endpoint dependencies (each input change → different output) ---
|
||
base = hash_with_domain(b"mt-lottery", t_r, cba, node_id, w_le)
|
||
diff_t_r = hash_with_domain(
|
||
b"mt-lottery", fixed_block(0xFF, 32), cba, node_id, w_le
|
||
)
|
||
diff_cba = hash_with_domain(
|
||
b"mt-lottery", t_r, fixed_block(0xFF, 32), node_id, w_le
|
||
)
|
||
diff_node = hash_with_domain(
|
||
b"mt-lottery", t_r, cba, fixed_block(0xFF, 32), w_le
|
||
)
|
||
diff_w = hash_with_domain(b"mt-lottery", t_r, cba, node_id, (8).to_bytes(4, "little"))
|
||
sensitivity = (
|
||
base != diff_t_r and base != diff_cba and base != diff_node and base != diff_w
|
||
)
|
||
results["compute_endpoint_input_sensitivity"] = (
|
||
"PASS" if sensitivity else "FAIL"
|
||
)
|
||
|
||
return results
|
||
|
||
|
||
def main():
|
||
results = run()
|
||
print("# External SHA-256 oracle (Python hashlib) для M4 composite hashes")
|
||
print("# Pass 25 Independent Oracle: cross-check Rust mt-crypto::hash output")
|
||
print()
|
||
for key, value in results.items():
|
||
print(f"{key:60s} = {value}")
|
||
# Exit non-zero если distinct/sensitivity checks fail
|
||
if (
|
||
results.get("distinct_domains_for_three_compositions") != "PASS"
|
||
or results.get("compute_endpoint_input_sensitivity") != "PASS"
|
||
):
|
||
print("\n[FAIL] domain separation либо input sensitivity broken")
|
||
sys.exit(1)
|
||
print("\n[PASS] all oracle invariants hold")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|