From c1481e17dde9cc1ac8e49d4bb7ea3eba6d913fc0 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 22 Aug 2026 18:10:15 +0200 Subject: [PATCH] fix(sdk): TypeScript had the same zero-value bug, and Go did not 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) --- zk/pedersen_test.go | 51 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/zk/pedersen_test.go b/zk/pedersen_test.go index 0961947..19d16e4 100644 --- a/zk/pedersen_test.go +++ b/zk/pedersen_test.go @@ -153,3 +153,54 @@ func TestAMalformedOpeningIsRefused(t *testing.T) { } } } + +// 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) + } + } +}