Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ots-temporal-anchor-conformance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: OTS temporal-anchor conformance

on:
pull_request:
paths:
- "scripts/verify_ots_anchor.py"
- "scripts/check_ots_anchor_vectors.py"
- "conformance/guardrail-decision-v1/ots/**"
- "docs/OTS_TEMPORAL_ANCHOR_PROFILE.md"
- ".github/workflows/ots-temporal-anchor-conformance.yml"
push:
branches:
- main
paths:
- "scripts/verify_ots_anchor.py"
- "scripts/check_ots_anchor_vectors.py"
- "conformance/guardrail-decision-v1/ots/**"
- "docs/OTS_TEMPORAL_ANCHOR_PROFILE.md"
- ".github/workflows/ots-temporal-anchor-conformance.yml"

permissions:
contents: read

jobs:
conformance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Run OTS parser vectors
run: |
python3 scripts/check_ots_anchor_vectors.py \
conformance/guardrail-decision-v1/ots/manifest.json
46 changes: 46 additions & 0 deletions conformance/guardrail-decision-v1/ots/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Bitcoin OpenTimestamps temporal-anchor vectors

This corpus tests the bounded adapter that turns official `ots verify` output
into a ProofPath temporal-anchor result.

It does **not** implement OpenTimestamps cryptography itself. Live verification
is delegated to the official OpenTimestamps client. The deterministic corpus
tests the fail-closed classification boundary around that client.

## Run the deterministic corpus

```bash
python3 scripts/check_ots_anchor_vectors.py \
conformance/guardrail-decision-v1/ots/manifest.json
```

Expected result: eight passing vectors.

## Live verification

Install the official client and provide access to a Bitcoin Core node:

```bash
pip install opentimestamps-client

python3 scripts/verify_ots_anchor.py \
decision.json \
decision.json.ots \
--bitcoin-node http://USER:PASS@127.0.0.1:8332 \
--output temporal-anchor-result.json
```

The verifier copies the exact target and proof into a temporary directory as
`payload` and `payload.ots`, then invokes `ots verify payload.ots`. This prevents
the proof from being accidentally checked against a different neighboring file.

## Result states

| State | Meaning |
| --- | --- |
| `TEMPORALLY_ANCHORED` | The official client exited successfully and reported exactly one Bitcoin block attestation. |
| `PENDING` | The proof is waiting for Bitcoin confirmation or calendar completion. |
| `INVALID` | The proof/output is conflicting, malformed, or does not contain a Bitcoin attestation. |
| `UNAVAILABLE` | The client/node/runtime was unavailable, so no temporal claim is made. |

A `temporal_anchor_ref` URL alone never reaches `TEMPORALLY_ANCHORED`.
93 changes: 93 additions & 0 deletions conformance/guardrail-decision-v1/ots/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
{
"schema": "proofpath.ots-anchor-conformance.v0.1",
"cases": [
{
"name": "verified-bitcoin-attestation",
"returncode": 0,
"expected": {
"status": "TEMPORALLY_ANCHORED",
"reason": "BITCOIN_ATTESTATION_VERIFIED",
"bitcoin_block_height": 358391,
"attested_before": "2015-05-28 CEST"
},
"output": "Assuming target filename is 'payload'\nSuccess! Bitcoin block 358391 attests existence as of 2015-05-28 CEST\n"
},
{
"name": "pending-calendar-proof-stays-pending",
"returncode": 1,
"expected": {
"status": "PENDING",
"reason": "BITCOIN_CONFIRMATION_PENDING",
"bitcoin_block_height": null,
"attested_before": null
},
"output": "Calendar https://alice.btc.calendar.opentimestamps.org: Pending confirmation in Bitcoin blockchain\n"
},
{
"name": "success-looking-output-with-nonzero-exit-fails-closed",
"returncode": 1,
"expected": {
"status": "INVALID",
"reason": "VERIFIER_EXIT_NONZERO",
"bitcoin_block_height": null,
"attested_before": null
},
"output": "Success! Bitcoin block 358391 attests existence as of 2015-05-28 CEST\n"
},
{
"name": "zero-exit-without-bitcoin-attestation-fails-closed",
"returncode": 0,
"expected": {
"status": "INVALID",
"reason": "NO_BITCOIN_ATTESTATION",
"bitcoin_block_height": null,
"attested_before": null
},
"output": "Timestamp syntax is valid.\n"
},
{
"name": "unknown-attestation-is-not-bitcoin-proof",
"returncode": 0,
"expected": {
"status": "INVALID",
"reason": "UNKNOWN_ATTESTATION",
"bitcoin_block_height": null,
"attested_before": null
},
"output": "Unknown Attestation\n"
},
{
"name": "conflicting-block-attestations-fail-closed",
"returncode": 0,
"expected": {
"status": "INVALID",
"reason": "CONFLICTING_BITCOIN_ATTESTATIONS",
"bitcoin_block_height": null,
"attested_before": null
},
"output": "Success! Bitcoin block 358391 attests existence as of 2015-05-28 CEST\nSuccess! Bitcoin block 428648 attests existence as of 2016-09-07 CEST\n"
},
{
"name": "duplicate-attestations-are-ambiguous",
"returncode": 0,
"expected": {
"status": "INVALID",
"reason": "AMBIGUOUS_MULTIPLE_ATTESTATIONS",
"bitcoin_block_height": null,
"attested_before": null
},
"output": "Success! Bitcoin block 358391 attests existence as of 2015-05-28 CEST\nSuccess! Bitcoin block 358391 attests existence as of 2015-05-28 CEST\n"
},
{
"name": "pending-and-confirmed-output-is-conflicting",
"returncode": 0,
"expected": {
"status": "INVALID",
"reason": "CONFLICTING_PENDING_AND_CONFIRMED",
"bitcoin_block_height": null,
"attested_before": null
},
"output": "Pending confirmation in Bitcoin blockchain\nSuccess! Bitcoin block 358391 attests existence as of 2015-05-28 CEST\n"
}
]
}
108 changes: 108 additions & 0 deletions docs/OTS_TEMPORAL_ANCHOR_PROFILE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# OpenTimestamps temporal-anchor profile

