#!/usr/bin/env python3 """ nostr_sig_verifier.py ===================== Trustless Verifier: "Nostr Event Schnorr Signature Authenticity" TASK CLASS ---------- Claim: "This Nostr event (id, pubkey, sig, content, kind, tags, created_at) was signed by the holder of private-key corresponding to `pubkey`." Evidence: The full NIP-01 event JSON object. MECHANISM --------- Deterministic re-execution using BIP-340 Schnorr verification. The verifier: 1. Re-serializes the event per NIP-01 canonical form. 2. Re-derives the event id (SHA-256 of serialization). 3. Runs BIP-340 Schnorr verify(msg=id, sig=sig, pubkey=pubkey). TRUST ASSUMPTIONS ----------------- Zero external trust. The only assumption is that the secp256k1 curve parameters published in SECG SEC2 v2 are correct — the same assumption every Bitcoin and Nostr client on earth makes. No oracle, no TEE, no ZK prover: just arithmetic over a prime field. COST PER VERIFICATION --------------------- ~10-50 ms on a modern CPU (two scalar multiplications over secp256k1). Wall-clock: <100 ms. Compute cost: ~0 sats (one API call or local Python). Target bounty threshold: 100 sats per verify for a 1000-sat task. We are orders of magnitude below that. LICENSE: MIT """ import hashlib import json import time # ── secp256k1 curve parameters (SECG SEC2 v2) ───────────────────────────────── P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 G = (Gx, Gy) # ── Field / point arithmetic ────────────────────────────────────────────────── def _modinv(a: int) -> int: return pow(a, P - 2, P) def _point_add(P1, P2): if P1 is None: return P2 if P2 is None: return P1 if P1[0] == P2[0]: if P1[1] != P2[1]: return None lam = (3 * P1[0] * P1[0] * _modinv(2 * P1[1])) % P else: lam = ((P2[1] - P1[1]) * _modinv(P2[0] - P1[0])) % P x = (lam * lam - P1[0] - P2[0]) % P return (x, (lam * (P1[0] - x) - P1[1]) % P) def _point_mul(pt, n: int): R, Q = None, pt while n: if n & 1: R = _point_add(R, Q) Q = _point_add(Q, Q) n >>= 1 return R # ── BIP-340 primitives ──────────────────────────────────────────────────────── def _tagged_hash(tag: str, data: bytes) -> bytes: h = hashlib.sha256(tag.encode()).digest() return hashlib.sha256(h + h + data).digest() def _pubkey_x(sk: int) -> bytes: return _point_mul(G, sk)[0].to_bytes(32, 'big') def _schnorr_sign(msg: bytes, sk: int) -> bytes: pt = _point_mul(G, sk) if pt[1] % 2: sk = N - sk k0 = int.from_bytes( _tagged_hash('BIP0340/nonce', sk.to_bytes(32, 'big') + msg), 'big' ) % N R = _point_mul(G, k0) k = k0 if R[1] % 2 == 0 else N - k0 pt2 = _point_mul(G, sk) e = int.from_bytes( _tagged_hash( 'BIP0340/challenge', R[0].to_bytes(32, 'big') + pt2[0].to_bytes(32, 'big') + msg ), 'big' ) % N s = (k + e * sk) % N return R[0].to_bytes(32, 'big') + s.to_bytes(32, 'big') def _schnorr_verify(msg: bytes, sig: bytes, pubkey_bytes: bytes) -> bool: if len(sig) != 64 or len(pubkey_bytes) != 32: return False Rx = int.from_bytes(sig[:32], 'big') s = int.from_bytes(sig[32:], 'big') px = int.from_bytes(pubkey_bytes, 'big') if s >= N or px == 0 or Rx >= P: return False y2 = (pow(px, 3, P) + 7) % P py = pow(y2, (P + 1) // 4, P) if py % 2 != 0: py = P - py pub = (px, py) e = int.from_bytes( _tagged_hash( 'BIP0340/challenge', Rx.to_bytes(32, 'big') + pubkey_bytes + msg ), 'big' ) % N R = _point_add(_point_mul(G, s), _point_mul(pub, N - e)) if R is None or R[1] % 2 != 0 or R[0] != Rx: return False return True # ── NIP-01 event helpers ────────────────────────────────────────────────────── def _event_id(event: dict) -> str: serial = json.dumps( [0, event['pubkey'], event['created_at'], event['kind'], event['tags'], event['content']], separators=(',', ':'), ensure_ascii=False ) return hashlib.sha256(serial.encode('utf-8')).hexdigest() def _make_event(sk: int, kind: int, tags: list, content: str) -> dict: pk = _pubkey_x(sk).hex() ts = int(time.time()) ev = {'pubkey': pk, 'created_at': ts, 'kind': kind, 'tags': tags, 'content': content} eid = _event_id(ev) sig = _schnorr_sign(bytes.fromhex(eid), sk) ev['id'] = eid ev['sig'] = sig.hex() return ev # ── Core verifier ───────────────────────────────────────────────────────────── def verify_nostr_event(event: dict) -> dict: """ Verify a NIP-01 Nostr event for Schnorr signature authenticity. Returns: { "verdict": "ACCEPT" | "REJECT", "checks": { "id_match": bool, # re-derived id matches event['id'] "sig_valid": bool, # BIP-340 Schnorr verify passed }, "reasoning": str, "cost_ms": float } """ t0 = time.perf_counter() checks = {} reason_parts = [] # 1. Re-derive id try: computed_id = _event_id(event) checks['id_match'] = (computed_id == event.get('id', '')) if not checks['id_match']: reason_parts.append( f"id mismatch: claimed={event.get('id','')[:16]}... " f"computed={computed_id[:16]}..." ) except Exception as exc: checks['id_match'] = False reason_parts.append(f"id computation failed: {exc}") # 2. Schnorr verify try: msg = bytes.fromhex(event['id']) sig = bytes.fromhex(event['sig']) pk = bytes.fromhex(event['pubkey']) checks['sig_valid'] = _schnorr_verify(msg, sig, pk) if not checks['sig_valid']: reason_parts.append("BIP-340 Schnorr signature invalid") except Exception as exc: checks['sig_valid'] = False reason_parts.append(f"signature parse failed: {exc}") verdict = "ACCEPT" if all(checks.values()) else "REJECT" if verdict == "ACCEPT": reasoning = ( f"id matches SHA-256(NIP-01 canonical serialization); " f"BIP-340 Schnorr verify(msg=id, sig=sig, pubkey) passed. " f"Event was signed by the holder of the corresponding private key." ) else: reasoning = "REJECT — " + "; ".join(reason_parts) cost_ms = (time.perf_counter() - t0) * 1000 return {"verdict": verdict, "checks": checks, "reasoning": reasoning, "cost_ms": round(cost_ms, 2)} # ── Demo test cases ─────────────────────────────────────────────────────────── def _generate_test_events(): """ Generate 3 valid ACCEPT events + 1 REJECT event using a fixed test key. The test private key is deterministically derived from a public string — no production keys are used. Anyone can reproduce by running this script. """ # Fixed test private key: sha256("nostr-verifier-test-v1") mod N seed = hashlib.sha256(b"nostr-verifier-test-v1").digest() test_sk = int.from_bytes(seed, 'big') % N # Fixed timestamp for full reproducibility fixed_ts = 1748476800 # 2026-05-28 16:00:00 UTC def make_fixed(kind, tags, content): pk = _pubkey_x(test_sk).hex() ev = {'pubkey': pk, 'created_at': fixed_ts, 'kind': kind, 'tags': tags, 'content': content} eid = _event_id(ev) sig = _schnorr_sign(bytes.fromhex(eid), test_sk) ev['id'] = eid ev['sig'] = sig.hex() return ev accept1 = make_fixed( kind=1, tags=[], content="Hello Nostr — trustless verifier test event #1" ) accept2 = make_fixed( kind=1, tags=[["t", "bitcoin"]], content="BTC/ETH 366-day OHLCV dataset available. Signed by test key." ) accept3 = make_fixed( kind=30023, tags=[["title", "Pure Python secp256k1"], ["t", "bitcoin"]], content="# Pure Python secp256k1\n\nBIP-340 Schnorr signing and verification with zero external dependencies." ) # REJECT: tamper accept1's content after signing → id will no longer match reject1 = dict(accept1) reject1['content'] = "TAMPERED CONTENT — this was not in the original signed event" return accept1, accept2, accept3, reject1 # ── Main ────────────────────────────────────────────────────────────────────── if __name__ == '__main__': print("=" * 68) print("Trustless Verifier: Nostr Event Schnorr Signature Authenticity") print("Mechanism: Deterministic re-execution (BIP-340, zero external trust)") print("=" * 68) print() accept1, accept2, accept3, reject1 = _generate_test_events() cases = [ ("ACCEPT-1", accept1, "Valid kind:1 note"), ("ACCEPT-2", accept2, "Valid kind:1 note with tags"), ("ACCEPT-3", accept3, "Valid kind:30023 long-form article"), ("REJECT-1", reject1, "Tampered content — id/sig no longer match"), ] all_pass = True for label, event, description in cases: result = verify_nostr_event(event) expected = "ACCEPT" if label.startswith("ACCEPT") else "REJECT" status = "PASS" if result['verdict'] == expected else "FAIL" if status == "FAIL": all_pass = False print(f"[{status}] {label} — {description}") print(f" event id : {event['id'][:32]}...") print(f" pubkey : {event['pubkey'][:32]}...") print(f" verdict : {result['verdict']}") print(f" checks : {result['checks']}") print(f" reasoning: {result['reasoning'][:120]}") print(f" cost : {result['cost_ms']} ms") print() # Cost analysis print("-" * 68) print("COST ANALYSIS") print("-" * 68) print(" Computation : 2× secp256k1 scalar multiplications (pure Python)") print(" Wall-clock : ~10–50 ms per verification on commodity hardware") print(" Sat cost : 0 sats (runs locally; no network, no oracle, no fee)") print(" At 1000-sat task threshold: cost is <0.1% of task value") print(" Trust model : secp256k1 curve params only (same as Bitcoin itself)") print() print("-" * 68) print("TRUST ASSUMPTIONS (explicit)") print("-" * 68) print(" 1. secp256k1 curve (P, N, G) are correctly defined — NIST/SECG standard.") print(" 2. Python's hashlib.sha256 is correct — OS/stdlib guarantee.") print(" 3. The verifier code itself is not malicious — audit 150 LOC above.") print(" No hardware vendor trust. No oracle. No ZK prover. Just math.") print() if all_pass: print("ALL 4 TEST CASES PASSED.") else: print("SOME TEST CASES FAILED — check output above.")