When crafting transactions to process a single script (smart contract) UTxO, enforcing spending requirements seems straightforward. However, in high-throughput applications, a more efficient approach is desired – allowing the processing (spending) of these script UTxOs in a "batch." Unfortunately, invoking the validator script for each input UTxO in a transaction repeats pre-processing steps, making it less optimal. To overcome this, the technique of "transaction level validation" is employed.
For a detailed implementation of transaction level validation using staking validators, refer to the Stake Validator Design pattern. This document outlines the details of implementing the same pattern via minting policies.
A batch transaction involves multiple input UTxOs at a specific script address, and spending can only occur when specific conditions apply (i.e., the validator function does not reject the transaction). In this scenario, the validator script is executed for each UTxO, and the transaction fails if any of the scripts reject it.
graph LR
TX[Transaction]
subgraph Spending Script
S1((UTxO 1))
S2((UTxO 2))
S3((UTxO 3))
end
S1 -->|validates conditions| TX
S2 -->|validates conditions| TX
S3 -->|validates conditions| TX
TX --> A1((Output 1))
TX --> A2((Output 2))
TX --> A3((Output 3))
A more efficient way to perform all the validations is by delegating them to a minting script, which will only be executed once for the entire transaction.
graph LR
TX[Transaction]
subgraph Spending Script
S1((UTxO 1))
S2((UTxO 2))
S3((UTxO 3))
end
S1 -->|mints validation token| TX
S2 -->|mints validation token| TX
S3 -->|mints validation token| TX
ST{{Minting policy}} -.-o |validates Business Logic| TX
TX --> A1((Output 1))
TX --> A2((Output 2))
TX --> A3((Output 3))
The drawback of this approach is that the minted validation tokens must be included in one of the outputs, potentially consuming unnecessary block space.
A more desirable solution involves burning the validation tokens instead of minting them in the batch transaction. However, this requires minting and storing the validation tokens in the input UTxOs beforehand, allowing for pre-validation steps attached to their creation.
graph LR
TX[Transaction]
TX --> A1((UTxO \n containing \n validation token))
MP{{Minting policy}} -.-o |validates token minting| TX
graph LR
TX[Transaction]
subgraph Spending Script
S1((UTxO 1))
S2((UTxO 2))
S3((UTxO 3))
end
S1 -->|burns \n validation token| TX
S2 -->|burns \n validation token| TX
S3 -->|burns \n validation token| TX
MP{{Minting policy}} -.-o |validates Business Logic| TX
TX --> A1((Output 1))
TX --> A2((Output 2))
TX --> A3((Output 3))
Transaction level validation can be implemented using minting policies. However, if minting validation tokens is impractical, the recommended approach is to implement transaction level validation using a staking validator due to lower ExUnits cost compared to minting policy checks, based on our experience.