Skip to content

Architecture Overview

System Stack

flowchart TD
    app["Application<br/>(wiring + lifecycle)"]
    http["HTTP Server<br/>(Servant REST API + Swagger UI)<br/>proof-bearing fact provider"]
    idx["CageFollower<br/>(ChainSync block processing)<br/>unified transaction per block"]
    mpf["MPF Trie<br/>(merkle-patricia-forestry)<br/>Proofs, insertion, deletion"]
    verify["cardano-mpfs-verify<br/>CSMT/MPF/read/facts verifiers"]
    cageTx["cardano-mpfs-cage-tx<br/>pure cage builders<br/>*WithEval API"]
    browser["Downstream browser clients<br/>HTTP + CIP-30"]
    submit["Submitter<br/>(LocalTxSubmission)"]
    n2c["Node Client<br/>(node-to-client)<br/>ChainSync + eval context + LTxS"]

    app --> http
    http --> verify
    http --> idx
    app --> idx --> mpf
    browser --> http
    browser --> cageTx
    cageTx --> verify
    http --> submit
    submit --> n2c
    idx --> n2c

The service connects to a Cardano node via two N2C connections:

  1. ChainSync — blocks are processed by the CageFollower, which applies UTxO, cage state, and trie mutations in a single atomic RocksDB transaction per block (see Block Processing).
  2. LocalStateQuery + LocalTxSubmission — LocalTxSubmission sends signed transactions. LocalStateQuery is used for protocol parameters, script evaluation, slot conversion, and the trusted interim GET /eval-context metadata. It is not used as a public UTxO fact source.

The public write path is facts-first. The server returns proof-bearing facts anchored to one indexed UTxO-CSMT snapshot; clients verify them with cardano-mpfs-verify, run cardano-mpfs-cage-tx locally, sign with wallet keys, and submit signed CBOR through POST /submit. The server must not return unsigned transactions for script-bearing cage operations. The remaining server-built transaction route is the owner-only /tx/sweep cleanup path.

Read endpoints ship only provable on-chain data plus proofs: witnessed UTxOs (TxIn + full TxOut + CSMT inclusion proof), the completeness / MPF-replay proofs binding them to the snapshot's utxo_root, and genuinely server-only data the client cannot derive (the snapshot, chain tip, and utxo_root). They must not ship server-side projections - parsed convenience JSON re-rendering data already present in a witnessed TxOut. Projections are unverified, frequently lossy, and tempt verifying clients into trusting non-provable data. A client reconstructs everything it needs (token id, token state, request payload) by decoding the inline datum of the witnessed TxOut; the server's job is to provide that provable material, not to interpret it.

POST /submit accepts only MPFS operations. Before relaying, the server runs a cheap scope gate on the decoded tx. A transaction is admitted when it touches the cage contract surface in any of these ways:

  • it mints or burns the cage state-token policy (boot, end);
  • it locks an output at the cage state address (boot, update, reject);
  • it produces a request output — a RequestDatum-bearing output sitting at this cage's per-token request validator address (requestAddrFromCfg for the token named in the datum), so a crafted RequestDatum at any other script address is rejected (request create);
  • it spends a cage-owned UTxO — the cage state UTxO or a request UTxO at a request validator address. This input-aware clause is what admits the spend-only operations retract and sweep, which refund or reclaim a request UTxO and so leave no cage mint and no cage output.

The mint and output checks are purely structural, but the spend check needs the chain: the handler resolves the tx's spent inputs against the indexer's UTxO set (one atomic read) and feeds the resolved TxOuts into the pure predicate. The gate is conservative — an input the indexer cannot resolve (its view may lag the chain) is treated as touching the cage, so a valid operation is never false-rejected; only a transaction that is definitely non-MPFS (no cage mint, no cage output, every spent input resolved to a non-cage UTxO) — a plain ADA transfer, say — is rejected, with a typed 400 "this service only submits MPFS operations". This is abuse prevention at the gateway, not a new trust boundary; the on-chain validators remain authoritative.

The 502 era-history failure was fixed by carrying live era history in GET /eval-context and deriving EpochInfo from it in the reactor. ScriptContext POSIXTimeRange costs now use the same era clock as the node instead of a hardcoded clock.