Status: **experimental sidecar profile**
Schema identifier: `proofpath.temporal-anchor.ots.v0.1`

## Purpose

`issued_at` inside a signed or hashed decision is tamper-evident after issuance,
but the issuer can still choose a favorable value before hashing. This sidecar
can establish an external upper bound:

> the exact subject bytes existed no later than the Bitcoin block attested by
> the verified OpenTimestamps proof.

OpenTimestamps describes this as proving that data existed prior to a point in
time. The official client validates `.ots` proofs against the Bitcoin
blockchain.

## Non-goals

A verified OTS proof does **not** prove:

- who authored or approved the subject;
- that the subject was correct, authorized, or current;
- the exact creation time;
- that the issuer did not possess different unpublished content;
- freshness of a knowledge source after the anchor.

It only anchors the exact committed bytes before a Bitcoin block time.

## Sidecar shape

```json
{
"schema": "proofpath.temporal-anchor.ots.v0.1",
"subject": {
"path": "decision.json",
"content_hash": "sha256:..."
},
"proof": {
"path": "decision.json.ots",
"media_type": "application/vnd.opentimestamps.ots",
"content_hash": "sha256:..."
},
"verification": {
"status": "TEMPORALLY_ANCHORED",
"reason": "BITCOIN_ATTESTATION_VERIFIED",
"bitcoin_block_height": 358391,
"attested_before": "2015-05-28 CEST"
},
"temporal_precedence_proven": true,
"verifier": {
"binary": "/usr/local/bin/ots",
"executed": true,
"returncode": 0,
"output_sha256": "sha256:..."
}
}
```

## Promotion rule

A sidecar may set `temporal_precedence_proven: true` only when all of the
following hold:

1. the exact target bytes were supplied to the verifier;
2. the exact `.ots` proof bytes were supplied;
3. the official verifier process completed successfully;
4. the output contains exactly one Bitcoin block attestation;
5. the output contains no pending or unknown attestation;
6. the verifier result is preserved as evidence.

A URL, proof filename, claimed block height, self-declared timestamp, or exit
code by itself is insufficient.

## Runtime trust boundary

This profile delegates cryptographic validation to the installed official
OpenTimestamps client and its Bitcoin data source. The host, executable, PATH,
Bitcoin node, and captured output remain part of the trusted computing base.

A hardened deployment should additionally pin the verifier package/binary by
version or digest, record the Bitcoin node configuration class without storing
credentials, and run inside a reproducible environment.

## Pending proofs

Fresh timestamps commonly remain pending until calendar data reaches a Bitcoin
block. `PENDING` is not failure and is not proof of temporal precedence. The
proof can later be upgraded and reverified.

## GuardrailDecision integration

The base decision should carry only a frozen reference to the sidecar:

```json
{
"temporal_anchor": {
"profile": "proofpath.temporal-anchor.ots.v0.1",
"result_ref": "temporal-anchor-result.json",
"result_hash": "sha256:..."
}
}
```

Consumers must verify the sidecar hash and require
`verification.status == "TEMPORALLY_ANCHORED"` before using the anchor for
ordering or “what was knowable at decision time.”
69 changes: 69 additions & 0 deletions scripts/check_ots_anchor_vectors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Run deterministic conformance vectors for the OTS output classifier."""

from __future__ import annotations

import importlib.util
import json
import sys
from pathlib import Path
from typing import Any, Sequence


def load_verifier(repo_root: Path):
script = repo_root / "scripts" / "verify_ots_anchor.py"
spec = importlib.util.spec_from_file_location("proofpath_verify_ots_anchor", script)
if spec is None or spec.loader is None:
raise RuntimeError(f"cannot load verifier: {script}")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module


def main(argv: Sequence[str] | None = None) -> int:
args = list(sys.argv[1:] if argv is None else argv)
if len(args) != 1:
print("usage: check_ots_anchor_vectors.py <manifest.json>", file=sys.stderr)
return 2

manifest_path = Path(args[0]).resolve()
repo_root = Path(__file__).resolve().parents[1]
verifier = load_verifier(repo_root)
manifest: dict[str, Any] = json.loads(manifest_path.read_text(encoding="utf-8"))

failures = 0
for case in manifest["cases"]:
output = str(case["output"])
stream = case.get("stream", "stdout")
result = verifier.classify_ots_output(
int(case["returncode"]),
output if stream == "stdout" else "",
output if stream == "stderr" else "",
)
actual = {
"status": result.status,
"reason": result.reason,
"bitcoin_block_height": result.bitcoin_block_height,
"attested_before": result.attested_before,
}
expected = case["expected"]

if actual != expected:
failures += 1
print(f"FAIL {case['name']}")
print(" expected:", json.dumps(expected, sort_keys=True))
print(" actual: ", json.dumps(actual, sort_keys=True))
else:
print(f"PASS {case['name']} -> {result.status}/{result.reason}")

if failures:
print(f"\nOTS temporal-anchor conformance failed: {failures}")
return 1

print(f"\nOTS temporal-anchor conformance passed: {len(manifest['cases'])}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading