nostr_helpers.py raw
1 """Minimal BIP-340 Schnorr signing + Nostr event builder for tests.
2
3 Pure Python, no external dependencies beyond stdlib.
4 """
5
6 import hashlib
7 import json
8 import os
9 import time
10
11 # secp256k1 curve parameters.
12 _P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
13 _N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
14 _G = (
15 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
16 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8,
17 )
18
19
20 def _modinv(a, m):
21 g, x, _ = _egcd(a % m, m)
22 return x % m
23
24
25 def _egcd(a, b):
26 if a == 0:
27 return b, 0, 1
28 g, x, y = _egcd(b % a, a)
29 return g, y - (b // a) * x, x
30
31
32 def _point_add(p1, p2):
33 if p1 is None:
34 return p2
35 if p2 is None:
36 return p1
37 if p1[0] == p2[0] and p1[1] != p2[1]:
38 return None
39 if p1 == p2:
40 lam = (3 * p1[0] * p1[0] * _modinv(2 * p1[1], _P)) % _P
41 else:
42 lam = ((p2[1] - p1[1]) * _modinv(p2[0] - p1[0], _P)) % _P
43 x = (lam * lam - p1[0] - p2[0]) % _P
44 y = (lam * (p1[0] - x) - p1[1]) % _P
45 return (x, y)
46
47
48 def _point_mul(k, point):
49 r = None
50 a = point
51 while k:
52 if k & 1:
53 r = _point_add(r, a)
54 a = _point_add(a, a)
55 k >>= 1
56 return r
57
58
59 def _tagged_hash(tag, msg):
60 t = hashlib.sha256(tag.encode()).digest()
61 return hashlib.sha256(t + t + msg).digest()
62
63
64 def _int_from_bytes(b):
65 return int.from_bytes(b, "big")
66
67
68 def _bytes_from_int(x):
69 return x.to_bytes(32, "big")
70
71
72 def pubkey_from_seckey(seckey: bytes) -> bytes:
73 pt = _point_mul(_int_from_bytes(seckey), _G)
74 return _bytes_from_int(pt[0])
75
76
77 def schnorr_sign(msg: bytes, seckey: bytes) -> bytes:
78 d = _int_from_bytes(seckey)
79 pt = _point_mul(d, _G)
80 if pt[1] % 2 != 0:
81 d = _N - d
82 aux = os.urandom(32)
83 t = bytes(
84 a ^ b for a, b in zip(_bytes_from_int(d), _tagged_hash("BIP0340/aux", aux))
85 )
86 rand = _tagged_hash("BIP0340/nonce", t + _bytes_from_int(pt[0]) + msg)
87 k = _int_from_bytes(rand) % _N
88 if k == 0:
89 raise ValueError("bad nonce")
90 r = _point_mul(k, _G)
91 if r[1] % 2 != 0:
92 k = _N - k
93 e = (
94 _int_from_bytes(
95 _tagged_hash(
96 "BIP0340/challenge",
97 _bytes_from_int(r[0]) + _bytes_from_int(pt[0]) + msg,
98 )
99 )
100 % _N
101 )
102 return _bytes_from_int(r[0]) + _bytes_from_int((k + e * d) % _N)
103
104
105 def make_event(seckey: bytes, content: str, kind=1, tags=None, created_at=None):
106 """Create a fully signed Nostr event."""
107 if tags is None:
108 tags = []
109 if created_at is None:
110 created_at = int(time.time())
111 pubkey = pubkey_from_seckey(seckey).hex()
112 serial = json.dumps(
113 [0, pubkey, created_at, kind, tags, content], separators=(",", ":")
114 )
115 event_id = hashlib.sha256(serial.encode()).digest()
116 sig = schnorr_sign(event_id, seckey)
117 return {
118 "id": event_id.hex(),
119 "pubkey": pubkey,
120 "created_at": created_at,
121 "kind": kind,
122 "tags": tags,
123 "content": content,
124 "sig": sig.hex(),
125 }
126
127
128 # Fixed test keypair — deterministic for reproducible tests.
129 TEST_SECKEY = hashlib.sha256(b"smesh-test-key-do-not-use").digest()
130 TEST_PUBKEY = pubkey_from_seckey(TEST_SECKEY).hex()
131