Skip to content
Sonr Crypto
Esc
navigateopen⌘Jpreview
On this page

Schnorr proofs

Non-interactive proof of knowledge of a discrete log over any curve in core/curves, with an optional commit-then-reveal variant used by this module's DKG and OT protocols.

zkp/schnorr implements a single, small, well-scoped thing: a Fiat-Shamir-compiled proof that you know the scalar behind a curve point. Its package doc names its source — Doerner et al., eprint 2018/499 — and implements Functionality 6 (the plain proof) and Functionality 7 (the committed variant) from that paper.

This is the most heavily used primitive in the repository. It is the proof that Alice and Bob exchange in DKLs threshold-ECDSA key generation, and the proof the sender uses to convince the receiver it knows its own base-OT secret key.

What is actually proved

Given a base point B and a witness scalar x, the prover publishes the statement X = x·B together with a challenge/response pair (C, S). Writing k for a fresh random nonce and sid for uniqueSessionId:

C=H(sidBXkB),S=Cx+kC = H(\text{sid} \parallel B \parallel X \parallel k \cdot B), \qquad S = C \cdot x + k

The verifier never sees k. It recovers the nonce point from the response and re-derives the challenge:

C=H(sidBX(SBCX))C' = H(\text{sid} \parallel B \parallel X \parallel (S \cdot B - C \cdot X))

and accepts only if C' equals C, compared with crypto/subtle.ConstantTimeCompare. The hash is SHA3-256; the digest is widened to a scalar with Scalar.SetBytesWide. A verifier learns that some x satisfying X = x·B is known to the prover, and learns nothing else about it.

When to use it

Reach for this when a protocol participant must demonstrate honest generation of a public value derived from a secret it keeps — a key share, an OT secret key, a nonce commitment. It is the standard defence against a party contributing a public point whose discrete log it does not know.

Do not reach for it as a signature scheme. The statement is not bound to a message, only to uniqueSessionId and the base point, so it authenticates nothing about payload data. For signing use ECDSA or BLS. Do not reach for it to prove anything other than discrete-log knowledge: there is no range, no set membership, and no relation between multiple statements here.

API

PropType
NewProverfunc(curve *curves.Curve, basepoint curves.Point, uniqueSessionId []byte) *Prover

Binds a curve, a base point, and a domain separator. Never returns an error.

Typefunc(curve *curves.Curve, basepoint curves.Point, uniqueSessionId []byte) *Prover
Prover.Provefunc(x curves.Scalar) (*Proof, error)

Computes Statement = x·basepoint and the (c, s) pair. One curve multiplication for the statement plus one for the nonce point.

Typefunc(x curves.Scalar) (*Proof, error)
Prover.ProveCommit?func(x curves.Scalar) (*Proof, Commitment, error)

Same proof, plus SHA3-256(c || s) as a commitment to open later.

Typefunc(x curves.Scalar) (*Proof, Commitment, error)
Verifyfunc(proof *Proof, curve *curves.Curve, basepoint curves.Point, uniqueSessionId []byte) error

Returns nil on success, an error on failure. There is no boolean return.

Typefunc(proof *Proof, curve *curves.Curve, basepoint curves.Point, uniqueSessionId []byte) error
DecommitVerify?func(proof *Proof, commitment Commitment, curve *curves.Curve, basepoint curves.Point, uniqueSessionId []byte) error

Checks the proof opens the commitment, then verifies the proof.

Typefunc(proof *Proof, commitment Commitment, curve *curves.Curve, basepoint curves.Point, uniqueSessionId []byte) error

Types

Commitment is a plain type alias for []byte — no wrapper, no methods.

PropType
Statementcurves.Point

The point whose discrete log is proved: x · basepoint. Constructed by Prove, not supplied by the caller.

Typecurves.Point
Ccurves.Scalar

The Fiat-Shamir challenge scalar.

Typecurves.Scalar
Scurves.Scalar

The response scalar, c·x + k.

Typecurves.Scalar

All three Proof fields are exported, so the struct serializes directly. tecdsa/dklsv1 transmits it with encoding/gob in dkgserializers.go.

