Redeemer Indexing
Redeemer Indexing design pattern (opens in a new tab) leverages the deterministic script evaluation property of the Cardano ledger to achieve substantial performance gains in onchain code.
In scenarios where the protocol necessitates spending from the script back to a specific output—such as returning funds from the script to the same script, directing them to another script, or transferring assets to a wallet, it is imperative to ensure that each script input is uniquely associated with an output.
This preventive measure is essential for mitigating the risk of Double Satisfaction Attack (opens in a new tab).
You can use a redeemer containing one-to-one correlation between script input UTxOs and output UTxOs. This is provided via ordered lists of input/output indices of required inputs/outputs present in the Script Context.
For example:
Inputs : [scriptInputA, scriptInputB, randomInput1, scriptInputC, randomInput2, randomInput3] // random inputs are not the concerned script inputs
Outputs : [outputA, outputB, outputC, randomOutput1, randomOutput2, randomOutput3]
InputIdxs : [0, 1, 3]
OutputIdxs : [0, 1, 2]
where type Redeemer = List<(inputIdx, outputIdx)>While it is pretty straightforward to determine output indices (the order in which ouputs are created during transaction building), obtaining input indices is more involved. Currently, all inputs in a transaction need to be sorted lexicographically (ascending order of transaction id and transaction index).
So even if a user obtains the required input indicies beforehand, coin
selection (post calculating the required fees and transaction balancing)
performed by a transaction builder could introduce new inputs thereby changing
the indices. Evolution library provides a high-level interface called
RedeemerBuilder that abstracts away this complexity and makes it easy to
obtain redeemer with required input indices.
Context-Dependent Redeemers
For redeemers that need more than input indices, collectFrom, mintAssets,
withdraw, and certificate operations such as registerStake /
register.Stake also accept BuildTxWithRedeemer:
const redeemer: BuildTxWithRedeemer = (ctx) => {
if (ctx.ownPurpose.tag !== "spend") return Data.void();
const ownInputIndex = ctx.inputIndex(ctx.ownPurpose.input);
const firstReturnOutput = ctx.outputIndex(
(output) => output.address === scriptAddress,
);
if (ownInputIndex === undefined || firstReturnOutput === undefined) {
return Data.void();
}
return Data.to(new Constr(0, [ownInputIndex, firstReturnOutput]));
};
const tx = await lucid
.newTx()
.collectFrom(scriptInputs, redeemer)
.pay.ToContract(scriptAddress, datum, { lovelace: 10_000_000n })
.complete();The callback receives a RedeemerContext derived from the final canonical
transaction after balancing, change, collateral, and evaluation. Its ordered
fields follow script-context ordering: inputs are canonical input order,
outputs preserve transaction-body order, withdrawals follow ledger credential
order, mint is the normalized final mint value, and redeemers contains only
script witnesses in final redeemer order.
ctx.inputs and ctx.referenceInputs are UTxO[], so each entry includes both
the out ref and the resolved output. The raw ctx.txBody is exposed for
advanced use, but the structured fields and helper functions are the
script-context-safe API.
Context-dependent redeemers are built by fixed-point replay. If a redeemer depends circularly on fields it changes, such as fees or execution units, the builder stops after eight attempts and reports a convergence error. Callbacks should be deterministic and free of side effects because completion may invoke them more than once while searching for a fixed point.
Certificate redeemers use the publish script purpose in the final context.
Redeemer Builder Kinds
const redeemer: RedeemerBuilder = {
kind: "selected",
makeRedeemer: (inputIdxs: bigint[]) => {
const tupleList = inputIdxs.map((inputIdx, i) => [inputIdx, BigInt(i)]); // List<(inputIdx, outputIdx)>
return Data.to(tupleList);
},
inputs: scriptInputs,
};This kind of redeemer builder takes in a list of selected inputs and
provides makeRedeemer with their indices in the same order. The user
supplied makeRedeemer function after having received the indices, builds
the redeemer as per validator requirements which would typically involve
mapping them to their corresponding output indices. Being a general purpose
redeemer builder it can be used to create redeemers for Spend, Mint and Withdraw script purposes.
const scriptInputs: UTxO[] = [
{
txHash: "a",
outputIndex: 1,
address: "scriptAddress",
assets: { lovelace: 10_000000n },
},
{
txHash: "b",
outputIndex: 2,
address: "scriptAddress",
assets: { lovelace: 20_000000n },
},
{
txHash: "d",
outputIndex: 4,
address: "scriptAddress",
assets: { lovelace: 40_000000n },
},
];
const randomInputs: UTxO[] = [
{
txHash: "c",
outputIndex: 3,
address: "randomAddress",
assets: { lovelace: 30_000000n },
},
{
txHash: "e",
outputIndex: 5,
address: "randomAddress",
assets: { lovelace: 50_000000n },
},
{
txHash: "f",
outputIndex: 6,
address: "randomAddress",
assets: { lovelace: 60_000000n },
},
];
const tx = await lucid
.newTx()
.collectFrom(scriptInputs, redeemer)
.attach.SpendingValidator(script)
.collectFrom(randomInputs)
.pay.ToContract("scriptAddress", datum, { lovelace: 10_000000n })
.pay.ToContract("scriptAddress", datum, { lovelace: 20_000000n })
.pay.ToContract("scriptAddress", datum, { lovelace: 40_000000n })
.pay.ToAddress("randomAddress", { lovelace: 30_000000n })
.pay.ToAddress("randomAddress", { lovelace: 50_000000n })
.pay.ToAddress("randomAddress", { lovelace: 60_000000n })
.addSigner("randomAddress")
.complete();