diff --git a/zk/pedersen.go b/zk/pedersen.go index 379bcb0..99237b0 100644 --- a/zk/pedersen.go +++ b/zk/pedersen.go @@ -72,6 +72,20 @@ func VerifyOpening(descriptor map[string]any, encodedValue uint64, blindingScala return false, fmt.Errorf("descriptor carries no commitment to open") } + // A value outside the declared domain cannot be what was committed, whatever + // the blinding. Checking it here rather than letting the commitment simply + // fail to match means the answer names the real reason, and matches the Rust + // core. + if encoding, ok := descriptor["encoding"].(map[string]any); ok { + minimum, hasMin := encoding["semantic_min_encoded"].(float64) + maximum, hasMax := encoding["semantic_max_encoded"].(float64) + if hasMin && hasMax { + if float64(encodedValue) < minimum || float64(encodedValue) > maximum { + return false, nil + } + } + } + raw, err := hex.DecodeString(blindingScalar) if err != nil || len(raw) != 32 { return false, fmt.Errorf("opening blinding is not 32 hex-encoded bytes") diff --git a/zk/pedersen_test.go b/zk/pedersen_test.go index 59a5a6a..0961947 100644 --- a/zk/pedersen_test.go +++ b/zk/pedersen_test.go @@ -130,6 +130,20 @@ func TestAWidenedEncodingDomainIsNotTheCurveChecksBusiness(t *testing.T) { } } +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)