Code

The same scheme from how it works — Schnorr identification, made non-interactive with the Fiat–Shamir transform — implemented in Python, JavaScript, and Rust, all on secp256k1. Every sample on this page has actually been run and checked for a correct roundtrip and two negative cases (a tampered proof, a proof built from the wrong secret); none of this is pseudocode.

Python

Uses coincurve, a wrapper around Bitcoin Core’s audited libsecp256k1 — the same dependency used on pedersen.foundation’s Code page.

import hashlib
import secrets
import coincurve

N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141  # curve order

def scalar_bytes(k: int) -> bytes:
    return (k % N).to_bytes(32, "big")

G = coincurve.PublicKey.from_valid_secret(scalar_bytes(1))  # standard base point

def random_scalar() -> int:
    return secrets.randbelow(N)

def point_mul(P: coincurve.PublicKey, k: int) -> coincurve.PublicKey:
    return P.multiply(scalar_bytes(k))

def point_add(P: coincurve.PublicKey, Q: coincurve.PublicKey) -> coincurve.PublicKey:
    return coincurve.PublicKey.combine_keys([P, Q])

def hash_to_scalar(*points: coincurve.PublicKey) -> int:
    """Fiat-Shamir challenge: hash the transcript down to a scalar mod N."""
    h = hashlib.sha256()
    for p in points:
        h.update(p.format(True))
    return int.from_bytes(h.digest(), "big") % N

def public_key(x: int) -> coincurve.PublicKey:
    return point_mul(G, x)

def prove(x: int, y: coincurve.PublicKey):
    """R = k*G, c = Hash(G, y, R), s = k + c*x mod N."""
    k = random_scalar()
    R = point_mul(G, k)
    c = hash_to_scalar(G, y, R)
    s = (k + c * x) % N
    return R, s

def verify(y: coincurve.PublicKey, R: coincurve.PublicKey, s: int) -> bool:
    """Recompute c, check s*G == R + c*y."""
    c = hash_to_scalar(G, y, R)
    lhs = point_mul(G, s)
    rhs = point_add(R, point_mul(y, c))
    return lhs.format(True) == rhs.format(True)

# --- usage ---
x = random_scalar()
y = public_key(x)
R, s = prove(x, y)
assert verify(y, R, s)
assert not verify(y, R, (s + 1) % N)  # tampered s is rejected

wrong_x = random_scalar()
wrong_R, wrong_s = prove(wrong_x, y)  # proving a different secret
assert not verify(y, wrong_R, wrong_s)  # rejected against the real y

JavaScript

This is the same implementation running live in the demo on this site — see src/lib/schnorr.js in the site’s own repository. Uses @noble/curves and @noble/hashes — audited, dependency-free implementations, the same curve library already used on pedersen.foundation.

import { secp256k1 } from '@noble/curves/secp256k1.js';
import { sha256 } from '@noble/hashes/sha2.js';

const G = secp256k1.Point.BASE;
const N = secp256k1.Point.Fn.ORDER; // curve order

function mod(a, m) {
  return ((a % m) + m) % m;
}

// Uniform random scalar in [0, N).
function randomScalar() {
  const bytes = new Uint8Array(48); // extra bytes over 32 to keep mod bias negligible
  crypto.getRandomValues(bytes);
  let hex = '0x0';
  for (const b of bytes) hex += b.toString(16).padStart(2, '0');
  return BigInt(hex) % N;
}

// Fiat-Shamir challenge: hash the transcript (G, y, R) down to a scalar mod N.
function hashToScalar(...points) {
  const parts = points.map((p) => p.toBytes(true));
  const total = parts.reduce((sum, p) => sum + p.length, 0);
  const buf = new Uint8Array(total);
  let offset = 0;
  for (const p of parts) {
    buf.set(p, offset);
    offset += p.length;
  }
  const digest = sha256(buf);
  let n = 0n;
  for (const b of digest) n = (n << 8n) | BigInt(b);
  return mod(n, N);
}

function publicKey(x) {
  return G.multiply(mod(x, N));
}

// R = k*G, c = Hash(G, y, R), s = k + c*x mod N.
function prove(x, y = publicKey(x)) {
  const k = randomScalar();
  const R = G.multiply(k);
  const c = hashToScalar(G, y, R);
  const s = mod(k + c * x, N);
  return { R, s, y, c };
}

// Recompute c, check s*G == R + c*y.
function verify(y, R, s) {
  const c = hashToScalar(G, y, R);
  const lhs = G.multiply(mod(s, N));
  const rhs = R.add(y.multiply(mod(c, N)));
  return lhs.equals(rhs);
}

Rust

Uses k256, the RustCrypto project’s pure-Rust secp256k1 implementation — the same crate used on pedersen.foundation’s Code page.

use k256::elliptic_curve::group::GroupEncoding;
use k256::elliptic_curve::ops::{MulByGenerator, Reduce};
use k256::elliptic_curve::Field;
use k256::{ProjectivePoint, Scalar, U256};
use rand::rngs::OsRng;
use sha2::{Digest, Sha256};

// Fiat-Shamir challenge: hash the transcript points down to a scalar mod N.
fn hash_to_scalar(points: &[&ProjectivePoint]) -> Scalar {
    let mut hasher = Sha256::new();
    for p in points {
        hasher.update(p.to_bytes());
    }
    let digest = hasher.finalize();
    let bytes: [u8; 32] = digest.into();
    Scalar::reduce(U256::from_be_slice(&bytes))
}

fn public_key(g: &ProjectivePoint, x: &Scalar) -> ProjectivePoint {
    g * x
}

/// Returns (R, s).
fn prove(g: &ProjectivePoint, x: &Scalar, y: &ProjectivePoint) -> (ProjectivePoint, Scalar) {
    let k = Scalar::random(&mut OsRng);
    let r = g * &k;
    let c = hash_to_scalar(&[g, y, &r]);
    let s = k + c * x;
    (r, s)
}

fn verify(g: &ProjectivePoint, y: &ProjectivePoint, r: &ProjectivePoint, s: &Scalar) -> bool {
    let c = hash_to_scalar(&[g, y, r]);
    let lhs = g * s;
    let rhs = *r + y * &c;
    lhs == rhs
}

fn main() {
    let g = ProjectivePoint::mul_by_generator(&Scalar::ONE); // standard base point

    let x = Scalar::random(&mut OsRng);
    let y = public_key(&g, &x);
    let (r, s) = prove(&g, &x, &y);
    assert!(verify(&g, &y, &r, &s));

    let tampered_s = s + Scalar::ONE;
    assert!(!verify(&g, &y, &r, &tampered_s)); // tampered s is rejected

    let wrong_x = Scalar::random(&mut OsRng);
    let (wrong_r, wrong_s) = prove(&g, &wrong_x, &y); // proving a different secret
    assert!(!verify(&g, &y, &wrong_r, &wrong_s)); // rejected against the real y
}

Sources: implements the construction derived on how it works. The Python sample uses coincurve; the JavaScript sample uses @noble/curves; the Rust sample uses k256.