tweag/cooked-validators

Proposal to get rid of initial distributions (clickbait issue title)

Opened this issue · 0 comments

Running a trace, which consists of a sequence of transactions (each specified by a transaction skeleton), requires to choose an initial state of the mockchain to run it on. This initial distribution is often an association list that maps a set of values to each wallet of interest. Over time, the concept has been enhanced to allow setting datums, or reference scripts. Now, an initial distribution can be any list of TxSkelOut, which is nice and convenient.

In the backstage, initial distributions are implemented as a transaction that is forced upon the mockchain, skipping validation.

I am proposing to modify this in a way that I think is

  • the logical continuation of previous work on initial distributions
  • most of all: a significant simplification of our user-facing API
  • an additional feature that might come in handy

Problem

Annoyance of initial distributions in the API

There are many functions in cooked that exist in two variants: one takes the default init dist, the other has an additional parameter to override it. PR #450 helps in this regard.
We also have a dedicated InitialDistribution type and module.

Conceptually awkward and inconvenient from the user/auditor side

It turns out in practice that we always need to define a custom initial distribution. Smart contracts in the wild involve custom tokens that we need from the start, we may want to tidy up the initial state to get rid of all the useless Ada utxos, we don't need the 10 known wallets, sometimes we like to set reference scripts at the beginning too.

When we design a trace, we always have a specific initial distribution in mind. Most traces would never even succeed on the default distribution.

Therefore, we specify a custom distribution (sometimes several) and we pass it as a parameter to every single unit test during audits or development. But this happens in the test tree, not near the definition of the trace although the trace is designed with this specific distribution in mind.

Proposal

I suggest that we add a new action in our mock chain monad. We would have both the classic validateTxSkel and a new forceTxSkel (or any better name).
forceTxSkel would apply a transaction without validation, just like it is already done with the transaction obtained from the [TxSkelOut] of InitialDistribution.

E.g.

foo :: MonadBlockChain m => m ()
foo = do
  forceTxSkel $
    txSkelTemplate
      { txSkelOuts =
          [ paysPK alice (apple 3)
            paysPK bob (banana 7) `withDatum` bar
          ],
        txSkelOpts = def { txOptEnsureMinAda = true }

(of course in practice you would define it and reuse it as a first action of other traces)

  • This would not make it any more difficult to define initial distributions. Initial distributions are already almost transactions type-wise.
  • We could use the txOptEnsureMinAda directly instead of toInitDistWithMinAda
  • We would get rid of initial distributions as parameters of many functions
  • We would no longer need two variants of these functions
  • We would have more self-contained traces in audits
  • We would have less verbose test trees that don't need to always refer to the initial distribution
  • As a transaction based on a skeleton, this would be possible to use tweaks on it
  • As a transaction, we could get the list of utxos in return as we have with validateTxSkel which would make it easier to extract a TxOutRef to use as an input when minting NFTs afterwards without having to use a search.
  • As a bonus, we would be able to force transactions at later points too. This would often be a bad idea but sometimes we run into technical difficulties when we interact with external smart contracts (versions mismatch, bugs in tx generation that lead to mismatching hashes, etc) and we are stuck at an early step although we know for certain that the transaction is conceptually valid. While we investigate how to solve it, we could force it on the chain and proceed anyway. Think of it as our undefined in Haskell dev.