Tweag
Technical groups
Dropdown arrow
Open source
Careers
Research
Blog
Contact
Consulting services
Technical groups
Dropdown arrow
Open source
Careers
Research
Blog
Contact
Consulting services

From minimal skeletons to comprehensive transactions with cooked-validators

20 February 2025 — by Mathieu Montin

Cooked Validators is a Haskell library designed to simplify the complex process of crafting and testing transactions on the Cardano blockchain. Writing proper transactions in Cardano can be challenging due to its UTXO-based model, which requires precise definitions and careful structuring of inputs, outputs, and complementary components. cooked-validators tackles these challenges by offering a powerful framework for defining transactions in a minimal and declarative manner while incorporating a significant degree of automation.

One of the library’s core strengths lies in its ability to help developers transform simple transaction templates, referred to as “skeletons”, or TxSkel, into fully-formed transactions that satisfy the technical requirements of Cardano’s validation process. This automation not only minimizes boilerplate code but also reduce the room for errors, thus streamlining the creation and testing of transactions. In particular, we’ve used cooked-validators extensively to rigorously audit smart contracts for many clients and well-known products now live on Cardano.

Although cooked-validators has been a reliable tool for years, no blog post has yet explored how it automates key aspects of transaction creation, simplifying complex processes into manageable workflows. This post aims to fill that gap by showcasing how the library helps developers build Cardano transactions with ease and efficiency, allowing them to focus on high-level design and intent rather than getting bogged down by low-level technical details.

Validating transactions in cooked-validators

cooked-validators provides a convenient way to interact with the blockchain through a type class abstraction, MonadBlockChain. Among the primitives provided by this type class, the most fundamental is validateTxSkel which:

  1. takes a transaction skeleton as input,
  2. expands the skeleton’s content based on missing parts and skeleton options,
  3. generates a transaction,
  4. submits this transaction for validation, and
  5. returns the validated transaction, or throws an error if it is invalid.

Thus, the function has the following type signature:

validateTxSkel :: (MonadBlockChain m) => TxSkel -> m CardanoTx

In the remainder of this post, we will explore the fields of the transaction skeleton (TxSkel) and how validateTxSkel behaves when automatically expanding this skeleton.

Transaction skeletons

Cardano transactions are usually represented by large Haskell records containing a predefined set of fields that evolve alongside the Cardano protocol. The traditional approach to building transactions involves directly creating instances of these records and submitting them for validation.

In cooked-validators, however, transactions are further abstracted through a custom record called TxSkel, which has its own set of fields, some of which map directly to corresponding fields in a Cardano transaction, while others guide the translation process. The primary motivation behind using this abstraction is to highlight the most relevant information for common use cases while hiding less critical details that can be inferred automatically based on the provided data1.

There are several additional reasons for the use of TxSkel:

  • Our transaction skeletons embed as much type information as possible for scripts and UTXOs, thus increasing type-safety.
  • Each transaction skeleton includes its own set of options to guide transaction generation, with sensible default values.
  • Our transaction skeletons have default values for all fields, allowing users to provide minimal information relevant to their use-case.
  • Our skeleton elements use meaningful, yet simple types, avoiding the need for the complex overlays and type annotations commonly found in Cardano or Ledger APIs, which are avoided by defaulting to the current Cardano era.

While TxSkel is designed to be lighter and more user-friendly than Cardano transactions, it does not compromise user flexibility. Since TxSkel ultimately generates Cardano transactions, users are provided with the option to manually tweak the generated transaction if desired. This ensures that users retain full control and can build their Cardano transactions in any way they prefer.

To build a transaction skeleton, users simply override the fields they need to set from the default skeleton, txSkelTemplate.

txSkelTemplate
  { txSkelIns = ...,
    txSkelMints = ...,
	...
  }

From manual ADA payments to automated transaction balancing

The first feature one might expect from a transaction is to pay assets to a given peer. Surprisingly, this can be quite complex due to the underlying extended UTXO model on which Cardano is based. Without diving too deeply into the details, it’s important to understand that exchanging assets in Cardano is done through “pouches” of various sizes, called UTXOs. If Alice wants to send 12 ADA (Cardano’s currency) to Bob, and she possesses one UTXO with 4 ADA and another with 10 ADA, she will have to provide both UTXOs, create a new UTXO with 12 ADA for Bob, and return a UTXO with 2 ADA for herself. Moreover, she will also need to account for transaction fees, meaning the returning UTXO will actually contain something like 1.998222 ADA, or 1,998,222 lovelace.

In summary, this seemingly simple payment of 12 ADA will result in a transaction with 2 inputs and 2 outputs, along with an additional “phantom” payment corresponding to the transaction fees. However, from the user’s perspective, the key point is that Alice needs to pay 12 ADA to Bob. cooked-validators allows users to focus on these high-level intentions, as demonstrated by the following skeleton:

txSkelTemplate
  { txSkelOuts = [paysPk bob $ ada 10],
    txSkelSigners = [alice]
  }