Singleton Dependency Graph

Every major component is a record of functions (no typeclasses). Records are created bottom-up and torn down top-down using bracket patterns.

graph TD
    APP["Application<br/>(withApplication)"]
    CTX["Context<br/>(facade record)"]
    PRV["Provider<br/>(N2C params/eval context)"]
    TM["TrieManager<br/>(per-token MPF tries)"]
    ST["State<br/>(tokens, requests, checkpoints)"]
    READS["Indexer Reads<br/>(atomic proof queries)"]
    IDX["Indexer<br/>(ChainSync)"]
    SUB["Submitter<br/>(N2C LocalTxSubmission)"]
    TXB["TxBuilder<br/>(internal proof envelopes<br/>/tx/sweep + legacy native paths)"]
    NODE["Cardano Node<br/>(Unix socket)"]

    APP --> CTX
    CTX --> PRV
    CTX --> TM
    CTX --> ST
    CTX --> READS
    CTX --> IDX
    CTX --> SUB
    CTX --> TXB
    PRV --> NODE
    SUB --> NODE

Application Wiring

withApplication creates and wires all components:

graph LR
    DB["RocksDB<br/>(14 CFs)"]
    DB --> ST["State<br/>(persistent)"]
    DB --> TM["TrieManager<br/>(persistent)"]
    DB --> CF["CageFollower<br/>(unified txn)"]
    DB --> READS["IndexerTx reads<br/>(proof snapshots)"]
    N2C1["N2C #1<br/>(ChainSync)"] --> CF
    N2C2["N2C #2<br/>(LSQ eval context + LTxS)"] --> PRV["Provider"]
    N2C2 --> SUB["Submitter"]
    PRV & SUB & ST & TM & READS --> TXB["TxBuilder"]
    PRV & SUB & ST & TM & READS & TXB --> CTX["Context"]

All components use real implementations backed by RocksDB and N2C connections. The CageFollower runs on connection 1 (ChainSync), processing each block in a single atomic transaction. The proof-bearing HTTP handlers read through Context.runIndexerTx, composing snapshot, UTxO, request-set, and MPF reads into a single underlying transaction. The Provider uses connection 2 only for ledger metadata/evaluation; Submitter uses it for LocalTxSubmission.

External Dependencies

graph TD
    OFFCHAIN["cardano-mpfs-offchain<br/>Service: indexer + fact API"]
    API["cardano-mpfs-api<br/>Wire DTOs + Servant API"]
    VERIFY["cardano-mpfs-verify<br/>Pure verifiers"]
    CAGETX["cardano-mpfs-cage-tx<br/>Pure cage tx builders"]
    CAGE["cardano-mpfs-cage<br/>On-chain types + scripts"]
    CLIENTS["cardano-node-clients<br/>TxBuild DSL, N2C provider,<br/>fee balancing"]
    MTS["haskell-mts<br/>CSMT + MPF libraries"]
    ONCHAIN["cardano-mpfs-onchain<br/>Aiken validators + cage lib"]
    LEDGER["cardano-ledger<br/>Conway era types"]

    OFFCHAIN --> API
    VERIFY --> API
    CAGETX --> VERIFY
    OFFCHAIN --> CAGE
    CAGETX --> CAGE
    OFFCHAIN --> CLIENTS
    CAGETX --> CLIENTS
    OFFCHAIN --> MTS
    VERIFY --> MTS
    OFFCHAIN --> LEDGER
    CAGETX --> LEDGER
    CAGE -.->|"lives in"| ONCHAIN

    style OFFCHAIN fill:#e1f5fe
    style CAGE fill:#fff3e0
    style CLIENTS fill:#e8f5e9
    style MTS fill:#e8f5e9
    style ONCHAIN fill:#fff3e0
    style LEDGER fill:#f3e5f5
Color Meaning
Blue This project
Blue-linked packages Split local packages in this repository
Orange On-chain repo (validators + cage library)
Green lambdasistemi libraries
Purple Cardano ecosystem dependencies

The current #62 bounded-refund validator cutover uses cage script hash ad0a8eeeec8b0a5ee9930be5d6ea2e80b285fc2f3e9675a13a392dd5. The old c0f05a30... exact-refund hash is legacy and should not be used for new preprod/browser flows.

