Menu
Nostr Python Executable Jan 31, 2026

Sign Nostr Events

Create and sign Nostr events using Schnorr signatures

#events #signatures #schnorr #nip-01

Overview

Sign Nostr events with Schnorr signatures per NIP-01. Events are the fundamental data structure in Nostr - every post, reaction, and profile update is a signed event.

The Code

"""
Nostr Event Signer
Create and sign events using BIP-340 Schnorr signatures

Requirements: hashlib (built-in)

Note: This is a simplified implementation for educational purposes.
For production, use pynostr or nostr-sdk.
"""

import hashlib
import json
import time
import secrets
from typing import List, Optional, Tuple

# secp256k1 curve parameters
P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
G = (
    0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
    0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
)


def modinv(a: int, m: int) -> int:
    """Modular multiplicative inverse."""
    def extended_gcd(a, b):
        if a == 0:
            return b, 0, 1
        g, x, y = extended_gcd(b % a, a)
        return g, y - (b // a) * x, x

    if a < 0:
        a = a % m
    g, x, _ = extended_gcd(a, m)
    return x % m


def point_add(p1, p2):
    """Add two points on secp256k1."""
    if p1 is None:
        return p2
    if p2 is None:
        return p1

    x1, y1 = p1
    x2, y2 = p2

    if x1 == x2 and y1 != y2:
        return None

    if x1 == x2:
        m = (3 * x1 * x1) * modinv(2 * y1, P) % P
    else:
        m = (y2 - y1) * modinv(x2 - x1, P) % P

    x3 = (m * m - x1 - x2) % P
    y3 = (m * (x1 - x3) - y1) % P
    return (x3, y3)


def scalar_mult(k: int, point) -> Tuple[int, int]:
    """Multiply a point by a scalar."""
    result = None
    addend = point

    while k:
        if k & 1:
            result = point_add(result, addend)
        addend = point_add(addend, addend)
        k >>= 1

    return result


def lift_x(x: int) -> Tuple[int, int]:
    """Lift x-coordinate to point with even y."""
    y_sq = (pow(x, 3, P) + 7) % P
    y = pow(y_sq, (P + 1) // 4, P)
    if pow(y, 2, P) != y_sq:
        return None
    return (x, y if y % 2 == 0 else P - y)


def tagged_hash(tag: str, msg: bytes) -> bytes:
    """BIP-340 tagged hash."""
    tag_hash = hashlib.sha256(tag.encode()).digest()
    return hashlib.sha256(tag_hash + tag_hash + msg).digest()


def schnorr_sign(msg: bytes, seckey: bytes) -> bytes:
    """
    Sign message with BIP-340 Schnorr signature.

    Args:
        msg: 32-byte message to sign
        seckey: 32-byte private key

    Returns:
        64-byte signature
    """
    d = int.from_bytes(seckey, 'big')
    if not (0 < d < N):
        raise ValueError("Invalid private key")

    P_point = scalar_mult(d, G)
    # Negate d if y is odd
    if P_point[1] % 2 != 0:
        d = N - d

    # Generate deterministic nonce
    t = d.to_bytes(32, 'big')
    aux = secrets.token_bytes(32)
    t = bytes(a ^ b for a, b in zip(t, tagged_hash("BIP0340/aux", aux)))

    k0 = int.from_bytes(
        tagged_hash("BIP0340/nonce", t + P_point[0].to_bytes(32, 'big') + msg),
        'big'
    ) % N

    if k0 == 0:
        raise ValueError("Failure: k0 is zero")

    R = scalar_mult(k0, G)
    k = k0 if R[1] % 2 == 0 else N - k0

    e = int.from_bytes(
        tagged_hash("BIP0340/challenge",
                   R[0].to_bytes(32, 'big') + P_point[0].to_bytes(32, 'big') + msg),
        'big'
    ) % N

    sig = R[0].to_bytes(32, 'big') + ((k + e * d) % N).to_bytes(32, 'big')
    return sig


def create_event(
    private_key_hex: str,
    kind: int,
    content: str,
    tags: Optional[List[List[str]]] = None,
    created_at: Optional[int] = None
) -> dict:
    """
    Create and sign a Nostr event.

    Args:
        private_key_hex: Private key as hex string
        kind: Event kind (1 = note, 0 = metadata, etc.)
        content: Event content
        tags: Optional list of tags
        created_at: Optional timestamp (defaults to now)

    Returns:
        Signed event as dict
    """
    if tags is None:
        tags = []

    if created_at is None:
        created_at = int(time.time())

    # Derive public key
    private_key = bytes.fromhex(private_key_hex)
    d = int.from_bytes(private_key, 'big')
    public_point = scalar_mult(d, G)
    public_key_hex = public_point[0].to_bytes(32, 'big').hex()

    # Create event structure
    event = {
        "pubkey": public_key_hex,
        "created_at": created_at,
        "kind": kind,
        "tags": tags,
        "content": content
    }

    # Calculate event ID (SHA256 of serialized event)
    serialized = json.dumps([
        0,                      # Reserved for future use
        event["pubkey"],
        event["created_at"],
        event["kind"],
        event["tags"],
        event["content"]
    ], separators=(',', ':'), ensure_ascii=False)

    event_id = hashlib.sha256(serialized.encode()).digest()
    event["id"] = event_id.hex()

    # Sign the event
    signature = schnorr_sign(event_id, private_key)
    event["sig"] = signature.hex()

    return event


def verify_event(event: dict) -> bool:
    """
    Verify a signed Nostr event.

    Args:
        event: Event dict with id, pubkey, and sig

    Returns:
        True if signature is valid
    """
    # Recreate serialized event
    serialized = json.dumps([
        0,
        event["pubkey"],
        event["created_at"],
        event["kind"],
        event["tags"],
        event["content"]
    ], separators=(',', ':'), ensure_ascii=False)

    # Verify ID
    expected_id = hashlib.sha256(serialized.encode()).hexdigest()
    if event["id"] != expected_id:
        return False

    # Verify signature (simplified - production should verify properly)
    return (
        len(event["sig"]) == 128 and
        len(event["pubkey"]) == 64
    )


# Example usage
if __name__ == "__main__":
    # Generate a test key (in production, load from secure storage)
    test_private_key = secrets.token_bytes(32).hex()

    print("=== Creating Nostr Event ===\n")

    # Create a kind:1 note (text post)
    event = create_event(
        private_key_hex=test_private_key,
        kind=1,
        content="Hello Nostr! This is a signed message from an agent.",
        tags=[
            ["t", "nostr"],
            ["t", "agent"]
        ]
    )

    print("Event created:")
    print(json.dumps(event, indent=2))

    print(f"\n=== Verification ===")
    is_valid = verify_event(event)
    print(f"Event valid: {is_valid}")

    print(f"\n=== Event Details ===")
    print(f"ID:      {event['id'][:16]}...")
    print(f"Pubkey:  {event['pubkey'][:16]}...")
    print(f"Kind:    {event['kind']}")
    print(f"Content: {event['content']}")
    print(f"Tags:    {event['tags']}")

Usage

# No external dependencies required
python sign_event.py

Example Output

=== Creating Nostr Event ===

Event created:
{
  "pubkey": "a1b2c3d4e5f6...",
  "created_at": 1706745600,
  "kind": 1,
  "tags": [
    ["t", "nostr"],
    ["t", "agent"]
  ],
  "content": "Hello Nostr! This is a signed message from an agent.",
  "id": "abc123def456...",
  "sig": "xyz789..."
}

=== Verification ===
Event valid: True

=== Event Details ===
ID:      abc123def456...
Pubkey:  a1b2c3d4e5f6...
Kind:    1
Content: Hello Nostr! This is a signed message from an agent.
Tags:    [['t', 'nostr'], ['t', 'agent']]

Agent Notes

Event kinds for agents:

KindDescriptionUse Case
0MetadataSet profile name, about, picture
1Text notePost messages
4Encrypted DMPrivate messages (deprecated, use NIP-44)
7ReactionReact to other events
9735Zap receiptLightning payment proof

Event ID: SHA256 hash of the serialized event. Used for references and threading.

Signature: BIP-340 Schnorr signature over the event ID. Proves authorship.

Tags: Key-value metadata. Common tags:

  • ["e", "<event_id>"] - Reference another event
  • ["p", "<pubkey>"] - Mention a user
  • ["t", "<hashtag>"] - Add hashtag