In this skeleton, we specify that the transaction pays 10 ADA to Bob and that Alice is a signer of the transaction. And that’s it.

Internally, cooked-validators processes this skeleton through a balancing phase. In this context, “balancing” is a multifaceted term. It not only refers to ensuring that the inputs and outputs of the transaction contain the same amount of ADA (and other assets)2, but also to calculating fees, accounting for them in the transaction, and handling associated collaterals when necessary (funds that are made available within the transaction in case a script failure occurs during validation). This automated process is a part of the added value provided by cooked-validators.

Computing fees, collaterals, and balancing transactions is notoriously difficult in Cardano due to circular dependencies (higher fees imply more collaterals, which increase transaction size, which in turn leads to higher fees…) and the unpredictable resource consumption of scripts in terms of memory space and computation cycles. See cooked-validators’s documentation for the details of what balancing involves, how cooked-validators performs it, and the options available to control balancing. Notably, cooked-validators is non-invasive, meaning that the automation can be disabled if needed. For instance, users can manually set fees and collaterals and even balance their transactions themselves.

After balancing, the skeleton will look like this:

txSkelTemplate
  { txSkelOuts = [paysPk bob $ ada 10, paysPk alice $ lovelace 1_998_222],
    txSkelIns = Map.fromList [(aliceUtxo1, emptyTxSkelRedeemer), (aliceUtxo2, emptyTxSkelRedeemer)],
    txSkelSigners = [alice]
  }

In most cases, this skeleton will remain hidden from the user, though it can be retrieved and used if necessary by manually invoking the balancing function or checking the logs.

From manual payments to automated minimal amount of ADA

While Alice is using Cardano, she might come across non-ADA tokens with custom names4, such as mySmartContractToken. These tokens are provided by smart contracts and dedicated to specific purposes such as NFTs to represent ownership of a certain resource. Alice might also want to send such a token to Bob:

txSkelTemplate
  { txSkelOuts = [paysPk bob $ mySmartContractToken 1],
    txSkelSigners = [alice]
  }

As shown above, cooked-validators will attempt to balance this skeleton by retrieving an instance of mySmartContractToken from Alice’s UTXOs, along with the necessary ADA to cover the transaction fee. However, validating the resulting balanced skeleton will fail because Cardano requires every UTXO to include a minimum amount of lovelace to cover its storage cost. This minimum amount, derived from the protocol parameters, also acts as a safeguard against potential security risks that could arise if UTXOs were allowed to exist without any ADA. Thankfully, cooked-validators can automatically calculate this required amount when the appropriate transaction option is enabled. The updated skeleton then becomes:

txSkelTemplate
  { txSkelOuts = [paysPk bob $ permanentToken 1],
    txSkelSigners = [alice],
    txSkelOpts = def { txOptEnsureMinAda = True }
  }

Enabling this option triggers an initial transformation pass, before balancing, which calculates the required amount of ADA to sustain the output and adds this amount to the transaction skeleton. After both passes, the skeleton will resemble something like:

txSkelTemplate
  { txSkelOuts = [paysPk bob $ permanentToken 1 <> lovelace 546_000],
    txSkelIns = Map.singleton aliceUtxo1 emptyTxSkelRedeemer,
    txSkelSigners = [alice],
    txSkelOpts = def { txOptEnsureMinAda = True }
  }

By default, txOptEnsureMinAda is set to False, which may seem counterintuitive. However, this prevents unexpected adjustments to ADA amounts that may have been carefully computed. If a transaction output is meant to contain a specific ADA amount based on a precise calculation, but the protocol requires a higher minimum, enabling this option would silently modify the value. This could obscure computation errors, allowing transactions to validate without the user realizing the discrepancy. To stay true to cooked-validators’s philosophy of minimal intervention, the option remains off by default, ensuring that any necessary adjustments are made explicitly.

From spending scripts to automated script witness binding

In the previous examples, we saw how cooked-validators can handle the addition of inputs in a transaction skeleton. However, there are cases where one might want to manually specify the inputs. This is typically necessary when a transaction needs to consume UTXOs belonging to scripts, in which case a redeemer must be provided, as it cannot be inferred automatically. A redeemer is a piece of information (which may be empty) required whenever a script from a smart contract is invoked. This redeemer usually informs the script as to why it has been called, and can also pass dynamic values as inputs to the script. In the examples above, the added inputs were UTXOs from peers, so emptyTxSkelRedeemer was automatically provided.

When consuming scripts, collaterals must be included in case the validation process fails after the script execution. These collaterals cover the computation resources used during validation, which cannot be covered by fees, as fees are only paid if the transaction is successfully validated. The inclusion (or omission) of collaterals, depending on whether the transaction involves scripts, is handled during balancing. Collaterals can only be provided as UTXOs from peers, so a signer is also required in such cases, even if no peer UTXO is consumed. A transaction skeleton that consumes a script can thus be written as:

txSkelTemplate
  { txSkelIns = Map.singleton scriptUtxo $ someTxSkelRedeemer scriptRedeemer,
    txSkelSigners = [alice],
  }

