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:
- takes a transaction skeleton as input,
- expands the skeleton’s content based on missing parts and skeleton options,
- generates a transaction,
- submits this transaction for validation, and
- 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 TxSkel
s, 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
.
- 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.↩ - the actual balancing equation is more complicated:
withdrawals + inputs + mints = burn + outputs + deposits + fees
↩ - this name here stands for the combination of a token name and a policy ID.↩
- 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 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.