noble rejects multiply(0n) exactly as libsodium rejects a zero scalar, and the TypeScript SDK read the refusal as an invalid opening the same way Python did. Both now multiply a zero scalar to the identity by hand. gtank/ristretto255 accepts it, so the Go module was correct all along. Three implementations: two agreed with each other and both were wrong, and the one that matched the Rust core stood alone. That is the argument for a shared corpus rather than per-language tests -- two implementations agreeing is not evidence. The zero vector is now consumed by all three, so the coverage contract holds every client to it: 27 of 27 in Python, Go and TypeScript. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
207 lines
7.5 KiB
Go
207 lines
7.5 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestAMeasurementOfZeroOpensItsCommitment pins the case that split the SDKs.
|
|
//
|
|
// Zero is legal wherever semantic_min_encoded is 0: a detector reporting exactly
|
|
// 0.0 produces one, and C = 0*B + r*H is an ordinary commitment to it. Two of
|
|
// the three clients got it wrong in the same way -- libsodium and noble both
|
|
// refuse a scalar multiplication whose result is the identity, which is right
|
|
// for a key exchange and wrong here, and both SDKs read the refusal as an
|
|
// invalid opening. This library accepts the zero scalar and was correct.
|
|
//
|
|
// That is the argument for a shared corpus rather than per-language tests: two
|
|
// implementations agreeing is not evidence, and the one that matched the Rust
|
|
// core was the odd one out.
|
|
func TestAMeasurementOfZeroOpensItsCommitment(t *testing.T) {
|
|
vector := loadVector(t, "provenance-private-numeric-opening-zero-value")
|
|
descriptor, value, blinding := openingFrom(t, vector)
|
|
if value != 0 {
|
|
t.Fatalf("vector is not the zero case: got %d", value)
|
|
}
|
|
ok, err := VerifyOpening(descriptor, value, blinding)
|
|
if err != nil {
|
|
t.Fatalf("verify: %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatal("a measurement of zero must open its commitment")
|
|
}
|
|
}
|
|
|
|
// TestZeroIsNotASkeletonKey guards the shape of the fix the other SDKs needed.
|
|
func TestZeroIsNotASkeletonKey(t *testing.T) {
|
|
zero := loadVector(t, "provenance-private-numeric-opening-zero-value")
|
|
other := loadVector(t, "provenance-private-numeric-opening-valid")
|
|
zeroDescriptor, _, zeroBlinding := openingFrom(t, zero)
|
|
otherDescriptor, _, otherBlinding := openingFrom(t, other)
|
|
|
|
for _, probe := range []struct {
|
|
name string
|
|
descriptor map[string]any
|
|
value uint64
|
|
blinding string
|
|
}{
|
|
{"zero against another commitment", otherDescriptor, 0, otherBlinding},
|
|
{"non-zero against the zero commitment", zeroDescriptor, 1, zeroBlinding},
|
|
{"zero blinding against a real commitment", zeroDescriptor, 0, strings.Repeat("00", 32)},
|
|
} {
|
|
ok, err := VerifyOpening(probe.descriptor, probe.value, probe.blinding)
|
|
if err == nil && ok {
|
|
t.Fatalf("%s: opened a commitment it must not", probe.name)
|
|
}
|
|
}
|
|
}
|