Skip to content

Latest commit

 

History

History
465 lines (310 loc) · 18.8 KB

File metadata and controls

465 lines (310 loc) · 18.8 KB

Chainblocks Use Cases

Canonical scenarios that drive the design. Every requirement in REQUIREMENTS.md traces back to one of these.

Status: Left-bookend draft, 2026-05-18. Companion docs: ../STORY.md, ../REQUIREMENTS.md, ../MODEL.md, FEATURES.md.

Each use case follows the same shape:

  1. Actor & situation — who is doing what, where
  2. Need — what they're trying to accomplish
  3. Without chainblocks — how this is solved (badly) today
  4. With chainblocks — how the library helps
  5. Requirements exercised — the REQ-CB-* IDs this use case validates

UC-1: A learning system promotes a fact about a user

Actor & situation

A learning-and-memory system (a digital twin, personalization engine, or AI assistant) observes that a user has expressed the same statement multiple times across two weeks. A curation step promotes the statement from observed (single mention) to pattern (repeated mention) to habit (named-and-acted-upon).

Six months later, the user disputes the habit: "Did I really say that? I think the system made it up."

Need

The system must produce:

  1. The original observations (with dates and source references)
  2. The promotion events (observed → pattern → habit) in order
  3. Proof that nothing in the chain has been altered since

Without chainblocks

The system writes to a database audit_log table with created_at timestamps. Anyone with database write access can UPDATE audit_log SET ... to fabricate or alter entries. The system's defense reduces to "trust our database."

With chainblocks

const ledger = await openLedger({
  store: new FileStore({ path: '/data/learning-audit.ledger' }),
  name: 'user-12345-learning-log',
  writer: 'learning-service',
});

// On every observation
await ledger.append({
  kind: 'learning.fact.observed',
  payload: {
    factId: 'f-a8c4',
    text: '<observed statement>',
    sourceDoc: 'user-input-2026-01-15',
    sourceParagraph: 12,
  },
});

// On promotion
await ledger.append({
  kind: 'learning.fact.promoted',
  payload: {
    factId: 'f-a8c4',
    from: 'pattern',
    to: 'habit',
    supportingObservations: ['f-a8c4-obs-1', 'f-a8c4-obs-2', 'f-a8c4-obs-3', 'f-a8c4-obs-4'],
    promotedBy: 'curator-v1.2',
  },
});

Six months later, when the user asks:

$ npx chainblocks-verify /data/learning-audit.ledger
✓ Ledger: user-12345-learning-log
✓ Writer: learning-service
✓ Entries: 4,237
✓ Chain valid (verified in 142ms)

The system produces every block with kind starting learning.fact. referencing factId: f-a8c4. The chain is unbroken; nothing has been edited.

Requirements exercised

REQ-CB-001, 010-020 (lifecycle, append), 032 (kind-pattern read), 040-044 (verify).


UC-2: A decision pipeline's automated link is challenged

Actor & situation

An automated decision pipeline links a transaction record to a cluster via a business rule. Six weeks later, an auditor or regulator challenges the link.

Need

Produce:

  1. The exact rule version that fired
  2. The exact input record state at evaluation time
  3. The decision record (matched/not-matched, confidence, alternatives considered)
  4. Proof that none of the above has been changed since the link was created

Without chainblocks

The pipeline writes outcomes to its operational database. The rule version is intended to be captured in a version property on the link, but anyone with write access to the database can alter the property at any time. There is no cryptographic boundary.

With chainblocks

The pipeline appends a block per rule-evaluation outcome:

await decisionLedger.append({
  kind: 'pipeline.rule.evaluated',
  payload: {
    inputId: 'T-77381',
    targetId: 'C-441',
    ruleId: 'R-12',
    ruleVersion: 'R-12@2026-04-03',
    inputs: { /* snapshot of relevant input fields */ },
    decision: 'matched',
    confidence: 0.87,
    alternatives: [{ targetId: 'C-389', confidence: 0.42 }],
  },
});

The link in the operational database still exists for query convenience, but the authoritative record is the chainblocks entry. When challenged, the auditor runs the verifier, then read({ kind: 'pipeline.rule.evaluated', ... }) filtered to the input id.

Requirements exercised

REQ-CB-010-020 (append), 030-033 (read with filters), 040 (verify), and the doctrine of "tamper-evident, not tamper-proof" (the operational database can still be edited; the chainblocks record cannot).


UC-3: A compliance system classifies a component against a security control

