From 4e5d9db5305102592b65210e0c4c517d4ddedb5a Mon Sep 17 00:00:00 2001 From: Jason Odoom Date: Sat, 5 Sep 2026 21:47:08 +0000 Subject: [PATCH] Pin why an inclusion receipt was refused All 33 reject vectors on this surface asserted a verdict and nothing else, so a verifier that refused for an unrelated reason satisfied every one of them. Each now names the check that must refuse it, both runners assert it, and the generator will not emit a reject vector without one. Two gate defects surfaced while doing it. The Go test cache does not track the corpus files the tests read, so a mutated expectation reported ok in 0.004 seconds and caught nothing, where the same run with -count=1 caught it. And a reject vector whose input the Go parser refused at the boundary skipped every assertion, so a parser that refused everything satisfied it. Three cases refuse at the boundary in Go and at a later step in the reference, because the raw-text checks required before the parse run there and not in the reference, which is handed an already-parsed object. They are listed by name rather than hidden. Signed-off-by: Jason Odoom --- .github/workflows/ci.yml | 6 +- .github/workflows/weekly-conformance.yml | 6 +- CHANGELOG.md | 14 +++ conformance/v1/generate.mjs | 50 +++++++++- conformance/v1/manifest.json | 2 +- conformance/v1/schema.json | 3 + conformance/v1/vectors/inclusion-receipt.json | 99 ++++++++++++------- go/ink/conformance_test.go | 37 ++++++- go/ink/receipt.go | 49 ++++++--- governance/releases/1.0-readiness-evidence.md | 2 +- scripts/check-doc-facts.ts | 11 +++ test/conformance.test.ts | 12 ++- 12 files changed, 230 insertions(+), 61 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c69649f..f8cc488e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -174,7 +174,7 @@ jobs: run: | test -z "$(gofmt -l .)" go vet ./... - go test ./... + go test -count=1 ./... CGO_ENABLED=0 go build -o "${RUNNER_TEMP:-/tmp}/ink" ./cmd/ink CGO_ENABLED=0 go build -o "${RUNNER_TEMP:-/tmp}/ink-verify-server" ./cmd/ink-verify-server CGO_ENABLED=0 go build -o "${RUNNER_TEMP:-/tmp}/ink-witness-server" ./cmd/ink-witness-server @@ -222,9 +222,9 @@ jobs: nix develop --command bash -c ' set -euo pipefail cd go - go test ./ink/ + go test -count=1 ./ink/ log="${RUNNER_TEMP:-/tmp}/staged.log" - go test ./ink/ -run TestAgentCardSignaturePhaseC -v | tee "$log" + go test -count=1 ./ink/ -run TestAgentCardSignaturePhaseC -v | tee "$log" # A skip exits 0, so assert the staged test actually ran and passed. grep -q "^--- PASS: TestAgentCardSignaturePhaseC" "$log" ' diff --git a/.github/workflows/weekly-conformance.yml b/.github/workflows/weekly-conformance.yml index 16b8b856..f9ceef74 100644 --- a/.github/workflows/weekly-conformance.yml +++ b/.github/workflows/weekly-conformance.yml @@ -239,7 +239,7 @@ jobs: - name: Go conformance and manifest integrity working-directory: go - run: go test ./... + run: go test -count=1 ./... - name: Go verifier, staged vectors working-directory: go @@ -247,9 +247,9 @@ jobs: INK_STAGED_CONFORMANCE: "1" run: | set -euo pipefail - go test ./ink/ + go test -count=1 ./ink/ log="${RUNNER_TEMP:-/tmp}/staged.log" - go test ./ink/ -run TestAgentCardSignaturePhaseC -v > "$log" + go test -count=1 ./ink/ -run TestAgentCardSignaturePhaseC -v > "$log" cat "$log" grep -q '^--- PASS: TestAgentCardSignaturePhaseC' "$log" diff --git a/CHANGELOG.md b/CHANGELOG.md index 8578cada..c6837540 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,20 @@ here. Pre-1.0 releases follow `0.Y.Z` semantics, see ### Changes +- Inclusion-receipt reject vectors name the check that must refuse them, and + both runners assert it. Every one of the 33 asserted a verdict and nothing + else, so a verifier that refused for an unrelated reason satisfied them. Go + gained a step alongside its verdict, the generator refuses to emit a reject + vector without one, and the corpus schema carries the member. +- Go conformance runs pass `-count=1`. The Go test cache does not track the + corpus files the tests read, so a vector edit could be validated against a + cached pass: mutating an expected step reported ok in 0.004 seconds and + caught nothing, while the same run with `-count=1` caught it. The rule is + gated, so an invocation without it fails the fact check. +- The Go conformance runner asserts a reject vector that its parser refused at + the boundary. Those cases used to skip every assertion, so a parser that + refused everything satisfied them. + - The card verifier rejects a present member of the wrong type wherever it reads one, rather than at the top level only. A non-string `currentSigningKeyId` was compared as a value by the reference and read as diff --git a/conformance/v1/generate.mjs b/conformance/v1/generate.mjs index a6b12c4b..b4b4c175 100644 --- a/conformance/v1/generate.mjs +++ b/conformance/v1/generate.mjs @@ -182,6 +182,11 @@ function writeSchema() { properties: { result: { type: "string", enum: ["accept", "reject"] }, reason: { type: "string" }, + // The check that refused, where the surface has one. A vector + // pinning only the verdict is satisfied by a refusal from the + // wrong check, so a reject vector names the step wherever the + // implementations can express it. + step: { type: "string" }, auditEvent: { type: "string" }, canonicalPrincipal: { type: "string" }, keyStatus: { type: "string", enum: ["active", "retired", "revoked"] }, @@ -1890,8 +1895,51 @@ const validReceipt = await makeReceipt(); function rcptAccept(caseId, description, input) { return { caseId, description, input, expect: { result: "accept" } }; } +// The check that MUST refuse each rejected receipt, asserted by both runners. +// Coarser than a per-case reason, since thirteen cases share `structure`, but +// it is what both implementations can express today. +const RECEIPT_REJECT_STEP = { + "checkpoint-bad-root-rejects": "checkpoint", + "checkpoint-fork-rejects": "checkpoint", + "checkpoint-negative-size-rejects": "checkpoint", + "checkpoint-rollback-rejects": "checkpoint", + "empty-event-id-rejects": "structure", + "empty-signature-rejects": "structure", + "empty-timestamp-rejects": "structure", + "event-hash-bad-format-rejects": "proof", + "event-hash-not-in-tree-rejects": "proof", + "event-id-mismatch-rejects": "proof", + "event-leaf-not-at-index-rejects": "proof", + "event-missing-id-rejects": "proof", + "leaf-index-ge-tree-size-rejects": "structure", + "malformed-signature-rejects": "signature", + "negative-leaf-index-rejects": "structure", + "non-integer-leaf-index-rejects": "structure", + "null-event-rejects": "proof", + "proof-bad-element-rejects": "structure", + "proof-not-array-rejects": "structure", + "proof-too-long-rejects": "structure", + "receipt-not-object-rejects": "structure", + "short-root-hash-rejects": "structure", + "surrogate-in-event-id-rejects": "signature", + "surrogate-in-event-rejects": "proof", + "tampered-event-id-rejects": "signature", + "tampered-leaf-index-rejects": "signature", + "tampered-proof-rejects": "proof", + "tampered-root-hash-rejects": "signature", + "tampered-timestamp-rejects": "signature", + "tampered-tree-size-rejects": "signature", + "uppercase-root-hash-rejects": "structure", + "wrong-witness-key-rejects": "signature", + "zero-tree-size-rejects": "structure", +}; + function rcptReject(caseId, description, input) { - return { caseId, description, input, expect: { result: "reject" } }; + const step = RECEIPT_REJECT_STEP[caseId]; + // A new reject vector has to declare which check refuses it. Generating one + // without a step would quietly reintroduce the verdict-only assertion. + if (!step) throw new Error(`inclusion-receipt: no reject step declared for ${caseId}`); + return { caseId, description, input, expect: { result: "reject", step } }; } const wpk = { witnessPublicKeyHex: publicKeyHex }; diff --git a/conformance/v1/manifest.json b/conformance/v1/manifest.json index 0a9622b5..f67be29f 100644 --- a/conformance/v1/manifest.json +++ b/conformance/v1/manifest.json @@ -153,7 +153,7 @@ "spec": "specs/ink-inclusion-receipt.md", "summary": "Composite inclusion-receipt verification.", "caseCount": 39, - "sha256": "e8f41905b2b482ac82cac2262f92f99b14045ffe8060c69a94de29e99b431e2f" + "sha256": "2be8699b9d047b6692c329902882ae12af99a00e9a4c7fd40b32164de2336ec0" }, { "id": "jcs-number", diff --git a/conformance/v1/schema.json b/conformance/v1/schema.json index 6b43d635..307229ed 100644 --- a/conformance/v1/schema.json +++ b/conformance/v1/schema.json @@ -119,6 +119,9 @@ "reason": { "type": "string" }, + "step": { + "type": "string" + }, "auditEvent": { "type": "string" }, diff --git a/conformance/v1/vectors/inclusion-receipt.json b/conformance/v1/vectors/inclusion-receipt.json index aafa1f17..6b96da8e 100644 --- a/conformance/v1/vectors/inclusion-receipt.json +++ b/conformance/v1/vectors/inclusion-receipt.json @@ -32,7 +32,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -54,7 +55,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -76,7 +78,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -98,7 +101,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -120,7 +124,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -142,7 +147,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -164,7 +170,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -183,7 +190,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -268,7 +276,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -289,7 +298,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -311,7 +321,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -333,7 +344,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -355,7 +367,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "signature" } }, { @@ -377,7 +390,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "signature" } }, { @@ -399,7 +413,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "signature" } }, { @@ -421,7 +436,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "signature" } }, { @@ -443,7 +459,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "signature" } }, { @@ -465,7 +482,8 @@ "witnessPublicKeyHex": "8dd3649d02ecde7b08636d86b15a923bc2ac3d301e82b58c18241661aeec4a2b" }, "expect": { - "result": "reject" + "result": "reject", + "step": "signature" } }, { @@ -487,7 +505,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "signature" } }, { @@ -541,7 +560,8 @@ } }, "expect": { - "result": "reject" + "result": "reject", + "step": "proof" } }, { @@ -568,7 +588,8 @@ } }, "expect": { - "result": "reject" + "result": "reject", + "step": "proof" } }, { @@ -594,7 +615,8 @@ } }, "expect": { - "result": "reject" + "result": "reject", + "step": "proof" } }, { @@ -640,7 +662,8 @@ "eventHash": "not-a-hash" }, "expect": { - "result": "reject" + "result": "reject", + "step": "proof" } }, { @@ -663,7 +686,8 @@ "eventHash": "1a01d742673069afdd4ae9b6643939e94935869dcfb605bc71624469c2a54dd0" }, "expect": { - "result": "reject" + "result": "reject", + "step": "proof" } }, { @@ -690,7 +714,8 @@ } }, "expect": { - "result": "reject" + "result": "reject", + "step": "proof" } }, { @@ -768,7 +793,8 @@ } }, "expect": { - "result": "reject" + "result": "reject", + "step": "checkpoint" } }, { @@ -794,7 +820,8 @@ } }, "expect": { - "result": "reject" + "result": "reject", + "step": "checkpoint" } }, { @@ -820,7 +847,8 @@ } }, "expect": { - "result": "reject" + "result": "reject", + "step": "checkpoint" } }, { @@ -846,7 +874,8 @@ } }, "expect": { - "result": "reject" + "result": "reject", + "step": "checkpoint" } }, { @@ -899,7 +928,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "signature" } }, { @@ -925,7 +955,8 @@ } }, "expect": { - "result": "reject" + "result": "reject", + "step": "proof" } }, { @@ -947,7 +978,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -970,7 +1002,8 @@ "event": null }, "expect": { - "result": "reject" + "result": "reject", + "step": "proof" } } ] diff --git a/go/ink/conformance_test.go b/go/ink/conformance_test.go index a53fe21e..f6178583 100644 --- a/go/ink/conformance_test.go +++ b/go/ink/conformance_test.go @@ -69,6 +69,7 @@ type conformanceCase struct { Expect struct { Result string `json:"result"` Reason string `json:"reason"` + Step string `json:"step"` AuditEvent string `json:"auditEvent"` CanonicalPrincipal string `json:"canonicalPrincipal"` KeyStatus string `json:"keyStatus"` @@ -603,6 +604,26 @@ func TestAuditQueryResponse(t *testing.T) { } } +// boundaryReject records that the receiver's parser refused the input before the +// verifier ran. That is a structural rejection, so the vector's step still has +// to match: skipping the assertion here let a reject vector pass without any +// check at all, which is the verdict-only weakness one layer lower. +func expectBoundaryStep(t *testing.T, caseID, wantStep, what string) { + t.Helper() + if wantStep == "" || wantStep == "structure" { + return + } + // These refuse here rather than at the step the vector names: this runner + // applies the raw-text checks the enforcement order requires before the + // parse, and the reference runner cannot, being handed a parsed object. + // Listed by name so the set cannot widen silently. + switch caseID { + case "surrogate-in-event-rejects", "null-event-rejects", "surrogate-in-event-id-rejects": + return + } + t.Errorf("%s: %s refused at the boundary (step structure), want step %q", caseID, what, wantStep) +} + func TestInclusionReceipt(t *testing.T) { vf := loadVectors(t, "inclusion-receipt") for _, c := range vf.Cases { @@ -624,6 +645,8 @@ func TestInclusionReceipt(t *testing.T) { if !ok { if want { t.Errorf("%s: receipt failed to parse but vector expects accept", c.CaseID) + } else { + expectBoundaryStep(t, c.CaseID, c.Expect.Step, "receipt") } continue } @@ -637,6 +660,8 @@ func TestInclusionReceipt(t *testing.T) { if err != nil { if want { t.Errorf("%s: event failed to parse but vector expects accept: %v", c.CaseID, err) + } else { + expectBoundaryStep(t, c.CaseID, c.Expect.Step, "event") } continue } @@ -644,6 +669,8 @@ func TestInclusionReceipt(t *testing.T) { if !isObj { if want { t.Errorf("%s: event is not an object but vector expects accept", c.CaseID) + } else { + expectBoundaryStep(t, c.CaseID, c.Expect.Step, "event") } continue } @@ -668,8 +695,14 @@ func TestInclusionReceipt(t *testing.T) { opts.LaterCheckpoint = &cp } - if got := VerifyInclusionReceipt(receipt, pub, opts); got != want { - t.Errorf("%s: VerifyInclusionReceipt = %v, want %v", c.CaseID, got, want) + got, step := VerifyInclusionReceiptStep(receipt, pub, opts) + if got != want { + t.Errorf("%s: VerifyInclusionReceipt = %v, want %v (step %s)", c.CaseID, got, want, step) + } + // The step is the reason for this surface. A vector pinning only the + // verdict is satisfied by a refusal from the wrong check. + if c.Expect.Step != "" && string(step) != c.Expect.Step { + t.Errorf("%s: step = %q, want %q", c.CaseID, step, c.Expect.Step) } } } diff --git a/go/ink/receipt.go b/go/ink/receipt.go index 2787abde..8d7c1b78 100644 --- a/go/ink/receipt.go +++ b/go/ink/receipt.go @@ -290,8 +290,28 @@ func verifyReceiptSignatureWith(r InclusionReceipt, s signerStrategy, artifactMs // optionally walks the inclusion proof (when Event or EventHash is given) and // cross-checks a later checkpoint (when LaterCheckpoint is given). It returns // false, never panics, on any failed step. +// ReceiptRejectStep names the check that refused a receipt, using the +// reference's step names, so a vector can pin why and not merely that. +type ReceiptRejectStep string + +const ( + ReceiptStepNone ReceiptRejectStep = "" + ReceiptStepStructure ReceiptRejectStep = "structure" + ReceiptStepSignature ReceiptRejectStep = "signature" + ReceiptStepProof ReceiptRejectStep = "proof" + ReceiptStepCheckpoint ReceiptRejectStep = "checkpoint" +) + func VerifyInclusionReceipt(receipt InclusionReceipt, witnessPublicKey []byte, opts ReceiptVerifyOptions) bool { - return verifyInclusionReceiptWith(receipt, fixedKey(witnessPublicKey), opts).Verified + ok, _ := VerifyInclusionReceiptStep(receipt, witnessPublicKey, opts) + return ok +} + +// VerifyInclusionReceiptStep is VerifyInclusionReceipt plus the step that +// refused. On acceptance the step is ReceiptStepNone. +func VerifyInclusionReceiptStep(receipt InclusionReceipt, witnessPublicKey []byte, opts ReceiptVerifyOptions) (bool, ReceiptRejectStep) { + result, step := verifyInclusionReceiptWith(receipt, fixedKey(witnessPublicKey), opts) + return result.Verified, step } // VerifyInclusionReceiptWithKeys verifies an INK inclusion receipt (INK @@ -312,24 +332,25 @@ func VerifyInclusionReceipt(receipt InclusionReceipt, witnessPublicKey []byte, o // matching the "no key attribution for a rejection" rule // VerifyInkSignatureForLiveAuth documents elsewhere in this package. func VerifyInclusionReceiptWithKeys(receipt InclusionReceipt, keys []CandidateKey, hintKeyID string, opts ReceiptVerifyOptions) MultiKeyResult { - return verifyInclusionReceiptWith(receipt, candidateKeys(keys, hintKeyID), opts) + result, _ := verifyInclusionReceiptWith(receipt, candidateKeys(keys, hintKeyID), opts) + return result } -func verifyInclusionReceiptWith(receipt InclusionReceipt, s signerStrategy, opts ReceiptVerifyOptions) MultiKeyResult { +func verifyInclusionReceiptWith(receipt InclusionReceipt, s signerStrategy, opts ReceiptVerifyOptions) (MultiKeyResult, ReceiptRejectStep) { if !checkReceiptShape(receipt) { - return MultiKeyResult{} + return MultiKeyResult{}, ReceiptStepStructure } var artifactMs int64 if s.needsClock { ms, ok := ParseInkTimestampMs(receipt.Timestamp) if !ok { - return MultiKeyResult{} + return MultiKeyResult{}, ReceiptStepSignature } artifactMs = ms } result := verifyReceiptSignatureWith(receipt, s, artifactMs) if !result.Verified { - return MultiKeyResult{} + return MultiKeyResult{}, ReceiptStepSignature } var leafHash string @@ -337,33 +358,33 @@ func verifyInclusionReceiptWith(receipt InclusionReceipt, s signerStrategy, opts if opts.Event != nil { id, ok := opts.Event["id"].(string) if !ok || id != receipt.EventID { - return MultiKeyResult{} + return MultiKeyResult{}, ReceiptStepProof } h, ok := ComputeAuditMerkleLeafHash(opts.Event) if !ok { - return MultiKeyResult{} + return MultiKeyResult{}, ReceiptStepProof } leafHash, hasLeaf = h, true } else if opts.EventHash != "" { if !isMerkleHashHex(opts.EventHash) { - return MultiKeyResult{} + return MultiKeyResult{}, ReceiptStepProof } leafHash, hasLeaf = opts.EventHash, true } if hasLeaf && !VerifyInclusionProof(leafHash, receipt.InclusionProof, receipt.LeafIndex, receipt.TreeSize, receipt.RootHash) { - return MultiKeyResult{} + return MultiKeyResult{}, ReceiptStepProof } if cp := opts.LaterCheckpoint; cp != nil { if cp.TreeSize < 0 || !isMerkleHashHex(cp.RootHash) { - return MultiKeyResult{} + return MultiKeyResult{}, ReceiptStepCheckpoint } if cp.TreeSize < receipt.TreeSize { - return MultiKeyResult{} + return MultiKeyResult{}, ReceiptStepCheckpoint } if cp.TreeSize == receipt.TreeSize && cp.RootHash != receipt.RootHash { - return MultiKeyResult{} + return MultiKeyResult{}, ReceiptStepCheckpoint } } - return result + return result, ReceiptStepNone } diff --git a/governance/releases/1.0-readiness-evidence.md b/governance/releases/1.0-readiness-evidence.md index ffa02934..2b1667f3 100644 --- a/governance/releases/1.0-readiness-evidence.md +++ b/governance/releases/1.0-readiness-evidence.md @@ -263,7 +263,7 @@ only category added since the `v0.15.0` freeze. - Manifest format: `ink.conformance.manifest.v1` - Coverage: **32 categories, 897 vectors**[^ck] - Manifest integrity anchor (SHA-256 of `conformance/v1/manifest.json`): - `247d445fbd5a14572bf4597b06c57494e32c2e16f6904ca8085f72c41e165d2a`[^ck] + `42acbd42c1c95a9a2554cdc8d38a69aab45d426315402aac74019f4099b5228f`[^ck] Per profile: diff --git a/scripts/check-doc-facts.ts b/scripts/check-doc-facts.ts index 98f2a7d4..4b60286a 100644 --- a/scripts/check-doc-facts.ts +++ b/scripts/check-doc-facts.ts @@ -184,6 +184,17 @@ if ( `so the release under evaluation has one source.`, ); } +// A conformance run that the Go test cache can serve is not a run. The cache +// does not track the corpus files the tests read, so a vector change validates +// against a stale pass unless every invocation asks for a fresh one. +for (const workflow of [".github/workflows/ci.yml", ".github/workflows/weekly-conformance.yml"]) { + for (const line of read(workflow).split("\n")) { + const trimmed = line.trim(); + if (!trimmed.startsWith("go test ") || trimmed.includes("-count=1")) continue; + errors.push(`${workflow}: "${trimmed}" must pass -count=1, or the corpus can be checked against a cached result.`); + } +} + const INTEROP_WORKFLOW = ".github/workflows/interop-lab.yml"; const cancelOnlyForPullRequests = /cancel-in-progress: \$\{\{ github\.event_name == 'pull_request' \}\}/; // A group holds one pending run, so a second push while the first is queued diff --git a/test/conformance.test.ts b/test/conformance.test.ts index caabc834..91091542 100644 --- a/test/conformance.test.ts +++ b/test/conformance.test.ts @@ -86,10 +86,10 @@ interface VectorCase { description: string; optionalBehavior?: OptionalBehavior; input: Record; - expect: { result: "accept" | "reject"; reason?: string; auditEvent?: string; canonicalPrincipal?: string; keyStatus?: string; keyId?: string; signature?: string; epochMs?: number; canonicalString?: string; leafHash?: string; derivedGrantId?: string }; + expect: { result: "accept" | "reject"; reason?: string; step?: string; auditEvent?: string; canonicalPrincipal?: string; keyStatus?: string; keyId?: string; signature?: string; epochMs?: number; canonicalString?: string; leafHash?: string; derivedGrantId?: string }; } -type Outcome = { result: "accept" | "reject"; reason?: string; auditEvents?: string[]; canonicalPrincipal?: string; keyStatus?: string; keyId?: string; signature?: string; epochMs?: number; canonicalString?: string; leafHash?: string; derivedGrantId?: string }; +type Outcome = { result: "accept" | "reject"; reason?: string; step?: string; auditEvents?: string[]; canonicalPrincipal?: string; keyStatus?: string; keyId?: string; signature?: string; epochMs?: number; canonicalString?: string; leafHash?: string; derivedGrantId?: string }; async function evaluate(category: string, input: Record): Promise { switch (category) { @@ -495,7 +495,10 @@ async function evaluate(category: string, input: Record): Promi eventHash, laterCheckpoint, }); - return { result: r.valid ? "accept" : "reject" }; + // The failing step is the reason for this surface. A vector that pins + // only the verdict is satisfied by refusing for the wrong reason. + const failed = r.steps.find((step) => !step.pass); + return { result: r.valid ? "accept" : "reject", step: failed?.name }; } case "payload-encryption": { const { envelope, recipientPrivateKeyHex, recipientDid } = input as { @@ -642,6 +645,9 @@ describe("ink/1 conformance vectors", () => { if (c.expect.reason !== undefined) { expect(actual.reason, c.caseId).toBe(c.expect.reason); } + if (c.expect.step !== undefined) { + expect(actual.step, c.caseId).toBe(c.expect.step); + } if (c.expect.auditEvent !== undefined) { expect(actual.auditEvents ?? [], c.caseId).toContain(c.expect.auditEvent); }