Cardano EUTxO has some quirks.
When you spend UTxOs from the same script, you incur different costs for each spend because spending is invoked on a per-UTxO basis. When withdrawing stake or minting value, there is a single invocation per script, and it is not possible to invoke the same mint or withdrawal endpoint twice within one transaction. This happens to make minting (and withdrawing) very cheap, but makes spending many UTxOs is expensive.
At Butane, where we’re building synthetics, we aim to allow users to repay as fast as Cardano allows, and to process liquidations efficiently. To achieve this, we’re optimising spending as I describe below.
Preliminary benchmarks for Butane:
- 50 actions per transaction
- theoretical maximums of about 450 actions (minting/repayments/liquidations) per block.
We’ll be releasing more accurate numbers after we finalise the codebase and complete audits.
One way we achieve this is by optimising the spend problem in the first paragraph.
Here’s a brief look at our spend validation:
let ScriptContext { transaction, .. } = ctx
let Transaction { withdrawals, .. } = transaction
let own_withdrawal = Inline(ScriptCredential(datum.script_credential))
dict.has_key(withdrawals, own_withdrawal)
We have an alternative version where the Inline(ScriptCredential(…)) constructor is put inside the datum, which reduces memory in the repayment and increases it in the mint. We might use this depending on the final constraints.
We minimise spend size because when spending 50 UTxOs (whether that’s closing 50 CDPs in Butane’s case, or executing 50 Cardano-Swaps ), every small call in your spend validator gets costed 50x. So the small removal of list.at
can lead to significant savings, more so than optimising the stake validator.
Next you might be asking: why do we withdraw from a stake validator? It’s a simple trick to offload logic to other scripts, which here we’re using to validate spend logic but inside a stake validator. This is handy because stake validators only get invoked once, and when we’re minimising the cost of actions which are very similar, we can make massive savings in memory and compute units.
This depends on a nuance in the ledger validation for stake withdrawal. You can imagine that you can make a similar optimisation to all your spends by exporting heavy logic into a mint validator. While possible, it is cumbersome to deal with minted tokens since you can’t mint zero. However, you can withdraw zero ada from a stake validator. This means that stake validation is the only method to logically invoke a script logically but to otherwise do nothing, because you aren’t invoking for the sake of withdrawing stake, only to reduce cost.
A public example employing these techniques is the Fortuna hardfork, made by two members of the Aiken team. You can see it here: https://github.com/aiken-lang/fortuna/blob/b3edeff1b6d63a281dcfb289129c1fb9ac0d508e/validators/hard_fork.ak#L522
They haven’t used my trick of incorporating the script address into the datum, but it probably isn’t necessary in their case.