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.