From this skeleton, cooked-validators offers two types of automation. The first is the balancing mechanism, which has already been discussed. Beyond computing fees and collaterals, the balancing process also creates an output at the first signer’s address to return any excess value from inputs and consumes a UTXO from the user to cover the transaction fees.

The second automation concerns the addition of script witnesses. On-chain, scripts are represented by their hash, which serves different purposes depending on the script’s type3—address for spending scripts, policy ID for minting scripts, or staking ID for staking scripts. However, during validation, scripts must be executed, and their hash alone is insufficient. Instead, users must supply the full scripts as witnesses, ensuring their hash matches the expected on-chain hash.

When a UTXO is created at a spending script’s address, cooked-validators retains the script, allowing it to automatically attach the required witness for these inputs in future transactions. However, for minting or staking scripts, the tool lacks knowledge of the necessary witnesses, so they must be specified manually.

Since recently, Cardano supports reference scripts, which are complete scripts stored on-chain in UTXOs. These reference scripts can be used as witnesses in place of the full script, reducing transaction size and fees. cooked-validators also automates the inclusion of such reference scripts. In practice, when a script witness is required, the following process unfolds:

  • if a witness is manually provided by the user, it is used as is.
  • if no such witness exists, cooked-validators attempts to find a reference witness among known UTXOs.
  • if no such witness could be found, and the script is used for spending a UTXO, cooked-validators attempts to find a direct witness among its known scripts.

In the previous example, assuming a reference witness was present on some UTXO, the skeleton will look like this:

txSkelTemplate
  { txSkelIns = Map.fromList
      [ (scriptUtxo, TxSkelRedeemer scriptRedeemer (Just referenceInputWithScript)),
        (aliceUtxoForFees, emptyTxSkelRedeemer)
      ],
    txSkelOut = [paysPK alice $ valueInScriptUtxo <> valueInAliceUtxo <> negate fee],
    txSkelSigners = [alice],
  }

From issuing proposals to automated deposit payment

The final type of automation we will discuss in this post involves proposals issued by users, a feature introduced in the Conway era. These proposals can vary, but the most common are parameter changes, where users propose new values for parameters that control on-chain behaviors. These proposals must obey a set of constitutional rules, which is checked by a constitution script. For example, here is a skeleton where Alice proposes to update the cost of fees per byte in the size of a transaction to 100 lovelace, witnessed by a given constitution script:

txSkelTemplate
  { txSkelProposals = [simpleTxSkelProposal alice (TxGovActionParameterChange [FeePerByte 100])
                        `withWitness` (constitutionScript, emptyTxSkelRedeemer),
    txSkelSigners = [alice],
  }

Each proposal requires a deposit of a certain amount of lovelace, as specified by the protocol parameters. cooked-validators takes such deposits into account during the balancing phase. It looks up the current required deposit amount and retrieves this amount from the available UTXOs from the balancing wallet to include in the transaction. After balancing, the skeleton will look like this:

txSkelTemplate
  { txSkelProposals = [simpleTxSkelProposal alice (TxGovActionParameterChange [FeePerByte 100])
                        `withWitness` (constitutionScript, emptyTxSkelRedeemer),
    txSkelIns = Map.singleton aliceUtxo emptyTxSkelRedeemer,
    txSkelOuts = [paysPK alice (valueInAliceUtxo <> negate fee <> negate depositValueFromParams)],
    txSkelSigners = [alice],
  }

Currently, cooked-validators allows the users to provide any constitution script to validate whether the proposal adheres to constitutional rules. In practice, the ledger prevents any such script that does not correspond to the current official Cardano constitution. Thus, in the future, cooked-validators might automatically fetch this script and attach it to proposals.

Conclusion

One of cooked-validators’ main strengths is its ability to allow users to express their high-level transaction requirements conveniently and efficiently, without having to deal with the intricate technical details of the resulting transaction. This is achieved through TxSkels, which are transaction abstractions that can be partially filled by users. cooked-validators performs several passes on these partial skeletons, such as filling in missing minimal ADA, balancing the transaction, and automatically adding witnesses, to translate these minimal skeletons into transactions that can be submitted for validation. This blog post has summarized these key automation steps, stay tuned for more posts around cooked-validators.


  1. it is always possible to override those fields in the generated transaction though, as cooked-validators never forces users to build their transactions one way or another.
  2. the actual balancing equation is more complicated: withdrawals + inputs + mints = burn + outputs + deposits + fees
  3. this name here stands for the combination of a token name and a policy ID.
  4. all scripts are defined in a same way following the Conway era, what we call script types are only abstractions to reference the way they are used.

About the author

Mathieu Montin

Mathieu is an engineer and doctor from France, with skills ranging from model driven engineering to functional programming and formal methods. Mathieu leads the High Assurance Software team.

If you enjoyed this article, you might be interested in joining the Tweag team.

This article is licensed under a Creative Commons Attribution 4.0 International license.

Company

AboutOpen SourceCareersContact Us

Connect with us

© 2024 Modus Create, LLC

Privacy PolicySitemap