The statement is what gets folded into the proof transcript, and the width is derived from its bounds. A client that ordered the fields differently or picked a different width would produce proofs nobody else could verify — and the symptom would read as a broken proof rather than a divergent implementation. Both are pure arithmetic and canonical JSON, so every SDK can check them and now does. Each client gains `zk_range_width` and `validate_range_statement`. The field set is exact rather than a minimum: an extra field would bind to nothing and a missing one would change the challenges. A float bound is refused rather than truncated, which is the encoding registry's whole purpose one layer up. The width table is checked in as a vector and the Rust core asserts against that file directly rather than against a second copy of the table. Changing one now fails the other, which a duplicated constant would not have done. Corpus coverage: 17/17 provenance and 12/12 zk-range in Python, Go and TypeScript. The cross-SDK vector item is closed; Sprint 12 is down to three open items, all of which need something local work cannot supply — other architectures, a curve-library decision, and a UI. Python 107, Go ok, TypeScript 119, Local Vault 375, edge 117. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
223 lines
7.5 KiB
Go
223 lines
7.5 KiB
Go
package attesto
|
|
|
|
// Go parity against the ATTESTO-ZK-RANGE-001 result corpus.
|
|
//
|
|
// The cryptography of a range proof is not checked here and cannot be: this SDK
|
|
// carries no ristretto255 arithmetic. What is checked is what an SDK can get
|
|
// wrong on its own — reporting the issuer's word as its own finding, or handing
|
|
// a consumer an object shaped like a verdict.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func zkRangeVectorDir(t *testing.T) string {
|
|
t.Helper()
|
|
dir := filepath.Join("..", "..", "golden-vectors", "zk-range-v0.1-dev")
|
|
if _, err := os.Stat(dir); err != nil {
|
|
t.Fatalf("no zk-range vectors at %s: %v", dir, err)
|
|
}
|
|
return dir
|
|
}
|
|
|
|
func loadZKRangeVector(t *testing.T, name string) map[string]any {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(filepath.Join(zkRangeVectorDir(t), name+".json"))
|
|
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 TestZKRangeCorpusIsPresentAndTyped(t *testing.T) {
|
|
entries, err := filepath.Glob(filepath.Join(zkRangeVectorDir(t), "*.json"))
|
|
if err != nil || len(entries) == 0 {
|
|
t.Fatalf("no zk-range vectors: %v", err)
|
|
}
|
|
for _, entry := range entries {
|
|
raw, err := os.ReadFile(entry)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", entry, err)
|
|
}
|
|
var vector map[string]any
|
|
if err := json.Unmarshal(raw, &vector); err != nil {
|
|
t.Fatalf("parse %s: %v", entry, err)
|
|
}
|
|
if vector["protocol"] != ZKRangeProtocol {
|
|
t.Fatalf("%s: wrong protocol %v", entry, vector["protocol"])
|
|
}
|
|
if vector["requires"] == nil {
|
|
t.Fatalf("%s: does not declare what it requires", entry)
|
|
}
|
|
switch vector["expectation"] {
|
|
case "valid", "invalid", "rejected", "differs":
|
|
default:
|
|
t.Fatalf("%s: bad expectation %v", entry, vector["expectation"])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestZKRangeClientWithoutCurveArithmeticSaysSo(t *testing.T) {
|
|
// The whole point. This SDK cannot verify the proof and reports that; the
|
|
// issuer's own verification block is kept under a separate key so a reader
|
|
// can tell a claim apart from a check.
|
|
vector := loadZKRangeVector(t, "zk-range-result-valid")
|
|
result := vector["result"].(map[string]any)
|
|
|
|
report, err := InspectPredicateResult(result, nil)
|
|
if err != nil {
|
|
t.Fatalf("inspect: %v", err)
|
|
}
|
|
if report.VerifiedHere["zk_predicate"] != "not_checked" {
|
|
t.Fatalf("this client cannot verify a proof but reported %q", report.VerifiedHere["zk_predicate"])
|
|
}
|
|
expected := vector["expected_verified_here"].(map[string]any)
|
|
for key, want := range expected {
|
|
if report.VerifiedHere[key] != want.(string) {
|
|
t.Fatalf("verified_here[%s]: got %q want %v", key, report.VerifiedHere[key], want)
|
|
}
|
|
}
|
|
if report.ReportedByIssuer["zk_predicate"] != "verified" {
|
|
t.Fatalf("the issuer's own claim was not carried through separately")
|
|
}
|
|
}
|
|
|
|
func TestZKRangeInclusionTheClientCheckedIsReported(t *testing.T) {
|
|
// Not everything is out of reach: two-hop inclusion is SHA-256.
|
|
vector := loadZKRangeVector(t, "zk-range-result-valid")
|
|
result := vector["result"].(map[string]any)
|
|
|
|
for _, testCase := range []struct {
|
|
checked bool
|
|
want string
|
|
}{{true, "verified"}, {false, "failed"}} {
|
|
checked := testCase.checked
|
|
report, err := InspectPredicateResult(result, &checked)
|
|
if err != nil {
|
|
t.Fatalf("inspect: %v", err)
|
|
}
|
|
if report.VerifiedHere["capsule_inclusion"] != testCase.want {
|
|
t.Fatalf("capsule_inclusion: got %q want %q", report.VerifiedHere["capsule_inclusion"], testCase.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestZKRangeResultCarriesTheThreeNonClaims(t *testing.T) {
|
|
vector := loadZKRangeVector(t, "zk-range-result-valid")
|
|
report, err := InspectPredicateResult(vector["result"].(map[string]any), nil)
|
|
if err != nil {
|
|
t.Fatalf("inspect: %v", err)
|
|
}
|
|
if len(report.NotClaimed) != len(RequiredNonClaims) {
|
|
t.Fatalf("expected %d non-claims, got %d", len(RequiredNonClaims), len(report.NotClaimed))
|
|
}
|
|
for index, required := range RequiredNonClaims {
|
|
if report.NotClaimed[index]["id"] != required {
|
|
t.Fatalf("non-claim %d: got %v want %s", index, report.NotClaimed[index]["id"], required)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestZKRangeMisleadingResultsAreRefused(t *testing.T) {
|
|
for _, name := range []string{
|
|
"zk-range-result-missing-non-claim",
|
|
"zk-range-result-verdict-field",
|
|
"zk-range-result-nested-verdict-field",
|
|
"zk-range-result-unbound",
|
|
"zk-range-result-unsupported-version",
|
|
} {
|
|
vector := loadZKRangeVector(t, name)
|
|
if vector["expectation"] != "rejected" {
|
|
t.Fatalf("%s: expected a rejected vector", name)
|
|
}
|
|
if _, err := InspectPredicateResult(vector["result"].(map[string]any), nil); err == nil {
|
|
t.Fatalf("%s was accepted", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestZKRangeRefusalsAreNotBlanket(t *testing.T) {
|
|
// The refusals above must not be a function that refuses everything.
|
|
vector := loadZKRangeVector(t, "zk-range-result-valid")
|
|
if _, err := InspectPredicateResult(vector["result"].(map[string]any), nil); err != nil {
|
|
t.Fatalf("a well-formed result was refused: %v", err)
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------- statement and width
|
|
|
|
func TestZKRangeStatementCarriesExactlyTheTranscriptBoundFields(t *testing.T) {
|
|
// Every field is folded into the proof transcript. An extra one would bind to
|
|
// nothing; a missing one would change the challenges. So the set is exact.
|
|
vector := loadZKRangeVector(t, "zk-range-statement-valid")
|
|
statement := vector["statement"].(map[string]any)
|
|
|
|
expected := vector["expected_fields"].([]any)
|
|
if len(statement) != len(expected) {
|
|
t.Fatalf("statement carries %d fields, corpus lists %d", len(statement), len(expected))
|
|
}
|
|
for _, field := range expected {
|
|
if _, ok := statement[field.(string)]; !ok {
|
|
t.Fatalf("statement is missing %v", field)
|
|
}
|
|
}
|
|
|
|
width, err := ValidateRangeStatement(statement)
|
|
if err != nil {
|
|
t.Fatalf("validate: %v", err)
|
|
}
|
|
if float64(width) != vector["expected_width"].(float64) {
|
|
t.Fatalf("width: got %d want %v", width, vector["expected_width"])
|
|
}
|
|
}
|
|
|
|
func TestZKRangeWidthIsDerivedFromThePublicBounds(t *testing.T) {
|
|
// Pinned across languages because a divergent width is invisible: a client
|
|
// picking a different one produces proofs nobody else can verify, and the
|
|
// symptom looks like a broken proof rather than a divergent rule.
|
|
vector := loadZKRangeVector(t, "zk-range-width-selection")
|
|
for _, raw := range vector["cases"].([]any) {
|
|
testCase := raw.(map[string]any)
|
|
lower := uint64(testCase["lower_bound"].(float64))
|
|
upper := uint64(testCase["upper_bound"].(float64))
|
|
width, err := ZKRangeWidth(lower, upper)
|
|
if err != nil {
|
|
t.Fatalf("[%d, %d]: %v", lower, upper, err)
|
|
}
|
|
if float64(width) != testCase["expected_width"].(float64) {
|
|
t.Fatalf("[%d, %d]: got %d want %v", lower, upper, width, testCase["expected_width"])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestZKRangeDishonestStatementsAreRefused(t *testing.T) {
|
|
for _, name := range []string{
|
|
"zk-range-statement-float-bound",
|
|
"zk-range-statement-inverted",
|
|
"zk-range-statement-unknown-field",
|
|
"zk-range-statement-empty-nonce",
|
|
} {
|
|
vector := loadZKRangeVector(t, name)
|
|
if vector["expectation"] != "rejected" {
|
|
t.Fatalf("%s: expected a rejected vector", name)
|
|
}
|
|
if _, err := ValidateRangeStatement(vector["statement"].(map[string]any)); err == nil {
|
|
t.Fatalf("%s was accepted", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestZKRangeStatementRefusalsAreNotBlanket(t *testing.T) {
|
|
vector := loadZKRangeVector(t, "zk-range-statement-valid")
|
|
if _, err := ValidateRangeStatement(vector["statement"].(map[string]any)); err != nil {
|
|
t.Fatalf("a well-formed statement was refused: %v", err)
|
|
}
|
|
}
|