diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3eb38b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Benchmarking + +cpu.prof +mem.prof +ring-go.test + +.DS_Store + +# IDE +.idea/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9a15276 --- /dev/null +++ b/Makefile @@ -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" \ No newline at end of file diff --git a/README.md b/README.md index 0d02bbb..59e955e 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,27 @@ -# 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 -### 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`. @@ -18,45 +29,109 @@ See `examples/main.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`). + diff --git a/audits/240704_Thesis_Defense-Pokt_Network_Ring-Go_Security_Audit_Report.pdf b/audits/240704_Thesis_Defense-Pokt_Network_Ring-Go_Security_Audit_Report.pdf new file mode 100644 index 0000000..03d4b92 Binary files /dev/null and b/audits/240704_Thesis_Defense-Pokt_Network_Ring-Go_Security_Audit_Report.pdf differ diff --git a/bench_test.go b/bench_test.go index e44d080..cf9ee85 100644 --- a/bench_test.go +++ b/bench_test.go @@ -3,22 +3,22 @@ package ring import ( "testing" - "github.com/athanorlabs/go-dleq/types" + "github.com/pokt-network/go-dleq/types" ) const idx = 0 -func benchmarkSign(b *testing.B, curve types.Curve, keyring *Ring, privkey types.Scalar, size, idx int) { +func benchmarkSign(b *testing.B, curve types.Curve, keyring *Ring, privKey types.Scalar, size, idx int) { for i := 0; i < b.N; i++ { - _, err := keyring.Sign(testMsg, privkey) + _, err := keyring.Sign(testMsg, privKey) if err != nil { panic(err) } } } -func mustKeyRing(curve types.Curve, privkey types.Scalar, size, idx int) *Ring { - keyring, err := NewKeyRing(curve, size, privkey, idx) +func mustKeyRing(curve types.Curve, privKey types.Scalar, size, idx int) *Ring { + keyring, err := NewKeyRing(curve, size, privKey, idx) if err != nil { panic(err) } @@ -28,113 +28,113 @@ func mustKeyRing(curve types.Curve, privkey types.Scalar, size, idx int) *Ring { func BenchmarkSign2_Secp256k1(b *testing.B) { const size = 2 curve := Secp256k1() - privkey := curve.NewRandomScalar() - keyring := mustKeyRing(curve, privkey, size, idx) - benchmarkSign(b, curve, keyring, privkey, size, idx) + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + benchmarkSign(b, curve, keyring, privKey, size, idx) } func BenchmarkSign4_Secp256k1(b *testing.B) { const size = 4 curve := Secp256k1() - privkey := curve.NewRandomScalar() - keyring := mustKeyRing(curve, privkey, size, idx) - benchmarkSign(b, curve, keyring, privkey, size, idx) + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + benchmarkSign(b, curve, keyring, privKey, size, idx) } func BenchmarkSign8_Secp256k1(b *testing.B) { const size = 8 curve := Secp256k1() - privkey := curve.NewRandomScalar() - keyring := mustKeyRing(curve, privkey, size, idx) - benchmarkSign(b, curve, keyring, privkey, size, idx) + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + benchmarkSign(b, curve, keyring, privKey, size, idx) } func BenchmarkSign16_Secp256k1(b *testing.B) { const size = 16 curve := Secp256k1() - privkey := curve.NewRandomScalar() - keyring := mustKeyRing(curve, privkey, size, idx) - benchmarkSign(b, curve, keyring, privkey, size, idx) + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + benchmarkSign(b, curve, keyring, privKey, size, idx) } func BenchmarkSign32_Secp256k1(b *testing.B) { const size = 32 curve := Secp256k1() - privkey := curve.NewRandomScalar() - keyring := mustKeyRing(curve, privkey, size, idx) - benchmarkSign(b, curve, keyring, privkey, size, idx) + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + benchmarkSign(b, curve, keyring, privKey, size, idx) } func BenchmarkSign64_Secp256k1(b *testing.B) { const size = 64 curve := Secp256k1() - privkey := curve.NewRandomScalar() - keyring := mustKeyRing(curve, privkey, size, idx) - benchmarkSign(b, curve, keyring, privkey, size, idx) + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + benchmarkSign(b, curve, keyring, privKey, size, idx) } func BenchmarkSign128_Secp256k1(b *testing.B) { const size = 128 curve := Secp256k1() - privkey := curve.NewRandomScalar() - keyring := mustKeyRing(curve, privkey, size, idx) - benchmarkSign(b, curve, keyring, privkey, size, idx) + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + benchmarkSign(b, curve, keyring, privKey, size, idx) } func BenchmarkSign2_Ed25519(b *testing.B) { const size = 2 curve := Ed25519() - privkey := curve.NewRandomScalar() - keyring := mustKeyRing(curve, privkey, size, idx) - benchmarkSign(b, curve, keyring, privkey, size, idx) + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + benchmarkSign(b, curve, keyring, privKey, size, idx) } func BenchmarkSign4_Ed25519(b *testing.B) { const size = 4 curve := Ed25519() - privkey := curve.NewRandomScalar() - keyring := mustKeyRing(curve, privkey, size, idx) - benchmarkSign(b, curve, keyring, privkey, size, idx) + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + benchmarkSign(b, curve, keyring, privKey, size, idx) } func BenchmarkSign8_Ed25519(b *testing.B) { const size = 8 curve := Ed25519() - privkey := curve.NewRandomScalar() - keyring := mustKeyRing(curve, privkey, size, idx) - benchmarkSign(b, curve, keyring, privkey, size, idx) + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + benchmarkSign(b, curve, keyring, privKey, size, idx) } func BenchmarkSign16_Ed25519(b *testing.B) { const size = 16 curve := Ed25519() - privkey := curve.NewRandomScalar() - keyring := mustKeyRing(curve, privkey, size, idx) - benchmarkSign(b, curve, keyring, privkey, size, idx) + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + benchmarkSign(b, curve, keyring, privKey, size, idx) } func BenchmarkSign32_Ed25519(b *testing.B) { const size = 32 curve := Ed25519() - privkey := curve.NewRandomScalar() - keyring := mustKeyRing(curve, privkey, size, idx) - benchmarkSign(b, curve, keyring, privkey, size, idx) + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + benchmarkSign(b, curve, keyring, privKey, size, idx) } func BenchmarkSign64_Ed25519(b *testing.B) { const size = 64 curve := Ed25519() - privkey := curve.NewRandomScalar() - keyring := mustKeyRing(curve, privkey, size, idx) - benchmarkSign(b, curve, keyring, privkey, size, idx) + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + benchmarkSign(b, curve, keyring, privKey, size, idx) } func BenchmarkSign128_Ed25519(b *testing.B) { const size = 128 curve := Ed25519() - privkey := curve.NewRandomScalar() - keyring := mustKeyRing(curve, privkey, size, idx) - benchmarkSign(b, curve, keyring, privkey, size, idx) + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + benchmarkSign(b, curve, keyring, privKey, size, idx) } func benchmarkVerify(b *testing.B, sig *RingSig) { @@ -147,10 +147,10 @@ func benchmarkVerify(b *testing.B, sig *RingSig) { } func mustSig(curve types.Curve, size int) *RingSig { - privkey := curve.NewRandomScalar() - keyring := mustKeyRing(curve, privkey, size, idx) + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) - sig, err := keyring.Sign(testMsg, privkey) + sig, err := keyring.Sign(testMsg, privKey) if err != nil { panic(err) } @@ -255,3 +255,148 @@ func BenchmarkVerify128_Ed25519(b *testing.B) { sig := mustSig(curve, size) benchmarkVerify(b, sig) } + +// SignWithContext benchmarks - measure performance with pre-computed values + +func benchmarkSignWithContext(b *testing.B, curve types.Curve, keyring *Ring, privKey types.Scalar, ctx *SignerContext) { + for i := 0; i < b.N; i++ { + _, err := keyring.SignWithContext(testMsg, privKey, ctx) + if err != nil { + panic(err) + } + } +} + +func mustSignerContext(keyring *Ring, privKey types.Scalar) *SignerContext { + ctx, err := keyring.NewSignerContext(privKey) + if err != nil { + panic(err) + } + return ctx +} + +func BenchmarkSignWithContext2_Secp256k1(b *testing.B) { + const size = 2 + curve := Secp256k1() + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + ctx := mustSignerContext(keyring, privKey) + benchmarkSignWithContext(b, curve, keyring, privKey, ctx) +} + +func BenchmarkSignWithContext4_Secp256k1(b *testing.B) { + const size = 4 + curve := Secp256k1() + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + ctx := mustSignerContext(keyring, privKey) + benchmarkSignWithContext(b, curve, keyring, privKey, ctx) +} + +func BenchmarkSignWithContext8_Secp256k1(b *testing.B) { + const size = 8 + curve := Secp256k1() + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + ctx := mustSignerContext(keyring, privKey) + benchmarkSignWithContext(b, curve, keyring, privKey, ctx) +} + +func BenchmarkSignWithContext16_Secp256k1(b *testing.B) { + const size = 16 + curve := Secp256k1() + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + ctx := mustSignerContext(keyring, privKey) + benchmarkSignWithContext(b, curve, keyring, privKey, ctx) +} + +func BenchmarkSignWithContext32_Secp256k1(b *testing.B) { + const size = 32 + curve := Secp256k1() + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + ctx := mustSignerContext(keyring, privKey) + benchmarkSignWithContext(b, curve, keyring, privKey, ctx) +} + +func BenchmarkSignWithContext64_Secp256k1(b *testing.B) { + const size = 64 + curve := Secp256k1() + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + ctx := mustSignerContext(keyring, privKey) + benchmarkSignWithContext(b, curve, keyring, privKey, ctx) +} + +func BenchmarkSignWithContext128_Secp256k1(b *testing.B) { + const size = 128 + curve := Secp256k1() + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + ctx := mustSignerContext(keyring, privKey) + benchmarkSignWithContext(b, curve, keyring, privKey, ctx) +} + +func BenchmarkSignWithContext2_Ed25519(b *testing.B) { + const size = 2 + curve := Ed25519() + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + ctx := mustSignerContext(keyring, privKey) + benchmarkSignWithContext(b, curve, keyring, privKey, ctx) +} + +func BenchmarkSignWithContext4_Ed25519(b *testing.B) { + const size = 4 + curve := Ed25519() + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + ctx := mustSignerContext(keyring, privKey) + benchmarkSignWithContext(b, curve, keyring, privKey, ctx) +} + +func BenchmarkSignWithContext8_Ed25519(b *testing.B) { + const size = 8 + curve := Ed25519() + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + ctx := mustSignerContext(keyring, privKey) + benchmarkSignWithContext(b, curve, keyring, privKey, ctx) +} + +func BenchmarkSignWithContext16_Ed25519(b *testing.B) { + const size = 16 + curve := Ed25519() + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + ctx := mustSignerContext(keyring, privKey) + benchmarkSignWithContext(b, curve, keyring, privKey, ctx) +} + +func BenchmarkSignWithContext32_Ed25519(b *testing.B) { + const size = 32 + curve := Ed25519() + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + ctx := mustSignerContext(keyring, privKey) + benchmarkSignWithContext(b, curve, keyring, privKey, ctx) +} + +func BenchmarkSignWithContext64_Ed25519(b *testing.B) { + const size = 64 + curve := Ed25519() + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + ctx := mustSignerContext(keyring, privKey) + benchmarkSignWithContext(b, curve, keyring, privKey, ctx) +} + +func BenchmarkSignWithContext128_Ed25519(b *testing.B) { + const size = 128 + curve := Ed25519() + privKey := curve.NewRandomScalar() + keyring := mustKeyRing(curve, privKey, size, idx) + ctx := mustSignerContext(keyring, privKey) + benchmarkSignWithContext(b, curve, keyring, privKey, ctx) +} diff --git a/benchmarks.md b/benchmarks.md index c2c409a..f5e7f88 100644 --- a/benchmarks.md +++ b/benchmarks.md @@ -1,16 +1,22 @@ # Benchmarks -The current library benchmarks for signing and verification are located below. For ring signatures, the signing and verification time are linearly proportional to the number of members of the ring (or "anonymity set"), which is what's observed. +The current library benchmarks for signing and verification are located below. +For ring signatures, the signing and verification time are linearly proportional +to the number of members of the ring (or "anonymity set"), which is what's observed. -> Note: the number directly after `BenchmarkSign` or `BenchmarkVerify` in the test name is the ring size being benchmarked. +> Note: the number directly after `BenchmarkSign` or `BenchmarkVerify` in the test +> name is the ring size being benchmarked. -> Note: the ns/op value on the right is the time it took for signing or verification (depending on the test). The middle value is the number of times the operation was executed by the Go benchmarker. +> Note: the ns/op value on the right is the time it took for signing or verification +> (depending on the test). The middle value is the number of times the operation +> was executed by the Go benchmarker. + +**Summary:** -Summary: - secp256k1 signing and verification is around 0.41ms per ring member - ed25519 signing and verification is around is around 0.12ms per ring member -``` +```bash goos: linux goarch: amd64 pkg: github.com/noot/ring-go diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..e902c42 --- /dev/null +++ b/example_test.go @@ -0,0 +1,72 @@ +package ring_test + +import ( + "fmt" + + ring "github.com/pokt-network/ring-go" + "golang.org/x/crypto/sha3" +) + +// 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. +func Example_signAndVerify() { + 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 +} + +// 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. +func ExampleRing_SignWithContext() { + 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 +} diff --git a/examples/main.go b/examples/main.go index 491ae24..4e7b695 100644 --- a/examples/main.go +++ b/examples/main.go @@ -3,12 +3,18 @@ package main import ( "fmt" - ring "github.com/noot/ring-go" "golang.org/x/crypto/sha3" + + ring "github.com/pokt-network/ring-go" ) +// signAndVerify uses the default Sign/Verify path. +// +// This path is CONSENSUS-SAFE: the amount of work it does is fixed and +// independent of any cache or call history, so it is deterministic across +// nodes. Use it on-chain / in gas-metered code. func signAndVerify(curve ring.Curve) { - privkey := curve.NewRandomScalar() + privKey := curve.NewRandomScalar() msgHash := sha3.Sum256([]byte("helloworld")) // size of the public key ring (anonymity set) @@ -17,12 +23,12 @@ func signAndVerify(curve ring.Curve) { // our key's secret index within the set const idx = 7 - keyring, err := ring.NewKeyRing(curve, size, privkey, idx) + keyring, err := ring.NewKeyRing(curve, size, privKey, idx) if err != nil { panic(err) } - sig, err := keyring.Sign(msgHash, privkey) + sig, err := keyring.Sign(msgHash, privKey) if err != nil { panic(err) } @@ -36,9 +42,65 @@ func signAndVerify(curve ring.Curve) { fmt.Println("verified signature!") } +// signManyWithContext uses the SignerContext / SignWithContext fast path. +// +// A SignerContext pre-computes, once, the values that are constant across +// messages for a given (private key, ring): the signer's public key, key image, +// and the hash-to-curve of every ring member. Signing many messages then reuses +// that work instead of recomputing it each time. The context holds NO secret — +// the private key is passed per call, just like Sign. +// +// WARNING: this is an OFF-CHAIN optimization. Because it does less work than the +// plain path, its cost is not identical across calls/nodes. Do NOT use it on +// consensus-critical or gas-metered paths — use Sign there. Verification of the +// resulting signatures is unchanged (sig.Verify). +func signManyWithContext(curve ring.Curve) { + privKey := curve.NewRandomScalar() + + const size = 16 + const idx = 7 + + keyring, err := ring.NewKeyRing(curve, size, privKey, idx) + if err != nil { + panic(err) + } + + // build the context once... + ctx, err := keyring.NewSignerContext(privKey) + if err != nil { + panic(err) + } + // optional: skip the closing self-checks for extra speed (off-chain only). + ctx.SkipSelfCheck = true + + // ...then reuse it to sign many messages. + for i := 0; i < 3; i++ { + msgHash := sha3.Sum256([]byte(fmt.Sprintf("message-%d", i))) + sig, err := keyring.SignWithContext(msgHash, privKey, ctx) + if err != nil { + panic(err) + } + if !sig.Verify(msgHash) { + fmt.Println("failed to verify :(") + return + } + } + + fmt.Println("verified all context-signed signatures!") +} + func main() { - fmt.Println("using secp256k1...") + // Default path — consensus-safe, use on-chain. + fmt.Println("Sign/Verify (consensus-safe):") + fmt.Println(" using secp256k1...") signAndVerify(ring.Secp256k1()) - fmt.Println("using ed25519...") + fmt.Println(" using ed25519...") signAndVerify(ring.Ed25519()) + + // Fast path — off-chain only, reuses a SignerContext across messages. + fmt.Println("SignWithContext (off-chain fast path):") + fmt.Println(" using secp256k1...") + signManyWithContext(ring.Secp256k1()) + fmt.Println(" using ed25519...") + signManyWithContext(ring.Ed25519()) } diff --git a/format_benchmark.py b/format_benchmark.py new file mode 100755 index 0000000..722d671 --- /dev/null +++ b/format_benchmark.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +""" +Benchmark comparison tool for ring-go crypto backends. +Compares Decred (pure Go) vs Ethereum (libsecp256k1) implementations. +""" + +import sys +import re +import subprocess +import os +from collections import defaultdict + +def run_benchmarks(): + """Run benchmarks for both backends and return results.""" + results = defaultdict(dict) + + print("🔬 Benchmarking ring signature crypto backends...") + print("=" * 70) + print() + + # Run Decred backend benchmarks + print("Running benchmarks with Decred backend (Pure Go)...") + cmd = ["go", "test", ".", "-bench=BenchmarkSign.*_Secp256k1|BenchmarkVerify.*_Secp256k1", + "-benchmem", "-run=^$", "-benchtime=2s"] + try: + output = subprocess.run(cmd, capture_output=True, text=True, timeout=45).stdout + parse_results(output, results, "Decred") + except subprocess.TimeoutExpired: + print("⚠️ Decred benchmarks timed out") + + # Run Ethereum backend benchmarks + print("Running benchmarks with Ethereum backend (CGO + libsecp256k1)...") + env = os.environ.copy() + env["CGO_ENABLED"] = "1" + cmd = ["go", "test", "-tags=ethereum_secp256k1", ".", + "-bench=BenchmarkSign.*_Secp256k1|BenchmarkVerify.*_Secp256k1", + "-benchmem", "-run=^$", "-benchtime=2s"] + try: + output = subprocess.run(cmd, capture_output=True, text=True, timeout=45, env=env).stdout + parse_results(output, results, "Ethereum") + except subprocess.TimeoutExpired: + print("⚠️ Ethereum benchmarks timed out") + + return results + +def parse_results(output, results, backend): + """Parse benchmark output and store in results dict.""" + for line in output.split('\n'): + # Pattern: BenchmarkSign2_Secp256k1-8 1000 1234567 ns/op + match = re.match(r'Benchmark(Sign|Verify)(\d+)_Secp256k1.*?\s+\d+\s+(\d+(?:\.\d+)?)\s+(ns|µs|ms)/op', line) + if match: + operation = match.group(1) + ring_size = int(match.group(2)) + time_val = float(match.group(3)) + time_unit = match.group(4) + + # Normalize to microseconds + if time_unit == "ns": + time_us = time_val / 1000 + elif time_unit == "µs": + time_us = time_val + elif time_unit == "ms": + time_us = time_val * 1000 + else: + time_us = time_val + + results[(operation, ring_size)][backend] = time_us + +def parse_stdin(): + """Parse benchmark output from stdin (for piped input).""" + results = defaultdict(dict) + current_backend = None + + for line in sys.stdin: + # Detect backend type + if "# Decred Backend Results:" in line: + current_backend = "Decred" + continue + elif "# Ethereum Backend Results:" in line: + current_backend = "Ethereum" + continue + + # Parse benchmark lines + match = re.match(r'Benchmark(Sign|Verify)(\d+)_(Decred|Ethereum|Secp256k1).*?\s+\d+\s+(\d+(?:\.\d+)?)\s+(ns|µs|ms)/op', line) + if match: + operation = match.group(1) + ring_size = int(match.group(2)) + backend_name = match.group(3) + time_val = float(match.group(4)) + time_unit = match.group(5) + + # If backend was renamed via sed, use current_backend + if backend_name in ["Decred", "Ethereum"]: + backend = backend_name + elif current_backend: + backend = current_backend + else: + continue + + # Normalize to microseconds + if time_unit == "ns": + time_us = time_val / 1000 + elif time_unit == "µs": + time_us = time_val + elif time_unit == "ms": + time_us = time_val * 1000 + else: + time_us = time_val + + results[(operation, ring_size)][backend] = time_us + + return results + +def format_time(microseconds): + """Format time in appropriate unit.""" + if microseconds < 1: + return f"{microseconds*1000:.1f} ns" + elif microseconds < 1000: + return f"{microseconds:.1f} µs" + else: + return f"{microseconds/1000:.1f} ms" + +def print_comparison(results): + """Print formatted comparison table.""" + if not results: + print("No benchmark results found.") + return + + print() + # Print Sign performance + print("🔍 SIGN PERFORMANCE (Ring Signatures):") + print("Ring Decred Ethereum Improvement") + print("Size (Pure Go) (libsecp256k1) (% faster)") + print("-" * 4 + " " + "-" * 15 + " " + "-" * 15 + " " + "-" * 11) + + sizes = sorted(set(size for op, size in results.keys() if op == "Sign")) + for size in sizes[:5]: # Limit to first 5 sizes + key = ("Sign", size) + if key in results and "Decred" in results[key] and "Ethereum" in results[key]: + decred = results[key]["Decred"] + ethereum = results[key]["Ethereum"] + improvement = ((decred - ethereum) / decred) * 100 + print(f"{size:<4} {format_time(decred):<15} {format_time(ethereum):<15} {improvement:.0f}%") + + print() + # Print Verify performance + print("🔍 VERIFY PERFORMANCE (Ring Signatures):") + print("Ring Decred Ethereum Improvement") + print("Size (Pure Go) (libsecp256k1) (% faster)") + print("-" * 4 + " " + "-" * 15 + " " + "-" * 15 + " " + "-" * 11) + + sizes = sorted(set(size for op, size in results.keys() if op == "Verify")) + for size in sizes[:5]: # Limit to first 5 sizes + key = ("Verify", size) + if key in results and "Decred" in results[key] and "Ethereum" in results[key]: + decred = results[key]["Decred"] + ethereum = results[key]["Ethereum"] + improvement = ((decred - ethereum) / decred) * 100 + print(f"{size:<4} {format_time(decred):<15} {format_time(ethereum):<15} {improvement:.0f}%") + + print() + print("=" * 70) + +def main(): + """Main entry point.""" + if "--run-benchmarks" in sys.argv: + # Run benchmarks directly + results = run_benchmarks() + print_comparison(results) + else: + # Parse from stdin (for piped input) + results = parse_stdin() + print_comparison(results) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/go.mod b/go.mod index bac16d2..6c0dbe6 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,19 @@ -module github.com/noot/ring-go +module github.com/pokt-network/ring-go -go 1.19 +go 1.26 require ( - filippo.io/edwards25519 v1.0.0 - github.com/athanorlabs/go-dleq v0.1.0 - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 - github.com/stretchr/testify v1.7.2 - golang.org/x/crypto v0.24.0 + filippo.io/edwards25519 v1.2.0 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 + github.com/pokt-network/go-dleq v0.0.0-20250925202155-488f42ad642a + github.com/stretchr/testify v1.11.1 + golang.org/x/crypto v0.53.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/kr/text v0.2.0 // indirect + github.com/ethereum/go-ethereum v1.17.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 18e0dd7..4523749 100644 --- a/go.sum +++ b/go.sum @@ -1,26 +1,29 @@ -filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= -filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= -github.com/athanorlabs/go-dleq v0.1.0 h1:0/llWZG8fz2uintMBKOiBC502zCsDA8nt8vxI73W9Qc= -github.com/athanorlabs/go-dleq v0.1.0/go.mod h1:DWry6jSD7A13MKmeZA0AX3/xBeQCXDoygX99VPwL3yU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/ethereum/go-ethereum v1.17.4 h1:uA4q+qiLp7QImBsjdRbINu8iX6OEVmj4DPc5/E5Fsxc= +github.com/ethereum/go-ethereum v1.17.4/go.mod h1:qMdgwqqRAen+aT8P7KKQKi0Qt6RzG4cfejVAbCpJgqA= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +github.com/pokt-network/go-dleq v0.0.0-20250925202155-488f42ad642a h1:zgSFcOX8m9dmriiqPiRbGJJpR3sJCFxLskxQ7/ofS0o= +github.com/pokt-network/go-dleq v0.0.0-20250925202155-488f42ad642a/go.mod h1:KVsT8HO2EXHwMY1wnyYhR/lSZFNFIit8dJ+aDb1dSkA= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/helpers.go b/helpers.go index 0d58988..e3d9e42 100644 --- a/helpers.go +++ b/helpers.go @@ -1,15 +1,15 @@ package ring import ( + "fmt" + "filippo.io/edwards25519" "filippo.io/edwards25519/field" - "fmt" dsecp256k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/pokt-network/go-dleq/ed25519" + "github.com/pokt-network/go-dleq/secp256k1" + "github.com/pokt-network/go-dleq/types" "golang.org/x/crypto/sha3" - - "github.com/athanorlabs/go-dleq/ed25519" - "github.com/athanorlabs/go-dleq/secp256k1" - "github.com/athanorlabs/go-dleq/types" ) func hashToCurve(pk types.Point) types.Point { @@ -125,7 +125,7 @@ func hashToCurveSecp256k1(pk *secp256k1.PointImpl) *secp256k1.PointImpl { for i := 0; i < safety; i++ { ok := dsecp256k1.DecompressY(fe, false, maybeY) if ok { - return secp256k1.NewPointFromCoordinates(*fe, *maybeY) + return newPointFromFieldVals(fe, maybeY) } hash = sha3.Sum256(hash[:]) diff --git a/helpers_test.go b/helpers_test.go index bc17158..785a9b8 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -8,14 +8,14 @@ import ( func TestHashToCurveSecp256k1(t *testing.T) { curve := Secp256k1() - privkey := curve.NewRandomScalar() - p := hashToCurve(curve.ScalarBaseMul(privkey)) + privKey := curve.NewRandomScalar() + p := hashToCurve(curve.ScalarBaseMul(privKey)) require.NotNil(t, p) } func TestHashToCurveEd25519(t *testing.T) { curve := Ed25519() - privkey := curve.NewRandomScalar() - p := hashToCurve(curve.ScalarBaseMul(privkey)) + privKey := curve.NewRandomScalar() + p := hashToCurve(curve.ScalarBaseMul(privKey)) require.NotNil(t, p) } diff --git a/ring.go b/ring.go index 6d68bb6..ed80280 100644 --- a/ring.go +++ b/ring.go @@ -4,8 +4,8 @@ import ( "errors" "fmt" - "github.com/athanorlabs/go-dleq/ed25519" - "github.com/athanorlabs/go-dleq/types" + "github.com/pokt-network/go-dleq/ed25519" + "github.com/pokt-network/go-dleq/types" ) // Ring represents a group of public keys such that one of the group created a signature. @@ -58,13 +58,13 @@ func (r *RingSig) Ring() *Ring { return r.ring } -// NewKeyRingFromPublicKeys takes public key ring and places the public key corresponding to `privkey` +// 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 NewKeyRingFromPublicKeys(curve types.Curve, pubkeys []types.Point, privkey types.Scalar, idx int) (*Ring, error) { +func NewKeyRingFromPublicKeys(curve types.Curve, pubkeys []types.Point, privKey types.Scalar, idx int) (*Ring, error) { size := len(pubkeys) + 1 newRing := make([]types.Point, size) - pubkey := curve.ScalarBaseMul(privkey) + pubkey := curve.ScalarBaseMul(privKey) if idx > len(pubkeys) { return nil, errors.New("index out of bounds: idx > len(pubkeys)") @@ -75,7 +75,7 @@ func NewKeyRingFromPublicKeys(curve types.Curve, pubkeys []types.Point, privkey } // ensure that privkey is nonzero - if privkey.IsZero() { + if privKey.IsZero() { return nil, errors.New("private key is zero") } @@ -128,20 +128,20 @@ func NewFixedKeyRingFromPublicKeys(curve types.Curve, pubkeys []types.Point) (*R } // NewKeyRing creates a ring with size specified by `size` and places the public key corresponding -// to `privkey` in index idx of the ring. +// to `privKey` in index idx of the ring. // It returns a ring of public keys of length `size`. -func NewKeyRing(curve types.Curve, size int, privkey types.Scalar, idx int) (*Ring, error) { +func NewKeyRing(curve types.Curve, size int, privKey types.Scalar, idx int) (*Ring, error) { if idx >= size { return nil, errors.New("index out of bounds") } // ensure that privkey is nonzero - if privkey.IsZero() { + if privKey.IsZero() { return nil, errors.New("private key is zero") } ring := make([]types.Point, size) - pubkey := curve.ScalarBaseMul(privkey) + pubkey := curve.ScalarBaseMul(privKey) ring[idx] = pubkey for i := 0; i < size; i++ { @@ -160,9 +160,9 @@ func NewKeyRing(curve types.Curve, size int, privkey types.Scalar, idx int) (*Ri // 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 (r *Ring) Sign(m [32]byte, privkey types.Scalar) (*RingSig, error) { +func (r *Ring) Sign(m [32]byte, privKey types.Scalar) (*RingSig, error) { ourIdx := -1 - pubkey := r.curve.ScalarBaseMul(privkey) + pubkey := r.curve.ScalarBaseMul(privKey) for i, pk := range r.pubkeys { if pk.Equals(pubkey) { ourIdx = i @@ -174,12 +174,12 @@ func (r *Ring) Sign(m [32]byte, privkey types.Scalar) (*RingSig, error) { return nil, errors.New("failed to find given key in public key set") } - return Sign(m, r, privkey, ourIdx) + return Sign(m, r, privKey, ourIdx) } // Sign creates a ring signature on the given message using the provided private key // and ring of public keys. -func Sign(m [32]byte, ring *Ring, privkey types.Scalar, ourIdx int) (*RingSig, error) { +func Sign(m [32]byte, ring *Ring, privKey types.Scalar, ourIdx int) (*RingSig, error) { size := len(ring.pubkeys) if size < 2 { return nil, errors.New("size of ring less than two") @@ -190,23 +190,52 @@ func Sign(m [32]byte, ring *Ring, privkey types.Scalar, ourIdx int) (*RingSig, e } // ensure that privkey is nonzero - if privkey.IsZero() { + if privKey.IsZero() { return nil, errors.New("private key is zero") } // check that key at index s is indeed the signer - pubkey := ring.curve.ScalarBaseMul(privkey) + pubkey := ring.curve.ScalarBaseMul(privKey) if !ring.pubkeys[ourIdx].Equals(pubkey) { return nil, errors.New("secret index in ring is not signer") } + // compute the remaining per-signer values and delegate to the shared core. + // Plain Sign uses no precomputed member hashes and keeps the self-checks on, + // so its work profile is identical to the un-optimized implementation. This + // is the consensus-safe path; off-chain callers wanting the precompute/cache + // use SignerContext + SignWithContext instead. + h := hashToCurve(pubkey) + image := ring.curve.ScalarMul(privKey, h) + return signInternal(m, ring, privKey, ourIdx, pubkey, h, image, nil, false) +} + +// signInternal is the core ring-signing routine. It accepts the signer's +// pre-computed public key P[j], hash-to-curve point h = H_p(P[j]), and key +// image I = x*h so a SignerContext can skip re-deriving them each signature. +// +// memberHashes, if non-nil, supplies H_p(P[i]) for every ring member so they +// need not be recomputed; when nil each member's hash-to-curve is computed +// inline (the consensus-safe default, no shared cache / no state). skipCheck +// omits the closing self-checks (off-chain opt-in only). +// +// The caller is responsible for validating that pubkey corresponds to privKey +// at ourIdx; the size and index guards below are kept as cheap safety nets. +func signInternal(m [32]byte, ring *Ring, privKey types.Scalar, ourIdx int, pubkey, h, image types.Point, memberHashes []types.Point, skipCheck bool) (*RingSig, error) { + size := len(ring.pubkeys) + if size < 2 { + return nil, errors.New("size of ring less than two") + } + if ourIdx >= size { + return nil, errors.New("secret index out of range of ring size") + } + // setup curve := ring.curve - h := hashToCurve(pubkey) sig := &RingSig{ ring: ring, - // calculate key image I = x * H_p(P) where H_p is a hash-to-curve function - image: curve.ScalarMul(privkey, h), + // key image I = x * H_p(P) where H_p is a hash-to-curve function + image: image, } // start at c[j] @@ -241,7 +270,12 @@ func Sign(m [32]byte, ring *Ring, privkey types.Scalar, ourIdx int) (*RingSig, e // calculate R_i = s_i*H_p(P_i) + c_i*I cI := curve.ScalarMul(c[idx], sig.image) - hp := hashToCurve(ring.pubkeys[idx]) + var hp types.Point + if memberHashes != nil { + hp = memberHashes[idx] // precomputed by SignerContext (off-chain) + } else { + hp = hashToCurve(ring.pubkeys[idx]) // consensus-safe default: inline + } sH := curve.ScalarMul(s[idx], hp) r := cI.Add(sH) @@ -250,31 +284,19 @@ func Sign(m [32]byte, ring *Ring, privkey types.Scalar, ourIdx int) (*RingSig, e } // close ring by finding s[j] = u - c[j]*x - cx := c[ourIdx].Mul(privkey) + cx := c[ourIdx].Mul(privKey) s[ourIdx] = u.Sub(cx) - // check that u*G = s[j]*G + c[j]*P[j] - cP := curve.ScalarMul(c[ourIdx], pubkey) - sG := curve.ScalarBaseMul(s[ourIdx]) - lNew := cP.Add(sG) - if !lNew.Equals(l) { - // this should not happen - return nil, errors.New("failed to close ring: uG != sG + cP") - } - - // check that u*H_p(P[j]) = s[j]*H_p(P[j]) + c[j]*I - cI := curve.ScalarMul(c[ourIdx], sig.image) - sH := curve.ScalarMul(s[ourIdx], h) - rNew := cI.Add(sH) - if !rNew.Equals(r) { - // this should not happen - return nil, errors.New("failed to close ring: uH(P) != sH(P) + cI") - } - - // check that H(m, L[j], R[j]) == c[j+1] - cCheck := challenge(ring.curve, m, l, r) - if !cCheck.Eq(c[(ourIdx+1)%size]) { - return nil, errors.New("challenge check failed") + // Self-check that the ring closes correctly. These re-derivations are an + // algebraic identity given correct arithmetic, so they only guard against a + // buggy curve backend, at a cost of ~4 scalar multiplications per signature. + // Always run on the plain Sign path; skippable only via SignerContext for + // off-chain callers that accept the trade-off. + if !skipCheck { + if err := verifyRingCloses(curve, m, l, r, pubkey, sig.image, h, + c[ourIdx], c[(ourIdx+1)%size], s[ourIdx]); err != nil { + return nil, err + } } // everything ok, add values to signature @@ -283,6 +305,93 @@ func Sign(m [32]byte, ring *Ring, privkey types.Scalar, ourIdx int) (*RingSig, e return sig, nil } +// 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. +type SignerContext struct { + pubkey types.Point // P = x*G (public) + h types.Point // H_p(P) == memberHashes[idx] (public) + image types.Point // key image I = x*H_p(P) (public; published in signatures) + idx int // signer's index in the ring + memberHashes []types.Point // H_p(P[i]) for every ring member (public) + + // 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 +} + +// 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 (r *Ring) NewSignerContext(privKey types.Scalar) (*SignerContext, error) { + if privKey.IsZero() { + return nil, errors.New("private key is zero") + } + + pubkey := r.curve.ScalarBaseMul(privKey) + + idx := -1 + for i, pk := range r.pubkeys { + if pk.Equals(pubkey) { + idx = i + break + } + } + if idx == -1 { + return nil, errors.New("private key does not correspond to any public key in the ring") + } + + // Pre-compute H_p(P[i]) for every member; the signer's own h is memberHashes[idx]. + memberHashes := make([]types.Point, len(r.pubkeys)) + for i, pk := range r.pubkeys { + memberHashes[i] = hashToCurve(pk) + } + h := memberHashes[idx] + image := r.curve.ScalarMul(privKey, h) + + return &SignerContext{ + pubkey: pubkey, + h: h, + image: image, + idx: idx, + memberHashes: memberHashes, + }, nil +} + +// 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. +func (r *Ring) SignWithContext(m [32]byte, privKey types.Scalar, ctx *SignerContext) (*RingSig, error) { + return signInternal(m, r, privKey, ctx.idx, ctx.pubkey, ctx.h, ctx.image, ctx.memberHashes, ctx.SkipSelfCheck) +} + // Verify verifies the ring signature for the given message. // It returns true if a valid signature, false otherwise. func (sig *RingSig) Verify(m [32]byte) bool { @@ -333,7 +442,11 @@ func Link(sigA, sigB *RingSig) bool { } func challenge(curve types.Curve, m [32]byte, l, r types.Point) types.Scalar { - t := append(m[:], append(l.Encode(), r.Encode()...)...) + le, re := l.Encode(), r.Encode() + t := make([]byte, 0, len(m)+len(le)+len(re)) + t = append(t, m[:]...) + t = append(t, le...) + t = append(t, re...) c, err := curve.HashToScalar(t) if err != nil { // this should not happen @@ -341,3 +454,48 @@ func challenge(curve types.Curve, m [32]byte, l, r types.Point) types.Scalar { } return c } + +// verifyRingCloses re-derives the signer's L[j] and R[j] from the closing +// scalar s[j] and checks they match the values (l = u*G, r = u*H_p(P[j])) +// chosen at the start of signing, then checks the challenge chain wraps around. +// +// Given correct curve arithmetic these equalities hold by construction +// (s[j] = u - c[j]*x), so they are a defense-in-depth guard against a buggy +// curve backend producing a malformed signature, not a security requirement. +// They cost ~4 scalar multiplications per Sign and run on every plain Sign; +// SignWithContext can skip them via SignerContext.SkipSelfCheck. +// +// l, r: u*G and u*H_p(P[j]) from the start of signing +// pubkey: signer's public key P[j] +// image: key image I +// h: H_p(P[j]) +// cJ: c[j] +// cNext: c[j+1] +// sJ: closing scalar s[j] +func verifyRingCloses( + curve types.Curve, + m [32]byte, + l, r, pubkey, image, h types.Point, + cJ, cNext, sJ types.Scalar, +) error { + // check that u*G = s[j]*G + c[j]*P[j] + cP := curve.ScalarMul(cJ, pubkey) + sG := curve.ScalarBaseMul(sJ) + if !cP.Add(sG).Equals(l) { + return errors.New("failed to close ring: uG != sG + cP") + } + + // check that u*H_p(P[j]) = s[j]*H_p(P[j]) + c[j]*I + cI := curve.ScalarMul(cJ, image) + sH := curve.ScalarMul(sJ, h) + if !cI.Add(sH).Equals(r) { + return errors.New("failed to close ring: uH(P) != sH(P) + cI") + } + + // check that H(m, L[j], R[j]) == c[j+1] + if !challenge(curve, m, l, r).Eq(cNext) { + return errors.New("challenge check failed") + } + + return nil +} diff --git a/ring_test.go b/ring_test.go index ebbf864..6bc6828 100644 --- a/ring_test.go +++ b/ring_test.go @@ -6,7 +6,7 @@ import ( "math/big" "testing" - "github.com/athanorlabs/go-dleq/types" + "github.com/pokt-network/go-dleq/types" "github.com/stretchr/testify/require" "golang.org/x/crypto/sha3" ) @@ -17,14 +17,14 @@ var ( func createSigWithCurve(t *testing.T, curve types.Curve, size, idx int) *RingSig { // instantiate private key - privkey := curve.NewRandomScalar() + privKey := curve.NewRandomScalar() // generate keyring - keyring, err := NewKeyRing(curve, size, privkey, idx) + keyring, err := NewKeyRing(curve, size, privKey, idx) require.NoError(t, err) // sign message - sig, err := keyring.Sign(testMsg, privkey) + sig, err := keyring.Sign(testMsg, privKey) require.NoError(t, err) return sig } @@ -56,8 +56,8 @@ func TestSign_Loop_Secp256k1(t *testing.T) { func TestNewKeyRing(t *testing.T) { curve := Secp256k1() - privkey := curve.NewRandomScalar() - keyring, err := NewKeyRing(curve, 2, privkey, 0) + privKey := curve.NewRandomScalar() + keyring, err := NewKeyRing(curve, 2, privKey, 0) require.NoError(t, err) require.NotNil(t, keyring) require.Equal(t, 2, len(keyring.pubkeys)) @@ -65,8 +65,8 @@ func TestNewKeyRing(t *testing.T) { func TestNewKeyRing3(t *testing.T) { curve := Secp256k1() - privkey := curve.NewRandomScalar() - keyring, err := NewKeyRing(curve, 3, privkey, 1) + privKey := curve.NewRandomScalar() + keyring, err := NewKeyRing(curve, 3, privKey, 1) require.NoError(t, err) require.NotNil(t, keyring) require.Equal(t, 3, len(keyring.pubkeys)) @@ -74,14 +74,14 @@ func TestNewKeyRing3(t *testing.T) { func TestNewKeyRing_IdxOutOfBounds(t *testing.T) { curve := Secp256k1() - privkey := curve.NewRandomScalar() - _, err := NewKeyRing(curve, 2, privkey, 3) + privKey := curve.NewRandomScalar() + _, err := NewKeyRing(curve, 2, privKey, 3) require.Error(t, err) } func TestGenKeyRing(t *testing.T) { curve := Secp256k1() - privkey := curve.NewRandomScalar() + privKey := curve.NewRandomScalar() s := 0 size := 3 @@ -92,29 +92,29 @@ func TestGenKeyRing(t *testing.T) { pubkeys[i] = curve.ScalarBaseMul(priv) } - keyring, err := NewKeyRingFromPublicKeys(curve, pubkeys, privkey, s) + keyring, err := NewKeyRingFromPublicKeys(curve, pubkeys, privKey, s) require.NoError(t, err) require.NotNil(t, keyring) require.Equal(t, size+1, keyring.Size()) - require.True(t, keyring.pubkeys[s].Equals(curve.ScalarBaseMul(privkey))) + require.True(t, keyring.pubkeys[s].Equals(curve.ScalarBaseMul(privKey))) - fixedkeys := make([]types.Point, size+1) - fixedkeys[0] = curve.ScalarBaseMul(privkey) - copy(fixedkeys[1:], pubkeys) - keyring, err = NewFixedKeyRingFromPublicKeys(curve, fixedkeys) + fixedKeys := make([]types.Point, size+1) + fixedKeys[0] = curve.ScalarBaseMul(privKey) + copy(fixedKeys[1:], pubkeys) + keyring, err = NewFixedKeyRingFromPublicKeys(curve, fixedKeys) require.NoError(t, err) require.NotNil(t, keyring) for i := 0; i < size; i++ { - require.True(t, keyring.pubkeys[i].Equals(fixedkeys[i])) + require.True(t, keyring.pubkeys[i].Equals(fixedKeys[i])) } } func TestRing_Equals(t *testing.T) { curve := Secp256k1() - privkey := curve.NewRandomScalar() - keyring, err := NewKeyRing(curve, 10, privkey, 0) + privKey := curve.NewRandomScalar() + keyring, err := NewKeyRing(curve, 10, privKey, 0) require.NoError(t, err) - keyring2, err := NewKeyRing(curve, 10, privkey, 0) + keyring2, err := NewKeyRing(curve, 10, privKey, 0) require.NoError(t, err) require.False(t, keyring.Equals(keyring2)) // NewKeyRing generates random pubkeys keyring3, err := NewFixedKeyRingFromPublicKeys(curve, keyring.pubkeys) @@ -124,14 +124,14 @@ func TestRing_Equals(t *testing.T) { func TestSig_RingEquals(t *testing.T) { curve := Secp256k1() - privkey := curve.NewRandomScalar() - keyring, err := NewKeyRing(curve, 10, privkey, 0) + privKey := curve.NewRandomScalar() + keyring, err := NewKeyRing(curve, 10, privKey, 0) require.NoError(t, err) - keyring2, err := NewKeyRing(curve, 10, privkey, 0) + keyring2, err := NewKeyRing(curve, 10, privKey, 0) require.NoError(t, err) - sig, err := keyring.Sign(testMsg, privkey) + sig, err := keyring.Sign(testMsg, privKey) require.NoError(t, err) - sig2, err := keyring2.Sign(testMsg, privkey) + sig2, err := keyring2.Sign(testMsg, privKey) require.NoError(t, err) require.False(t, sig.Ring().Equals(keyring2)) require.True(t, sig.Ring().Equals(keyring)) @@ -169,23 +169,23 @@ func TestVerifyWrongMessage(t *testing.T) { func TestLinkabilityTrue(t *testing.T) { curve := Secp256k1() - privkey := curve.NewRandomScalar() + privKey := curve.NewRandomScalar() msg1 := "helloworld" msgHash1 := sha3.Sum256([]byte(msg1)) - keyring1, err := NewKeyRing(curve, 2, privkey, 0) + keyring1, err := NewKeyRing(curve, 2, privKey, 0) require.NoError(t, err) - sig1, err := keyring1.Sign(msgHash1, privkey) + sig1, err := keyring1.Sign(msgHash1, privKey) require.NoError(t, err) msg2 := "hello world" msgHash2 := sha3.Sum256([]byte(msg2)) - keyring2, err := NewKeyRing(curve, 2, privkey, 0) + keyring2, err := NewKeyRing(curve, 2, privKey, 0) require.NoError(t, err) - sig2, err := keyring2.Sign(msgHash2, privkey) + sig2, err := keyring2.Sign(msgHash2, privKey) require.NoError(t, err) require.True(t, Link(sig1, sig2)) } @@ -226,24 +226,164 @@ func TestLinkabilityTrue_imageSmallSubgroup(t *testing.T) { func TestLinkabilityFalse(t *testing.T) { curve := Secp256k1() - privkey1 := curve.NewRandomScalar() + privKey1 := curve.NewRandomScalar() msg1 := "helloworld" msgHash1 := sha3.Sum256([]byte(msg1)) - keyring1, err := NewKeyRing(curve, 2, privkey1, 0) + keyring1, err := NewKeyRing(curve, 2, privKey1, 0) require.NoError(t, err) - sig1, err := keyring1.Sign(msgHash1, privkey1) + sig1, err := keyring1.Sign(msgHash1, privKey1) require.NoError(t, err) - privkey2 := curve.NewRandomScalar() + privKey2 := curve.NewRandomScalar() msg2 := "hello world" msgHash2 := sha3.Sum256([]byte(msg2)) - keyring2, err := NewKeyRing(curve, 2, privkey2, 0) + keyring2, err := NewKeyRing(curve, 2, privKey2, 0) require.NoError(t, err) - sig2, err := keyring2.Sign(msgHash2, privkey2) + sig2, err := keyring2.Sign(msgHash2, privKey2) require.NoError(t, err) require.False(t, Link(sig1, sig2)) } + +func TestSign_OneKey_Fails(t *testing.T) { + curve := Secp256k1() + privKey := curve.NewRandomScalar() + keyring, err := NewKeyRing(curve, 1, privKey, 0) + require.NoError(t, err) + require.NotNil(t, keyring) + require.Equal(t, 1, len(keyring.pubkeys)) + _, err = keyring.Sign(testMsg, privKey) + require.Error(t, err) + require.Equal(t, "size of ring less than two", err.Error()) +} + +// TestSignWithContext tests that SignWithContext produces valid signatures +func TestSignWithContext_Secp256k1(t *testing.T) { + curve := Secp256k1() + privKey := curve.NewRandomScalar() + keyring, err := NewKeyRing(curve, 10, privKey, 3) + require.NoError(t, err) + + ctx, err := keyring.NewSignerContext(privKey) + require.NoError(t, err) + require.NotNil(t, ctx) + + // Sign multiple messages with the same context + for i := 0; i < 5; i++ { + msg := sha3.Sum256([]byte("message" + string(rune(i)))) + sig, err := keyring.SignWithContext(msg, privKey, ctx) + require.NoError(t, err) + require.True(t, sig.Verify(msg)) + } +} + +func TestSignWithContext_Ed25519(t *testing.T) { + curve := Ed25519() + privKey := curve.NewRandomScalar() + keyring, err := NewKeyRing(curve, 10, privKey, 3) + require.NoError(t, err) + + ctx, err := keyring.NewSignerContext(privKey) + require.NoError(t, err) + require.NotNil(t, ctx) + + for i := 0; i < 5; i++ { + msg := sha3.Sum256([]byte("message" + string(rune(i)))) + sig, err := keyring.SignWithContext(msg, privKey, ctx) + require.NoError(t, err) + require.True(t, sig.Verify(msg)) + } +} + +func TestSignWithContext_InvalidPrivKey(t *testing.T) { + curve := Secp256k1() + privKey := curve.NewRandomScalar() + wrongPrivKey := curve.NewRandomScalar() + keyring, err := NewKeyRing(curve, 10, privKey, 3) + require.NoError(t, err) + + _, err = keyring.NewSignerContext(wrongPrivKey) + require.Error(t, err) + require.Contains(t, err.Error(), "does not correspond to any public key") +} + +func TestSignWithContext_ZeroPrivKey(t *testing.T) { + curve := Secp256k1() + privKey := curve.NewRandomScalar() + keyring, err := NewKeyRing(curve, 10, privKey, 3) + require.NoError(t, err) + + zeroKey := curve.ScalarFromBytes([32]byte{}) + _, err = keyring.NewSignerContext(zeroKey) + require.Error(t, err) + require.Contains(t, err.Error(), "private key is zero") +} + +func TestSignWithContext_Linkability(t *testing.T) { + curve := Secp256k1() + privKey := curve.NewRandomScalar() + + keyring1, err := NewKeyRing(curve, 5, privKey, 2) + require.NoError(t, err) + keyring2, err := NewKeyRing(curve, 5, privKey, 2) + require.NoError(t, err) + + ctx1, err := keyring1.NewSignerContext(privKey) + require.NoError(t, err) + ctx2, err := keyring2.NewSignerContext(privKey) + require.NoError(t, err) + + msg1 := sha3.Sum256([]byte("message1")) + msg2 := sha3.Sum256([]byte("message2")) + + sig1, err := keyring1.SignWithContext(msg1, privKey, ctx1) + require.NoError(t, err) + sig2, err := keyring2.SignWithContext(msg2, privKey, ctx2) + require.NoError(t, err) + + // Same private key should produce linkable signatures + require.True(t, Link(sig1, sig2)) +} + +func TestSignWithContext_MatchesSign(t *testing.T) { + // SignWithContext and Sign use different randomness, so signatures differ, + // but both must verify and share the same key image (linkable). + curve := Secp256k1() + privKey := curve.NewRandomScalar() + keyring, err := NewKeyRing(curve, 10, privKey, 5) + require.NoError(t, err) + + ctx, err := keyring.NewSignerContext(privKey) + require.NoError(t, err) + + sig1, err := keyring.Sign(testMsg, privKey) + require.NoError(t, err) + + sig2, err := keyring.SignWithContext(testMsg, privKey, ctx) + require.NoError(t, err) + + require.True(t, sig1.Verify(testMsg)) + require.True(t, sig2.Verify(testMsg)) + require.True(t, Link(sig1, sig2)) +} + +// TestSignWithContext_KeyMismatchRejected checks that a context is not a way to +// sign with the wrong key: because the context holds no secret and the key is +// passed per call, a mismatched key is caught by the closing self-check. +func TestSignWithContext_KeyMismatchRejected(t *testing.T) { + curve := Secp256k1() + privKey := curve.NewRandomScalar() + keyring, err := NewKeyRing(curve, 5, privKey, 2) + require.NoError(t, err) + + ctx, err := keyring.NewSignerContext(privKey) + require.NoError(t, err) + + // Sign with a key that does not match the context; self-check must reject it. + wrongKey := curve.NewRandomScalar() + _, err = keyring.SignWithContext(testMsg, wrongKey, ctx) + require.Error(t, err) +} diff --git a/secp256k1_decred.go b/secp256k1_decred.go new file mode 100644 index 0000000..d688d8b --- /dev/null +++ b/secp256k1_decred.go @@ -0,0 +1,15 @@ +//go:build !ethereum_secp256k1 +// +build !ethereum_secp256k1 + +package ring + +import ( + dsecp256k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/pokt-network/go-dleq/secp256k1" +) + +// newPointFromFieldVals creates a secp256k1 point from Decred FieldVal coordinates +// For Decred backend, use FieldVal directly +func newPointFromFieldVals(fe, maybeY *dsecp256k1.FieldVal) *secp256k1.PointImpl { + return secp256k1.NewPointFromCoordinates(*fe, *maybeY) +} diff --git a/secp256k1_ethereum.go b/secp256k1_ethereum.go new file mode 100644 index 0000000..76ad940 --- /dev/null +++ b/secp256k1_ethereum.go @@ -0,0 +1,23 @@ +//go:build cgo && ethereum_secp256k1 +// +build cgo,ethereum_secp256k1 + +package ring + +import ( + "math/big" + + dsecp256k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/pokt-network/go-dleq/secp256k1" +) + +// newPointFromFieldVals creates a secp256k1 point from Decred FieldVal coordinates +// For Ethereum backend, convert FieldVal to *big.Int +func newPointFromFieldVals(fe, maybeY *dsecp256k1.FieldVal) *secp256k1.PointImpl { + // Convert FieldVal to *big.Int for Ethereum backend API + // Need to normalize first for proper byte representation + fe.Normalize() + maybeY.Normalize() + x := new(big.Int).SetBytes(fe.Bytes()[:]) + y := new(big.Int).SetBytes(maybeY.Bytes()[:]) + return secp256k1.NewPointFromCoordinates(x, y) +} diff --git a/serde.go b/serde.go index b0f8d4f..951c000 100644 --- a/serde.go +++ b/serde.go @@ -5,7 +5,7 @@ import ( "encoding/binary" "errors" - "github.com/athanorlabs/go-dleq/types" + "github.com/pokt-network/go-dleq/types" ) // Serialize converts the signature to a byte array. diff --git a/serde_test.go b/serde_test.go index 588b5e3..4f747a9 100644 --- a/serde_test.go +++ b/serde_test.go @@ -10,13 +10,13 @@ import ( ) func testSerializeAndDeserialize(t *testing.T, curve Curve, size, idx int) { - privkey := curve.NewRandomScalar() + privKey := curve.NewRandomScalar() msgHash := sha3.Sum256([]byte("helloworld")) - keyring, err := NewKeyRing(curve, size, privkey, idx) + keyring, err := NewKeyRing(curve, size, privKey, idx) require.NoError(t, err) - sig, err := Sign(msgHash, keyring, privkey, idx) + sig, err := Sign(msgHash, keyring, privKey, idx) require.NoError(t, err) byteSig, err := sig.Serialize() diff --git a/types.go b/types.go index e8eb4ce..99c373d 100644 --- a/types.go +++ b/types.go @@ -1,9 +1,9 @@ package ring import ( - "github.com/athanorlabs/go-dleq/ed25519" - "github.com/athanorlabs/go-dleq/secp256k1" - "github.com/athanorlabs/go-dleq/types" + "github.com/pokt-network/go-dleq/ed25519" + "github.com/pokt-network/go-dleq/secp256k1" + "github.com/pokt-network/go-dleq/types" ) type ( @@ -16,7 +16,9 @@ func Ed25519() types.Curve { return ed25519.NewCurve() } -// Secp256k1 returns a new secp256k1 curve instance +// Secp256k1 returns a new secp256k1 curve instance. +// BUILD-TIME CONFIGURATION: With ethereum_secp256k1 build tag, +// expensive operations are accelerated using libsecp256k1 via go-ethereum. func Secp256k1() types.Curve { return secp256k1.NewCurve() }