mirror of
https://github.com/sonr-io/crypto.git
synced 2026-09-17 01:26:23 +00:00
feat: init docs
This commit is contained in:
@@ -0,0 +1,452 @@
|
||||
---
|
||||
title: Accumulator
|
||||
description: Pairing-based ECC accumulator — a constant-size commitment to a set, constant-size membership witnesses, and a zero-knowledge membership proof that hides which element is held.
|
||||
sidebar:
|
||||
label: Accumulator
|
||||
order: 3
|
||||
icon: list-checks
|
||||
---
|
||||
|
||||
The `accumulator` package implements the pairing-based accumulator of
|
||||
[eprint 2020/777](https://eprint.iacr.org/2020/777.pdf), together with the zero-knowledge
|
||||
proof of knowledge from section 7 of that paper. Its own package doc states the scope limit
|
||||
up front: **only the membership-witness case is implemented**. Non-membership witnesses, and
|
||||
the accumulator initialisation those would require, are deliberately absent —
|
||||
`Accumulator.New` simply sets the initial value to the G1 generator.
|
||||
|
||||
## The value proposition
|
||||
|
||||
Three properties, and they are the entire reason to reach for this instead of a Merkle tree
|
||||
or a plain list:
|
||||
|
||||
- **The accumulator is one curve point** regardless of how many elements it holds. On
|
||||
BLS12-381 that is a 48-byte compressed G1 point; `Accumulator.MarshalBinary` returns 60
|
||||
bytes with its BARE framing, at 1 element and at 5000 alike.
|
||||
- **A membership witness is one curve point plus its element.**
|
||||
`MembershipWitness.MarshalBinary` is 92 bytes, again independent of set size.
|
||||
- **The membership proof hides the element.** A verifier learns that the prover holds a valid
|
||||
witness for *some* accumulated element, not which one.
|
||||
|
||||
That last property is what makes this a privacy-preserving revocation mechanism. An issuer
|
||||
accumulates one element per valid credential and publishes the accumulator. A holder proves
|
||||
its credential is still accumulated without identifying the credential, and therefore without
|
||||
being linkable across presentations. Revocation is a `Remove` by the manager.
|
||||
|
||||
## When not to use it
|
||||
|
||||
Only the holder of the `SecretKey` can mutate the set or issue a witness — `Add`, `Remove`,
|
||||
`AddElements`, `Update`, and `MembershipWitness.New` all take `*SecretKey`. If you need a
|
||||
publicly-updatable set, or set membership without a trusted manager, this is the wrong tool.
|
||||
If you need non-membership proofs, they are not implemented. If you need proofs cheaper than
|
||||
a multi-pairing per verification, look elsewhere.
|
||||
|
||||
Also note the operational cost: every update invalidates every outstanding witness. See the
|
||||
[witness staleness](#witness-staleness-is-the-hard-part) warning below before committing to
|
||||
this design.
|
||||
|
||||
## Types
|
||||
|
||||
Everything here is over a **pairing** curve — in practice `curves.BLS12381(&curves.PointBls12381G1{})`.
|
||||
The accumulator value lives in G1; the public key lives in G2.
|
||||
|
||||
| Type | Definition | Notes |
|
||||
| --- | --- | --- |
|
||||
| `Element` | `curves.Scalar` | A set member. Callers hash application data into it, e.g. `curve.Scalar.Hash([]byte("credential-id"))`. |
|
||||
| `Coefficient` | `curves.Point` | Batch-update polynomial coefficients published by the manager alongside an `Update`. |
|
||||
| `Accumulator` | struct, unexported `value curves.Point` | The set commitment. |
|
||||
| `SecretKey` | struct, unexported `value curves.Scalar` | The manager's alpha. |
|
||||
| `PublicKey` | struct, unexported `value curves.PairingPoint` | `alpha · G2`. |
|
||||
| `Delta` | struct, unexported `d curves.Scalar`, `p curves.Point` | Witness-update material. See the caveat — you cannot build one. |
|
||||
| `MembershipWitness` | struct, unexported `c curves.Point`, `y curves.Scalar` | A holder's witness for element `y`. |
|
||||
|
||||
Every one of these types implements `MarshalBinary() ([]byte, error)` and
|
||||
`UnmarshalBinary([]byte) error` — the encoding is BARE (`git.sr.ht/~sircmpwn/go-bare`). Note
|
||||
that all fields are unexported, so binary marshalling is the *only* way to move these values
|
||||
across a process boundary; there is no JSON codec and no field access.
|
||||
|
||||
## Keys
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
"SecretKey.New": {
|
||||
type: "func(curve *curves.PairingCurve, seed []byte) (*SecretKey, error)",
|
||||
required: true,
|
||||
description: "Derives alpha as curve.Scalar.Hash(seed). Fully deterministic in the seed; performs no validation of seed quality or length.",
|
||||
},
|
||||
"SecretKey.GetPublicKey": {
|
||||
type: "func(curve *curves.PairingCurve) (*PublicKey, error)",
|
||||
required: true,
|
||||
description: "Returns alpha times the G2 generator. Errors if the key or curve is nil.",
|
||||
},
|
||||
"SecretKey.BatchAdditions": {
|
||||
type: "func(additions []Element) (Element, error)",
|
||||
description: "product(y + alpha) over the additions. The multiplier applied to the accumulator on a batch add.",
|
||||
},
|
||||
"SecretKey.BatchDeletions": {
|
||||
type: "func(deletions []Element) (Element, error)",
|
||||
description: "1/product(y + alpha) over the deletions.",
|
||||
},
|
||||
"SecretKey.CreateCoefficients": {
|
||||
type: "func(additions, deletions []Element) ([]Element, error)",
|
||||
description: "Batch polynomial coefficients per page 7 of the paper. Update calls this for you; call it directly only if you are reimplementing Update.",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
:::warning[SecretKey.New is a bare hash of the seed]
|
||||
`SecretKey.New` is `sk.value = curve.Scalar.Hash(seed)` and always returns a nil error. It
|
||||
does not check seed length or entropy. A short or predictable seed yields a guessable alpha,
|
||||
and alpha is total control over the set. Feed it at least 32 bytes from a CSPRNG, or a
|
||||
properly derived key — see [Key derivation](/symmetric/key-derivation).
|
||||
:::
|
||||
|
||||
## Accumulator operations
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
"Accumulator.New": {
|
||||
type: "func(curve *curves.PairingCurve) (*Accumulator, error)",
|
||||
required: true,
|
||||
description: "Sets the value to the G1 generator. Called on the receiver, so the idiom is new(Accumulator).New(curve).",
|
||||
},
|
||||
"Accumulator.WithElements": {
|
||||
type: "func(curve *curves.PairingCurve, key *SecretKey, m []Element) (*Accumulator, error)",
|
||||
description: "New plus a batch add: V = product(y + alpha) · V0. The usual way to bootstrap a populated set.",
|
||||
},
|
||||
"Accumulator.Add": {
|
||||
type: "func(key *SecretKey, e Element) (*Accumulator, error)",
|
||||
description: "V' = (y + alpha) · V. Errors if the accumulator value is nil or the identity.",
|
||||
},
|
||||
"Accumulator.AddElements": {
|
||||
type: "func(key *SecretKey, m []Element) (*Accumulator, error)",
|
||||
description: "Batch add. Does not emit coefficients, so holders cannot update from it — use Update if witnesses are outstanding.",
|
||||
},
|
||||
"Accumulator.Remove": {
|
||||
type: "func(key *SecretKey, e Element) (*Accumulator, error)",
|
||||
description: "V' = 1/(y + alpha) · V. Does not verify the element was ever added; removing an absent element silently produces a different accumulator.",
|
||||
},
|
||||
"Accumulator.Update": {
|
||||
type: "func(key *SecretKey, additions, deletions []Element) (*Accumulator, []Coefficient, error)",
|
||||
description: "Batch add and delete in one step, returning the coefficients holders need for BatchUpdate. This is the update method to use in production.",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
:::danger[Every mutator mutates the receiver in place]
|
||||
`Add`, `AddElements`, `Remove`, `Update`, `WithElements`, and `New` all assign to
|
||||
`acc.value` and then return the same pointer. The returned `*Accumulator` is **not** a new
|
||||
object — it is the receiver. This idiom compiles cleanly and reads like a functional API:
|
||||
|
||||
```go
|
||||
newAcc, _, err := acc.Update(sk, additions, deletions) // newAcc == acc
|
||||
```
|
||||
|
||||
but `acc` has already changed. If you need the previous state — to serve an older epoch, or
|
||||
to roll back — call `MarshalBinary()` **before** the mutation and keep the bytes.
|
||||
`MembershipWitness.New`, `ApplyDelta`, `BatchUpdate`, and `MultiBatchUpdate` behave the same
|
||||
way on their receivers.
|
||||
:::
|
||||
|
||||
## Witnesses
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
"MembershipWitness.New": {
|
||||
type: "func(y Element, acc *Accumulator, sk *SecretKey) (*MembershipWitness, error)",
|
||||
required: true,
|
||||
description: "Issues a witness for y against the current accumulator: C = 1/(y + alpha) · V. Requires the secret key, so only the manager can issue.",
|
||||
},
|
||||
"MembershipWitness.Verify": {
|
||||
type: "func(pk *PublicKey, acc *Accumulator) error",
|
||||
required: true,
|
||||
description: "Multi-pairing check e(C, y·P̃ + Q̃) · e(-V, P̃) == 1. Returns nil on success. Public — any holder or verifier can run it.",
|
||||
},
|
||||
"MembershipWitness.BatchUpdate": {
|
||||
type: "func(additions, deletions []Element, coefficients []Coefficient) (*MembershipWitness, error)",
|
||||
required: true,
|
||||
description: "Refreshes a stale witness against one published Update. This is the update path callers should use.",
|
||||
},
|
||||
"MembershipWitness.MultiBatchUpdate": {
|
||||
type: "func(A [][]Element, D [][]Element, C [][]Coefficient) (*MembershipWitness, error)",
|
||||
description: "Catches up across several epochs at once. All three outer slices must have the same length; index i is the i-th epoch's additions, deletions, and coefficients.",
|
||||
},
|
||||
"MembershipWitness.ApplyDelta": {
|
||||
type: "func(delta *Delta) (*MembershipWitness, error)",
|
||||
description: "Applies precomputed update material. Effectively unreachable — see caveats.",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### Witness staleness is the hard part
|
||||
|
||||
:::danger[Updating the set invalidates every outstanding witness]
|
||||
The witness for element `y` is `1/(y + alpha) · V`, defined *relative to a specific
|
||||
accumulator value*. The moment the manager adds or removes anything, `V` changes and every
|
||||
holder's `Verify(pk, acc)` against the new accumulator fails. This is not a bug; it is
|
||||
inherent to the construction, and it is the dominant operational cost of deploying it.
|
||||
|
||||
The manager must therefore publish, for each update, the `[]Coefficient` returned by
|
||||
`Update` along with the exact `additions` and `deletions` element lists. Holders call
|
||||
`BatchUpdate(additions, deletions, coefficients)` to move their witness forward. A holder
|
||||
that misses epochs uses `MultiBatchUpdate` with the per-epoch slices.
|
||||
|
||||
Consequences you must design for:
|
||||
|
||||
- Coefficients and element lists are **not secret**, but they *do* reveal exactly which
|
||||
elements were added and removed. Batch your updates if that leakage matters.
|
||||
- A holder that skips an epoch and cannot obtain that epoch's coefficients can never repair
|
||||
its witness without the manager reissuing via `MembershipWitness.New`.
|
||||
- `AddElements` returns no coefficients at all. Use `Update` — even with an empty deletion
|
||||
slice — whenever witnesses are outstanding.
|
||||
- A stale witness does not report itself as stale. `Verify` returns the generic
|
||||
`"invalid result"` from the failed pairing check, which is indistinguishable from a forged
|
||||
witness. Track the accumulator epoch alongside the witness in your own state.
|
||||
:::
|
||||
|
||||
## Zero-knowledge membership proof
|
||||
|
||||
The proof protocol from section 7 of the paper. Unlike the witness check, this hides `y`.
|
||||
It is a three-move sigma protocol compiled with Fiat-Shamir, and — unusually — verification
|
||||
is expressed as *recomputing the challenge and comparing it*, not as a `Verify` method.
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
"ProofParams.New": {
|
||||
type: "func(curve *curves.PairingCurve, pk *PublicKey, entropy []byte) (*ProofParams, error)",
|
||||
required: true,
|
||||
description: "Samples the public G1 generators X, Y, Z (and K) from the entropy, the public key, and the curve. Both prover and verifier must use identical params.",
|
||||
},
|
||||
"MembershipProofCommitting.New": {
|
||||
type: "func(witness *MembershipWitness, acc *Accumulator, pp *ProofParams, pk *PublicKey) (*MembershipProofCommitting, error)",
|
||||
required: true,
|
||||
description: "Prover's commit phase. Holds all the blinding values; keep it private and short-lived.",
|
||||
},
|
||||
"MembershipProofCommitting.GetChallengeBytes": {
|
||||
type: "func() []byte",
|
||||
required: true,
|
||||
description: "The transcript to hash for the challenge: V || Ec || T_sigma || T_rho || R_E || R_sigma || R_rho || R_delta_sigma || R_delta_rho.",
|
||||
},
|
||||
"MembershipProofCommitting.GenProof": {
|
||||
type: "func(c curves.Scalar) *MembershipProof",
|
||||
required: true,
|
||||
description: "Computes the s values for the given challenge. Returns the proof to send. No error return.",
|
||||
},
|
||||
"MembershipProof.Finalize": {
|
||||
type: "func(acc *Accumulator, pp *ProofParams, pk *PublicKey, challenge curves.Scalar) (*MembershipProofFinal, error)",
|
||||
required: true,
|
||||
description: "Verifier side: recomputes the commitment values from the proof, the accumulator, and the params.",
|
||||
},
|
||||
"MembershipProofFinal.GetChallenge": {
|
||||
type: "func(curve *curves.PairingCurve) curves.Scalar",
|
||||
required: true,
|
||||
description: "Recomputes the Fiat-Shamir challenge from the finalized values. Verification succeeds iff this equals the challenge the prover used.",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
## Full lifecycle
|
||||
|
||||
<Steps>
|
||||
<Step title="Set up the manager">
|
||||
Derive a `SecretKey` from a strong seed and publish the `PublicKey`.
|
||||
</Step>
|
||||
<Step title="Accumulate the initial members">
|
||||
`new(Accumulator).WithElements(curve, sk, elements)` and publish the accumulator bytes.
|
||||
</Step>
|
||||
<Step title="Issue a witness">
|
||||
For each holder, `new(MembershipWitness).New(element, acc, sk)` and deliver the witness
|
||||
bytes privately. This step needs the secret key.
|
||||
</Step>
|
||||
<Step title="Prove membership">
|
||||
The holder builds `ProofParams`, runs `MembershipProofCommitting`, hashes
|
||||
`GetChallengeBytes()` into a challenge, and sends the challenge plus `GenProof(challenge)`.
|
||||
</Step>
|
||||
<Step title="Verify">
|
||||
The verifier calls `Finalize` then `GetChallenge`, and accepts iff the recomputed
|
||||
challenge equals the one it was given.
|
||||
</Step>
|
||||
<Step title="Update the set">
|
||||
The manager calls `Update(sk, additions, deletions)` and publishes the new accumulator,
|
||||
the element lists, and the coefficients.
|
||||
</Step>
|
||||
<Step title="Refresh witnesses">
|
||||
Every holder calls `BatchUpdate(additions, deletions, coefficients)`, then can prove again
|
||||
against the new accumulator.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Membership proof end to end
|
||||
|
||||
Grounded in `accumulator/proof_test.go` (`TestMembershipProof`) and
|
||||
`accumulator/witness_test.go` (`Test_Membership`, `Test_Membership_Batch_Update`).
|
||||
|
||||
```go membership.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/sonr-io/crypto/accumulator"
|
||||
"github.com/sonr-io/crypto/core/curves"
|
||||
)
|
||||
|
||||
func main() {
|
||||
curve := curves.BLS12381(&curves.PointBls12381G1{})
|
||||
|
||||
// --- Manager setup -----------------------------------------------------
|
||||
sk, err := new(accumulator.SecretKey).New(curve, []byte("32-plus-bytes-of-real-entropy..."))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pk, err := sk.GetPublicKey(curve)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Application data is hashed into set elements.
|
||||
elements := []accumulator.Element{
|
||||
curve.Scalar.Hash([]byte("credential-3")),
|
||||
curve.Scalar.Hash([]byte("credential-4")),
|
||||
curve.Scalar.Hash([]byte("credential-5")),
|
||||
curve.Scalar.Hash([]byte("credential-6")),
|
||||
}
|
||||
|
||||
acc, err := new(accumulator.Accumulator).WithElements(curve, sk, elements)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// --- Issue a witness (manager only, needs sk) --------------------------
|
||||
wit, err := new(accumulator.MembershipWitness).New(elements[3], acc, sk)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// The plain witness check reveals which element is held. Use it for
|
||||
// self-diagnosis, not as a privacy-preserving presentation.
|
||||
if err := wit.Verify(pk, acc); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// --- Zero-knowledge membership proof -----------------------------------
|
||||
// Both sides must derive identical ProofParams.
|
||||
params, err := new(accumulator.ProofParams).New(curve, pk, []byte("proof-params/v1"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
mpc, err := new(accumulator.MembershipProofCommitting).New(wit, acc, params, pk)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
challenge := curve.Scalar.Hash(mpc.GetChallengeBytes())
|
||||
proof := mpc.GenProof(challenge)
|
||||
|
||||
// Verifier: it has acc, pk, params, the proof, and the challenge.
|
||||
final, err := proof.Finalize(acc, params, pk, challenge)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if final.GetChallenge(curve).Cmp(challenge) != 0 {
|
||||
panic("membership proof rejected")
|
||||
}
|
||||
fmt.Println("membership proved without revealing which element")
|
||||
|
||||
// --- Manager revokes and adds ------------------------------------------
|
||||
additions := []accumulator.Element{curve.Scalar.Hash([]byte("credential-7"))}
|
||||
deletions := []accumulator.Element{curve.Scalar.Hash([]byte("credential-5"))}
|
||||
|
||||
// Note: this mutates acc in place and also returns it.
|
||||
_, coefficients, err := acc.Update(sk, additions, deletions)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// --- Holder refreshes its now-stale witness ----------------------------
|
||||
if _, err := wit.BatchUpdate(additions, deletions, coefficients); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := wit.Verify(pk, acc); err != nil {
|
||||
panic(err) // would fail without the BatchUpdate above
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that a fresh `ProofParams` per presentation is fine and is what the test does — proof
|
||||
params are public and only need to agree between the two parties for that one exchange.
|
||||
|
||||
### Catching up across epochs
|
||||
|
||||
`MultiBatchUpdate` takes three parallel outer slices, one entry per epoch, and errors with
|
||||
`"a, d, c should have same length"` if they disagree. From
|
||||
`Test_Membership_Multi_Batch_Update`:
|
||||
|
||||
```go catchup.go
|
||||
_, coeffs1, _ := acc.Update(sk, adds1, dels1)
|
||||
_, coeffs2, _ := acc.Update(sk, []accumulator.Element{}, dels2)
|
||||
_, coeffs3, _ := acc.Update(sk, []accumulator.Element{}, dels3)
|
||||
|
||||
a := [][]accumulator.Element{adds1, {}, {}}
|
||||
d := [][]accumulator.Element{dels1, dels2, dels3}
|
||||
c := [][]accumulator.Coefficient{coeffs1, coeffs2, coeffs3}
|
||||
|
||||
if _, err := wit.MultiBatchUpdate(a, d, c); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := wit.Verify(pk, acc); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
```
|
||||
|
||||
## Caveats
|
||||
|
||||
:::danger[ApplyDelta is unreachable from outside the package]
|
||||
`ApplyDelta(delta *Delta)` is exported, but `Delta`'s fields are unexported and the only
|
||||
constructors — `evaluateDelta` and `evaluateDeltas` — are unexported. No exported function
|
||||
anywhere in the package returns a `*Delta`. From another package you can therefore obtain one
|
||||
only by `UnmarshalBinary`-ing bytes that some in-package code produced, and no in-package code
|
||||
hands them to you. Treat `ApplyDelta` as an internal helper and use `BatchUpdate` /
|
||||
`MultiBatchUpdate`, which construct the delta for you.
|
||||
:::
|
||||
|
||||
:::warning[Remove does not check membership]
|
||||
`Remove` applies `1/(y + alpha)` unconditionally. Removing an element that was never added
|
||||
yields a mathematically valid but semantically meaningless accumulator, and every outstanding
|
||||
witness silently stops verifying with no diagnostic. The package keeps no member list — the
|
||||
manager must track set contents itself.
|
||||
:::
|
||||
|
||||
:::warning[No length or duplicate checks on batch inputs]
|
||||
`BatchAdditions`, `BatchDeletions`, and `Update` accept whatever elements you pass, including
|
||||
duplicates. Adding the same element twice multiplies the accumulator by `(y + alpha)` twice,
|
||||
and one `Remove` will not undo both.
|
||||
:::
|
||||
|
||||
:::note[Non-membership is not implemented]
|
||||
The paper's non-membership witnesses require a different accumulator initialisation
|
||||
(`V0 = product(y + alpha) · P` over a designated set). This package's `New` sets `V0` to the
|
||||
plain G1 generator and its own doc comment flags this as the reason non-membership is out of
|
||||
scope. Do not attempt to derive non-membership proofs from this API.
|
||||
:::
|
||||
|
||||
:::info[There is no MembershipProof.Verify]
|
||||
Verification is a two-call sequence — `Finalize` then `GetChallenge` — followed by a scalar
|
||||
comparison you write yourself. Forgetting the comparison, or comparing against a challenge
|
||||
the *prover* supplied without deriving it from a transcript you control, defeats the proof.
|
||||
The prover's challenge in the test is `curve.Scalar.Hash(mpc.GetChallengeBytes())`; a verifier
|
||||
that wants soundness against a chosen-challenge prover should recompute the challenge from the
|
||||
proof transcript rather than trusting a transmitted scalar.
|
||||
:::
|
||||
|
||||
:::warning[A revoked holder gets a cryptic error, not a clean rejection]
|
||||
`BatchUpdate` inverts `product(yD_i - y)` over the deletion list. If the holder's own element
|
||||
`y` appears in `deletions`, that product is zero and the call fails with
|
||||
`"no inverse exists"`. That is the revoked-credential path, and it surfaces as an internal
|
||||
arithmetic error rather than a "you were revoked" signal. Handle it explicitly.
|
||||
:::
|
||||
|
||||
On ordering: the `additions` and `deletions` slices a holder passes to `BatchUpdate` enter
|
||||
only as products, so their internal order is irrelevant — but they must be the same *sets*
|
||||
the manager passed to `Update`. The `[]Coefficient` slice is different: it is evaluated as a
|
||||
polynomial by index, so it must be passed exactly as `Update` returned it, unreordered and
|
||||
untruncated.
|
||||
@@ -0,0 +1,367 @@
|
||||
---
|
||||
title: Bulletproofs
|
||||
description: Logarithmic-size inner-product argument and the single and batched range proofs built on top of it — plus the exported-API gap that currently makes the range layer callable only from inside the package.
|
||||
sidebar:
|
||||
label: Bulletproofs
|
||||
order: 4
|
||||
icon: ruler
|
||||
---
|
||||
|
||||
`bulletproof` implements the protocol of [eprint 2017/1066](https://eprint.iacr.org/2017/1066.pdf)
|
||||
in two layers, and the distinction between them is the most important thing on this page.
|
||||
|
||||
The **inner-product argument** (IPP) is the engine. It proves knowledge of two scalar vectors
|
||||
whose dot product is a claimed value, in proof size logarithmic in the vector length: a
|
||||
length-256 pair of vectors yields 8 pairs of `L`/`R` points, not 256.
|
||||
|
||||
The **range proof** is the application. It encodes a secret value as its bit vector, expresses
|
||||
"every bit is 0 or 1, and the bits sum to the committed value" as a single inner-product
|
||||
relation, and then delegates to the IPP. That is why one prover constructor takes *two*
|
||||
domain separators — the range layer and the IPP layer each need their own generator vectors.
|
||||
|
||||
:::danger[The range-proof API cannot be called from another package today]
|
||||
`RangeProver.Prove`, `BatchProve`, `RangeVerifier.Verify`, and `VerifyBatched` all take a
|
||||
`RangeProofGenerators` value. That struct's three fields — `g`, `h`, `u` — are unexported,
|
||||
and the package exports no constructor, setter, or default for it. From outside
|
||||
`package bulletproof` the compiler rejects any attempt to populate it:
|
||||
|
||||
```text
|
||||
cannot refer to unexported field g in struct literal of type bulletproof.RangeProofGenerators
|
||||
cannot refer to unexported field h in struct literal of type bulletproof.RangeProofGenerators
|
||||
cannot refer to unexported field u in struct literal of type bulletproof.RangeProofGenerators
|
||||
```
|
||||
|
||||
A zero-valued `RangeProofGenerators{}` does compile, but its points are `nil`, so `Prove`
|
||||
panics on the first `proofGenerators.h.Mul(alpha)`. The verifier side has a second gap: it
|
||||
needs the Pedersen commitment `capV`, and the helper that builds it (`getcapV`) is
|
||||
unexported too.
|
||||
|
||||
The IPP layer has the mirror-image problem: `InnerProductVerifier.Verify` needs `capP`, and
|
||||
the only thing that computes it is `InnerProductProver.getP`, whose own doc comment says
|
||||
"This method should only be used for testing" — and which is unexported regardless.
|
||||
|
||||
The mathematics in this package is complete and its tests pass. The Go surface is not
|
||||
finished. Until `RangeProofGenerators` gains an exported constructor and the commitment
|
||||
helpers are exported, treat `bulletproof` as an in-repo building block rather than a public
|
||||
API, and read the examples below as descriptions of the in-package tests.
|
||||
:::
|
||||
|
||||
## When to use it
|
||||
|
||||
Range proofs are the right tool when a committed number must be shown to be well-formed
|
||||
without being revealed — confidential amounts that must be non-negative and non-overflowing,
|
||||
bounded bids, reserve proofs. The batched variant is the right tool when several such values
|
||||
are proved at once by the same party, because it amortises them into a single proof.
|
||||
|
||||
They are the wrong tool for set membership (use the [accumulator](/zero-knowledge/accumulator)),
|
||||
for proving knowledge of a discrete log (use [Schnorr](/zero-knowledge/schnorr), which is
|
||||
orders of magnitude smaller and simpler), or for arbitrary statements — there is no
|
||||
general-purpose circuit layer here.
|
||||
|
||||
## Layer 1: the inner-product argument
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
"NewInnerProductProver": {
|
||||
type: "func(maxVectorLength int, domain []byte, curve curves.Curve) (*InnerProductProver, error)",
|
||||
required: true,
|
||||
description: "Derives 2·maxVectorLength generator points by hashing Shake256(domain) to the curve, split into the G and H vectors. Note the curve is passed by value, not pointer.",
|
||||
},
|
||||
"InnerProductProver.Prove": {
|
||||
type: "func(a, b []curves.Scalar, u curves.Point, transcript *merlin.Transcript) (*InnerProductProof, error)",
|
||||
required: true,
|
||||
description: "Proves knowledge of a and b with the inner product blinded into P by u. len(a) must equal len(b), be a power of two, and be at most maxVectorLength.",
|
||||
},
|
||||
"NewInnerProductVerifier": {
|
||||
type: "func(maxVectorLength int, domain []byte, curve curves.Curve) (*InnerProductVerifier, error)",
|
||||
required: true,
|
||||
description: "Must be constructed with the same maxVectorLength, domain, and curve as the prover, or the generators differ and verification fails.",
|
||||
},
|
||||
"InnerProductVerifier.Verify": {
|
||||
type: "func(capP, u curves.Point, proof *InnerProductProof, transcript *merlin.Transcript) (bool, error)",
|
||||
required: true,
|
||||
description: "capP is the commitment ⟨G,a⟩ + ⟨H,b⟩ + ⟨a,b⟩·u. Returns (false, nil) — not an error — when the proof simply does not check out.",
|
||||
},
|
||||
"InnerProductVerifier.VerifyFromRangeProof": {
|
||||
type: "func(proofG, proofH []curves.Point, capPhmuinv, u curves.Point, tHat curves.Scalar, proof *InnerProductProof, transcript *merlin.Transcript) (bool, error)",
|
||||
description: "The entry point RangeVerifier.Verify uses. It takes explicit generator slices and the range proof's P·h^-mu instead of a plain capP.",
|
||||
},
|
||||
"NewInnerProductProof": {
|
||||
type: "func(curve *curves.Curve) *InnerProductProof",
|
||||
description: "An empty proof to unmarshal into. Pair it with UnmarshalBinary; do not use it for anything else.",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
`MarshalBinary() []byte` on both `InnerProductProof` and `RangeProof` returns **no error** —
|
||||
an unusual signature that does not satisfy `encoding.BinaryMarshaler`. `UnmarshalBinary` does
|
||||
return an error, and must be called on a proof from the matching `New…Proof(curve)`
|
||||
constructor so that the curve is set.
|
||||
|
||||
```go ipp.go
|
||||
// From bulletproof/ipp_verifier_test.go (TestIPPVerifyHappyPath).
|
||||
curve := curves.ED25519()
|
||||
vecLength := 256
|
||||
|
||||
prover, err := bulletproof.NewInnerProductProver(vecLength, []byte("test"), *curve)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
a := randScalarVec(vecLength, *curve) // in-package test helper
|
||||
b := randScalarVec(vecLength, *curve)
|
||||
u := curve.Point.Random(crand.Reader)
|
||||
|
||||
transcriptProver := merlin.NewTranscript("test")
|
||||
proof, err := prover.Prove(a, b, u, transcriptProver)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// len(proof.capLs) == log2(256) == 8
|
||||
|
||||
verifier, err := bulletproof.NewInnerProductVerifier(vecLength, []byte("test"), *curve)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
capP, err := prover.getP(a, b, u) // unexported: see the danger callout
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
transcriptVerifier := merlin.NewTranscript("test")
|
||||
verified, err := verifier.Verify(capP, u, proof, transcriptVerifier)
|
||||
// verified == true
|
||||
```
|
||||
|
||||
The Fiat-Shamir transcript is [merlin](https://github.com/gtank/merlin). Both sides construct
|
||||
it with the *same label* — `merlin.NewTranscript("test")` in the tests — and each side must
|
||||
start from a fresh transcript in the same state. A transcript is consumed by proving or
|
||||
verifying; you cannot reuse one.
|
||||
|
||||
## Layer 2: range proofs
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
"NewRangeProver": {
|
||||
type: "func(maxVectorLength int, rangeDomain, ippDomain []byte, curve curves.Curve) (*RangeProver, error)",
|
||||
required: true,
|
||||
description: "Two domains: rangeDomain seeds the bit-vector generators, ippDomain seeds the inner-product generators. They must differ from each other, and must match the verifier's exactly.",
|
||||
},
|
||||
"RangeProver.Prove": {
|
||||
type: "func(v, gamma curves.Scalar, n int, proofGenerators RangeProofGenerators, transcript *merlin.Transcript) (*RangeProof, error)",
|
||||
required: true,
|
||||
description: "Proves the value v committed as gamma·h + v·g lies in [0, 2^n). gamma is the Pedersen blinding factor and must be kept secret.",
|
||||
},
|
||||
"RangeProver.BatchProve": {
|
||||
type: "func(v, gamma []curves.Scalar, n int, proofGenerators RangeProofGenerators, transcript *merlin.Transcript) (*RangeProof, error)",
|
||||
required: true,
|
||||
description: "Aggregated proof for len(v) values, each in [0, 2^n). Requires n·len(v) ≤ maxVectorLength. Output is one RangeProof, not a slice.",
|
||||
},
|
||||
"NewRangeVerifier": {
|
||||
type: "func(maxVectorLength int, rangeDomain, ippDomain []byte, curve curves.Curve) (*RangeVerifier, error)",
|
||||
required: true,
|
||||
description: "Same four arguments as the prover, or verification fails.",
|
||||
},
|
||||
"RangeVerifier.Verify": {
|
||||
type: "func(proof *RangeProof, capV curves.Point, proofGenerators RangeProofGenerators, n int, transcript *merlin.Transcript) (bool, error)",
|
||||
required: true,
|
||||
description: "capV is the single Pedersen commitment gamma·h + v·g. n must equal the prover's n.",
|
||||
},
|
||||
"RangeVerifier.VerifyBatched": {
|
||||
type: "func(proof *RangeProof, capV []curves.Point, proofGenerators RangeProofGenerators, n int, transcript *merlin.Transcript) (bool, error)",
|
||||
description: "Takes one commitment per proved value, in the same order the prover passed v.",
|
||||
},
|
||||
"NewRangeProof": {
|
||||
type: "func(curve *curves.Curve) *RangeProof",
|
||||
description: "An empty proof to unmarshal into.",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### What the parameters actually mean
|
||||
|
||||
**`n` is a bit width, and it defines the range.** `Prove` decodes `v` into an `n`-element bit
|
||||
vector via `getaL`, so the provable range is the set of values representable in `n` bits:
|
||||
`[0, 2^n)`. `n = 256` on ED25519 is the whole scalar field; `n = 64` is a u64-shaped amount.
|
||||
`n` must be a power of two, because the inner-product recursion halves the vectors at every
|
||||
step. The range prover does not check this up front — unlike `InnerProductProver.Prove`,
|
||||
which has an explicit `isPowerOfTwo` gate — so a non-power-of-two `n` surfaces late as
|
||||
`"length of scalars must be even"` from inside the recursion.
|
||||
|
||||
**`maxVectorLength` is a generator budget, not the range.** The constructor precomputes
|
||||
`2 · maxVectorLength` curve points by hashing the domain. `Prove` requires
|
||||
`n <= maxVectorLength` and trims the generator vectors to `n`. `BatchProve` requires
|
||||
`n · len(v) <= maxVectorLength` — proving four 256-bit values needs
|
||||
`NewRangeProver(256*4, …)`, exactly as `range_batch_prover_test.go` does.
|
||||
|
||||
**The two domains seed two independent generator sets.** `rangeDomain` produces the `G`/`H`
|
||||
vectors that commit to the bit vectors; `ippDomain` produces the generators for the nested
|
||||
inner-product argument. They must be distinct strings — reusing one string for both would
|
||||
make the two generator sets identical, which is not a configuration the protocol's security
|
||||
argument covers. The tests use `[]byte("rangeDomain")` and `[]byte("ippDomain")`.
|
||||
|
||||
**`proofGenerators` holds `g`, `h`, `u`, and both sides must use the same three points.**
|
||||
`g` and `h` are the Pedersen bases for the value commitment; `u` blinds the inner product.
|
||||
Because the commitment `capV = gamma·h + v·g` is computed from `g` and `h`, a verifier with
|
||||
different points is verifying a commitment to a different value and gets `false`.
|
||||
|
||||
### Single-value range proof
|
||||
|
||||
Grounded in `bulletproof/range_prover_test.go` and `range_verifier_test.go`. The unexported
|
||||
identifiers are marked; they are why this cannot be lifted verbatim into your own package.
|
||||
|
||||
```go range.go
|
||||
curve := curves.ED25519()
|
||||
n := 256
|
||||
|
||||
prover, err := bulletproof.NewRangeProver(n, []byte("rangeDomain"), []byte("ippDomain"), *curve)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
v := curve.Scalar.Random(crand.Reader) // the secret value
|
||||
gamma := curve.Scalar.Random(crand.Reader) // the secret blinding factor
|
||||
|
||||
g := curve.Point.Random(crand.Reader)
|
||||
h := curve.Point.Random(crand.Reader)
|
||||
u := curve.Point.Random(crand.Reader)
|
||||
proofGenerators := RangeProofGenerators{g: g, h: h, u: u} // unexported fields
|
||||
|
||||
transcript := merlin.NewTranscript("test")
|
||||
proof, err := prover.Prove(v, gamma, n, proofGenerators, transcript)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Verifier: same n, same domains, same curve, same g/h/u, fresh transcript
|
||||
// with the same label.
|
||||
verifier, err := bulletproof.NewRangeVerifier(n, []byte("rangeDomain"), []byte("ippDomain"), *curve)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
transcriptVerifier := merlin.NewTranscript("test")
|
||||
capV := getcapV(v, gamma, g, h) // unexported: h.Mul(gamma).Add(g.Mul(v))
|
||||
verified, err := verifier.Verify(proof, capV, proofGenerators, n, transcriptVerifier)
|
||||
// verified == true
|
||||
```
|
||||
|
||||
In production the verifier receives `capV` over the wire; it never learns `v` or `gamma`.
|
||||
The commitment is a plain Pedersen commitment, `h·gamma + g·v`, so you can compute it
|
||||
yourself with `core/curves` point arithmetic without touching this package.
|
||||
|
||||
### Batched range proof
|
||||
|
||||
`BatchProve` proves `m` values in one proof. From `range_batch_prover_test.go`, four
|
||||
256-bit values:
|
||||
|
||||
```go batch.go
|
||||
curve := curves.ED25519()
|
||||
n := 256
|
||||
|
||||
// maxVectorLength must cover n * m = 1024.
|
||||
prover, err := bulletproof.NewRangeProver(n*4, []byte("rangeDomain"), []byte("ippDomain"), *curve)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
v := []curves.Scalar{
|
||||
curve.Scalar.Random(crand.Reader),
|
||||
curve.Scalar.Random(crand.Reader),
|
||||
curve.Scalar.Random(crand.Reader),
|
||||
curve.Scalar.Random(crand.Reader),
|
||||
}
|
||||
gamma := []curves.Scalar{
|
||||
curve.Scalar.Random(crand.Reader),
|
||||
curve.Scalar.Random(crand.Reader),
|
||||
curve.Scalar.Random(crand.Reader),
|
||||
curve.Scalar.Random(crand.Reader),
|
||||
}
|
||||
|
||||
transcript := merlin.NewTranscript("test")
|
||||
proof, err := prover.BatchProve(v, gamma, n, proofGenerators, transcript)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// One proof, log2(1024) == 10 L/R pairs, covering all four values.
|
||||
|
||||
verifier, _ := bulletproof.NewRangeVerifier(n*4, []byte("rangeDomain"), []byte("ippDomain"), *curve)
|
||||
capV := getcapVBatched(v, gamma, g, h) // unexported; one commitment per value
|
||||
verified, err := verifier.VerifyBatched(proof, capV, proofGenerators, n, merlin.NewTranscript("test"))
|
||||
// verified == true
|
||||
```
|
||||
|
||||
Note that the batch call still takes the *per-value* bit width `n`, while the prover was
|
||||
constructed with `n*4`. Mixing those two up is the easiest way to get a confusing failure.
|
||||
`VerifyBatched` returns an error (rather than `false`) if a commitment in `capV` is tampered
|
||||
with in a way that breaks the point arithmetic — `range_batch_verifier_test.go` exercises
|
||||
that path — but a merely *wrong* proof still comes back as `(false, nil)`.
|
||||
|
||||
## Caveats
|
||||
|
||||
:::warning[Parameter mismatches fail silently]
|
||||
`Verify` and `VerifyBatched` return `(false, nil)`. There is no descriptive error and no
|
||||
distinction between "the prover was dishonest" and "we disagree about the parameters". All of
|
||||
the following produce an indistinguishable `false`:
|
||||
|
||||
- different `rangeDomain` or `ippDomain` between prover and verifier
|
||||
- different `maxVectorLength` (different generator counts, hence different trimmed vectors)
|
||||
- different `n`
|
||||
- different `g`, `h`, or `u`
|
||||
- a different merlin transcript label, or a transcript that was already consumed
|
||||
- a `capV` computed from a different `g`/`h` pair
|
||||
|
||||
Pin all of these in one shared configuration value. Do not let two sides derive them
|
||||
independently.
|
||||
:::
|
||||
|
||||
:::danger[n larger than the scalar's byte width panics]
|
||||
`getaL` reads bit `i` of the value as `vBytes[i>>3] >> (i & 0x07) & 0x01`, where `vBytes` is
|
||||
`v.Bytes()`. It performs no bounds check against `len(vBytes)`. With a 32-byte scalar
|
||||
encoding, any `n > 256` indexes past the end of the slice and panics with an index-out-of-range
|
||||
runtime error rather than returning an error. `NewRangeProver` will happily accept
|
||||
`maxVectorLength` above 256, so nothing stops you from reaching this. Keep `n <= 256` on the
|
||||
curves in this module.
|
||||
:::
|
||||
|
||||
:::warning[Bit extraction assumes little-endian scalar bytes]
|
||||
`getaL` treats byte 0 of `Scalar.Bytes()` as holding the least significant bits. That is
|
||||
correct for Ed25519 scalars, which is the only curve any bulletproof test exercises. If a
|
||||
curve's `Scalar.Bytes()` is big-endian, the bit vector is reversed and the proof commits to a
|
||||
different number than `capV` does — verification fails, or worse, succeeds for the wrong
|
||||
range. Do not assume this package works on a curve until you have checked that curve's scalar
|
||||
byte order in [`core/curves`](/foundations/curves).
|
||||
:::
|
||||
|
||||
:::note[Off-by-one in the input range check]
|
||||
`Prove` rejects `v < 0` and `v > 2^n`, so a value of exactly `2^n` passes the input check —
|
||||
but `2^n` is not representable in `n` bits, so `getaL` encodes it as `0` and the resulting
|
||||
proof does not match `capV`. The unexported `checkRange` helper (used by `BatchProve`) has
|
||||
the same `> 2^n` comparison despite a doc comment claiming it enforces `[0, 2^n - 1]`.
|
||||
Treat the usable range as `[0, 2^n)` and validate the boundary yourself. `checkRange` also
|
||||
does not reject negative values, unlike the inline check in `Prove`.
|
||||
:::
|
||||
|
||||
:::note[Stale generator documentation]
|
||||
`getGeneratorPoints`' comment claims it returns `2·lenVector + 1` points "split between a
|
||||
single u generator and G and H lists". It returns `2·lenVector` points split evenly into `G`
|
||||
and `H`; there is no `u` generator in the output. `u` is always caller-supplied. Similarly
|
||||
`RangeProver.Prove`'s comment says the range is `[0, 2^n]`; the encoding makes it `[0, 2^n)`.
|
||||
:::
|
||||
|
||||
:::info[isPowerOfTwo accepts zero]
|
||||
The helper is `i&(i-1) == 0`, which is true for `i == 0`. A zero-length vector therefore
|
||||
passes the power-of-two gate in `InnerProductProver.Prove` and fails later, or recurses
|
||||
oddly. Validate non-empty inputs before calling.
|
||||
:::
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Curves" href="/foundations/curves" icon="git-branch">
|
||||
The `Curve`, `Point`, and `Scalar` types every signature here is generic over, plus the
|
||||
scalar byte-order details the bit extraction depends on.
|
||||
</Card>
|
||||
<Card title="Schnorr proofs" href="/zero-knowledge/schnorr" icon="badge-check">
|
||||
Far simpler and fully usable from outside its package. Prefer it whenever the statement is
|
||||
just discrete-log knowledge.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: Zero-Knowledge
|
||||
description: Four unrelated tools that all let a verifier learn a fact without learning the secret behind it — discrete-log proofs, set-membership accumulators, range proofs, and additively homomorphic encryption.
|
||||
sidebar:
|
||||
label: Overview
|
||||
order: 1
|
||||
icon: eye-off
|
||||
---
|
||||
|
||||
This section covers four packages that have almost nothing in common structurally, but which
|
||||
solve the same shape of problem: a party holds a secret and needs a counterparty to accept a
|
||||
statement about it without seeing it.
|
||||
|
||||
They are not interchangeable, and picking the wrong one is expensive. `zkp/schnorr` proves
|
||||
you know a discrete log and nothing else. `accumulator` commits to a *set* and proves
|
||||
membership. `bulletproof` proves a committed number lies in a range. `paillier` is not a
|
||||
proof system at all — it is an encryption scheme that lets a third party compute on
|
||||
ciphertexts, and it ships with one proof (`PsfProof`) about the shape of its own public key,
|
||||
which is why it lives on this page rather than under symmetric or signature primitives.
|
||||
|
||||
## Which one do I need
|
||||
|
||||
| Goal | Package | What the verifier learns | What stays hidden |
|
||||
| --- | --- | --- | --- |
|
||||
| "I know the private key behind this public point" | `zkp/schnorr` | that some `x` with `Statement = x·B` exists and the prover knows it | `x` itself |
|
||||
| "My credential is in the issuer's current set" | `accumulator` | that the holder possesses a valid witness for *some* accumulated element | which element, and the rest of the set |
|
||||
| "This committed amount is between 0 and 2^n" | `bulletproof` | that the value behind a Pedersen commitment is in range | the value and the blinding factor |
|
||||
| "I know two vectors whose dot product is c" | `bulletproof` (inner-product layer) | the claimed inner product | both vectors |
|
||||
| "Compute on my data without seeing it" | `paillier` | nothing about the plaintexts | every plaintext |
|
||||
| "Your Paillier modulus is not malformed" | `paillier` (`PsfProof`) | that `N` is square-free | the factorization of `N` |
|
||||
|
||||
## Maturity is uneven
|
||||
|
||||
These four packages are at very different levels of usability, and this matters more than the
|
||||
cryptography when you are choosing between them.
|
||||
|
||||
`zkp/schnorr` is the most mature: small, exercised across eight curves, and load-bearing
|
||||
inside this repo's own threshold ECDSA and oblivious-transfer stacks. `accumulator` is
|
||||
complete and well tested, with one dead branch in its API. `paillier` is complete for
|
||||
encryption but its proof layer has a missing length check. `bulletproof` implements the full
|
||||
protocol correctly but does not export enough of its own types to be callable from another
|
||||
package.
|
||||
|
||||
:::warning[Read the caveats sections]
|
||||
Every page below ends with a caveats section that names concrete defects found in the source
|
||||
— unconstructible parameter structs, panics on malformed input, off-by-one range checks, and
|
||||
endianness assumptions. None of these are theoretical. Check them before you build on a
|
||||
package. Nothing in this module carries a security audit.
|
||||
:::
|
||||
|
||||
## Shared foundation
|
||||
|
||||
Every package here except `paillier` is generic over the curve abstraction in `core/curves`.
|
||||
`accumulator` additionally requires a *pairing* curve (`*curves.PairingCurve`, in practice
|
||||
BLS12-381), because its witness check is a pairing equation. `paillier` is the odd one out:
|
||||
it works over `math/big` integers modulo a composite, and its PSF proof takes a
|
||||
`crypto/elliptic` curve rather than a `core/curves` one.
|
||||
|
||||
See [Curves](/foundations/curves) for the `Curve` / `Point` / `Scalar` types that appear in
|
||||
nearly every signature on these pages, and [Arithmetic](/foundations/arithmetic) for the
|
||||
`core` modular-arithmetic helpers that `paillier` is built on.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Schnorr proofs" href="/zero-knowledge/schnorr" icon="badge-check">
|
||||
Non-interactive proof of knowledge of a discrete log, with an optional commit-then-reveal
|
||||
variant. The building block used by this module's own MPC protocols.
|
||||
</Card>
|
||||
<Card title="Accumulator" href="/zero-knowledge/accumulator" icon="list-checks">
|
||||
Constant-size commitment to a set, constant-size membership witnesses, and a
|
||||
zero-knowledge membership proof. Built for revocation lists.
|
||||
</Card>
|
||||
<Card title="Bulletproofs" href="/zero-knowledge/bulletproof" icon="ruler">
|
||||
Logarithmic-size inner-product argument, and the range proof built on top of it.
|
||||
Single and batched.
|
||||
</Card>
|
||||
<Card title="Paillier" href="/zero-knowledge/paillier" icon="calculator">
|
||||
Additively homomorphic encryption over a composite modulus, plus the square-free proof
|
||||
that keeps a malicious key from breaking protocols above it.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineMeta } from "blume";
|
||||
|
||||
export default defineMeta({
|
||||
title: "Zero-Knowledge",
|
||||
icon: "eye-off",
|
||||
order: 6,
|
||||
pages: ["index", "schnorr", "accumulator", "bulletproof", "paillier"],
|
||||
});
|
||||
@@ -0,0 +1,431 @@
|
||||
---
|
||||
title: Paillier
|
||||
description: Additively homomorphic public-key encryption over a composite modulus, plus the PSF proof that a Paillier modulus is square-free — the check that keeps a malformed key from breaking protocols above it.
|
||||
sidebar:
|
||||
label: Paillier
|
||||
order: 5
|
||||
icon: calculator
|
||||
---
|
||||
|
||||
`paillier` implements Paillier's 1999 cryptosystem. Its package doc cites the original paper —
|
||||
*Public-Key Cryptosystems Based on Composite Degree Residuosity Class* — and states that all
|
||||
routines follow the pseudocode of §2.5, Fig. 1. Unlike everything else in this section it is
|
||||
not built on `core/curves`: plaintexts, ciphertexts, and keys are all `*math/big.Int` values
|
||||
modulo a composite `N = PQ`.
|
||||
|
||||
It is here rather than under encryption because of what it is *used for*. Paillier's value in
|
||||
this module is that a party can compute on data it cannot read, which is the multiplication
|
||||
primitive underpinning several MPC protocols — and the accompanying `PsfProof` is a
|
||||
zero-knowledge proof about the public key itself.
|
||||
|
||||
## Additively homomorphic, and only additively
|
||||
|
||||
Two operations work on ciphertexts:
|
||||
|
||||
| Operation | Call | Plaintext effect | Implementation |
|
||||
| --- | --- | --- | --- |
|
||||
| Ciphertext + ciphertext | `pk.Add(c, d)` | `Dec(result) = a + b` | `c · d mod N²` |
|
||||
| Known scalar × ciphertext | `pk.Mul(a, c)` | `Dec(result) = a · b` | `c^a mod N²` |
|
||||
|
||||
That is the whole homomorphic surface, and the boundary is hard:
|
||||
|
||||
:::danger[You cannot multiply two ciphertexts' plaintexts]
|
||||
There is no operation, and no combination of the available operations, that takes `Enc(a)`
|
||||
and `Enc(b)` and produces `Enc(a·b)`. Paillier is additively homomorphic — a group
|
||||
homomorphism from addition mod `N` to multiplication mod `N²`, nothing more. `pk.Mul` takes a
|
||||
*plaintext* `*big.Int` as its first argument, not a second ciphertext; its name refers to
|
||||
multiplying the plaintext by a known constant.
|
||||
|
||||
If you need ciphertext-ciphertext multiplication you need a different primitive — a
|
||||
fully-homomorphic scheme, or an interactive multiplication protocol such as the
|
||||
oblivious-transfer-based multiplier in this repo. See
|
||||
[Oblivious transfer](/threshold/oblivious-transfer).
|
||||
:::
|
||||
|
||||
Note also that both operands must be in range. `Add` requires `c, d ∈ Z_N²`; `Mul` requires
|
||||
`a ∈ Z_N` and `c ∈ Z_N²`. In particular `a` must be non-negative and less than `N` — you
|
||||
cannot pass a negative scalar to subtract. To subtract, add the modular negation
|
||||
`new(big.Int).Sub(pk.N, x)`.
|
||||
|
||||
## Keys
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
"NewKeys": {
|
||||
type: "func() (*PublicKey, *SecretKey, error)",
|
||||
required: true,
|
||||
description: "Generates a fresh keypair with two PaillierPrimeBits-sized safe primes. Slow: see the note below.",
|
||||
},
|
||||
"NewSecretKey": {
|
||||
type: "func(p, q *big.Int) (*SecretKey, error)",
|
||||
required: true,
|
||||
description: "Derives lambda, totient, and U from primes you supply. Performs no primality or safety check on p and q.",
|
||||
},
|
||||
"NewPubkey": {
|
||||
type: "func(n *big.Int) (*PublicKey, error)",
|
||||
required: true,
|
||||
description: "Wraps a modulus received from a counterparty and caches N².",
|
||||
},
|
||||
"PaillierPrimeBits": {
|
||||
type: "int constant = 1024",
|
||||
required: true,
|
||||
description: "Bit size of each safe prime, so N is 2048 bits. Not configurable through the exported API.",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
`PublicKey` exposes `N` (the modulus) and `N2` (`N²`, cached to avoid recomputation).
|
||||
`SecretKey` embeds `PublicKey` and adds:
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
Lambda: {
|
||||
type: "*big.Int",
|
||||
required: true,
|
||||
description: "lcm(P-1, Q-1), the decryption exponent.",
|
||||
},
|
||||
Totient: {
|
||||
type: "*big.Int",
|
||||
required: true,
|
||||
description: "Euler's totient (P-1)(Q-1). Used by the PSF proof, not by decryption.",
|
||||
},
|
||||
U: {
|
||||
type: "*big.Int",
|
||||
required: true,
|
||||
description: "L((N+1)^lambda mod N²)^-1 mod N, the precomputed decryption multiplier.",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
Both key types implement `MarshalJSON`/`UnmarshalJSON`. `PublicKeyJson` and `SecretKeyJson`
|
||||
are the exported-but-internal wire shapes. `P` and `Q` are **not** retained on the secret key
|
||||
and are not serialized — only the derived values are, so you cannot recover the factors from a
|
||||
marshalled `SecretKey`.
|
||||
|
||||
`Ciphertext` is a defined type over `*big.Int`, so it serializes as an integer with no
|
||||
wrapper.
|
||||
|
||||
:::note[NewKeys is slow, by construction]
|
||||
`NewKeys` calls `core.GenerateSafePrime` twice at 1024 bits. A safe prime `p` requires
|
||||
`(p-1)/2` to also be prime, which makes them rare — safe-prime search is orders of magnitude
|
||||
slower than ordinary prime generation, and the cost is highly variable run to run. Generate
|
||||
keys once at setup and persist them; never generate inside a request path. `NewKeys` is the
|
||||
only function in the package that performs a search — everything else is a bounded number of
|
||||
modular operations.
|
||||
:::
|
||||
|
||||
:::warning[NewSecretKey trusts its inputs completely]
|
||||
`NewSecretKey(p, q)` computes `lcm(p-1, q-1)`, `(p-1)(q-1)`, `N`, `N²`, and `U` and returns.
|
||||
It does not test whether `p` and `q` are prime, whether they are safe primes, whether they are
|
||||
distinct, or whether they are large enough. Passing composites yields a key whose `Decrypt`
|
||||
returns garbage without error. It exists so that callers with pre-generated primes (and the
|
||||
package's own tests) can skip the expensive search — not as a general constructor.
|
||||
:::
|
||||
|
||||
## Encryption
|
||||
|
||||
`Encrypt` returns **three** values, and the middle one is easy to discard by accident:
|
||||
|
||||
```go
|
||||
func (pk *PublicKey) Encrypt(msg *big.Int) (Ciphertext, *big.Int, error)
|
||||
```
|
||||
|
||||
The second return is `r`, the randomness the ciphertext was built with. Internally
|
||||
`c = (N+1)^msg · r^N mod N²`, where `r` is drawn uniformly from `Z_N` and rejected if zero.
|
||||
|
||||
You need to keep `r` whenever a later step must prove something about *this* ciphertext.
|
||||
`r` and `msg` together are the witness for essentially every zero-knowledge statement about a
|
||||
Paillier ciphertext ("this encrypts a value in range", "these two ciphertexts encrypt the same
|
||||
value", "this encrypts the plaintext behind that commitment"). Without `r` you cannot produce
|
||||
such a proof and cannot recompute the ciphertext deterministically. If you are only encrypting
|
||||
and never proving, discard it with `_`.
|
||||
|
||||
```go paillier.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/sonr-io/crypto/paillier"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// One-time, slow: two 1024-bit safe primes.
|
||||
pk, sk, err := paillier.NewKeys()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
a := big.NewInt(1234)
|
||||
b := big.NewInt(5678)
|
||||
|
||||
// The second return is the encryption randomness r. Keep it if you will
|
||||
// later need to prove a statement about this ciphertext.
|
||||
ca, ra, err := pk.Encrypt(a)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_ = ra
|
||||
|
||||
cb, _, err := pk.Encrypt(b)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Add plaintexts by multiplying ciphertexts.
|
||||
csum, err := pk.Add(ca, cb)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
sum, err := sk.Decrypt(csum)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(sum) // 6912
|
||||
|
||||
// Scale a plaintext by a known constant.
|
||||
cscaled, err := pk.Mul(big.NewInt(3), ca)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
scaled, err := sk.Decrypt(cscaled)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(scaled) // 3702
|
||||
|
||||
// A counterparty that only received N can encrypt but not decrypt.
|
||||
remote, err := paillier.NewPubkey(pk.N)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_, _, _ = remote.Encrypt(big.NewInt(1))
|
||||
}
|
||||
```
|
||||
|
||||
Plaintexts must satisfy `msg ∈ Z_N`. Arithmetic wraps modulo `N`, so a sum of two large
|
||||
plaintexts that exceeds `N` decrypts to the reduced value, not the integer sum. If you are
|
||||
encoding signed or fixed-point quantities, choose a representation with headroom and check it
|
||||
yourself — the package does not.
|
||||
|
||||
## The PSF proof: proving a modulus is square-free
|
||||
|
||||
A protocol that accepts a Paillier public key from an untrusted party accepts an arbitrary
|
||||
integer `N`. If `N` is malformed — not square-free, or sharing a factor with the ambient group
|
||||
order — the homomorphic structure the protocol relies on breaks down, and a malicious key
|
||||
holder can extract information or force a decryption to a value of its choosing. The PSF
|
||||
(Paillier square-free) proof forces `N` to be well-formed before anything is encrypted under it.
|
||||
|
||||
The implementation cites its spec as `[spec] §10.2` and `fig. 15` (`ProvePSF`, `VerifyPSF`),
|
||||
and the source carries explicit notes where it deviates from that pseudocode to fix errors in
|
||||
it — the modulus for the exponentiation in step 5, and the inclusion of `N` in the challenge
|
||||
commitment.
|
||||
|
||||
### What it actually asserts
|
||||
|
||||
The prover, who knows the factorization, computes `M = N⁻¹ mod φ(N)` and returns
|
||||
`y_i = x_i^M mod N` for 13 deterministically derived challenges `x_i`. The verifier checks
|
||||
`y_i^N ≡ x_i mod N` for every `i`.
|
||||
|
||||
That check passes for all `i` only if raising to the `N`-th power is a bijection on `Z_N`,
|
||||
which holds exactly when `gcd(N, φ(N)) = 1` — that is, when `N` is **square-free**. The
|
||||
verifier additionally rejects `N` if the curve subgroup order `q` divides `N`.
|
||||
|
||||
Be precise about the limits of this:
|
||||
|
||||
- It proves `N` is square-free.
|
||||
- It does **not** prove `N` is a product of exactly two primes.
|
||||
- It does **not** prove the factors are safe primes, or of equal size, or large.
|
||||
- It does **not** prove the prover knows the factorization beyond what square-freeness needs.
|
||||
|
||||
If your protocol needs a biprime or safe-prime guarantee, PSF alone is insufficient.
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
"PsfProofParams.Prove": {
|
||||
type: "func() (PsfProof, error)",
|
||||
required: true,
|
||||
description: "Returns 13 big.Ints. Errors with ErrNilArguments if Curve, SecretKey, or Y is nil, or if Pi is zero.",
|
||||
},
|
||||
"PsfProof.Verify": {
|
||||
type: "func(psf *PsfVerifyParams) error",
|
||||
required: true,
|
||||
description: "Returns nil on success. Same nil/zero argument validation as Prove.",
|
||||
},
|
||||
"PsfProofLength": {
|
||||
type: "int constant = 13",
|
||||
required: true,
|
||||
description: "The number of challenges, and therefore the exact length of a valid PsfProof.",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
`PsfProof` is `[]*big.Int`, so `encoding/json` round-trips it directly with no custom codec —
|
||||
`psf_test.go` marshals and unmarshals it that way.
|
||||
|
||||
### Parameters
|
||||
|
||||
The prover's and verifier's parameter structs are deliberately near-identical: the only
|
||||
difference is that the prover holds the `*SecretKey` while the verifier holds only the
|
||||
`*PublicKey`. Every other field is a public value both sides must agree on, because all three
|
||||
are hashed into the challenge derivation.
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
Curve: {
|
||||
type: "elliptic.Curve",
|
||||
required: true,
|
||||
description: "A crypto/elliptic curve — not a core/curves one. Its Params() supply the generator and subgroup order for challenge derivation. Tests use btcec.S256() and elliptic.P256().",
|
||||
},
|
||||
SecretKey: {
|
||||
type: "*SecretKey",
|
||||
required: true,
|
||||
description: "Prover only (PsfProofParams). Supplies N and Totient for computing M.",
|
||||
},
|
||||
PublicKey: {
|
||||
type: "*PublicKey",
|
||||
required: true,
|
||||
description: "Verifier only (PsfVerifyParams). Supplies N.",
|
||||
},
|
||||
Pi: {
|
||||
type: "uint32",
|
||||
required: true,
|
||||
description: "Party index bound into the challenges. Must be non-zero — zero is rejected as a nil argument on both sides.",
|
||||
},
|
||||
Y: {
|
||||
type: "*curves.EcPoint",
|
||||
required: true,
|
||||
description: "A public point bound into the challenges, tying the proof to a protocol-specific value.",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### Prove and verify
|
||||
|
||||
Grounded in `paillier/psf_test.go` (`TestPsfProofWorks`).
|
||||
|
||||
```go psf.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/elliptic"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/sonr-io/crypto/core/curves"
|
||||
"github.com/sonr-io/crypto/paillier"
|
||||
)
|
||||
|
||||
// Y is an ordinary EC point. In psf_test.go it is built with
|
||||
// curves.NewScalarBaseMult over the same crypto/elliptic curve.
|
||||
func bindingPoint(k *big.Int) (*curves.EcPoint, error) {
|
||||
return curves.NewScalarBaseMult(elliptic.P256(), k)
|
||||
}
|
||||
|
||||
func provePSF(sk *paillier.SecretKey, pi uint32, y *curves.EcPoint) (paillier.PsfProof, error) {
|
||||
return (&paillier.PsfProofParams{
|
||||
Curve: elliptic.P256(),
|
||||
SecretKey: sk,
|
||||
Pi: pi, // must be non-zero
|
||||
Y: y,
|
||||
}).Prove()
|
||||
}
|
||||
|
||||
func verifyPSF(
|
||||
proof paillier.PsfProof,
|
||||
pk *paillier.PublicKey,
|
||||
pi uint32,
|
||||
y *curves.EcPoint,
|
||||
) error {
|
||||
// Do this length check yourself: Verify does not, and indexes 13 elements.
|
||||
if len(proof) != paillier.PsfProofLength {
|
||||
return fmt.Errorf(
|
||||
"malformed psf proof: want %d elements, got %d",
|
||||
paillier.PsfProofLength, len(proof),
|
||||
)
|
||||
}
|
||||
return proof.Verify(&paillier.PsfVerifyParams{
|
||||
Curve: elliptic.P256(),
|
||||
PublicKey: pk,
|
||||
Pi: pi,
|
||||
Y: y,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Wiring it into a key exchange:
|
||||
|
||||
<Steps>
|
||||
<Step title="Generate once">
|
||||
Each party runs `paillier.NewKeys()` at setup and persists the keypair.
|
||||
</Step>
|
||||
<Step title="Agree on the binding values">
|
||||
Both sides fix the `elliptic.Curve`, the non-zero party index `Pi`, and the public point
|
||||
`Y` from protocol state. A mismatch in any of the three produces different challenges and
|
||||
a failed verification.
|
||||
</Step>
|
||||
<Step title="Publish key plus proof">
|
||||
Send `pk.N` (via `MarshalJSON`) together with the 13-element `PsfProof`.
|
||||
</Step>
|
||||
<Step title="Verify before use">
|
||||
The receiver rebuilds the public key with `paillier.NewPubkey(n)`, checks the proof length,
|
||||
calls `proof.Verify(...)`, and only then encrypts anything under that key.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Caveats
|
||||
|
||||
:::danger[Verify panics on a short proof]
|
||||
`PsfProof.Verify` validates that the *challenge* array has length `PsfProofLength`, but never
|
||||
checks `len(p)` — the proof itself. It then indexes `p[j]` for `j` in `0..12`. A proof shorter
|
||||
than 13 elements — for instance one decoded from attacker-supplied JSON — panics inside
|
||||
`Verify` instead of returning an error. A three-element proof produces
|
||||
`runtime error: index out of range [3] with length 3`.
|
||||
|
||||
Check `len(proof) == paillier.PsfProofLength` yourself immediately after deserialization,
|
||||
before calling `Verify`. This is unconditional: any code path where the proof arrives from
|
||||
outside your process needs the guard.
|
||||
:::
|
||||
|
||||
:::warning[The proof does not authenticate the sender]
|
||||
`Pi` and `Y` bind the proof to a protocol position and a public point, but the PSF proof is not
|
||||
a signature over `N`. A relayed valid `(N, proof)` pair from an honest party remains valid.
|
||||
If you need to know *who* sent a modulus, authenticate the transport or sign the key material
|
||||
separately.
|
||||
:::
|
||||
|
||||
:::warning[Constant-time behaviour is partial]
|
||||
Some paths use constant-time helpers — `Add` and `Mul` accumulate their two range-check errors
|
||||
before branching, and `encrypt` uses `core.ConstantTimeEq` to reject a zero nonce. Others are
|
||||
plain `math/big` operations, and `math/big` is not constant-time. Do not treat this package as
|
||||
side-channel hardened. No timing analysis of this code exists in the repository.
|
||||
:::
|
||||
|
||||
:::note[No key-size configurability, and no key validation on import]
|
||||
`NewKeys` is hardcoded to `PaillierPrimeBits = 1024` per prime; the parameterised generator is
|
||||
unexported. `NewPubkey(n)` accepts any modulus you hand it — it caches `N²` and returns. It
|
||||
performs no size check, no square-free check, and no primality-related check. Validating an
|
||||
imported modulus is exactly what the PSF proof is for, and it is your responsibility to run it.
|
||||
:::
|
||||
|
||||
:::info[Decryption does not authenticate]
|
||||
Paillier is not an authenticated encryption scheme. `Decrypt` will happily return a plaintext
|
||||
for any `c ∈ Z_N²`, including one an adversary derived homomorphically from a ciphertext you
|
||||
sent. There is no integrity tag. If you need to know that a ciphertext is the one you expected,
|
||||
you need an additional proof or a MAC over a separate channel. For authenticated symmetric
|
||||
encryption see [AEAD](/symmetric/aead).
|
||||
:::
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Arithmetic" href="/foundations/arithmetic" icon="binary">
|
||||
The `core` modular-arithmetic layer this package is built on — `Inv`, `Exp`, `Mul`, `In`,
|
||||
`Rand`, and safe-prime generation.
|
||||
</Card>
|
||||
<Card title="Oblivious transfer" href="/threshold/oblivious-transfer" icon="shuffle">
|
||||
The interactive multiplication primitive this module actually uses where Paillier's
|
||||
additive-only homomorphism is not enough.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,275 @@
|
||||
---
|
||||
title: Schnorr proofs
|
||||
description: 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.
|
||||
sidebar:
|
||||
label: Schnorr
|
||||
order: 2
|
||||
icon: badge-check
|
||||
---
|
||||
|
||||
`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](https://eprint.iacr.org/2018/499.pdf) — 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(\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(\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](/signatures/ecdsa) or [BLS](/signatures/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
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
"NewProver": {
|
||||
type: "func(curve *curves.Curve, basepoint curves.Point, uniqueSessionId []byte) *Prover",
|
||||
required: true,
|
||||
description: "Binds a curve, a base point, and a domain separator. Never returns an error.",
|
||||
},
|
||||
"Prover.Prove": {
|
||||
type: "func(x curves.Scalar) (*Proof, error)",
|
||||
required: true,
|
||||
description: "Computes Statement = x·basepoint and the (c, s) pair. One curve multiplication for the statement plus one for the nonce point.",
|
||||
},
|
||||
"Prover.ProveCommit": {
|
||||
type: "func(x curves.Scalar) (*Proof, Commitment, error)",
|
||||
description: "Same proof, plus SHA3-256(c || s) as a commitment to open later.",
|
||||
},
|
||||
"Verify": {
|
||||
type: "func(proof *Proof, curve *curves.Curve, basepoint curves.Point, uniqueSessionId []byte) error",
|
||||
required: true,
|
||||
description: "Returns nil on success, an error on failure. There is no boolean return.",
|
||||
},
|
||||
"DecommitVerify": {
|
||||
type: "func(proof *Proof, commitment Commitment, curve *curves.Curve, basepoint curves.Point, uniqueSessionId []byte) error",
|
||||
description: "Checks the proof opens the commitment, then verifies the proof.",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### Types
|
||||
|
||||
`Commitment` is a plain type alias for `[]byte` — no wrapper, no methods.
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
Statement: {
|
||||
type: "curves.Point",
|
||||
required: true,
|
||||
description: "The point whose discrete log is proved: x · basepoint. Constructed by Prove, not supplied by the caller.",
|
||||
},
|
||||
C: {
|
||||
type: "curves.Scalar",
|
||||
required: true,
|
||||
description: "The Fiat-Shamir challenge scalar.",
|
||||
},
|
||||
S: {
|
||||
type: "curves.Scalar",
|
||||
required: true,
|
||||
description: "The response scalar, c·x + k.",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
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`:
|
||||
|
||||
```go
|
||||
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.
|
||||
|
||||
```go proof.go
|
||||
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`:
|
||||
|
||||
<Steps>
|
||||
<Step title="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`.
|
||||
</Step>
|
||||
<Step title="Bob proves in the clear">
|
||||
Bob stores `round2Output.Commitment`, builds his own prover, and calls
|
||||
`Prove(bob.secretKeyShare)`, sending the full proof.
|
||||
</Step>
|
||||
<Step title="Alice verifies and reveals">
|
||||
`Round4VerifyAndReveal` calls `schnorr.Verify` on Bob's proof, then returns Alice's
|
||||
previously withheld proof.
|
||||
</Step>
|
||||
<Step title="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)`.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
```go committed.go
|
||||
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
|
||||
|
||||
:::warning[uniqueSessionId is load-bearing]
|
||||
`uniqueSessionId` is the first thing hashed into the challenge. It is the domain separator
|
||||
that binds a proof to one execution of one protocol, and prover and verifier **must** pass
|
||||
byte-identical values or verification fails with a generic
|
||||
`"schnorr verification failed"` — you get no hint that the session ids diverged.
|
||||
|
||||
Two failure modes matter:
|
||||
|
||||
- **Reuse across contexts.** A proof made under session id `S` verifies under session id `S`
|
||||
anywhere. If you use a constant, a proof captured from one sub-protocol replays into
|
||||
another. Derive it from a live transcript. The repo's own callers do: `dklsv1` and
|
||||
`simplest` build it from a hash of protocol-specific salts and seeds.
|
||||
- **Attacker-chosen ids.** If a remote party picks the session id you verify under, it picks
|
||||
the domain the proof is bound to. Derive it from data both sides contributed, never from
|
||||
one side's unilateral input.
|
||||
:::
|
||||
|
||||
:::note[The commitment covers only (C, S)]
|
||||
`ProveCommit` hashes `C.Bytes()` and `S.Bytes()` — it does **not** hash `Statement`. The
|
||||
statement is still bound, but indirectly: `Verify` recomputes the challenge from the statement,
|
||||
so an opened proof only verifies against the statement it was made for. Do not, however,
|
||||
treat the `Commitment` as a standalone commitment to the public point; it is not one, and it
|
||||
carries no information about which statement will be revealed.
|
||||
:::
|
||||
|
||||
:::warning[No message binding]
|
||||
The challenge covers `uniqueSessionId`, the base point, the statement, and the nonce point.
|
||||
It does not cover any application message. This is a proof of knowledge, not a signature. If
|
||||
you need to bind a payload, fold that payload into `uniqueSessionId` before constructing the
|
||||
prover.
|
||||
:::
|
||||
|
||||
:::info[Error shape]
|
||||
`Verify` and `DecommitVerify` return `error`, not `(bool, error)`. A `nil` return is the only
|
||||
success signal. Do not ignore the error value; there is no other output to inspect.
|
||||
:::
|
||||
|
||||
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
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Threshold ECDSA" href="/threshold/threshold-ecdsa" icon="users">
|
||||
`tecdsa/dklsv1` uses the committed variant in DKG rounds 3–5, and the plain variant with a
|
||||
custom base point during signing.
|
||||
</Card>
|
||||
<Card title="Oblivious transfer" href="/threshold/oblivious-transfer" icon="shuffle">
|
||||
`ot/base/simplest` has the sender prove knowledge of its base-OT secret key in round 1,
|
||||
which the receiver verifies before any transfer.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
Reference in New Issue
Block a user