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

Accumulator

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.

The accumulator package implements the pairing-based accumulator of eprint 2020/777, 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 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

PropType
SecretKey.Newfunc(curve *curves.PairingCurve, seed []byte) (*SecretKey, error)

Derives alpha as curve.Scalar.Hash(seed). Fully deterministic in the seed; performs no validation of seed quality or length.

Typefunc(curve *curves.PairingCurve, seed []byte) (*SecretKey, error)
SecretKey.GetPublicKeyfunc(curve *curves.PairingCurve) (*PublicKey, error)

Returns alpha times the G2 generator. Errors if the key or curve is nil.

Typefunc(curve *curves.PairingCurve) (*PublicKey, error)
SecretKey.BatchAdditions?func(additions []Element) (Element, error)

product(y + alpha) over the additions. The multiplier applied to the accumulator on a batch add.

Typefunc(additions []Element) (Element, error)
SecretKey.BatchDeletions?func(deletions []Element) (Element, error)

1/product(y + alpha) over the deletions.

Typefunc(deletions []Element) (Element, error)
SecretKey.CreateCoefficients?func(additions, deletions []Element) ([]Element, error)

Batch polynomial coefficients per page 7 of the paper. Update calls this for you; call it directly only if you are reimplementing Update.

Typefunc(additions, deletions []Element) ([]Element, error)

Accumulator operations

PropType
Accumulator.Newfunc(curve *curves.PairingCurve) (*Accumulator, error)

Sets the value to the G1 generator. Called on the receiver, so the idiom is new(Accumulator).New(curve).

Typefunc(curve *curves.PairingCurve) (*Accumulator, error)
Accumulator.WithElements?func(curve *curves.PairingCurve, key *SecretKey, m []Element) (*Accumulator, error)

New plus a batch add: V = product(y + alpha) · V0. The usual way to bootstrap a populated set.

Typefunc(curve *curves.PairingCurve, key *SecretKey, m []Element) (*Accumulator, error)
Accumulator.Add?func(key *SecretKey, e Element) (*Accumulator, error)

V' = (y + alpha) · V. Errors if the accumulator value is nil or the identity.

Typefunc(key *SecretKey, e Element) (*Accumulator, error)
Accumulator.AddElements?func(key *SecretKey, m []Element) (*Accumulator, error)

Batch add. Does not emit coefficients, so holders cannot update from it — use Update if witnesses are outstanding.

Typefunc(key *SecretKey, m []Element) (*Accumulator, error)
Accumulator.Remove?func(key *SecretKey, e Element) (*Accumulator, error)

V' = 1/(y + alpha) · V. Does not verify the element was ever added; removing an absent element silently produces a different accumulator.

Typefunc(key *SecretKey, e Element) (*Accumulator, error)
Accumulator.Update?func(key *SecretKey, additions, deletions []Element) (*Accumulator, []Coefficient, error)

Batch add and delete in one step, returning the coefficients holders need for BatchUpdate. This is the update method to use in production.

Typefunc(key *SecretKey, additions, deletions []Element) (*Accumulator, []Coefficient, error)

Witnesses

PropType
MembershipWitness.Newfunc(y Element, acc *Accumulator, sk *SecretKey) (*MembershipWitness, error)

Issues a witness for y against the current accumulator: C = 1/(y + alpha) · V. Requires the secret key, so only the manager can issue.

Typefunc(y Element, acc *Accumulator, sk *SecretKey) (*MembershipWitness, error)
MembershipWitness.Verifyfunc(pk *PublicKey, acc *Accumulator) error

Multi-pairing check e(C, y·P̃ + Q̃) · e(-V, P̃) == 1. Returns nil on success. Public — any holder or verifier can run it.

Typefunc(pk *PublicKey, acc *Accumulator) error
MembershipWitness.BatchUpdatefunc(additions, deletions []Element, coefficients []Coefficient) (*MembershipWitness, error)

Refreshes a stale witness against one published Update. This is the update path callers should use.

Typefunc(additions, deletions []Element, coefficients []Coefficient) (*MembershipWitness, error)
MembershipWitness.MultiBatchUpdate?func(A [][]Element, D [][]Element, C [][]Coefficient) (*MembershipWitness, error)

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.

Typefunc(A [][]Element, D [][]Element, C [][]Coefficient) (*MembershipWitness, error)
MembershipWitness.ApplyDelta?func(delta *Delta) (*MembershipWitness, error)

Applies precomputed update material. Effectively unreachable — see caveats.

Typefunc(delta *Delta) (*MembershipWitness, error)

Witness staleness is the hard part

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.

PropType
ProofParams.Newfunc(curve *curves.PairingCurve, pk *PublicKey, entropy []byte) (*ProofParams, error)

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.

Typefunc(curve *curves.PairingCurve, pk *PublicKey, entropy []byte) (*ProofParams, error)
MembershipProofCommitting.Newfunc(witness *MembershipWitness, acc *Accumulator, pp *ProofParams, pk *PublicKey) (*MembershipProofCommitting, error)

Prover's commit phase. Holds all the blinding values; keep it private and short-lived.

Typefunc(witness *MembershipWitness, acc *Accumulator, pp *ProofParams, pk *PublicKey) (*MembershipProofCommitting, error)
MembershipProofCommitting.GetChallengeBytesfunc() []byte

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.

Typefunc() []byte
MembershipProofCommitting.GenProoffunc(c curves.Scalar) *MembershipProof

Computes the s values for the given challenge. Returns the proof to send. No error return.

Typefunc(c curves.Scalar) *MembershipProof
MembershipProof.Finalizefunc(acc *Accumulator, pp *ProofParams, pk *PublicKey, challenge curves.Scalar) (*MembershipProofFinal, error)

Verifier side: recomputes the commitment values from the proof, the accumulator, and the params.

Typefunc(acc *Accumulator, pp *ProofParams, pk *PublicKey, challenge curves.Scalar) (*MembershipProofFinal, error)
MembershipProofFinal.GetChallengefunc(curve *curves.PairingCurve) curves.Scalar

Recomputes the Fiat-Shamir challenge from the finalized values. Verification succeeds iff this equals the challenge the prover used.

Typefunc(curve *curves.PairingCurve) curves.Scalar

Full lifecycle

Set up the manager

Derive a SecretKey from a strong seed and publish the PublicKey.

Accumulate the initial members

new(Accumulator).WithElements(curve, sk, elements) and publish the accumulator bytes.

Issue a witness

For each holder, new(MembershipWitness).New(element, acc, sk) and deliver the witness bytes privately. This step needs the secret key.

Prove membership

The holder builds ProofParams, runs MembershipProofCommitting, hashes GetChallengeBytes() into a challenge, and sends the challenge plus GenProof(challenge).

Verify

The verifier calls Finalize then GetChallenge, and accepts iff the recomputed challenge equals the one it was given.

Update the set

The manager calls Update(sk, additions, deletions) and publishes the new accumulator, the element lists, and the coefficients.

Refresh witnesses

Every holder calls BatchUpdate(additions, deletions, coefficients), then can prove again against the new accumulator.

Membership proof end to end

Grounded in accumulator/proof_test.go (TestMembershipProof) and accumulator/witness_test.go (Test_Membership, Test_Membership_Batch_Update).

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:

_, 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

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.

Last updated on September 2, 2026

Was this page helpful?