Skip to Content
Scalus Club is now open! Join us to get an early access to new features 🎉

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:

  1. Transaction builder constructs the transaction
  2. Builder calculates input/output indices after construction
  3. Indices are embedded in the redeemer
  4. 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

FunctionUse Case
validateInputValidate a single input at a known index
oneToOneMap one input to one output
oneToManyMap one input to multiple outputs
multiOneToOneNoRedeemerMap multiple script inputs to outputs (same redeemer)
multiOneToOneWithRedeemerMap 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.

Resources

Last updated on