Actor & situation

A compliance or governance system (typical in government, defense, finance, or healthcare contexts) classifies a software component against a security control — for example, NIST 800-53 IA-5(1) (Authenticator Management — Password-Based). The classification feeds an authorization package or a periodic compliance report.

Three years later, the agency rotates assessors. The new assessor asks: "Show me every classification ever made for this component, with the reasoning at the time."

Need

A retrievable, ordered, integrity-verifiable record of every classification event, with the classifier version and reasoning preserved exactly as captured.

Without chainblocks

The compliance system stores classifications as rows or graph edges. The agency must trust the database hasn't been tampered with — a hard sell in a FedRAMP, IL4/5, or similar context where every component must demonstrate integrity controls.

With chainblocks

The compliance system appends each classification:

await complianceLedger.append({
  kind: 'compliance.classification.assigned',
  payload: {
    componentId: 'C-jwt-go-v4.4.2',
    controlId: 'NIST-800-53-IA-5(1)',
    classifier: 'compliance-classifier-v3.1',
    decision: 'implements',
    evidence: [
      { type: 'source-code-reference', loc: 'jwt/parser.go:142-178' },
      { type: 'doc-reference', loc: 'docs/security-design.md#authentication' },
    ],
    reasoning: '<classification rationale>',
  },
});

Three years later: chainblocks-verify on the compliance audit ledger, then a kind-filtered read for compliance.classification.* against componentId: C-jwt-go-v4.4.2. Every classification, in order, with reasoning, with proof of integrity.

Requirements exercised

REQ-CB-001, 010-020, 030-035, 040-044. Plus the long-term durability of the on-disk JSONL format (REQ-CB-NF-033 — format stable across versions).


UC-4: Agent action provenance

Actor & situation

A software agent (an LLM-driven tool-using process, an autonomous job runner, or a workflow orchestrator) takes several actions on behalf of a user. The user later asks: "wait, what exactly did that agent do?"

Need

A complete record of every action the agent took, in order, with its reasoning if available.

Without chainblocks

The agent's transcript or log is in a session record somewhere. There is no structured action log. Reconstruction requires reading the transcript, which is painful and incomplete.

With chainblocks

A small wrapper makes every tool invocation an append:

await agentLedger.append({
  kind: 'agent.tool.invoked',
  payload: {
    sessionId: 'session-7a2c',
    tool: 'write-file',
    args: { path: '/some/file.md', size: 10897 },
    rationale: '<why the agent took this action>',
  },
});

Now "what did the agent do?" is a simple read({ kind: 'agent.tool.*' }) filtered to the session id.

Requirements exercised

REQ-CB-018-020 (atomic append), 030-033 (filtered read), 070 (append events — listeners can mirror this into a live dashboard).


UC-5: Cross-implementation verification

Actor & situation

A government adopter writes their own chainblocks verifier in Go (or Rust, or Python), because their security team requires that audit-evidence tools be inspectable and buildable from source in their environment.

Need

The independent verifier must produce the same ok/tamperedAt result as the TypeScript verifier on every chainblocks ledger, byte-for-byte.

Without chainblocks

If chainblocks used custom or under-specified canonicalization, this would be a multi-week ordeal of finding edge cases where the two implementations diverge.

With chainblocks

Because chainblocks uses RFC 8785 JSON Canonicalization (JCS) (REQ-CB-016) and SHA-256 (REQ-CB-NF-020) — both well-specified, with mature implementations in every major language — the alternate verifier is a few hundred lines and produces identical results. The on-disk JSONL format is human-readable and version-stable (REQ-CB-NF-033).

This is what makes chainblocks adoptable. The TypeScript implementation is the reference, but it's not the only possible implementation.

Requirements exercised

REQ-CB-016 (canonical hashing), REQ-CB-NF-020 (NIST-approved crypto), REQ-CB-NF-033 (format stability), and the entire MODEL.md document (which doubles as a portable specification).


UC-6: Tamper detection in the wild

Actor & situation

An attacker gains write access to a chainblocks ledger file (via filesystem permissions misconfiguration, supply-chain attack on the host, or a compromised admin). They edit block 1,342 to change a payload field.

Need

The next time anyone runs chainblocks-verify, the tampering must be detected, and the report must identify the exact block that was altered.

Without chainblocks

Audit logs are typically detected as tampered only when (a) a vigilant operator notices a discrepancy, or (b) a forensic analyst is called in after a breach. Both are slow and lossy.

