From 3aff9fa0fbe540d8360554a875b0129e28701463 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 21 Aug 2026 18:37:21 +0200 Subject: [PATCH] feat(attesto3): pin the range statement and its width across all three SDKs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- provenance.go | 88 ++++++++++++++++++++++++++++++++++ zk_range_result_parity_test.go | 70 +++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) diff --git a/provenance.go b/provenance.go index 513a006..ba46b51 100644 --- a/provenance.go +++ b/provenance.go @@ -573,3 +573,91 @@ func InspectPredicateResult(result map[string]any, capsuleInclusion *bool) (*Pre NotClaimed: notClaimed, }, nil } + +// ZKRangeWidths are the proof widths ATTESTO-ZK-RANGE-001 permits. Pinned rather +// than derived: a client offering a width outside this set would build +// statements the proving library refuses after the transcript is already bound. +var ZKRangeWidths = [4]uint{8, 16, 32, 64} + +// zkRangeStatementFields is exactly what a range statement carries. Every field +// is folded into the proof transcript, so an extra one would bind to nothing and +// a missing one would change the challenges. +var zkRangeStatementFields = map[string]struct{}{ + "capsule_root": {}, "claim_id": {}, "claim_descriptor_version": {}, + "provider_id": {}, "provider_version": {}, "commitment_c": {}, + "predicate_type": {}, "lower_bound": {}, "upper_bound": {}, + "encoding_version": {}, "proof_scheme_version": {}, "verifier_nonce": {}, +} + +// ZKRangeWidth returns the smallest permitted width covering the whole interval. +// +// Both proved differences are bounded by upper-lower, so one width serves both. +// It is derived from the public bounds and never chosen by the prover: a prover +// who picked it could prove a wider range than the statement says. +func ZKRangeWidth(lowerBound, upperBound uint64) (uint, error) { + if upperBound < lowerBound { + return 0, fmt.Errorf("upper bound is below its lower bound") + } + span := upperBound - lowerBound + for _, width := range ZKRangeWidths { + if width == 64 || span < (uint64(1)<