A practical walkthrough of the v1.2 Public Review Draft, written from the VSO standards team. We open the bundle, install dependencies, generate signed events, verify them, package a Regulatory Evidence Bundle, and run the full conformance test suite. Everything in this article is reproducible on a stock Ubuntu 24.04 box with Python 3.12.
VeritasChain Standards Organization (VSO) has opened the Public Review Draft of VeritasChain Protocol v1.2. This article is the hands-on companion to the implementer's preview on our blog and the longer-form Medium piece. Where those articles explain what v1.2 changes and why, this one runs the code.
If you have read the announcements and want to know whether the schemas, the verifier, and the CI flow actually work on your machine before you commit a roadmap decision — this is the article for you.
A few preliminaries:
- This is a Public Review Draft, not a GA release. The protocol surface is feature-frozen except for corrections required by material interoperability, security, or regulatory-readability issues raised during the 45-day review window.
- v1.2 is a vocabulary extension of v1.1, not a new protocol. The cryptographic substrate — COSE_Sign1, RFC 6962 Merkle trees, RFC 8785 JCS canonicalisation, RFC 3161 / eIDAS-qualified TSA / SCITT external anchoring — is unchanged. Every v1.0 and v1.1 anchor remains valid under v1.2 verification tools.
- Honest scoping. VCP is a private-industry specification. It is not a harmonised standard under Regulation (EU) 1025/2012 and confers no presumption of conformity with the EU AI Act or any other regulation. It supports evidence generation for AI Act Articles 12, 19, 26, and 72; it does not by itself make any system compliant.
With that out of the way, let's open the bundle.
Table of contents
- Setting up the workspace
- The bundle layout
- Signing a minimal v1.2 event
- Adding
regulatory_profileandclock_evidence - Emitting a
gov.change_event(and why ML retraining lives here now) - Pre-trade controls:
trade.ptc_snapshotandtrade.ptc_breach - Crypto-shredding with Erasure Certificate v2
- Assembling a Regulatory Evidence Bundle
- Hybrid post-quantum signature vectors
- Running the full conformance suite
- Writing your own verifier (in any language)
- Wiring it into CI
- Migrating from a v1.1 emitter
- Where to send your Public Review comments
1. Setting up the workspace
We start with a stock Ubuntu 24.04 box (any modern Linux works). Install Python 3.12 if you don't have it, then fetch the PRD bundle:
# Either clone the repo
$ git clone https://github.com/veritaschain/vcp-spec
$ cd vcp-spec
# Or download the release archive
$ curl -L -O https://github.com/veritaschain/vcp-spec/releases/download/v1.2-prd/vcp-v1.2-prd.zip
$ unzip vcp-v1.2-prd.zip
$ cd vcp-v1.2-prd
Install the Python dependencies declared at the repository root:
$ pip install -r requirements.txt
Successfully installed PyNaCl-1.5.0 jsonschema-4.21.1
PyNaCl gives us Ed25519 signing and verification. jsonschema gives us Draft 2020-12 validation. No web framework, no database, no service — the reference tools are deliberately minimal so you can read them in an afternoon and reimplement them in any language you like.
Verify the install is sane:
$ python3 -c "from nacl.signing import SigningKey; print('PyNaCl OK')"
PyNaCl OK
$ python3 -c "from jsonschema import Draft202012Validator; print('jsonschema OK')"
jsonschema OK
2. The bundle layout
Take a quick look at what shipped:
$ ls -F
.github/
DISPOSITION_LOG.md
GA_CUTOVER_PATCH.md
README.md
VCP-Specification-v1_2-PRD.md
pqc-vectors/
requirements.txt
schemas/
tests/
tools/
Three directories matter for this tutorial:
-
schemas/— the JSON Schemas for CORE, TRADE, GOV, RISK, PRIVACY, RECOVERY, and the Regulatory Evidence Bundle (REB). Their$idvalues resolve tohttps://standards.veritaschain.org/vcp/v1.2/…at GA. During the PRD period, treat them as placeholders that point to the canonical content in this repo. -
tests/silver/andtests/gold/— the conformance test suite. Pre-built signed sample bundles plus expected results. -
tools/— Python scripts you can run against the bundle. The five scripts you'll meet in this article:-
tools/vcp_verify.py— the reference verifier -
tools/validate_schemas.py— JSON Schema validator -
tools/generate_test_bundles.py— deterministic test-bundle generator -
tools/generate_pqc_vectors.py— PQC reference vector generator -
tools/pqc_verify_check.py— hybrid vector pass/fail check
-
And one file you'll grow to appreciate: .github/workflows/conformance.yml, which runs all the checks above on every push. We'll come back to that in section 11.
3. Signing a minimal v1.2 event
The simplest possible conformant v1.2.0 event is one that does nothing new beyond bumping the version constant from "1.1" to "1.2". Let's build one.
Create tutorial/01_minimal.py:
import json, hashlib, base64, uuid, datetime
from nacl.signing import SigningKey
def jcs(value):
"""RFC 8785 canonicalisation — production code should use a vetted library.
This is the same minimal implementation used by tools/vcp_verify.py."""
return _j(value).encode("utf-8")
def _j(v):
if v is None: return "null"
if v is True: return "true"
if v is False: return "false"
if isinstance(v, str):
return json.dumps(v, ensure_ascii=False, separators=(",", ":"))
if isinstance(v, (int, float)):
return json.dumps(v, allow_nan=False)
if isinstance(v, list):
return "[" + ",".join(_j(x) for x in v) + "]"
if isinstance(v, dict):
items = sorted(v.items(), key=lambda kv: kv[0].encode("utf-16-be"))
return "{" + ",".join(_j(k) + ":" + _j(val) for k, val in items) + "}"
raise TypeError(type(v))
def utc_now_iso():
return datetime.datetime.utcnow().isoformat(timespec="milliseconds") + "Z"
def main():
# Deterministic seed so anyone reading this article gets the same output
sk = SigningKey(bytes.fromhex("11" * 32))
issuer_id = "did:web:tutorial.example"
payload = {"order_id": "demo-001", "side": "BUY", "stub": True}
event = {
"vcp_version": "1.2",
"event_id": str(uuid.uuid4()), # production code uses UUIDv7
"event_type": "trade.order",
"issuer_id": issuer_id,
"policy_id": "tutorial-policy-2026-06",
"issued_at": utc_now_iso(),
"payload_hash": hashlib.sha256(jcs(payload)).hexdigest(),
"hash_alg": "sha-256",
"sig_alg": "ed25519",
"external_anchor": {
"anchor_type": "RFC3161",
"anchor_value": "PLACEHOLDER_TSA_TOKEN",
"anchor_time": utc_now_iso(),
},
}
# Sign over the JCS form with the signature field absent
event["signature"] = base64.b64encode(sk.sign(jcs(event)).signature).decode()
print(json.dumps(event, indent=2))
if __name__ == "__main__":
main()
Run it:
$ python3 tutorial/01_minimal.py
{
"vcp_version": "1.2",
"event_id": "5dac…",
"event_type": "trade.order",
"issuer_id": "did:web:tutorial.example",
"policy_id": "tutorial-policy-2026-06",
"issued_at": "2026-06-06T05:42:00.123Z",
"payload_hash": "f3a1…",
"hash_alg": "sha-256",
"sig_alg": "ed25519",
"external_anchor": { "anchor_type": "RFC3161", "anchor_value": "PLACEHOLDER_TSA_TOKEN", … },
"signature": "PsK1…"
}
This event is conformant v1.2.0 at the regulatory_profile = NONE profile (omission defaults to NONE for v1.1 compatibility). It will validate against schemas/core.schema.json and verify against tools/vcp_verify.py's Ed25519 check.
If you are migrating from v1.1, this is your one-line change: "vcp_version": "1.1" → "vcp_version": "1.2". Everything else continues to work.
4. Adding regulatory_profile and clock_evidence
The minimal event above is fine for regulatory_profile = NONE. The moment you declare MIFID_ALGO or MIFID_HFT, clock_evidence becomes MUST (per the spec's tier matrix in §7.5).
Why? Because once you're claiming to be readable as algorithmic-trading evidence under MiFID II / RTS 6, an auditor reading your trail needs to know how trustworthy the timestamps actually are — not just that you claimed "PTP-locked".
Let's add both. Create tutorial/02_mifid_algo.py:
import json, hashlib, base64, uuid, datetime, subprocess, os
from nacl.signing import SigningKey
# ...jcs(), _j(), utc_now_iso() as in 01_minimal.py
def collect_clock_evidence():
"""Reference clock-evidence collector. In production, read from your
PTP daemon (phc2sys) or NTP daemon (chronyd). Here we return a static
representative example."""
return {
"utc_source": "PTP-grandmaster:dc1",
"offset_us": 12.4,
"max_error_us": 38.0,
"holdover_state": "LOCKED",
"sync_method": "PTPv2_LOCKED",
"last_sync_at": "2026-06-06T05:41:55Z",
"clock_state": "NOMINAL",
}
def main():
sk = SigningKey(bytes.fromhex("11" * 32))
payload = {"order_id": "demo-002", "side": "BUY", "venue_mic": "XEUR"}
event = {
"vcp_version": "1.2",
"event_id": str(uuid.uuid4()),
"event_type": "trade.order",
"issuer_id": "did:web:tutorial.example",
"policy_id": "tutorial-policy-2026-06",
"regulatory_profile": "MIFID_ALGO", # NEW
"agent_id": "agent:mr-strategy-v3.4.1", # NEW
"issued_at": utc_now_iso(),
"clock_evidence": collect_clock_evidence(), # NEW
"payload_hash": hashlib.sha256(jcs(payload)).hexdigest(),
"hash_alg": "sha-256",
"sig_alg": "ed25519",
"external_anchor": {
"anchor_type": "RFC3161",
"anchor_value": "PLACEHOLDER_TSA_TOKEN",
"anchor_time": utc_now_iso(),
},
}
event["signature"] = base64.b64encode(sk.sign(jcs(event)).signature).decode()
print(json.dumps(event, indent=2))
if __name__ == "__main__":
main()
A note on agent_id. It is a VCP technical identifier — purely for tracing an action back to the AI component that produced it. It does not imply that the system is legally classified as an autonomous agent under any regulatory regime. We make this point in the schema's description field too, because the legal connotation is real and we want to keep the technical identifier free of it.
One important rule: an implementation that cannot collect real clock evidence MUST NOT fabricate it. If your emitter runs in a container without access to the host PTP daemon, emit:
{
"clock_evidence": {
"utc_source": "container-unknown",
"max_error_us": 1000000,
"holdover_state": "UNKNOWN",
"sync_method": "BEST_EFFORT",
"last_sync_at": "1970-01-01T00:00:00Z",
"clock_state": "UNRELIABLE"
}
}
This is a true statement and an auditor can read it. A fabricated "clock_state": "NOMINAL" would not be.
5. Emitting a gov.change_event (and why ML retraining lives here now)
ESMA's Supervisory Briefing of 26 February 2026 introduces a six-category material-change taxonomy and states that retraining of ML components used in algorithmic trading constitutes a material change. v1.2 makes this recordable as gov.change_event.
The rule: emit the ChangeEvent before the change takes effect in production. Every CORE event issued after the change references the change_id until a superseding ChangeEvent supersedes it.
# tutorial/03_change_event.py
def emit_change_event(sk, issuer_id, policy_id):
ev = {
"vcp_version": "1.2",
"event_id": str(uuid.uuid4()),
"event_type": "gov.change_event",
"issuer_id": issuer_id,
"policy_id": policy_id,
"regulatory_profile": "MIFID_ALGO",
"issued_at": utc_now_iso(),
"clock_evidence": collect_clock_evidence(),
# gov.change_event-specific fields
"change_id": str(uuid.uuid4()),
"supersedes_id": None,
"categories": ["adaptive_retraining", "logic_change"],
"scope": {
"strategy_ids": ["alpha-mr-001"],
"venues": ["XEUR", "XPAR"],
"asset_classes": ["EQUITY"],
},
"model_version": { "from": "v3.4.1", "to": "v3.4.2" },
"approval": {
"approver_role": "Head of Algo Trading",
"approval_doc_hash": hashlib.sha256(b"approval-doc-2026-06-06").hexdigest(),
},
"go_live_at": "2026-06-15T08:00:00Z",
"test_evidence_refs": ["did:web:tutorial.example/tests/run-2026-06-05"],
"rollback_plan_ref": "did:web:tutorial.example/runbooks/rollback-mr",
# Required CORE envelope tail
"payload_hash": hashlib.sha256(jcs({"stub": True})).hexdigest(),
"hash_alg": "sha-256",
"sig_alg": "ed25519",
"external_anchor": {
"anchor_type": "RFC3161",
"anchor_value": "PLACEHOLDER_TSA_TOKEN",
"anchor_time": utc_now_iso(),
},
}
ev["signature"] = base64.b64encode(sk.sign(jcs(ev)).signature).decode()
return ev
What this lets an auditor reconstruct: "On 2026-06-06 at 05:42 UTC, the Head of Algo Trading approved a v3.4.1→v3.4.2 model update on strategy alpha-mr-001 for XEUR/XPAR equity trading, scheduled to go live 2026-06-15, with test evidence at the named reference and a documented rollback plan."
For ML retraining, the conventional sequence emits three events in this order:
-
gov.change_eventwithcategories: ["adaptive_retraining"]recording the approval and the go-live time. -
recovery.model_retrainedrecording the actual training run with the dataset hash and the training-run ID. - The first CORE event after go-live carrying a reference to the new
change_id.
The cross-references mean a supervisor reviewing an incident at time T can ask "what was changing in the system at time T-1?" and get a structured answer that ties strategy, model version, approval chain, and post-change activity into one chain of evidence.
The second event in the sequence — recovery.model_retrained — looks like this:
# tutorial/03_change_event.py — continued
def emit_model_retrained(sk, issuer_id, policy_id, change_event_id):
ev = base_event(sk, issuer_id, policy_id,
event_type="recovery.model_retrained")
ev.update({
"model_id": "alpha-mr-001",
"dataset_hash": hashlib.sha256(b"training-dataset-2026-Q2").hexdigest(),
"training_run_id": "trn-2026-06-06-001",
"change_event_id": change_event_id, # links back to the gov.change_event
})
ev["signature"] = base64.b64encode(sk.sign(jcs({k:v for k,v in ev.items() if k != "signature"})).signature).decode()
return ev
# Wiring it all together
change_ev = emit_change_event(sk, issuer_id, policy_id)
ledger.append(change_ev)
# ... operator runs the training pipeline ...
retrained_ev = emit_model_retrained(sk, issuer_id, policy_id,
change_ev["change_id"])
ledger.append(retrained_ev)
# ... at go_live_at, the first CORE event references change_ev["change_id"] ...
The change_event_id field on the recovery.model_retrained event is the cross-reference that makes the chain navigable. A supervisor with one of the three event_ids can pull the other two and reconstruct the entire approve→train→deploy chain.
6. Pre-trade controls: trade.ptc_snapshot and trade.ptc_breach
Article 13 of RTS 6 requires pre-trade controls. v1.1 had no first-class structure for them. v1.2 introduces trade.ptc_snapshot (the state of controls at a moment in time) and trade.ptc_breach (what happened when a control fired).
The implementation pattern is straightforward:
# tutorial/04_pre_trade_controls.py
def emit_ptc_snapshot(sk, issuer_id, policy_id):
ev = base_event(sk, issuer_id, policy_id, event_type="trade.ptc_snapshot")
ev.update({
"scope": "ACCOUNT",
"scope_id": "live-account-7",
"controls": {
"price_collar": { "method": "absolute", "value": "100.00" },
"max_order_value": { "currency": "EUR", "amount": 250000 },
"max_order_volume": { "shares": 10000 },
"max_message_rate": { "messages_per_second": 200 },
"repeated_exec_throttle":{ "window_ms": 100, "max_executions": 10 },
},
"active_kill_switches": ["primary", "escalation"],
"approving_role": "Head of Algo Trading",
})
ev["signature"] = base64.b64encode(sk.sign(jcs({k:v for k,v in ev.items() if k != "signature"})).signature).decode()
return ev
def check_order_against_snapshot(order, snapshot):
controls = snapshot["controls"]
if order["price"] > controls["price_collar"]["value"]:
return ("PRICE_COLLAR", order["price"], controls["price_collar"]["value"])
if order["quantity"] * order["price"] > controls["max_order_value"]["amount"]:
return ("MAX_ORDER_VALUE",
order["quantity"] * order["price"],
controls["max_order_value"]["amount"])
# ... similar checks for max_order_volume, max_message_rate, repeated_exec_throttle
return None
def emit_ptc_breach(sk, issuer_id, policy_id, snapshot_id, breach, order_id):
breach_type, observed, limit = breach
ev = base_event(sk, issuer_id, policy_id, event_type="trade.ptc_breach")
ev.update({
"snapshot_id": snapshot_id,
"breach_type": breach_type,
"order_id": order_id,
"observed": { "value": observed, "limit": limit },
"action_taken": "BLOCK",
"operator": "AUTO",
})
ev["signature"] = base64.b64encode(sk.sign(jcs({k:v for k,v in ev.items() if k != "signature"})).signature).decode()
return ev
The supervisory question PTC events answer is: given these controls, why did this order get through? With v1.2, the answer is recorded in evidence at the moment of the order, not reconstructed from system logs months later.
When to emit a snapshot: at system start-up; at every change to PTC parameters; at minimum once per business day; and at every breach. Orders carry a ptc_snapshot_ref field indicating which snapshot was in force at order time. This creates the bidirectional link an auditor needs: from order to controls, and from controls to all orders they touched.
Here is what a rejection flow looks like end-to-end. Suppose an account-7 trader sends an order at price 305,000 EUR against a max_order_value of 250,000 EUR. The emitter sequence is:
# tutorial/04_pre_trade_controls.py — continued
snapshot = emit_ptc_snapshot(sk, issuer_id, policy_id) # at boot or change
ledger.append(snapshot)
order = {"order_id":"ord-2026-06-06-12345","price":305000,
"quantity":1,"venue_mic":"XEUR"}
breach = check_order_against_snapshot(order, snapshot)
if breach:
breach_event = emit_ptc_breach(sk, issuer_id, policy_id,
snapshot["event_id"], breach,
order["order_id"])
ledger.append(breach_event)
reject_order(order, reason=breach[0])
else:
order_event = emit_order_event(sk, issuer_id, policy_id,
order, snapshot["event_id"])
ledger.append(order_event)
route_order(order)
What an auditor sees in the ledger, in chronological order:
event_id event_type scope detail
0196…ab trade.ptc_snapshot ACCOUNT live-acct-7 max_order_value EUR 250,000
0196…cd trade.ptc_breach (refs ab) MAX_ORDER_VALUE 305000 vs 250000 → BLOCK
The supervisor can run the verifier against this slice of the ledger and confirm — without trusting the operator's narrative — that the breach was recorded, the action was taken, and no order event was issued. That's the structural difference between v1.1 (which would have left this to system logs) and v1.2 (which captures it in cryptographically anchored evidence).
7. Crypto-shredding with Erasure Certificate v2
The PRIVACY module addresses GDPR Article 17 (right to erasure) through crypto-shredding: the ciphertext stays in the ledger, but its decryption key is destroyed in an HSM. The legal argumentation is in Annex J of the specification (explicitly labelled as not legal advice).
What changed from Erasure Certificate v1: structured GDPR Article 17(1) ground reference, KMS key URI, HSM attestation block, explicit backup_treatment statement, anonymisation-claim basis, and optional co-signers (DPO + CISO).
# tutorial/05_erasure.py
def emit_erasure_certificate(sk, subject_pseudonym):
ev = base_event(sk, "did:web:tutorial.example",
"tutorial-policy-2026-06",
event_type="privacy.erasure")
ev.update({
"subject_pseudonym": subject_pseudonym,
"erasure_basis": "GDPR-17(1)(b)",
"kms_key_uri": "kms://eu-west-1/keys/01HX-tutorial-key",
"hsm_attestation": {
"vendor": "VendorName",
"fips_140_3_level": 3,
"attestation_evidence": "attestation-token-2026-06-06",
},
"backup_treatment": "scheduled_overwrite_within_90d",
"anonymisation_claim_basis":"k=20, l=3 over the residual graph",
"co_signers": ["did:web:dpo.tutorial.example", "did:web:ciso.tutorial.example"],
})
ev["signature"] = base64.b64encode(sk.sign(jcs({k:v for k,v in ev.items() if k != "signature"})).signature).decode()
return ev
Three operational rules to remember:
-
subject_pseudonymMUST be a stable pseudonymous identifier. Plaintext PII MUST NOT appear here. -
kms_key_uriMUST point to the destroyed key (KMIP locator, cloud KMS ARN, or HSM key reference). -
co_signersis OPTIONAL but RECOMMENDED at Gold and Platinum, with DPO + CISO being the typical configuration.
The crypto-shredding pillars (from Annex J) — Article 17(1) is satisfied where data are made inaccessible given the state of the art; HSM-attested key destruction is a state-of-the-art mechanism; the EDPB-flagged backup gap is addressed by the documented backup_treatment timeline — are how the operator argues that destruction of the key is, in substance, erasure. Your local DPA remains the final word.
8. Assembling a Regulatory Evidence Bundle
The Regulatory Evidence Bundle (REB) is the structural innovation of v1.2. It is not a new event; it is a packaging contract that lets you deliver, on demand, a self-contained, independently verifiable evidence set to a supervisor or auditor.
Three normative rules:
- Reproducibility. Two independent assemblers, given the same period, scope, and source ledger, MUST produce the same content hash.
- Profile-specific minima. Each Regulatory Profile carries a required minimum component set.
- Signing. The REB MUST be signed by the operator and SHOULD be co-signed by the operator's compliance function.
A reference assembler:
# tutorial/06_reb.py
def assemble_reb(period, scope, profile, ledger):
components = {
"core_events": {
"merkle_root": ledger.merkle_root_for(period, scope).hex(),
"count": ledger.count_for(period, scope),
"path_index": ledger.path_index_for(period, scope),
},
"policy_id_history": sorted(ledger.distinct_values("policy_id", period, scope)),
"regulatory_profile_history": sorted(ledger.distinct_values("regulatory_profile", period, scope)),
}
if profile in {"MIFID_ALGO", "MIFID_HFT", "MIXED_FINANCIAL_AI"}:
components["clock_evidence_summary"] = ledger.summarise_clock(period, scope)
components["change_events"] = ledger.event_ids("gov.change_event", period, scope)
components["ptc_snapshots"] = ledger.event_ids("trade.ptc_snapshot", period, scope)
components["ptc_breaches"] = ledger.event_ids("trade.ptc_breach", period, scope)
components["provider_dependencies"] = ledger.event_ids("gov.provider_dependency", period, scope)
components["outage_notices"] = ledger.event_ids_prefix("recovery.outage_", period, scope)
if profile in {"AI_ACT_HRAI", "MIXED_FINANCIAL_AI"}:
components["pmm_manifests"] = ledger.event_ids("gov.pmm_manifest", period, scope)
components["recovery_integrity_tests"] = ledger.event_ids("recovery.integrity_test", period, scope)
components["erasure_certificates"] = ledger.event_ids("privacy.erasure", period, scope)
reb = {
"reb_version": "1.0",
"vcp_version": "1.2",
"regulatory_profile": profile,
"period": period,
"scope": scope,
"components": components,
"external_anchors": ledger.anchors_for(period, scope),
"verifier_recipe": "https://standards.veritaschain.org/verify/reb-1.0",
}
# Sign over the JCS form with the signature field absent
sig_bytes = sk.sign(jcs(reb)).signature
reb["issuer_signature"] = base64.b64encode(sig_bytes).decode()
return reb
Reproducibility, in practice, means that two independent teams writing assemble_reb against the same ledger and the same period/scope MUST produce REBs whose content hashes are identical. This is the property that makes a REB independently verifiable. It is not what the operator says happened — it is what anyone with access to the source ledger can reproduce.
The REB SHOULD be served via a SCRAPI-compatible endpoint at /v1/reb/{reb_id}. When an authority asks "show us your Q2 algorithmic-trading evidence in EUR-denominated venues," the operational answer is one URL that returns one self-contained bundle.
8.1 Serving a REB over HTTP
The SCRAPI compatibility point matters because it lets an auditor's tooling — or a regulatory technologist's script — fetch your REB without negotiating a custom protocol. A minimal Flask-based serving endpoint:
# tutorial/07_reb_server.py
from flask import Flask, request, jsonify, abort
import json, hashlib
app = Flask(__name__)
REB_STORE = {} # in production, this is your durable storage
def reb_content_hash(reb: dict) -> str:
"""Reproducibility check: derive deterministic content hash."""
reb_without_sig = {k: v for k, v in reb.items() if k != "issuer_signature"}
return hashlib.sha256(jcs(reb_without_sig)).hexdigest()
@app.route("/v1/reb/<reb_id>", methods=["GET"])
def get_reb(reb_id: str):
reb = REB_STORE.get(reb_id)
if reb is None:
abort(404)
# Match the Accept header — SCITT convention is application/cose or application/json
if "application/json" in request.headers.get("Accept", "application/json"):
return jsonify(reb)
abort(406)
@app.route("/v1/reb/<reb_id>/verify", methods=["GET"])
def verify_reb(reb_id: str):
"""Independent verifier convenience endpoint: recompute content hash."""
reb = REB_STORE.get(reb_id)
if reb is None:
abort(404)
return jsonify({
"reb_id": reb_id,
"regulatory_profile": reb["regulatory_profile"],
"period": reb["period"],
"computed_content_hash": reb_content_hash(reb),
"merkle_root": reb["components"]["core_events"]["merkle_root"],
"issuer_signature_present": "issuer_signature" in reb,
})
@app.route("/v1/reb", methods=["POST"])
def publish_reb():
reb = request.get_json()
# Validate against schema before accepting
from jsonschema import Draft202012Validator
schema = json.loads(open("schemas/reb.schema.json").read())
errs = list(Draft202012Validator(schema).iter_errors(reb))
if errs:
return jsonify({"errors": [e.message for e in errs]}), 400
reb_id = reb_content_hash(reb)[:16]
REB_STORE[reb_id] = reb
return jsonify({"reb_id": reb_id}), 201
What a regulator's verifier sees when they call this:
$ curl https://your-firm.example/v1/reb/a1b2c3d4e5f67890
{
"reb_version": "1.0",
"vcp_version": "1.2",
"regulatory_profile": "MIFID_ALGO",
"period": { "from": "2026-04-01", "to": "2026-06-30" },
"components": { … },
"external_anchors": [ … ],
"issuer_signature": "..."
}
$ curl https://your-firm.example/v1/reb/a1b2c3d4e5f67890/verify
{
"reb_id": "a1b2c3d4e5f67890",
"regulatory_profile": "MIFID_ALGO",
"computed_content_hash":"a1b2c3d4e5f6789012345...",
"merkle_root": "...",
"issuer_signature_present": true
}
The /verify convenience endpoint is optional but valuable: it tells the requester what content hash this server computed, which they can compare against the content hash their independent assembler would compute from the same source ledger. If those two hashes match, the REB is reproducible, which is the v1.2 normative requirement.
In production you would also:
- Pin TLS to mutual authentication for supervisor access
- Add rate limiting and audit logging on the REB endpoint itself (which is, ironically, a VCP event in its own right)
- Cache REBs by content hash, since a reproducible REB is by definition idempotent
- Serve
application/cosecontent type alongside JSON for SCITT verifier compatibility
9. Hybrid post-quantum signature vectors
v1.2 reserves ed25519+mldsa65 as the canonical hybrid SignAlgo, following the LAMPS composite-signatures combiner (draft-ietf-lamps-pq-composite-sigs-06). The verify-then-emit ratchet:
| Version | Verify hybrid | Emit hybrid |
|---|---|---|
| v1.2.0 (now) | MAY at all tiers | MAY at all tiers; registry reserves ed25519+mldsa65
|
| v1.2.1 (Q1 2027 target) | MUST at Platinum, SHOULD at Gold | SHOULD at Platinum, MAY at Gold/Silver |
| v1.3.0 | MUST at Platinum, MUST at Gold | MUST at Platinum, SHOULD at Gold (subject to maturity) |
The PRD ships with two vectors in pqc-vectors/. The classical half uses real Ed25519. The PQC half is a clearly-labelled PRD_PQC_PLACEHOLDER until vetted ML-DSA-65 vectors replace it before v1.3.0's emit-MUST cut-over.
Run the vector check:
$ python3 tools/pqc_verify_check.py
PQC hybrid vector check (classical-half only — PQC half is PRD placeholder)
[PASS] verify-pass.json: expected=VERIFY_PASS observed=VERIFY_PASS
[PASS] verify-fail-tampered.json: expected=VERIFY_FAIL observed=VERIFY_FAIL
The verify-fail-tampered.json vector has a single bit flipped in byte 0 of the Ed25519 signature. Any conformant verifier MUST reject it. The verify-pass.json vector is the same message with an intact signature; any conformant verifier MUST accept it.
If you are writing your own verifier — and we encourage you to, since independent implementations are part of the GA-readiness checklist — your implementation MUST produce identical pass/fail outcomes on these two vectors. That is the operational meaning of conformance for the PQC vectors.
For implementers planning the v1.2.1 → v1.3.0 transition: ML-DSA-65 signatures are about 3.3 KB; the composite is about 3.4 KB. At HFT order-flow rates, the realistic deployment pattern is batch-signing Merkle leaves and emitting one composite signature per batch, anchoring at the batch root.
10. Running the full conformance suite
Time to run everything together:
$ python3 tools/validate_schemas.py
Silver bundle — schema validation
[PASS] silver event #0 validates against core.schema.json
[PASS] silver REB validates against reb.schema.json
Gold bundle — schema validation
[PASS] gold event #0 validates against core.schema.json
[PASS] gold event #1 validates against core.schema.json
[PASS] gold event #2 validates against core.schema.json
[PASS] gold REB validates against reb.schema.json
Summary: 0 schema-validation failures
$ python3 tools/vcp_verify.py --tier silver --bundle tests/silver/
VCP v1.2-prd reference verifier — tier=silver bundle=tests/silver
Event #0 0196a98b-4980-7abc-8123-4567890def012 type=trade.order
[PASS] policy_id present
[PASS] event_id is UUIDv7
[PASS] event canonicalisation deterministic — jcs_sha256=fb720ff9bc11539b…
[PASS] ed25519 signature verify
[PASS] external_anchor present
Tier-specific checks
[PASS] ClockEvidence present (Silver SHOULD)
Regulatory Evidence Bundle
[PASS] REB reproducibility (same input → same content hash)
Summary: 7 pass, 0 fail
$ python3 tools/vcp_verify.py --tier gold --bundle tests/gold/
… (3 events × 4-5 checks each, plus tier-specific PTC and ChangeEvent checks)
Summary: 19 pass, 0 fail
$ python3 tools/pqc_verify_check.py
[PASS] verify-pass.json: expected=VERIFY_PASS observed=VERIFY_PASS
[PASS] verify-fail-tampered.json: expected=VERIFY_FAIL observed=VERIFY_FAIL
Four categories, all green. The conformance properties the verifier exercises:
- RFC 8785 JCS canonicalisation (deterministic event-hash recomputation)
- Event-hash verification against the recomputed canonical form
- Ed25519 signature verification (RFC 8032)
- Merkle proof verification (RFC 6962-style)
- External-anchor presence
- PolicyID presence
- ClockEvidence presence (Silver: SHOULD; Gold: MUST when MIFID_ALGO)
- REB reproducibility (deterministic content hash)
- PTCSnapshot presence (Gold: SHOULD)
- ChangeEvent presence (Gold: SHOULD)
If you write your own verifier in Go, Rust, TypeScript, or whatever else, your implementation MUST produce identical pass/fail outcomes on these test bundles. That is what makes you a conformant implementation. We will list independent implementations in the GA acknowledgements — so if you build one, let us know.
11. Writing your own verifier (in any language)
The GA-readiness checklist requires "at least one independent implementation passes the conformance test suite at Silver and Gold." If you build that implementation, you become part of the GA story. Here is the recipe in language-agnostic pseudocode, drawn from tools/vcp_verify.py.
The minimum your verifier needs to do, in order:
function verify_event(event, public_key_hex):
# 1. Canonicalisation
event_for_sig = event without "signature"
canon_bytes = jcs_canonicalise(event_for_sig) # RFC 8785
# 2. Hash and signature
computed_hash = sha256(canon_bytes)
sig_bytes = base64_decode(event["signature"])
# 3. Verify Ed25519
if not ed25519_verify(public_key_hex, canon_bytes, sig_bytes):
return FAIL("signature invalid")
# 4. Mandatory fields
if not event["policy_id"]:
return FAIL("policy_id missing")
if not is_uuidv7(event["event_id"]):
return FAIL("event_id is not UUIDv7")
if not event["external_anchor"]["anchor_value"]:
return FAIL("external_anchor missing")
# 5. Profile-conditional fields
profile = event.get("regulatory_profile", "NONE")
if profile in {"MIFID_ALGO", "MIFID_HFT", "MIXED_FINANCIAL_AI"}:
if not event.get("clock_evidence"):
return FAIL("clock_evidence MUST under MIFID_* profiles")
return PASS
The four properties that are unambiguous and language-portable:
| Property | What you need | RFC / spec reference |
|---|---|---|
| JCS canonicalisation | UTF-16 code-unit ordered keys, ECMA 262 number serialisation, no insignificant whitespace | RFC 8785 |
| Ed25519 verification | Pure Ed25519, no pre-hash, no domain separation | RFC 8032 |
| Merkle proof verification | Leaf hash = SHA-256(0x00 ‖ leaf); node hash = SHA-256(0x01 ‖ left ‖ right) | RFC 6962 §2.1.1 |
| UUIDv7 detection | Version nibble == 7; variant bits == 10 | RFC 9562 §5.7 |
A Rust implementer would lean on ed25519-dalek for signatures, sha2 for hashing, and write the JCS serialiser from scratch (it's about 60 lines). A Go implementer would use crypto/ed25519 from the standard library and either bring their own JCS or use one of the community implementations. A TypeScript implementer would use @noble/ed25519 (no native dependencies, runs in Node and the browser) and again write JCS by hand.
The fact that JCS is the easiest part to get subtly wrong is worth flagging. The two most common failure modes:
- Object key ordering by UTF-8 byte instead of UTF-16 code unit. These agree for ASCII keys but diverge for keys containing characters in the supplementary planes. RFC 8785 specifies UTF-16, so use UTF-16.
-
Number serialisation that emits
1.0instead of1. ECMA 262 says integers serialise without a decimal point. If you serialise via your language's default JSON number formatter, check this case explicitly.
If your verifier ingests tests/silver/sample-events.jsonl and tests/silver/sample-reb.json, runs the verification steps above, and emits 7 pass, 0 fail, it is conformant at Silver. Same exercise on tests/gold/, with additional checks for PTC snapshot presence and ChangeEvent presence, gives you Gold. Send us a link and we will add your implementation to the GA acknowledgements section.
A note for the truly motivated: a fully featured independent verifier would also re-derive Merkle roots from the leaf hashes, verify the external anchor (TSA token chain, or SCITT Receipt inclusion proof), and run the REB reproducibility check by reassembling the REB from your own copy of the ledger. None of that is required to claim Silver/Gold conformance, but all of it is welcome and is the path to becoming the kind of independent supervisory-side verifier that the GA-readiness checklist is hoping for.
11.1 Common pitfalls (with actual verifier output)
What follows are four ways your verifier — or your emitter — can be subtly wrong, with the exact output the reference verifier produces when each one occurs. Use these as integration tests against your own implementation.
Pitfall 1: JCS key ordering using UTF-8 instead of UTF-16.
If your JCS serialiser orders object keys by UTF-8 byte sequence, your event hash will diverge from the spec's for any event containing non-ASCII keys. The reference verifier catches this because the recomputed canonical form does not match the signed payload:
Event #0 0196a98b-...
[FAIL] ed25519 signature verify — signature invalid
The signature is mathematically intact; the verifier just hashed different bytes than the emitter signed. If you see this failure mode on events that contain CJK keys, fields with combining characters, or supplementary-plane symbols, check your key sort.
Pitfall 2: Number serialisation with trailing .0.
If your JSON encoder emits 1.0 instead of 1 for integer-valued floats, the canonical form diverges from the spec. Same failure shape as Pitfall 1:
[FAIL] ed25519 signature verify — signature invalid
ECMA 262 says integers serialise without a decimal point. Most modern JSON encoders get this right, but some Python libraries (and some Java BigDecimal paths) emit .0 for integer floats. If you see verify failures correlated with events containing whole-number quantities like "quantity": 1000.0, this is the cause.
Pitfall 3: Missing policy_id (v1.1 hard deadline in force).
If you migrate from a pre-v1.1 emitter and forget to add policy_id, the verifier rejects the event regardless of signature validity:
Event #0 0196a98b-...
[PASS] event canonicalisation deterministic
[FAIL] policy_id present — policy_id missing
[PASS] ed25519 signature verify
The signature is valid; the schema-level requirement is not. The Policy Identification deadline of 25 March 2026 is in force and v1.2 does not relax it.
Pitfall 4: clock_evidence claimed at MIFID_ALGO but absent.
If you set regulatory_profile: "MIFID_ALGO" but forget to emit clock_evidence, the Gold-tier verifier flags the inconsistency:
Tier-specific checks
[FAIL] ClockEvidence MUST for MIFID_ALGO/HFT/MIXED — MUST violation per §7.5
This is the one your emitter-side defensive check (the RuntimeError in V12Emitter._needs_clock) should prevent at issuance time. If a v1.2.0 event slips through with MIFID_ALGO and no clock_evidence, it is not conformant; the verifier is correct to reject it.
The pattern across all four pitfalls is the same: the v1.2 verifier surfaces the inconsistency at the earliest possible point — at signature verification for canonicalisation errors, at field-presence checks for missing required fields, at tier-policy checks for profile-conditional fields. If your own verifier reproduces these four behaviours, you are well on the way to Silver/Gold conformance.
12. Wiring it into CI
The PRD includes a GitHub Actions workflow you can use as-is or adapt:
# .github/workflows/conformance.yml
name: VCP v1.2-prd Conformance
on:
push:
branches: [main, "v1.2-prd"]
pull_request:
branches: [main, "v1.2-prd"]
workflow_dispatch:
jobs:
conformance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements.txt
# Reproducibility check: regenerate then diff against checked-in
- run: python3 tools/generate_test_bundles.py
- run: git diff --exit-code tests/silver/ tests/gold/
- run: python3 tools/generate_pqc_vectors.py
- run: git diff --exit-code pqc-vectors/
# Schema and tier conformance
- run: python3 tools/validate_schemas.py
- run: python3 tools/vcp_verify.py --tier silver --bundle tests/silver/
- run: python3 tools/vcp_verify.py --tier gold --bundle tests/gold/
- run: python3 tools/pqc_verify_check.py
The pattern worth stealing is the reproducibility check at the top: a deterministic generator plus checked-in output plus git diff --exit-code. If anyone (including the maintainer) changes either side without keeping them in sync, the build fails. We use this for tests/silver/, tests/gold/, and pqc-vectors/. It eliminates an entire class of bug where checked-in test data and the generator drift silently.
If you adopt this pattern in your own VCP-emitter project, you get a free guarantee that your sample bundles continue to match your generator, and your CI surfaces any drift on the next push.
13. Migrating from a v1.1 emitter
For most v1.1 deployments, migration to v1.2.0 is a single-line change.
A v1.1 emitter:
class V11Emitter:
def emit(self, event_type, payload, anchor):
ev = {
"vcp_version": "1.1", # changes to "1.2"
"event_id": str(uuid.uuid4()),
"event_type": event_type,
"issuer_id": self.issuer_id,
"policy_id": self.policy_id,
"issued_at": utc_now_iso(),
"payload_hash": hashlib.sha256(jcs(payload)).hexdigest(),
"hash_alg": "sha-256",
"sig_alg": "ed25519",
"external_anchor": anchor,
}
ev["signature"] = base64.b64encode(self.sk.sign(jcs(ev)).signature).decode()
return ev
The same emitter, minimally migrated to v1.2.0 and conformant at regulatory_profile = MIFID_ALGO:
class V12Emitter:
def __init__(self, sk, issuer_id, policy_id,
regulatory_profile="NONE",
clock_evidence_provider=None,
agent_id=None):
self.sk = sk
self.issuer_id = issuer_id
self.policy_id = policy_id
self.regulatory_profile = regulatory_profile
self.clock_evidence_provider = clock_evidence_provider
self.agent_id = agent_id
self._needs_clock = regulatory_profile in (
"MIFID_ALGO", "MIFID_HFT", "MIXED_FINANCIAL_AI"
)
def emit(self, event_type, payload, anchor):
ev = {
"vcp_version": "1.2", # CHANGED
"event_id": str(uuid.uuid4()),
"event_type": event_type,
"issuer_id": self.issuer_id,
"policy_id": self.policy_id,
"issued_at": utc_now_iso(),
"payload_hash": hashlib.sha256(jcs(payload)).hexdigest(),
"hash_alg": "sha-256",
"sig_alg": "ed25519",
"external_anchor": anchor,
"regulatory_profile": self.regulatory_profile, # ADDED
}
if self.agent_id is not None: # ADDED
ev["agent_id"] = self.agent_id
if self._needs_clock: # ADDED
if self.clock_evidence_provider is None:
raise RuntimeError(
"ClockEvidence MUST be present under MIFID_* profiles"
)
ev["clock_evidence"] = self.clock_evidence_provider()
ev["signature"] = base64.b64encode(self.sk.sign(jcs(ev)).signature).decode()
return ev
Three things change, none of them break v1.1 semantics:
-
"vcp_version"becomes"1.2". -
regulatory_profileand (optionally)agent_idare populated. - A
clock_evidencecallback is invoked when the profile requires it.
Nothing about how events are signed, hashed, anchored, or canonicalised changes. A relying party verifying a v1.2 event uses the same JCS canonicalisation, the same Ed25519 signature check, the same external-anchor verification path. This is what we mean by "vocabulary extension, not protocol revision."
What you are not required to do:
- Re-issue, re-sign, or re-anchor v1.0 or v1.1 events. They remain valid under v1.2 verification tools.
- Migrate to hybrid signatures in v1.2.0. The reservation of
ed25519+mldsa65is forward-positioning, not a v1.2.0 requirement. - Adopt every Provisional Regulatory Profile. They are reserved for community comment and may change.
14. Where to send your Public Review comments
Public Review runs for 45 days from the PRD opening. Comments go through GitHub Issues using the Public Review Comment template at github.com/veritaschain/vcp-spec. Every substantive comment receives one of six dispositions — Accepted, Accepted with modification, Deferred, Declined, Out of scope, Editorial — recorded openly in DISPOSITION_LOG.md and updated weekly.
The categories of comment we especially welcome from dev.to readers:
-
Does the
clock_evidencestructure cover what your time infrastructure actually produces? In particular, can your PTP or NTP stack produceoffset_usandmax_error_us, or do you have to fabricate them? -
Does
trade.ptc_snapshotcapture every pre-trade control your venue or jurisdiction requires? Are there controls that wouldn't fit one of the five categories shown? -
Does the
gov.change_eventtaxonomy cover every change you'd actually emit one for? In particular, where doadaptive_retrainingandexternal_dependency_changeoverlap, and what should the rule be? - Does the REB minimum-content matrix, by Regulatory Profile, match what your supervisors actually request?
-
Does the
requirements.txtinstall cleanly on your CI image? Does the reproducibility check pass on first run? - Did this tutorial work end-to-end on your machine? If not, where did it break? That information is gold for us.
Security findings — cryptographic vulnerabilities, schema injection paths, signature-bypass conditions — go privately to security@veritaschain.org under a 90-day responsible-disclosure timeline.
The GA-readiness checklist is six items: every $id URI resolves to its canonical artefact, the next IETF individual-draft iteration reflects the PRD's terminology, at least one independent implementation passes the conformance suite at Silver and Gold, the PQC verify vectors are reproducible across implementations, comments are addressed in a published disposition log, and the v1.1 hard-deadline counters (PolicyIdentification 2026-03-25, Silver external anchoring 2026-06-25) are confirmed unaffected. Until those six are cleared, this is a PRD. After they are cleared, v1.2.0 ships.
Closing notes
VCP v1.2 PRD is now open. The repository is at github.com/veritaschain/vcp-spec. The full specification is VCP-Specification-v1_2-PRD.md. Independent implementations are welcome and will be acknowledged.
Three things to take away if you read this far:
- v1.2 is the vocabulary regulators are about to start asking for in algorithmic-trading and AI audit trails. It is not a new protocol. It is not a rewrite.
- The reference tools work. We have shown the commands above; you can run them yourself. If they don't work on your machine, that is exactly the kind of finding we need before GA.
- Honest scoping holds. VCP supports evidence generation for AI Act Articles 12, 19, 26, and 72. It does not by itself make any system compliant.
Eighty-seven days until 2 August 2026. Forty-five for review. The rest is the work.
If you write a verifier in your favourite language and want it linked from our acknowledgements, let us know. If you find a bug, file an issue. If you find a security issue, email security@veritaschain.org. If you build something on VCP v1.2 we don't know about yet, we would genuinely like to hear about it.
About VSO. VeritasChain Standards Organization is a Tokyo-based standards body developing open cryptographic specifications for AI and algorithmic systems under the principle "Verify, Don't Trust." We maintain the VeritasChain Protocol (VCP) and the Verifiable AI Provenance (VAP) Framework. We are registered in Japan (D-U-N-S 698368529). VeritasChain株式会社, a separate commercial entity, operates in the same jurisdiction. This article is published from the VSO official dev.to account.
Companion publications
- Implementer's preview on our blog: https://veritaschain.org/blog/vcp-v1-2-prd-implementer-preview
- Long-form technical article on Medium
- LinkedIn Article from the VSO official account
- Announcement post and thread on X (English and Japanese)
Contact
- General: info@veritaschain.org
- Standards: standards@veritaschain.org
- Technical questions during Public Review: GitHub Issues (preferred) or technical@veritaschain.org
- Security: security@veritaschain.org (90-day responsible disclosure)
Published under Creative Commons Attribution 4.0 International. The VCP specification itself is governed by the licence stated in its repository.
veritaschain
/
vcp-spec
Official specification for the VeritasChain Protocol (VCP) v1.0 – global audit standard for algorithmic trading.
VeritasChain Protocol (VCP)
VeritasChain Protocol (VCP) is an open, vendor-neutral standard for
cryptographically verifiable audit trails in algorithmic and AI-driven
trading systems.
VCP enables regulators, auditors, and market participants to
verify — not merely trust — the integrity, completeness, and ordering of
trading decisions, orders, executions, and risk controls.
This repository is maintained by the
VeritasChain Standards Organization (VSO).
📌 Canonical Specification Location (IMPORTANT)
The canonical (normative) specification of VCP is located under:
/spec/
├─ v1.0/
├─ v1.1/
└─ v1.2/
- Each version directory contains the authoritative specification (
SPEC.md) - Files outside
/spec/are non-normative - HTML, PDF, or translated documents (if any) are provided for convenience only
If there is any conflict, the content under /spec/ always prevails.
📘 Available Versions
▶ Current Stable
-
v1.2 — latest specification with strengthened integrity guarantees
→
/spec/v1.2/
▶ Legacy
-
v1.0 — initial released version
→
/spec/v1.0/
Migration notes and compatibility considerations are…
Top comments (0)