Conformance vectors
Every sibling protocol ships normative test vectors in its oc-*-protocol
repo. The reference implementations (TypeScript + Python) vendor those vectors
into their test suites and run them on every CI push. Any drift between the spec
and the implementations is a CI failure.
This page describes the pattern. Per-protocol vector details live in the protocol's own repo.
Why conformance vectors
The attestation ID is a content hash of the canonical message. Two implementations that agree on the inputs but disagree on the canonical bytes produce different IDs — which breaks Nostr discovery, verifier trust, and every downstream protocol that references an attestation.
Conformance vectors pin the bytes. Each vector is a triple
(inputs, expected_bytes, expected_id). A passing run proves the implementation
produces the same bytes and the same hash as the spec.
Vector set per protocol
Every spec repo ships its vectors under test-vectors/ (the family standard).
OC Attest, the progenitor spec, additionally mirrors them under
conformance/vectors/ for backward compatibility — both directories contain the
same files.
| Protocol | Spec repo path | # vectors |
|---|---|---|
| OC Attest | oc-attest-protocol/test-vectors/ | 23 (tv01–tv23) |
| OC Lock | oc-lock-protocol/test-vectors/ | 4 (v01–v04) |
| OC Stamp | oc-stamp-protocol/test-vectors/ | 5 (v01–v05) |
| OC Vote | oc-vote-protocol/test-vectors/ | 5 (v01–v05) |
| OC Agent | oc-agent-protocol/test-vectors/ | 5 (v01–v05) |
| OC Pledge | oc-pledge-protocol/test-vectors/ | 28 (v01–v28) |
Each protocol's reference core (@orangecheck/<verb>-core) vendors the matching
test-vectors/ directory verbatim and runs every vector on each CI push.
Vector categories (OC Attest example)
The 23 OC Attest vectors are organized by test category:
| Category | Count | What they pin |
|---|---|---|
canonical_message | 9 | Byte-exact canonical-message output for a given input set |
identities_format | 1 | Identity-list sort order and escape rules |
attestation_id | 2 | sha256(canonical_bytes) matches |
score_v0 | 4 | Scoring formula output (tv10–tv13) |
reject | 4 | MUST-reject cases — malformed identifiers, wrong header, etc. |
bip322_signature | 3 | Real BIP-322 signatures against fixed addresses |
The categories exist so a developer debugging a specific failure can skim to the relevant block rather than re-reading all 23.
How the CI gate works
Each reference SDK has a test:conformance target (or a dedicated test file)
that loads the vendored vector JSON and asserts every expected output. A typical
flow:
cd packages/sdk && yarn test # TS passes all 23 vectors
cd packages/sdk-py && pytest # Python passes all 23 vectors
If either fails, the PR is blocked. If both pass, byte-identity between the two
implementations is proven (modulo floating-point edge cases in score_v0, which
are handled with a tolerance of 0.01).
Cross-impl drift is prevented by a third CI job in oc-packages: after both
suites pass, a diff job fetches oc-protocol/main/conformance/ and compares it
against what's vendored in each SDK. If the vendored copy is out of date, CI
fails and the SDK must re-vendor.
Adding a vector
For any sibling protocol, the flow is the same:
- Write the vector JSON in
oc-<verb>-protocol/test-vectors/<id>.jsonfollowing the existing shape for that spec. OC Attest additionally has agenerate.mjshelper for vectors that are derivable from a reference calculation. - Update the spec's
test-vectors/index.json(orREADME.mdlisting) so tooling can discover the new file. - Re-vendor the directory into the matching reference core (
oc-packages/ <verb>-core/src/__tests__/vectors/) and run the test suite locally. - The new vector must pass before the spec PR merges; the core PR re-vendoring
the spec's
test-vectors/is paired with the spec PR.
Specs that add a new MUST-reject case should include a vector in the spec's reject category so the behavior is pinned.
See also
- Canonical message — what the bytes look like
- SDKs at a glance — the per-protocol cores that vendor and run these vectors