Files
attesto-go/zk/pedersen_test.go
T
CodexandClaude Opus 5 0b881e1a74 feat(sdk): verify Pedersen openings in all three clients, without imposing a curve library
The eight private-numeric vectors were a declared boundary: verifying them needs
ristretto255 scalar arithmetic no SDK carried. Adding one everywhere would have
cost something real — the Go and TypeScript SDKs have *zero* dependencies, which
is a property their consumers get for free today.

So it is optional in each, and the shape differs per ecosystem: a `attesto[zk]`
extra in Python, an optional peer dependency in TypeScript, and a separate
`go.attesto.eu/sdk/zk` module in Go. A consumer who never opens a private
numeric inherits nothing. `not_checked` now means "this installation did not
check" rather than "nobody can", which is a better answer to the same question.

Each was verified against a real Rust commitment before being chosen: pysodium
over libsodium, @noble/curves, and gtank/ristretto255 all reproduce the core's
bytes exactly. PyNaCl was tried first and ruled out — 1.6.2 exposes no
ristretto255 bindings at all.

This closes Pedersen opening verification, not range proofs. A range proof needs
a full bulletproofs implementation, not curve arithmetic, and stays the core's
job.

Two things the last vector forced:

* A value outside the descriptor's declared domain now returns invalid for the
  right reason. The commitment would fail to match anyway, but attributing that
  to the arithmetic when the real answer is "that value is outside the declared
  domain" blames the wrong layer. All three match the Rust core here.
* A skipped suite is a gate that proves nothing, so CI sets
  ATTESTO_REQUIRE_ZK_EXTRA and an environment that was supposed to install the
  dependency and did not now fails instead of reporting green over skips.

Corpus coverage is 25/25 and 12/12 in all three languages, with no exemption
left. The exemption mechanism is removed rather than emptied: reintroducing one
should be a visible decision, not a constant someone left lying around.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 19:15:20 +02:00

156 lines
5.4 KiB
Go

