Skip to Content
GuidesProvenance Chains

Provenance Chains

Provenance chains provide cryptographic tamper evidence for pipeline decisions. Each record includes the SHA-256 hash of the previous record — modifying any entry invalidates all subsequent hashes, making tampering detectable. Merkle proofs allow compact verification that a specific record exists without revealing others.

Setup

import { createAgent } from '@txfence/core' import { createFileProvenanceChain } from '@txfence/provenance' const chain = createFileProvenanceChain('./provenance.jsonl') const agent = createAgent( config, adapters, rpcUrls, executor, undefined, undefined, undefined, undefined, // 5–8: capLockProvider, metadataVerifier, approvalProvider, receiptStore undefined, undefined, undefined, undefined, // 9–12: auditLog, telemetryProvider, capLockConfigs, policyNode undefined, // 13: notificationProvider chain, // 14: provenanceChain )

Backends

createMemoryProvenanceChain() — in-memory, for testing.

createFileProvenanceChain(path) — JSONL file with atomic write-then-rename appends. Survives process crashes without corruption.

ProvenanceRecord

Each record captures every input to an authorization decision. The id, previousHash, and entryHash fields are computed by append() — callers supply everything else via ProvenanceRecordInput.

type ProvenanceRecord = { id: string // assigned by append() previousHash: string // assigned by append() entryHash: string // assigned by append() timestamp: number agentId: string policyVersionId: string action: Action simulationResult?: SimulationResult approvalDecision?: 'approved' | 'rejected' | 'not_required' approver?: string submittedAtBlock?: number // plain number, not bigint submittedAtBlockHash?: string outcome: ProvenanceOutcome // discriminated union, see below receipt?: SuccessReceipt } type ProvenanceOutcome = | { status: 'success'; txHash: string; confirmedAtBlock: number; gasUsed: string } | { status: 'policy_rejected'; reason: PolicyRejectionReason } | { status: 'simulation_failed' } | { status: 'approval_timeout' } | { status: 'execution_failed'; reason: ExecutionFailureReason } | { status: 'simulation_stale'; stalenessMs: number }

Verifying integrity

const result = await chain.verify() if (!result.valid) { for (const violation of result.violations) { console.error(`Entry ${violation.entryId} (${violation.entryHash}): ${violation.violation}`) console.error(` details: ${violation.details}`) // violation.violation: 'hash_mismatch' | 'chain_broken' | 'invalid_hash' } }

ProvenanceVerificationResult returns the full picture, not just valid:

type ProvenanceVerificationResult = { valid: boolean entryCount: number firstHash: string lastHash: string merkleRoot: string violations: ChainViolation[] verifiedAt: number } type ChainViolation = { entryId: string entryHash: string violation: 'hash_mismatch' | 'chain_broken' | 'invalid_hash' details: string }

Run this on a schedule or before any compliance audit.

Merkle proofs

Prove a specific record exists without revealing the full chain:

const root = await chain.getMerkleRoot() // async const proof = await chain.generateProof(entryHash) // async, returns MerkleProof | null const valid = chain.verifyProof(proof) // synchronous — no await

The Merkle root commits to all records. Share the root publicly and generate proofs on demand — each proof is O(log n) in size.

Hash chaining

Each entry’s hash is computed as: SHA-256(previousHash + canonicalJSON(record))

The first entry uses GENESIS_HASH ('0'.repeat(64)). Canonical JSON sorts all keys and serializes bigints as strings for deterministic output.

CLI

# Verify chain integrity txfence provenance verify --chain ./provenance.jsonl # Generate a Merkle proof for a specific record txfence provenance proof --chain ./provenance.jsonl --hash <entryHash> # JSON output for CI txfence provenance verify --chain ./provenance.jsonl --json

Both commands exit 0 when valid, 1 when invalid.

Known limitations

  • agentId in pipeline-recorded entries is 'unknown' — the pipeline does not have agent identity. For named agent IDs, record provenance directly via chain.append().
  • Chain file must be stored on tamper-evident infrastructure for strict compliance.
  • No PostgreSQL or S3 backend yet — planned.
Last updated on