ring

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 2, 2026 License: LGPL-3.0 Imports: 11 Imported by: 8

README

ring-go

Implementation of linkable ring signatures using elliptic curve crypto in Go. It supports ring signatures over both ed25519 and secp256k1.

Requirements

go 1.26

Install

go get github.com/pokt-network/ring-go

References

This implementation is based off of Ring Confidential Transactions, in particular section 2, which defines MLSAG (Multilayered Linkable Spontaneous Anonymous Group signatures).

Usage

See examples/main.go.

package main

import (
    "fmt"

    ring "github.com/pokt-network/ring-go"
    "golang.org/x/crypto/sha3"
)

func signAndVerify(curve ring.Curve) {
    privKey := curve.NewRandomScalar()
    msgHash := sha3.Sum256([]byte("helloworld"))

    // size of the public key ring (anonymity set)
    const size = 16

    // our key's secret index within the set
    const idx = 7

    keyring, err := ring.NewKeyRing(curve, size, privKey, idx)
    if err != nil {
        panic(err)
    }

    sig, err := keyring.Sign(msgHash, privKey)
    if err != nil {
        panic(err)
    }

    ok := sig.Verify(msgHash)
    if !ok {
        fmt.Println("failed to verify :(")
        return
    }

    fmt.Println("verified signature!")
}

func main() {
    fmt.Println("using secp256k1...")
    signAndVerify(ring.Secp256k1())
    fmt.Println("using ed25519...")
    signAndVerify(ring.Ed25519())
}

Signing paths: on-chain vs off-chain

There are two ways to sign. They produce interchangeable signatures — Verify and Link behave identically — but they differ in how much work they do, which matters if you run inside consensus.

Sign — default, consensus-safe
sig, err := keyring.Sign(msgHash, privKey)

Sign (and Verify) perform a fixed amount of work that does not depend on any cache, shared state, or call history. The cost is therefore deterministic across nodes. Use this path on-chain / in gas-metered code. It carries no extra state and requires no setup.

SignWithContext — off-chain fast path
ctx, err := keyring.NewSignerContext(privKey) // pre-compute once (does NOT store the key)
ctx.SkipSelfCheck = true                       // optional, off-chain only

sig, err := keyring.SignWithContext(msgHash, privKey, ctx) // key passed per call; reuse ctx

NewSignerContext pre-computes, once, the values that are constant across messages for a given (privKey, ring): the signer's public key, key image, and the hash-to-curve of every ring member. Signing many messages then reuses that work. All precomputed state lives on the caller-owned SignerContext; the Ring is never mutated.

The context holds no secret material — every field is derived from public data (the key image and public key are already published in signatures). The private key is not retained; it is supplied per call to SignWithContext, exactly as with Sign. So a SignerContext is safe to hold, reuse, and pass around.

⚠️ Off-chain only. Because SignWithContext does less work than Sign, its cost is not identical across calls or nodes. Do not use it on consensus-critical or gas-metered paths — use Sign there. A context is tied to one ring and one key: pass the key it was built for (a mismatch yields an invalid signature, caught by the self-check). Setting SkipSelfCheck = true drops the closing self-checks (~4 scalar multiplications) — and with them the mismatch guard — an off-chain speed trade only.

Benchmarked gains

Measured with go test -bench . -benchmem (Apple M1). Allocations are deterministic and machine-independent; wall-clock is directional and varies by machine, so reproduce it on your own hardware. Numbers below keep the self-check on (SkipSelfCheck = false); enabling it saves a little more.

curve · ring size Sign allocs/op SignWithContext allocs/op reduction
secp256k1 · 8 235 205 ~-13%
secp256k1 · 32 861 762 ~-11%
ed25519 · 8 202 169 ~-16%

The saving comes from not recomputing every ring member's hash-to-curve on each signature; it grows with how many times you reuse the same context. Run the suite yourself with make benchmark_all (see bench_test.go).

Documentation

Overview

Example (SignAndVerify)

Example_signAndVerify shows the default signing path.