package zk
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
// Go parity on the Pedersen opening vectors.
//
// These were a declared boundary: verifying them needs ristretto255 scalar
// arithmetic the dependency-free SDK does not carry. This module carries it, so
// a consumer who needs exact-opening verification can have it without imposing a
// curve library on everyone else.
func loadVector(t *testing.T, name string) map[string]any {
t.Helper()
path := filepath.Join("..", "..", "..", "golden-vectors", "provenance-v0.1-dev", name+".json")
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", name, err)
}
var vector map[string]any
if err := json.Unmarshal(raw, &vector); err != nil {
t.Fatalf("parse %s: %v", name, err)
}
return vector
}
func openingFrom(t *testing.T, vector map[string]any) (map[string]any, uint64, string) {
t.Helper()
descriptor := vector["descriptor"].(map[string]any)
value := uint64(vector["encoded_value"].(float64))
return descriptor, value, vector["blinding_scalar"].(string)
}
func TestGeneratorPairMatchesTheFrozenRegistry(t *testing.T) {
// Constants that drifted would commit under a different pair than the
// protocol declares, and every commitment would be unopenable elsewhere.
raw, err := os.ReadFile(filepath.Join("..", "..", "..", "docs", "protocol", "zk-generator-registry.md"))
if err != nil {
t.Fatalf("read registry: %v", err)
}
registry := string(raw)
for _, pinned := range []string{GeneratorSetID, GeneratorBHex, GeneratorHHex} {
if !strings.Contains(registry, pinned) {
t.Fatalf("registry no longer carries %s", pinned)
}
}
}
func TestAValidOpeningReproducesTheRustCommitment(t *testing.T) {
vector := loadVector(t, "provenance-private-numeric-opening-valid")
descriptor, value, blinding := openingFrom(t, vector)
ok, err := VerifyOpening(descriptor, value, blinding)
if err != nil {
t.Fatalf("verify: %v", err)
}
if ok != vector["expected_valid"].(bool) {
t.Fatalf("got %v want %v", ok, vector["expected_valid"])
}
}
func TestAWrongOpeningDoesNotReproduceTheCommitment(t *testing.T) {
for _, name := range []string{
"provenance-private-numeric-opening-wrong-value",
"provenance-private-numeric-opening-wrong-blinding",
} {
vector := loadVector(t, name)
descriptor, value, blinding := openingFrom(t, vector)
ok, err := VerifyOpening(descriptor, value, blinding)
if err != nil {
t.Fatalf("%s: verify: %v", name, err)
}
if ok != vector["expected_valid"].(bool) {
t.Fatalf("%s: got %v want %v", name, ok, vector["expected_valid"])
}
}
}
func TestTheCommittedCommitmentIsWhatAValidOpeningProduces(t *testing.T) {
// Reads the commitment back out, so a check that always returned true fails.
vector := loadVector(t, "provenance-private-numeric-commitment-valid")
descriptor := vector["descriptor"].(map[string]any)
value := uint64(vector["expected_encoded_value"].(float64))
blinding := vector["blinding_scalar"].(string)
ok, err := VerifyOpening(descriptor, value, blinding)
if err != nil || !ok {
t.Fatalf("a valid opening did not verify: ok=%v err=%v", ok, err)
}
pedersen := descriptor["pedersen"].(map[string]any)
pedersen["commitment"] = strings.Repeat("00", 32)
ok, err = VerifyOpening(descriptor, value, blinding)
if err != nil {
t.Fatalf("verify: %v", err)
}
if ok {
t.Fatal("a tampered commitment still verified")
}
}
func TestTheSameMeasurementUnderTwoBlindingsIsUnlinkable(t *testing.T) {
vector := loadVector(t, "provenance-private-numeric-commitment-randomized")
if vector["first_commitment"] == vector["second_commitment"] {
t.Fatal("two blindings produced the same commitment")
}
}
func TestADescriptorNamingAnotherGeneratorSetIsRefused(t *testing.T) {
// "rejected" is not "invalid": the input is refused before any check runs.
vector := loadVector(t, "provenance-private-numeric-wrong-generator-set")
descriptor, value, blinding := openingFrom(t, vector)
if _, err := VerifyOpening(descriptor, value, blinding); err == nil {
t.Fatal("a foreign generator set was accepted")
}
}
func TestAWidenedEncodingDomainIsNotTheCurveChecksBusiness(t *testing.T) {
// A claim about semantics is refused by whoever validates the profile. The
// commitment still opens, and saying otherwise would blame the wrong layer.
vector := loadVector(t, "provenance-private-numeric-encoding-mismatch")
descriptor, value, blinding := openingFrom(t, vector)
ok, err := VerifyOpening(descriptor, value, blinding)
if err != nil || !ok {
t.Fatalf("the commitment should still open: ok=%v err=%v", ok, err)
}
}
func TestAValueOutsideTheDeclaredDomainCannotOpen(t *testing.T) {
// "invalid", not "rejected": the check ran and answered no. The commitment
// would fail to match anyway, but for the wrong reason.
vector := loadVector(t, "provenance-private-numeric-semantic-bound-invalid")
descriptor, value, blinding := openingFrom(t, vector)
ok, err := VerifyOpening(descriptor, value, blinding)
if err != nil {
t.Fatalf("verify: %v", err)
}
if ok != vector["expected_valid"].(bool) {
t.Fatalf("got %v want %v", ok, vector["expected_valid"])
}
}
func TestAMalformedOpeningIsRefused(t *testing.T) {
vector := loadVector(t, "provenance-private-numeric-opening-valid")
descriptor, value, _ := openingFrom(t, vector)
for _, blinding := range []string{"nothex", strings.Repeat("ab", 31), "", strings.Repeat("ff", 32)} {
if _, err := VerifyOpening(descriptor, value, blinding); err == nil {
t.Fatalf("a malformed blinding %q was accepted", blinding)
}
}
}