Language-agnostic compliance test vectors for the did:webvh v1.0 specification.
Multi-implementation cross-resolution pattern:
- Human-readable YAML scripts in
vectors/<scenario>/script.yamldescribe each test scenario. - Each implementation reads the scripts, generates its own signed DID log artifacts, and resolves them to produce expected resolution results — all committed under
vectors/<scenario>/<impl>/. - Each implementation then resolves every implementation's committed logs (including its own) and compares raw resolver output against the committed expected results, writing
implementations/<impl>/status.md. - Divergences surface as differences in
status.md— candidates for Working Group discussion and spec clarification.
There is no single "reference" implementation. The spec and Working Group are the arbiter of correctness.
Each implementation stands on its own. The resolutionResult.json files committed for each implementation are the raw output of that implementation's own resolver — no normalization is applied to make them conform to any other implementation's format. When an implementation resolves its own logs, the result should always match exactly (PASS). Cross-resolution DIFFs represent genuine differences in resolver behaviour between implementations and are the primary analytical output of this test suite.
- Docker (used to build and run each implementation in isolation)
- No other toolchain installation required — everything runs inside containers
scripts/run-allThis does two passes of all five implementations. Pass 1 regenerates each implementation's own vectors; pass 2 cross-resolves with everyone's fresh vectors. Results are written directly into vectors/ and implementations/<impl>/status.md.
To run a subset:
scripts/run-all ts pythonscripts/run ts
scripts/run python
scripts/run rust
scripts/run java
scripts/run java-eeccEach call:
- Builds (or rebuilds from cache) a Docker image for that implementation at the
mainbranch of its library. - Runs the container, mounting
vectors/andimplementations/<impl>/from the local repo. - Appends a provenance footer to
status.mdrecording the exact library commit used.
# Use the default repo, custom branch
scripts/run ts my-feature-branch
# Use a fork at a specific branch
scripts/run ts https://github.com/me/didwebvh-ts:feat/my-pr
scripts/run python https://github.com/me/didwebvh-py:fix/next-key-hashes
scripts/run rust https://github.com/me/didwebvh-rs:fix/witness-formatDocker layer caching means rebuilds are fast when only the branch tip has changed.
After a run, check implementations/<impl>/status.md. Each file has three sections:
| Section | Meaning |
|---|---|
| DID Creation | Did the implementation successfully generate its own vectors from the scripts? |
| Negative Resolution | Does the implementation correctly reject each intentionally invalid log? |
| Cross-Resolution | Does the implementation resolve every other impl's committed logs correctly? |
Result codes:
| Code | Meaning |
|---|---|
| ✅ PASS | Exact match (raw resolver output matches committed expected result) |
| 🔶 DIFF | Resolved successfully but output differs from the committed expected result — a genuine interop difference to analyse |
| ❌ FAIL | Resolution threw an error or was incorrectly accepted/rejected |
| No artifacts committed (impl doesn't support this scenario) |
Comparisons use JCS (JSON Canonicalization Scheme) to eliminate irrelevant differences in key ordering and whitespace, but no semantic normalizations are applied. A DIFF means the two resolvers genuinely disagree on the output.
The footer of each status.md records the exact library commit used for that run:
---
Built from: https://github.com/decentralized-identity/didwebvh-ts @ main (ab41190)
vectors/ # Committed artifacts, organized by scenario then implementation
<scenario-name>/
script.yaml # YAML DSL describing the scenario
ts/
did.jsonl # DID log generated by the TS implementation
resolutionResult.json # Resolution result at HEAD
resolutionResult.<n>.json # Resolution result at version N (optional)
did-witness.json # Witness proofs (optional)
python/
did.jsonl
resolutionResult*.json
rust/
did.jsonl
resolutionResult*.json
java/
did.jsonl
resolutionResult*.json
java-eecc/
did.jsonl
resolutionResult*.json
implementations/
ts/
Dockerfile # Docker build for the TS harness
src/generator.ts # Generates vectors + runs cross-resolution
status.md # Generated results (do not edit by hand)
diffs.txt
config.yaml # Library home repo, version, commit
python/
Dockerfile
generate.py
test_vectors.py
status.md
config.yaml
rust/
Dockerfile
src/main.rs
src/generate.rs
status.md
config.yaml
java/
Dockerfile
src/...
status.md
config.yaml
java-eecc/
Dockerfile
src/...
status.md
config.yaml
scripts/
run # Build + run a single implementation
run-all # Two-pass run of all implementations
| Scenario | Description |
|---|---|
basic-create |
Minimal create + resolve at HEAD |
basic-update |
Create + single update + resolve |
key-rotation |
Create + update rotating the update key |
pre-rotation |
Create with nextKeyHashes commitment |
pre-rotation-consume |
Create with nextKeyHashes; update signed by the pre-rotated key |
deactivate |
Create + deactivate + resolve (deactivated state) |
portable |
Create with portable: true |
portable-move |
Create portable DID; update migrating to a new domain |
witness-threshold |
Create with a witness list; witness proof provided on resolve |
witness-update |
Create with 2-of-2 witness config; update reducing to 1-of-1 |
multi-update |
Create + two updates; resolve at v1, v2, and HEAD |
multiple-update-keys |
Create with two updateKeys; update signed by the second key |
services |
Create with service endpoints; update adding a second service |
16 negative-* scenarios cover intentionally invalid logs. Each implementation resolves them and reports PASS (correctly rejected) or FAIL (incorrectly accepted).
Each script.yaml uses a small DSL:
description: "Create a DID and resolve at genesis"
spec_ref: "https://identity.foundation/didwebvh/v1.0/#creating-a-did"
keys:
- id: key-0
type: ed25519
seed: "0000000000000000000000000000000000000000000000000000000000000001"
steps:
- op: create
domain: example.com
signer: key-0
params:
updateKeys: ["key-0"]
- op: resolve
expect: resolutionResult.json| op | description |
|---|---|
create |
Create the DID log (first entry) |
update |
Append an update entry |
deactivate |
Append a deactivation entry |
resolve |
Assert resolution output matches the named expect file |
| param | type | notes |
|---|---|---|
updateKeys |
string[] | Keys authorised for future updates |
nextKeyHashes |
string[] | Pre-rotated key hashes |
witness |
object | {threshold, witnesses:[{id,weight}]} |
portable |
boolean | Whether the DID is portable |
context |
string[] | Additional @context entries |
alsoKnownAs |
string[] | alsoKnownAs entries |
services |
object[] | Service endpoint entries |
verificationMethods |
object[] | Additional verification methods |
Keys are always derived deterministically from seed — the generator never calls crypto.getRandomValues(), so re-running it produces bit-for-bit identical output.
The Docker approach above is the recommended way to run. For local development without Docker:
corepack enable # one-time: activates the pinned pnpm version
pnpm install
pnpm run generate # regenerate all TS vectors + cross-resolution
pnpm run generate <scenario> # single scenario
pnpm run verify # CI check: verify committed artifacts match generator outputcd implementations/python
python -m venv .venv && source .venv/bin/activate
pip install did-webvh pytest pytest-asyncio pyyaml
python generate.py # generate Python vectors
pytest test_vectors.py # cross-resolution testscd implementations/rust
cargo run --bin generate-vectors # generate Rust vectors
cargo run --bin test-vectors # cross-resolution + negative testscd implementations/java
mvn compile exec:java@generate-vectors # generate Java vectors
mvn compile exec:java # cross-resolution + negative testscd implementations/java-eecc
mvn compile exec:java@generate-vectors
mvn compile exec:java