The basepoint == nil shorthand

Both NewProver and Verify accept basepoint == nil and substitute curve.NewGeneratorPoint(). Passing nil on one side and an explicit generator on the other is safe because it resolves to the same point. Passing a different point on the two sides is not: the base point is hashed into the challenge, so verification simply fails.

Proving with respect to a non-generator base point is a real use case, not a curiosity. tecdsa/dklsv1/sign proves knowledge of Alice’s nonce kA with respect to Bob’s point DB, so that the statement is exactly R = kA · DB:

rSchnorrProver := schnorr.NewProver(alice.curve, round2Output.DB, uniqueSessionId[:])
round3Output.RSchnorrProof, err = rSchnorrProver.Prove(kA)

Basic proof and verification

Grounded in zkp/schnorr/schnorr_test.go, which runs this exact flow over K256, P256, PALLAS, BLS12-377 G1/G2, BLS12-381 G1/G2, and ED25519.

package main

import (
	"crypto/rand"
	"fmt"

	"golang.org/x/crypto/sha3"

	"github.com/sonr-io/crypto/core/curves"
	"github.com/sonr-io/crypto/zkp/schnorr"
)

func main() {
	curve := curves.K256()

	// Both sides must agree on these bytes, byte for byte.
	uniqueSessionId := sha3.New256().Sum([]byte("my-protocol/dkg/round-3"))

	// Prover side: nil basepoint means the curve's default generator.
	prover := schnorr.NewProver(curve, nil, uniqueSessionId)
	secret := curve.Scalar.Random(rand.Reader)

	proof, err := prover.Prove(secret)
	if err != nil {
		panic(err)
	}

	// proof.Statement == secret * G, and is what the verifier will treat
	// as the public key.
	fmt.Println("statement:", proof.Statement.ToAffineCompressed())

	// Verifier side: same curve, same basepoint convention, same session id.
	if err := schnorr.Verify(proof, curve, nil, uniqueSessionId); err != nil {
		panic(err) // "schnorr verification failed"
	}
}

The committed variant

ProveCommit returns the proof and SHA3-256(C.Bytes() || S.Bytes()). A protocol sends the commitment first, waits for the counterparty to commit to its own contribution, and only then reveals the proof, which DecommitVerify checks against the earlier commitment before verifying it.

The reason is ordering, not secrecy. Without it, whichever party speaks second can choose its key share after seeing the first party’s public point, and bias the combined public key. Committing first removes that freedom.

This is precisely how DKLs 2-of-2 DKG is wired in tecdsa/dklsv1/dkg:

Alice commits

Alice builds a prover over her session id and calls ProveCommit(alice.secretKeyShare). She keeps the *schnorr.Proof in memory and sends only the schnorr.Commitment.

Bob proves in the clear

Bob stores round2Output.Commitment, builds his own prover, and calls Prove(bob.secretKeyShare), sending the full proof.

Alice verifies and reveals

Round4VerifyAndReveal calls schnorr.Verify on Bob’s proof, then returns Alice’s previously withheld proof.

Bob decommits and verifies

Round5DecommitmentAndStartOt calls schnorr.DecommitVerify(proof, bob.aliceCommitment, bob.curve, nil, bob.aliceSalt[:]). Only after this does Bob derive bob.publicKey = proof.Statement.Mul(bob.secretKeyShare).

prover := schnorr.NewProver(curve, nil, uniqueSessionId)

proof, commitment, err := prover.ProveCommit(secret)
if err != nil {
	panic(err)
}

// ... round trip: send `commitment`, receive the peer's contribution ...
// ... then send `proof` ...

if err := schnorr.DecommitVerify(proof, commitment, curve, nil, uniqueSessionId); err != nil {
	panic(err) // "initial hash decommitment failed" or "schnorr verification failed"
}

Caveats

The Prover struct itself is stateless with respect to the witness — it holds only the curve, base point, and session id, so a single prover can produce proofs for many different witnesses under the same domain. Each Prove call draws a fresh nonce k from crypto/rand.

Where this is used in the module

Last updated on September 2, 2026

Was this page helpful?