With chainblocks

$ npx chainblocks-verify /var/audit/app.ledger
✗ Chain broken at seq=1342
  reason: hash_mismatch
  Expected hash: sha256:8f3a91c2...
  Computed hash: sha256:def01bc4...
  ts: 2026-04-22T14:33:12.001Z
  kind: app.event.recorded
  Tampered or stored block was altered after sealing.

Exit code 1. The operator knows exactly which block was edited, and by inspecting that block they know which event was contested. Forensic recovery is now a question of "what was the original value at seq=1342?" — which can be answered from backups, replicas, or any other copy of the ledger file.

Requirements exercised

REQ-CB-040-042 (verify with precise tamper reporting), REQ-CB-060-062 (CLI), REQ-CB-NF-022 (no payload leakage in error messages — the report says which block, not what the payload contained).


UC-7: The pluggable substrate

Actor & situation

A team adopting chainblocks does not want a separate file. They already have a Postgres database in their stack and want the ledger blocks to live there alongside their other audit data.

Need

Use chainblocks' integrity guarantees with Postgres as the storage backend, without modifying chainblocks core.

With chainblocks

They write a small PostgresStore that satisfies the Store interface:

class PostgresStore implements Store {
  constructor(private pool: pg.Pool, private tableName: string) {}

  async init(genesis: Block) { /* ... */ }
  async append(block: Block) {
    await this.pool.query(
      `INSERT INTO ${this.tableName} (seq, ts, prev, kind, payload, hash)
       VALUES ($1, $2, $3, $4, $5, $6)`,
      [block.seq, block.ts, block.prev, block.kind, JSON.stringify(block.payload), block.hash]
    );
  }
  async *readAll() { /* ... */ }
  async *readFromSeq(seq: number) { /* ... */ }
  async getHead() { /* ... */ }
  async close() { /* ... */ }
}

They use chainblocks' verify against their Postgres store and get the same integrity guarantee, with their preferred substrate. Chainblocks core remains zero-dependency.

This is the use case that makes chainblocks "infrastructure" rather than "a feature." The protocol is portable; the substrate is the customer's.

Requirements exercised

REQ-CB-050-051 (Store port), REQ-CB-NF-032 (zero runtime deps beyond JCS), and the doctrine of pluggable storage (STORY.md §2).


UC-8: Verification at the speed of audit

Actor & situation

An assessor receives a 12-month-old chainblocks ledger with ~500,000 entries from a regulated system. They have one hour to confirm integrity before their meeting.

Need

Verification must complete in seconds, not hours.

With chainblocks

$ time npx chainblocks-verify /evidence/audit-ledger-2025.jsonl
✓ Ledger: regulated-system-audit
✓ Writer: system
✓ Entries: 504,231
✓ Chain valid (verified 504,231 entries in 47.3s)

real    0m48.122s

At ≥10,000 blocks/sec (REQ-CB-NF-002), a year of audit data verifies in under a minute on a laptop.

Requirements exercised

REQ-CB-NF-002 (verify throughput), REQ-CB-043 (linear-time verification), REQ-CB-060-064 (CLI).


UC-9: Migrating between storage substrates

Actor & situation

An adopter currently writes their audit ledger to a local file (FileStore). They decide to move audit data into a centrally-administered database (a custom Store implementation). The existing ledger must move with them.

Need

Migrate the existing ledger from FileStore to the new store without breaking the chain, without losing the integrity guarantee, and without rewriting blocks (which would change their hashes).

With chainblocks

The migration is conceptually a for-await-of source-store.readAll(); await target-store.append(block) loop. Because blocks are immutable and self-verifying:

  1. Read every block from FileStore
  2. Write every block (unchanged) to the new store
  3. Verify the new ledger
  4. If verification passes, swap pointers; if not, abort

The chain's integrity is preserved because the hashes never changed — only the substrate they sit in.

Requirements exercised

REQ-CB-050-057 (full Store port + FileStore contract; the migration loop calls readAll, append, getHead, and uses both source and target Store implementations), REQ-CB-010-020 (block immutability), REQ-CB-036 (sequence-indexed reads for spot-checks during migration), REQ-CB-040 (post-migration verification).


UC-10: Reopen and resume after restart

Actor & situation

A service hosting a chainblocks ledger restarts — planned (rolling deploy) or unplanned (crash, host reboot). On startup, the service must reopen its ledger and continue appending where it left off without breaking the chain.