Sign/Verify are CONSENSUS-SAFE: they perform a fixed amount of work independent of any cache or call history, so their cost is deterministic across nodes. Use this path on-chain / in gas-metered code.

package main

import (
	"fmt"

	ring "github.com/pokt-network/ring-go"
	"golang.org/x/crypto/sha3"
)

func main() {
	curve := ring.Secp256k1()
	privKey := curve.NewRandomScalar()

	// ring of 16 keys; ours sits at secret index 7.
	keyring, err := ring.NewKeyRing(curve, 16, privKey, 7)
	if err != nil {
		panic(err)
	}

	msg := sha3.Sum256([]byte("hello"))
	sig, err := keyring.Sign(msg, privKey)
	if err != nil {
		panic(err)
	}

	fmt.Println(sig.Verify(msg))
}
Output:
true

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Ed25519

func Ed25519() types.Curve

Ed25519 returns a new ed25519 curve instance.

func Link(sigA, sigB *RingSig) bool

Link returns true if the two signatures were created by the same signer, false otherwise.

func Secp256k1

func Secp256k1() types.Curve

Secp256k1 returns a new secp256k1 curve instance. BUILD-TIME CONFIGURATION: With ethereum_secp256k1 build tag, expensive operations are accelerated using libsecp256k1 via go-ethereum.

Types

type Curve

type Curve = types.Curve

Curve represents an elliptic curve that can be used for signing.

type Ring

type Ring struct {
	// contains filtered or unexported fields
}

Ring represents a group of public keys such that one of the group created a signature.

func NewFixedKeyRingFromPublicKeys

func NewFixedKeyRingFromPublicKeys(curve types.Curve, pubkeys []types.Point) (*Ring, error)

NewFixedKeyRingFromPublicKeys takes public keys and a curve to create a ring

func NewKeyRing

func NewKeyRing(curve types.Curve, size int, privKey types.Scalar, idx int) (*Ring, error)

NewKeyRing creates a ring with size specified by `size` and places the public key corresponding to `privKey` in index idx of the ring. It returns a ring of public keys of length `size`.

func NewKeyRingFromPublicKeys

func NewKeyRingFromPublicKeys(curve types.Curve, pubkeys []types.Point, privKey types.Scalar, idx int) (*Ring, error)

NewKeyRingFromPublicKeys takes public key ring and places the public key corresponding to `privKey` in index idx of the ring. It returns a ring of public keys of length `len(ring)+1`.

func (*Ring) Equals

func (r *Ring) Equals(other *Ring) bool

Equals checks whether the supplied ring is equal to the current ring. The ring's public keys must be in the same order for the rings to be equal

func (*Ring) NewSignerContext added in v0.2.0

func (r *Ring) NewSignerContext(privKey types.Scalar) (*SignerContext, error)

