Skip to content

Singletons

Every major component is a record of functions, polymorphic in the monad m. No typeclasses are used — dependencies are explicit and passed as values. This makes testing trivial: swap a record for a mock.

Provider

Ledger metadata queries. The real implementation uses N2C LocalStateQuery for protocol parameters, script evaluation, and POSIX/slot conversion. queryUTxOs still exists in the low-level record, but it is forbidden on server-side proof/write hot paths: production UTxO facts come from the local indexer's CSMT through Context.runIndexerTx.

Implementation: mkNodeClientProvider (real N2C, in Provider.NodeClient)

data Provider m = Provider
    { queryUTxOs
        :: Addr
        -> m [(TxIn, TxOut ConwayEra)]
    -- ^ Low-level LSQ address query. Forbidden on
    -- proof/write hot paths.
    , queryProtocolParams
        :: m (PParams ConwayEra)
    -- ^ Fetch current protocol parameters
    , evaluateTx
        :: ConwayTx -> m (EvaluateTxResult ConwayEra)
    -- ^ Evaluate script execution units
    , posixMsToSlot :: Integer -> m SlotNo
    -- ^ Convert POSIX time (ms) to slot (floor)
    , posixMsCeilSlot :: Integer -> m SlotNo
    -- ^ Convert POSIX time (ms) to slot (ceiling)
    }
graph LR
    PRV["Provider"]
    NODE["Cardano Node<br/>(LocalStateQuery)"]

    PRV -->|PParams| NODE
    PRV -->|evaluateTx| NODE
    PRV -->|slot conversion| NODE

GET /eval-context is exposed through the top-level Context, not the Provider record itself. It returns trusted interim PlutusV3 evaluation metadata: protocol parameters/cost models, SystemStart, epoch size, slot length, and live era history.


TrieManager

Manages a map of token identifiers to MPF tries. Each token has its own isolated trie, sharing the same RocksDB column families with per-token HexKey prefix scoping.

Transactional: mkUnifiedTrieManager — composes into the caller's Transaction (used by CageFollower for atomic block processing) IO: mkPersistentTrieManager — auto-commits + IORef caches (used by TxBuilder and speculative sessions) Test: mkPureTrieManager — in-memory (in Trie.PureManager)

data TrieManager m = TrieManager
    { withTrie
        :: forall a
         . TokenId
        -> (Trie m -> m a)
        -> m a
    -- ^ Run an action with access to a token's trie
    , withSpeculativeTrie
        :: forall a
         . TokenId
        -> (forall n. Monad n => Trie n -> n a)
        -> m a
    -- ^ Run read-your-writes trie mutations and
    -- discard them at the end
    , createTrie :: TokenId -> m ()
    -- ^ Create a new empty trie for a token
    , deleteTrie :: TokenId -> m ()
    -- ^ Delete a token's trie
    , hideTrie :: TokenId -> m ()
    -- ^ Hide a burned trie while preserving data
    , unhideTrie :: TokenId -> m ()
    -- ^ Restore a hidden trie during rollback
    }

data Trie m = Trie
    { insert
        :: ByteString -> ByteString -> m Root
    , delete :: ByteString -> m Root
    , lookup :: ByteString -> m (Maybe ByteString)
    , enumerate :: m [(ByteString, ByteString)]
    , getRoot :: m Root
    , getProof :: ByteString -> m (Maybe Proof)
    , getProofSteps :: ByteString -> m (Maybe [ProofStep])
    }

State

Token and request state tracking. Three sub-records for tokens, requests, and chain sync checkpoints.

Transactional: mkTransactionalState — composes into the caller's Transaction (used by CageFollower) IO: mkPersistentState — auto-commits via hoistState (in Indexer.Persistent) Test: mkMockState — in-memory (in Mock.State)

data State m = State
    { tokens :: Tokens m
    , requests :: Requests m
    , checkpoints :: Checkpoints m
    }

data Tokens m = Tokens
    { getToken :: TokenId -> m (Maybe LocatedTokenState)
    , putToken :: TokenId -> LocatedTokenState -> m ()
    , removeToken :: TokenId -> m ()
    , listTokens :: m [TokenId]
    }

data Requests m = Requests
    { getRequest :: TxIn -> m (Maybe LocatedRequest)
    , putRequest :: LocatedRequest -> m ()
    , removeRequest :: TxIn -> m ()
    , requestsByToken :: TokenId -> m [LocatedRequest]
    }

data Checkpoints m = Checkpoints
    { getCheckpoint :: m (Maybe (SlotNo, BlockId))
    , putCheckpoint :: SlotNo -> BlockId -> m ()
    }

Indexer Reads And Follower

The ChainSync follower is now wired by Application rather than exposed as an Indexer field on Context. Its operational role is still the same: CageFollower processes every block in a single atomic transaction covering UTxO CSMT, cage state, MPF tries, and rollback data.