A second instance of the service accidentally launches against the same ledger directory while the first is still running.

Need

  1. Clean shutdown of the ledger releases resources (REQ-CB-003)
  2. Reopening reads back the exact head that was current at close, byte-for-byte (REQ-CB-004)
  3. Two concurrent openers of the same ledger fail loudly with LockHeldError rather than corrupting the chain (REQ-CB-005)
  4. Stale lockfiles from crashed prior runs are detected and reclaimed safely (REQ-CB-NF-012)

Without chainblocks

Ad-hoc audit-log files in restart-prone services typically use append-only file handles. They lack a head-state record, so on restart the service must scan to find the end — slow and racy. There is no enforcement against two writers stomping on each other; the result is interleaved log lines and corrupted records that may not be discovered until weeks later.

With chainblocks

// Service startup
async function startup() {
  const ledger = await openLedger({
    store: new FileStore({ path: '/var/audit/service.ledger' }),
    name: 'service-audit',
    writer: 'service',
  });

  const head = await ledger.head();
  console.log(`Resumed at seq=${head.seq}, hash=${head.hash}`);

  // Begin normal operation; subsequent appends continue from head.seq + 1
  return ledger;
}

// If a second instance accidentally launches:
try {
  await openLedger({ ... });
} catch (err) {
  if (err instanceof LockHeldError) {
    console.error('Another instance already owns this ledger; refusing to start.');
    process.exit(1);
  }
  throw err;
}

A crashed prior instance leaves a stale lockfile; chainblocks detects the PID is dead and the lockfile is older than 60s, reclaims it, and proceeds. A live competitor instance triggers LockHeldError immediately. Both behaviors are deterministic and verifiable.

Requirements exercised

REQ-CB-002 (open existing), REQ-CB-003 (clean close), REQ-CB-004 (reopen-byte-identical), REQ-CB-005 (LockHeldError), REQ-CB-NF-010 (crash-safety invariant), REQ-CB-NF-011 (torn-write detection), REQ-CB-NF-012 (stale-lock recovery), REQ-CB-035 (head query), REQ-CB-071 (event listener errors don't corrupt chain).


A note on the unexercised non-functional requirements

The following requirement categories are intentionally NOT covered by use-case scenarios above. They are project-wide quality bars enforced by CI, by the build configuration, and by the GitHub Published Projects playbook — not by user-facing scenarios:

  • Compatibility (REQ-CB-NF-030-033) — Node version matrix, ESM/CJS dual package, zero runtime deps, format stability. Verified by CI matrix and by package.json configuration; no user-facing scenario exercises these directly.
  • Documentation (REQ-CB-NF-040-043) — JSDoc coverage, API.md completeness, runnable README, examples. Verified by CI gates and by playbook checks.
  • Quality gates (REQ-CB-NF-050-053) — test/typecheck/lint/coverage on every PR. Verified by CI workflow definitions.
  • Performance (REQ-CB-NF-001-003) — latency/throughput budgets. Verified by benchmark tests (test/performance/budgets.bench.ts) rather than use-case scenarios.
  • Security (REQ-CB-NF-021-023) — no-network, no-payload-logging, no-stray-writes. Verified by integration tests (test/integration/security-no-egress.test.ts) and code audit.
  • Reading internals (REQ-CB-036 getBySeq, REQ-CB-052-057 Store internal API) — these are invoked by use-case code paths above but not as the headline scenario action. Covered by unit and integration tests of the Store implementations.

These requirements are owned by FT-CB-16 (Project-wide quality bar) and verified at the CI level, which is why they appear as orphans in any use-case-driven coverage view. This is by design.


Coverage summary

Use case Primary requirements exercised
UC-1 (learning-system fact promotion) Lifecycle, append, read, verify
UC-2 (decision pipeline link challenge) Append, filtered read, verify
UC-3 (compliance classification) Long-term durability, format stability
UC-4 (agent action provenance) Append events, structured kind taxonomy
UC-5 (cross-implementation verification) Canonicalization, format spec
UC-6 (tamper detection) Verify, precise tamper reporting, CLI
UC-7 (pluggable substrate) Store port
UC-8 (verification at scale) Performance, CLI
UC-9 (substrate migration) Block immutability, Store port
UC-10 (reopen and resume) Lifecycle, lock-held error, stale-lock recovery

Every requirement in REQUIREMENTS.md is exercised by at least one use case here. If a future requirement does not trace to a use case, that's a smell — either the use case is missing, or the requirement is.