/** * @pwngh/economy-lab * * Copyright (c) Preston Neal * * This source code is licensed under the MIT license found in the * LICENSE.md file in the root directory of this source tree. * * @license MIT */ import type { AccountRef } from './accounts.js'; import type { ArchiveHead, ArchiveState, Checkpoint, CheckpointStore, Digest, Ids, Clock, Ledger, CallOptions, Posting, Signer, StoredLink } from './ports.js'; /** * One step in an account's hash chain: the new head after a posting and the head it followed. * Each account has its own chain, and its latest head summarizes its whole history. * * @see {@link https://economy-lab-docs.pages.dev/economy/concepts/integrity/ Integrity} for the * tamper-evidence construction these links and checkpoints implement. */ export type ChainLink = { account: AccountRef; /** * Head hash before this posting, lowercase hex. For an account's first posting this is the * genesis value (64 zeros). */ prevHash: string; /** Head hash after this posting, lowercase hex. */ hash: string; }; /** * The first broken link the prover finds in an account's chain. Returned instead of a bare * boolean so a caller can see which posting failed and how. */ export type ChainBreak = { account: AccountRef; txnId: string; /** * 'broken-link' means the stored "previous head" does not match the head reached by walking the * chain so far, so the chain is not continuous. 'tampered-hash' means re-hashing the stored * entries and metadata no longer produces the recorded head hash, so the contents were changed * after the fact. */ reason: 'broken-link' | 'tampered-hash'; /** * The hash that should have been found. For 'broken-link' this is the head reached by walking the * chain. For 'tampered-hash' this is the recomputed hash. */ expected: string; /** The stored hash that failed to match `expected`. */ actual: string; }; export type ChainReport = { intact: boolean; firstBreak: ChainBreak | null; /** How many account chains were checked. */ count: number; }; /** * Computes the new head for each distinct account a posting touches, in first-appearance order. It * produces one link per account, not one per leg, so a posting that names an account in several legs * still advances that account's chain a single step. Each account hashes only its own legs onto its * own prior head, so chains never cross. `prevHeadOf` returns an account's current head; an undefined * result or the genesis value starts a fresh chain. */ export declare function advanceHeads(digest: Digest, posting: Posting, prevHeadOf: (account: AccountRef) => string | undefined): Promise; /** * Re-checks every account's chain. It walks each account's postings from genesis, recomputes each * head with the write path's hash function, and stops at the first mismatch. Accounts are checked in * a fixed order, sorted by id char by char, so a break is reported identically whatever order a * runtime or database returns accounts in. * * @see {@link https://economy-lab-docs.pages.dev/economy/concepts/integrity/ Integrity} for the * re-derivation this proves and what a break means. */ export declare function proveChain(deps: { ledger: Ledger; digest: Digest; /** The verified archival boundary; absent or null means nothing has been archived. */ boundary?: ArchiveBoundary | null; }, options?: CallOptions): Promise; /** * Loads a posting and proves its stored content against its own chain links before returning it — * the read every handler that derives money from history must use. Each touched account's link * hash is recomputed from the stored legs and metadata (both inside the preimage), so an in-place * edit faults CHAIN_BROKEN here instead of shaping a reversal; the zero-sum check catches a leg * deleted together with its link. What this deliberately does not prove: an attacker who * re-derives an account's whole chain moves its head, which the next seal flags as dirty and * breaks on — that variant is bounded by seal cadence, and restoring the head exactly requires a * hash collision. Null on an unknown id, exactly like {@link Ledger.posting}. */ export declare function verifiedPosting(deps: { ledger: Ledger; digest: Digest; }, txnId: string, options?: CallOptions): Promise; /** * Re-derives each link's hash from its stored content — the rolling re-proof's page check * (src/worker/reproof.ts). Content only, on purpose: prevHash continuity across links is proven * inductively by the seals (every tail was replayed while its account was dirty, anchored to a * previously signed head), so the page order never matters and the walk stays resumable anywhere. */ export declare function reproveLinks(digest: Digest, links: ReadonlyArray<{ account: AccountRef; } & StoredLink>): Promise; /** * Reduces every account's head into one Merkle root, so signing the root (see `recordCheckpoint`) * covers every chain in one signature. The root changes if any head changes. The root is * reproducible across machines because leaves are sorted by account id, the RFC 6962 domain tags * (`MERKLE_LEAF` and `MERKLE_NODE`) are applied, and each pair is hashed left then right so order * matters. With no accounts the root is the genesis value of 32 zero bytes, so a fresh ledger still * has a stable root to sign. * * @see {@link https://economy-lab-docs.pages.dev/economy/concepts/integrity/ Integrity} for how the * root anchors the whole ledger under one signature. */ export declare function merkleRoot(digest: Digest, heads: ReadonlyArray): Promise; /** * Reduces every account's (head, sum) pair into one sum-carrying Merkle root: each node holds its * subtree's balance sum, and each node's hash commits to (both child hashes + that sum), so the * root hash fixes every head AND every sum at once, and the root sum is as tamper-evident as the * root hash. `sum` per leaf is the account's raw signed leg total (debit positive), which is why * a consistent ledger's root sums to zero: legs net to zero per currency, so they net to zero in * total. Reproducible across machines the same way `merkleRoot` is, with versioned domain tags and * the sum encoded as fixed-width big-endian bytes (`toInt64BE`), never as formatted text. With no * accounts the root is the genesis value of 32 zero bytes and a zero sum. */ export declare function merkleSumRoot(digest: Digest, leaves: ReadonlyArray): Promise<{ hash: Uint8Array; sum: bigint; }>; /** * Takes a tamper-evident snapshot, called a "checkpoint". It first proves the chain re-derives — * from genesis, or, when the store carries the previous seal's authenticated leaves, only the * dirty tails since that seal — then signs the sum-carrying Merkle root (v2) over every head and * saves it. On a break this throws a * non-retryable CHAIN_BROKEN fault and persists nothing, so a signed root never attests to a * tampered ledger, and the caller sets the job aside for an operator rather than retrying. It also * refuses to sign when the collected sums do not net to zero (a non-retryable LEDGER_UNBALANCED * fault): the chain hashes cover every leg, so an edit breaks CHAIN_BROKEN first — a nonzero total * with intact chains means the write path itself recorded unbalanced money, which is exactly the * enforcement bug this last-resort check exists to catch. The signature covers the root hash and * the root sum together, so neither stored field can be edited under a valid signature. The save * goes through the checkpoint store, outside the money transaction, so a rolled-back operation * cannot undo an already-recorded checkpoint. * * @see {@link https://economy-lab-docs.pages.dev/economy/concepts/integrity/ Integrity} for the * checkpoint's role in the tamper-evidence story. */ export declare function recordCheckpoint(deps: { ledger: Ledger; checkpoints: CheckpointStore; digest: Digest; signer: Signer; clock: Clock; ids: Ids; }, options?: CallOptions): Promise; /** * Checks a saved checkpoint against the current ledger: recomputes the root the same way the * checkpoint's version sealed it (v1: hash-only over heads; v2: sum-carrying over heads and * sums), compares it to the stored root, then verifies the signature (accepting still-valid * rotated-out keys, so a checkpoint signed before a rotation keeps verifying). Rows from before * versioning verify forever down the v1 path, byte for byte. Returns false on a normal mismatch; * a live head count below the recorded one is one such mismatch, since deleting accounts to * shrink the root's coverage is itself tampering, and on v2 an edited stored sum is another, * since the signature covers the sum. Throws only on malformed stored hex, which is a corrupt * row, not a failed verification. * * @see {@link https://economy-lab-docs.pages.dev/economy/concepts/integrity/ Integrity} for why fewer * heads than recorded is itself a tamper signal. */ export declare function verifyCheckpoint(deps: { ledger: Ledger; digest: Digest; signer: Signer; }, checkpoint: Checkpoint): Promise; /** The archive-head signing payload: the domain tag then the v2 root encoding. */ export declare function archivePayload(root: { hash: Uint8Array; sum: bigint; }): Uint8Array; export declare function verifyArchiveHeads(deps: { digest: Digest; signer: Signer; }, heads: ReadonlyArray, state: ArchiveState): Promise; /** * The verified archival boundary the provers and the seal anchor on: per-account archived head * hashes and raw leg sums, trusted only because their recomputed root matches the signed * {@link ArchiveState}. Null when nothing has ever been archived. */ export type ArchiveBoundary = { anchors: ReadonlyMap; rawSums: ReadonlyMap; state: ArchiveState; }; /** * Loads and authenticates the archival boundary. Faults CHAIN_BROKEN when the stored rows fail * their signature — an unauthenticated boundary is an anchor an attacker can move, so every * prover and seal refuses to proceed over one. */ export declare function loadArchiveBoundary(checkpoints: CheckpointStore, deps: { digest: Digest; signer: Signer; }, options?: CallOptions): Promise; /** The v2 signing payload: root hash then the 8-byte big-endian sum. Exported for the archival * mover, whose archive-head signatures reuse this encoding under their own domain tag. */ export declare function sumRootPayload(root: { hash: Uint8Array; sum: bigint; }): Uint8Array;