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) <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user