Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Benchmarking

cpu.prof
mem.prof
ring-go.test

.DS_Store

# IDE
.idea/
69 changes: 69 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
.SILENT:

#####################
### General ###
#####################

.PHONY: help
.DEFAULT_GOAL := help
help: ## Prints all the targets in all the Makefiles
@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

.PHONY: list
list: ## List all make targets
@${MAKE} -pRrn : -f $(MAKEFILE_LIST) 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | sort

#####################
#### Testing ####
#####################

.PHONY: test_all
test_all: ## runs the test suite
go test -v -p 1 ./... -mod=readonly -race

##########################
#### Benchmarking ####
##########################

.PHONY: benchmark_all
benchmark_all: ## Run all benchmarks (tests both Decred and Ethereum backends)
@echo "🔬 Running benchmarks with Decred backend (Pure Go)..."
@echo "=================================================="
go test -v -bench=. -benchmem -run=^$$ ./...
@echo ""
@echo "🔬 Running benchmarks with Ethereum backend (CGO + libsecp256k1)..."
@echo "=================================================================="
CGO_ENABLED=1 go test -tags=ethereum_secp256k1 -v -bench=. -benchmem -run=^$$ ./...

.PHONY: benchmark_report
benchmark_report: ## Compare crypto backends with formatted report for ring signatures
@python3 format_benchmark.py --run-benchmarks

###########################
### Release Helpers ###
###########################

# List tags: git tag
# Delete tag locally: git tag -d v1.2.3
# Delete tag remotely: git push --delete origin v1.2.3

.PHONY: tag_bug_fix
tag_bug_fix: ## Tag a new bug fix release (e.g., v1.0.1 -> v1.0.2)
@$(eval LATEST_TAG=$(shell git tag --sort=-v:refname | head -n 1))
@$(eval NEW_TAG=$(shell echo $(LATEST_TAG) | awk -F. -v OFS=. '{ $$NF = sprintf("%d", $$NF + 1); print }'))
@git tag $(NEW_TAG)
@echo "New bug fix version tagged: $(NEW_TAG)"
@echo "Run the following commands to push the new tag:"
@echo " git push origin $(NEW_TAG)"
@echo "And draft a new release at https://github.com/pokt-network/smt/releases/new"


.PHONY: tag_minor_release
tag_minor_release: ## Tag a new minor release (e.g. v1.0.0 -> v1.1.0)
@$(eval LATEST_TAG=$(shell git tag --sort=-v:refname | head -n 1))
@$(eval NEW_TAG=$(shell echo $(LATEST_TAG) | awk -F. '{$$2 += 1; $$3 = 0; print $$1 "." $$2 "." $$3}'))
@git tag $(NEW_TAG)
@echo "New minor release version tagged: $(NEW_TAG)"
@echo "Run the following commands to push the new tag:"
@echo " git push origin $(NEW_TAG)"
@echo "And draft a new release at https://github.com/pokt-network/smt/releases/new"
157 changes: 116 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,62 +1,137 @@
# ring-go
Implementation of linkable ring signatures using elliptic curve crypto in pure Go. It supports ring signatures over both ed25519 and secp256k1.
# ring-go <!-- omit in toc -->

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

### get
`go get github.com/noot/ring-go`
- [Requirements](#requirements)
- [Install](#install)
- [References](#references)
- [Usage](#usage)
- [Signing paths: on-chain vs off-chain](#signing-paths-on-chain-vs-off-chain)

## Requirements

go 1.26

## Install

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

## References

### references
This implementation is based off of [Ring Confidential Transactions](https://eprint.iacr.org/2015/1098.pdf), in particular section 2, which defines MLSAG (Multilayered Linkable Spontaneous Anonymous Group signatures).

### usage
## Usage

See `examples/main.go`.

```go
package main

import (
"fmt"
"fmt"

ring "github.com/noot/ring-go"
"golang.org/x/crypto/sha3"
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!")
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())
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

```go
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

```go
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`).

Binary file not shown.
Loading