Module Hierarchy

The repository is split into several packages. The server lives in cardano-mpfs-offchain; shared wire types live in cardano-mpfs-api; pure verifiers live in cardano-mpfs-verify; pure cage builders live in cardano-mpfs-cage-tx; and native clients can consume the re-exporting cardano-mpfs-client.

The cardano-mpfs-offchain library is organized in layers. Server modules live under Cardano.MPFS.

Core — domain types and pure logic

Module Purpose
Core.Types TokenId, Root, Request, TokenState, CageConfig
Core.OnChain Re-exports from cage + offchain-specific cagePolicyId, cageAddr
Core.Blueprint Re-exports from cage: blueprint loading, validation
Core.Proof Re-exports from cage: proof serialization

Interfaces — record-of-functions singletons

Module Purpose
Context Facade bundling all singletons
Provider protocol params, evaluation, slot conversion, and trusted interim eval context support; LSQ UTxO queries are forbidden on proof/write hot paths
State Tokens, Requests, Checkpoints sub-records
Trie TrieManager — per-token MPF trie access
TxBuilder internal proof-envelope builders and /tx/sweep; browser-facing script-bearing transactions are built client-side from facts
Indexer Chain follower lifecycle (start, stop, getTip)
Submitter submitTx :: Tx ConwayEra -> m SubmitResult
Application withApplication — wiring and lifecycle

HTTP — REST API

Module Purpose
HTTP.API Server-local API wrapper around shared Servant API plus metrics
HTTP.Types Server compatibility re-exports and ledger conversion helpers
HTTP.Encoding Hex newtype for binary-as-hex transport
HTTP.Server WAI application wiring, mkApp
HTTP.Swagger OpenAPI spec generation, Swagger UI

The shared API and DTO definitions live in cardano-mpfs-api:

Package module Purpose
Cardano.MPFS.API Canonical Servant API: GET /eval-context, proof-bearing reads, facts endpoints, /tx/sweep, /submit
Cardano.MPFS.API.Types Status, token, request, read-response, submit, and sweep JSON types
Cardano.MPFS.API.Types.Facts Facts-only response types for boot/request/update/retract/reject/end
Cardano.MPFS.API.Types.Common Shared snapshot, UTxO witness, token id, and eval-context primitives

Indexer — chain sync and persistence

Module Purpose
Indexer.CageFollower Unified rollForward/rollBackwardone block = one transaction
Indexer.Event detectCageEvents — cage tx classification
Indexer.Follower detectCageBlockEvents, applyCageBlockEvents — generic over Monad m
Indexer.Persistent mkTransactionalState (composable) + mkPersistentState (IO)
Indexer.Columns AllColumns + UnifiedColumns GADTs — full DB schema
Indexer.Codecs CBOR serialization for column key-value types
Indexer.Rollback storeRollbackT, rollbackToSlotT — transactional rollback

Node integration — thin wrappers over cardano-node-clients

Module Purpose
Provider.NodeClient N2C-backed Provider (PParams, script eval, slot conversion, queryEvalContext)
Submitter.N2C N2C-backed Submitter (LocalTxSubmission)

TxBuilder — internal server builders

Module Purpose
TxBuilder.Config CageConfig loading
TxBuilder.Real mkRealTxBuilder entry point
TxBuilder.Real.Boot Mint cage token
TxBuilder.Real.Request Submit insert/delete request
TxBuilder.Real.Update Consume requests, update root
TxBuilder.Real.Retract Cancel pending request
TxBuilder.Real.End Burn cage token
TxBuilder.Real.Internal Shared helpers, POSIX-to-slot conversion

These server builders now return proof envelopes and consume BundleSnapshot/indexed UTxO facts rather than querying the node for UTxO state. The public browser path uses facts endpoints plus the cardano-mpfs-cage-tx package, whose *WithEval builders run after cardano-mpfs-verify verifies the supplied facts.

Client verification and cage reactors

