UTxO Indexer Pattern
Map input UTxOs to output UTxOs using pre-computed indices, reducing on-chain validation from O(n) search to O(1) lookup.
The Challenge
When smart contracts handle multiple inputs and outputs in a single transaction, validation challenges arise:
- Multiple satisfaction - A single output can be paired with multiple inputs, leading to value loss
- Unaccounted outputs - Additional outputs can be added without validator checks
- Pairing complexity - Determining how inputs and outputs correspond becomes expensive on-chain
Searching through all inputs/outputs on-chain costs O(n) per lookup, making batch operations expensive.
How It Works
Instead of searching on-chain, the validator receives UTxO indices via the redeemer:
- Transaction builder constructs the transaction
- Builder calculates input/output indices after construction
- Indices are embedded in the redeemer
- Validator verifies correctness at known indices (O(1))
This leverages Cardano’s deterministic script evaluation property, which guarantees that script interpreter arguments are fixed and outcomes depend solely on transaction contents.
When to Use This Pattern
Best for:
- Validators handling multiple inputs and outputs
- Protocols requiring explicit input-to-output mapping
- Preventing double satisfaction attacks
- Reducing on-chain search costs
Not ideal for:
- Simple single input/output scenarios
- Cases where input-output pairing is implicit
- Validators with minimal state transitions
API Reference
| Function | Use Case |
|---|---|
validateInput | Validate a single input at a known index |
oneToOne | Map one input to one output |
oneToMany | Map one input to multiple outputs |
multiOneToOneNoRedeemer | Map multiple script inputs to outputs (same redeemer) |
multiOneToOneWithRedeemer | Map multiple script inputs with different redeemers |
Implementation Guide
One-to-One Indexer
The most common case - map one input to one output:
import scalus.patterns.UtxoIndexer
case class IndexerRedeemer(inputIdx: BigInt, outputIdx: BigInt) derives FromData, ToData
@Compile
object MyValidator extends Validator {
inline override def spend(
datum: Option[Data],
redeemer: Data,
tx: TxInfo,
ownRef: TxOutRef
): Unit = {
val IndexerRedeemer(inputIdx, outputIdx) = redeemer.to[IndexerRedeemer]
UtxoIndexer.oneToOne(
ownRef,
inputIdx,
outputIdx,
tx,
validator = (input, output) => {
// Your validation logic: check values, datums, addresses, etc.
input.resolved.value.getLovelace === output.value.getLovelace
}
)
}
}One-to-Many Indexer
Map one input to multiple outputs:
UtxoIndexer.oneToMany(
ownRef,
inputIdx,
outputIndices = List(0, 2, 4), // Non-contiguous indices supported
tx,
perOutputValidator = (input, idx, output) => {
// Validate each output individually
output.value.getLovelace >= minAmount
},
collectiveValidator = (input, outputs) => {
// Validate all outputs together
outputs.foldLeft(BigInt(0))(_ + _.value.getLovelace) === input.resolved.value.getLovelace
}
)Multiple One-to-One (No Redeemer)
Process multiple script UTxOs with the same validation logic:
UtxoIndexer.multiOneToOneNoRedeemer(
indexPairs = List((0, 0), (2, 1), (3, 2)), // (inputIdx, outputIdx) pairs
scriptHash = ownScriptHash,
tx = txInfo,
validator = (inIdx, input, outIdx, output) => {
// Validate each input-output pair
input.resolved.value.getLovelace === output.value.getLovelace
}
)Multiple One-to-One (With Redeemer)
When each input needs different redeemer data. Requires a staking script as coupling mechanism:
UtxoIndexer.multiOneToOneWithRedeemer[MyRedeemer](
indexPairs = List((0, 0), (1, 1)),
spendingScriptHash = spendScriptHash,
stakeScriptHash = stakeScriptHash,
tx = txInfo,
redeemerCoercerAndStakeExtractor = (data: Data) => {
val r = data.to[MySpendRedeemer]
(r.payload, r.stakeCredential)
},
validator = (inIdx, input, redeemer, outIdx, output) => {
// Validate with per-input redeemer data
true
}
)Off-Chain Index Computation
Use TxBuilder with a redeemer builder function to compute indices after the transaction is assembled:
import scalus.cardano.txbuilder.TxBuilder
TxBuilder(env)
.spend(
scriptUtxo,
redeemerBuilder = (tx: Transaction) => {
val inputIdx = tx.body.value.inputs.toSeq.indexOf(scriptUtxo.input)
val outputIdx = tx.body.value.outputs.indexWhere(_.address == recipientAddress)
IndexerRedeemer(BigInt(inputIdx), BigInt(outputIdx)).toData
},
script
)
.payTo(recipientAddress, value)Input Ordering: Inputs are reordered deterministically (first by transaction hash, then by output index). Transaction builders must account for this before generating redeemers.
Double Satisfaction: The singular UTxO indexer patterns (oneToOne, oneToMany) do not provide built-in protection against double satisfaction. Implement your own protection based on your contract’s needs. See Common Vulnerabilities.
Related Patterns
- Stake Validator - For reducing per-input execution costs
- Linked List - For ordered on-chain data structures
Resources
- Anastasia Labs: UTxO Indexers - Original pattern documentation with all variations
- Scalus Design Patterns - Implementation and tests
- UtxoIndexerExample - Complete example with off-chain code