NewSignerContext pre-computes, for privKey (which must correspond to one of the ring's public keys), the signer's public key, key image, index, and the hash-to-curve of every ring member. The private key is used only to derive these public values and is NOT stored on the returned context. All state lives on the context; the shared Ring is not mutated.

func (*Ring) Sign

func (r *Ring) Sign(m [32]byte, privKey types.Scalar) (*RingSig, error)

Sign creates a ring signature on the given message using the public key ring and a private key of one of the members of the ring.

func (*Ring) SignWithContext added in v0.2.0

func (r *Ring) SignWithContext(m [32]byte, privKey types.Scalar, ctx *SignerContext) (*RingSig, error)

SignWithContext creates a ring signature using the values pre-computed in ctx, avoiding the per-signer and per-member setup that Sign repeats each call. The private key is supplied here (not stored on the context) and must be the same key the context was built for; otherwise the resulting signature is invalid (and rejected by the closing self-check unless ctx.SkipSelfCheck is set). It is an off-chain optimization; see SignerContext for the determinism caveat.

Example

ExampleRing_SignWithContext shows the off-chain fast path: build a SignerContext once (which pre-computes the signer's key image and the hash-to-curve of every ring member), then reuse it to sign many messages without redoing that work.

WARNING: this path does less work than Sign, so its cost is not identical across calls/nodes. Do NOT use it on consensus-critical or gas-metered paths — use Sign there. Verification is unchanged.

package main

import (
	"fmt"

	ring "github.com/pokt-network/ring-go"
	"golang.org/x/crypto/sha3"
)

func main() {
	curve := ring.Secp256k1()
	privKey := curve.NewRandomScalar()

	keyring, err := ring.NewKeyRing(curve, 16, privKey, 7)
	if err != nil {
		panic(err)
	}

	// Pre-compute once.
	ctx, err := keyring.NewSignerContext(privKey)
	if err != nil {
		panic(err)
	}
	ctx.SkipSelfCheck = true // optional extra speed, off-chain only

	// Reuse across many messages.
	allVerified := true
	for i := 0; i < 3; i++ {
		msg := sha3.Sum256([]byte(fmt.Sprintf("message-%d", i)))
		sig, err := keyring.SignWithContext(msg, privKey, ctx)
		if err != nil {
			panic(err)
		}
		allVerified = allVerified && sig.Verify(msg)
	}

	fmt.Println(allVerified)
}
Output:
true

func (*Ring) Size

func (r *Ring) Size() int

Size returns the size of the ring, ie. the number of public keys in it.

type RingSig

type RingSig struct {
	// contains filtered or unexported fields
}

RingSig represents a ring signature.

func Sign

func Sign(m [32]byte, ring *Ring, privKey types.Scalar, ourIdx int) (*RingSig, error)

Sign creates a ring signature on the given message using the provided private key and ring of public keys.

func (*RingSig) Deserialize

func (sig *RingSig) Deserialize(curve Curve, in []byte) error

Deserialize converts the byteified signature into a *RingSig.

func (*RingSig) PublicKeys

func (r *RingSig) PublicKeys() []types.Point

PublicKeys returns a copy of the ring signature's public keys.

func (*RingSig) Ring

func (r *RingSig) Ring() *Ring

Ring returns the ring from the RingSig struct

func (*RingSig) Serialize

func (r *RingSig) Serialize() ([]byte, error)

Serialize converts the signature to a byte array.

func (*RingSig) Verify

func (sig *RingSig) Verify(m [32]byte) bool

Verify verifies the ring signature for the given message. It returns true if a valid signature, false otherwise.

type SignerContext added in v0.2.0

type SignerContext struct {

	// SkipSelfCheck, when true, omits the ~4-scalar-mul closing self-checks in
	// SignWithContext. Off-chain speed opt-in only; the signature is still fully
	// verifiable via RingSig.Verify. Leave false unless you trust the backend
	// and have measured the win. NOTE: the self-checks are also what catch a
	// privKey that does not match this context; with them skipped, a mismatched
	// key silently produces an invalid (not forged) signature.
	SkipSelfCheck bool
	// contains filtered or unexported fields
}

SignerContext holds values that are constant across messages for a given (private key, ring): the signer's public key, hash-to-curve point, key image, index, and the hash-to-curve of every ring member. Pre-computing them once lets repeated signing skip all per-signer and per-member setup.

ctx, err := ring.NewSignerContext(privKey)
sig1, _ := ring.SignWithContext(msg1, privKey, ctx)
sig2, _ := ring.SignWithContext(msg2, privKey, ctx) // reuses ctx precompute

A SignerContext holds NO secret material: every field is derived from public data (the key image and public key are already published in signatures). The private key is not retained — it is supplied per call to SignWithContext, exactly as with Sign — so a context is safe to hold, reuse, and pass around. It is tied to the ring it was created from; do not use it with a different ring or a different private key than the one it was built for.

IMPORTANT: this is an OFF-CHAIN optimization. The precomputed member hashes mean SignWithContext performs less work than Sign for the same signature, so its cost is not identical to the plain path. Do NOT use SignerContext on consensus-critical / gas-metered paths where the amount of work performed must be deterministic across nodes — use Sign there.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL