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