Proof-bearing HTTP handlers use composable read primitives from Indexer.Reads and discharge them through one Context field:

runIndexerTx :: forall a. IndexerTx a -> m a

That field is the fact-provider boundary. A handler composes snapshot, wallet input, state UTxO, request-set, and MPF reads, then runs the composition in one underlying transaction so the emitted witnesses all target the same indexed utxo_root.


Submitter

Transaction submission via N2C LocalTxSubmission. Takes a full ledger Tx ConwayEra and returns a SubmitResult.

Implementation: mkN2CSubmitter (real N2C, in Submitter.N2C)

data SubmitResult
    = Submitted TxId
    | Rejected ByteString

newtype Submitter m = Submitter
    { submitTx :: Tx ConwayEra -> m SubmitResult
    }

TxBuilder

Internal server transaction builders. They return proof envelopes around unsigned ledger transactions and consume a caller-supplied BundleSnapshot; they must not query the node for UTxO state. Public script-bearing browser flows use facts endpoints plus cardano-mpfs-cage-tx instead of asking the server to return unsigned transactions.

Mock: mkMockTxBuilder (in Mock.TxBuilder) Real: mkRealTxBuilder (in TxBuilder.Real)

data TxBuilder m = TxBuilder
    { bootToken
        :: BundleSnapshot
        -> [ResolvedWalletInput]
        -> Addr
        -> m (ProofEnvelope BootProof)
    -- ^ Create a new MPFS token
    , requestInsert
        :: BundleSnapshot
        -> TokenId -> ByteString -> ByteString
        -> Addr -> m (ProofEnvelope RequestProof)
    -- ^ Request inserting a key-value pair
    , requestDelete
        :: BundleSnapshot
        -> TokenId -> ByteString -> ByteString
        -> Addr -> m (ProofEnvelope RequestProof)
    -- ^ Request deleting a key
    , requestUpdate
        :: BundleSnapshot
        -> TokenId -> ByteString -> ByteString -> ByteString
        -> Addr -> m (ProofEnvelope RequestProof)
    -- ^ Request updating a key
    , updateToken
        :: BundleSnapshot
        -> TokenId -> Addr -> m (ProofEnvelope UpdateProof)
    -- ^ Process pending requests for a token
    , retractRequest
        :: BundleSnapshot
        -> TxIn -> Addr -> m (ProofEnvelope RetractProof)
    -- ^ Cancel a pending request
    , rejectRequests
        :: BundleSnapshot
        -> TokenId -> Addr -> m (ProofEnvelope RejectProof)
    -- ^ Reject Phase 3 requests
    , endToken
        :: BundleSnapshot
        -> TokenId -> Addr -> m (ProofEnvelope EndProof)
    -- ^ Retire an MPFS token
    }

Evaluation And Balance

Transaction construction uses ledger-native balancing helpers in TxBuilder.Real.Internal on the server side and Cardano.MPFS.Client.Cage.Eval in cardano-mpfs-cage-tx on the client/reactor side. The *WithEval cage APIs first decode GET /eval-context, derive EpochInfo from the live era history, and then evaluate/balance transactions with real ex-units before wallet signing.

bootCageTxWithEval
    :: DecodedEvalContext
    -> CageConfig
    -> WalletPolicy
    -> VerifiedBootFacts
    -> Either BuildError ConwayTx

Update, retract, reject, end, and boot are eval-only because they carry scripts and must use current PlutusV3 cost models and era history. Request insert/delete/update builders do not run scripts, but they still consume verified facts and the same cage configuration.


Context

Facade record that bundles all singletons into a single environment.

data Context m = Context
    { provider :: Provider m
    , trieManager :: TrieManager m
    , state :: State m
    , submitter :: Submitter m
    , txBuilder :: TxBuilder m
    , cfgCage :: CageConfig
    , utxoExists :: TxIn -> m Bool
    , resolveUtxo :: TxIn -> m (Maybe ByteString)
    , awaitUtxo :: TxIn -> Maybe Int -> m (Maybe ByteString)
    , utxoRoot :: m (Maybe ByteString)
    , utxoProof :: TxIn -> m (Maybe ByteString)
    , indexerProofsReady :: m Bool
    , evalContext :: m EvalContext
    , runIndexerTx :: forall a. IndexerTx a -> m a
    , readMetrics :: m (Maybe Metrics)
    }
graph TD
    CTX["Context"]
    PRV["Provider"]
    TM["TrieManager"]
    ST["State"]
    READS["IndexerTx Reads"]
    SUB["Submitter"]
    TXB["TxBuilder"]
    EVAL["EvalContext"]

    CTX --> PRV
    CTX --> TM
    CTX --> ST
    CTX --> READS
    CTX --> SUB
    CTX --> TXB
    CTX --> EVAL

HTTP Server

mkApp from HTTP.Server wraps a Context IO into a WAI Application with Swagger UI at /swagger-ui. Each HTTP handler extracts the relevant interface from Context and delegates to it.