Package module Purpose
Cardano.MPFS.Client.Verify Re-exported verifier facade
Cardano.MPFS.Client.Verify.Read verifyTokenState, verifyTokenFacts, verifyTokenRequests
Cardano.MPFS.Client.Facts verifyBootFacts, verifyRequest*Facts, verifyUpdateFacts, verifyRetractFacts, verifyRejectFacts, verifyEndFacts
Cardano.MPFS.Client.Verify.Completeness CSMT completeness witness checks
Cardano.MPFS.Client.Verify.MPF MPF proof replay
Cardano.MPFS.Client.Cage.Reactor JSON envelope dispatcher for native/wasm cage transactions
Cardano.MPFS.Client.Cage.Eval Decodes GET /eval-context and derives EpochInfo from live era history

Trie — MPF backends

Module Purpose
Trie.Persistent mkUnifiedTrieManager (transactional) + mkPersistentTrieManager (IO with caches)
Trie.PureManager mkPureTrieManager — in-memory TrieManager for tests

Mock — test doubles

Module Purpose
Mock.Context withMockContext — full mock wiring
Mock.Provider In-memory UTxO store
Mock.State mkMockStateIORef-backed state
Mock.Submitter Always-succeeds submitter
Mock.TxBuilder mkMockTxBuilder — placeholder builder

Utilities

Module Purpose
Trace AppTrace structured tracing type, jsonLinesTracer for stderr JSON-lines logging

Design Principles

  • No typeclasses — closed world with explicit records of functions.
  • All types from cardano-ledgerTx ConwayEra, PParams ConwayEra, Addr, TxIn, etc.
  • Visible dependency graph — no implicit resolution surprises.
  • Trivial testing — swap the record for a mock backend.
  • Fact-provider boundary — public script-bearing writes return proof-bearing material, not unsigned transactions.
  • No LSQ UTxO hot path — UTxO and request facts come from the indexed CSMT snapshot. LSQ UTxO scans bypass the proof system and are forbidden on production write/read proof paths.
  • One verifier, many targets — the same pure verifier and cage builder code runs native, wasm32-wasi, and GHC-JS.
  • No orphan instances.

Implementation Phases

graph LR
    P0["Phase 0<br/>MPF Library ✓"]
    P1["Phase 1<br/>Service Interfaces ✓"]
    P2["Phase 2<br/>N2C Client +<br/>Provider ✓"]
    P3["Phase 3<br/>Transaction<br/>Builders"]
    P4["Phase 4<br/>ChainSync Indexer +<br/>Persistent State"]
    P5["Phase 5<br/>HTTP API +<br/>Deployment"]
    P6["Phase 6<br/>Fact Provider +<br/>Browser Reactor"]

    P0 --> P1 --> P2 --> P3 --> P4 --> P5 --> P6

    style P0 fill:#2d6,color:#fff
    style P1 fill:#2d6,color:#fff
    style P2 fill:#2d6,color:#fff
    style P3 fill:#2d6,color:#fff
    style P4 fill:#2d6,color:#fff
    style P5 fill:#2d6,color:#fff
    style P6 fill:#2d6,color:#fff
Phase Description Status
0 MPF library — 16-ary Merkle Patricia Forestry, Blake2b-256 hashing, insertion/deletion/proofs, pure and RocksDB backends Done
1 Service interfaces — Provider, Submitter, TxBuilder, State, TrieManager, Context records; mock implementations; on-chain type encodings; CIP-57 blueprint validation; Aiken-compatible proof serialization Done
2 N2C client + Provider — ouroboros-network LocalStateQuery and LocalTxSubmission clients; mkNodeClientProvider for PParams/evaluation/slot conversion; mkN2CSubmitter for transaction submission; E2E tests with cardano-node subprocess Done
3 Transaction builders — real TxBuilder implementations for boot, update, reject, retract, end operations with Plutus script witnesses, proof envelopes, and on-chain datum construction Done
4 ChainSync indexer + persistent state — real ChainSync follower; RocksDB-backed UTxO CSMT, State, and TrieManager; block processing with rollback support Done
5 HTTP API + deployment — Servant HTTP layer with Swagger UI, proof-bearing token/trie/request reads, facts endpoints, signed submission, WAI application wiring Done
6 Trust-minimized client flow — facts-only script-bearing writes, cardano-mpfs-verify, cardano-mpfs-cage-tx, wasm cage reactor, and trusted interim GET /eval-context for ex-unit evaluation Done