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 awaitThe 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 --jsonBoth commands exit 0 when valid, 1 when invalid.
Known limitations
agentIdin pipeline-recorded entries is'unknown'— the pipeline does not have agent identity. For named agent IDs, record provenance directly viachain.append().- Chain file must be stored on tamper-evident infrastructure for strict compliance.
- No PostgreSQL or S3 backend yet — planned.