#!/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()