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>
156 lines
5.4 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|