|
| 1 | +--- |
| 2 | +title: Redeemer Indexing |
| 3 | +description: Deferred redeemer construction for Plutus script index optimization |
| 4 | +--- |
| 5 | + |
| 6 | +import { Mermaid } from "@/components/mdx/mermaid" |
| 7 | + |
| 8 | +## Abstract |
| 9 | + |
| 10 | +Plutus validators receive transaction context including sorted input indices. Validators can use these indices for efficient lookups instead of traversing entire input lists. However, input indices depend on the **final sorted order** of all transaction inputs, including those added during coin selection. This creates a circular dependency: redeemer construction needs final indices, but final indices aren't known until after coin selection. The architecture resolves this through **deferred redeemer construction**: store a builder function instead of a concrete redeemer, then invoke it during the build phase once all inputs are known and sorted. |
| 11 | + |
| 12 | +## Purpose and Scope |
| 13 | + |
| 14 | +**Purpose**: Enable Plutus validators to use input indices efficiently by deferring redeemer construction until final input ordering is determined. |
| 15 | + |
| 16 | +**Scope**: |
| 17 | +- Defines the `RedeemerBuilder` pattern for deferred redeemer construction |
| 18 | +- Supports two variants: `"batch"` (multiple inputs) and `"self"` (single input per UTxO) |
| 19 | +- Provides both index and UTxO data to builder functions |
| 20 | +- Integrates with deferred execution model |
| 21 | +- Resolves indices after coin selection when canonical input order is final |
| 22 | + |
| 23 | +**Out of Scope**: |
| 24 | +- Redeemer encoding/serialization |
| 25 | +- Script evaluation and execution units |
| 26 | +- Plutus validator implementation patterns (on-chain concerns) |
| 27 | + |
| 28 | +## Design Philosophy |
| 29 | + |
| 30 | +### The Index Problem |
| 31 | + |
| 32 | +Cardano transactions sort inputs lexicographically by `(txHash, outputIndex)` before validation. When a Plutus validator executes, it receives inputs in this canonical order. Validators often need to correlate their own input with corresponding outputs. This is the classic "UTxO indexer" pattern for preventing double satisfaction attacks. |
| 33 | + |
| 34 | +<Mermaid chart={` |
| 35 | +graph LR |
| 36 | + subgraph Naive["Naive: O(n) Lookup"] |
| 37 | + N1[Iterate through<br/>all inputs] --> N2[Compare each<br/>to own reference] --> N3[Found after<br/>n comparisons] |
| 38 | + end |
| 39 | + |
| 40 | + subgraph Optimized["Optimized: O(1) Lookup"] |
| 41 | + O1[Redeemer contains<br/>input index] --> O2[Direct array access<br/>inputs#91;index#93;] --> O3[Single equality<br/>check] |
| 42 | + end |
| 43 | + |
| 44 | + style Naive fill:#fee2e2,stroke:#b91c1c,stroke-width:2px |
| 45 | + style Optimized fill:#d1fae5,stroke:#059669,stroke-width:2px |
| 46 | +`} /> |
| 47 | + |
| 48 | +This optimization requires the off-chain code to know the input's index. But the index depends on the final sorted order of **all** inputs, including wallet UTxOs added during coin selection for fees and balancing. |
| 49 | + |
| 50 | +### The Circular Dependency |
| 51 | + |
| 52 | +<Mermaid chart={` |
| 53 | +graph TD |
| 54 | + subgraph Problem["The Circular Dependency"] |
| 55 | + P1["1. User specifies script inputs<br/>with redeemer"] --> P2["2. Redeemer needs input index..."] |
| 56 | + P2 -.->|"depends on"| P4 |
| 57 | + P3["3. Transaction structure completed"] --> P4["4. Coin selection adds<br/>wallet UTxOs"] |
| 58 | + P4 --> P5["5. Inputs sorted canonically<br/>→ indices now known"] |
| 59 | + P5 --> P6["6. Too late!<br/>Redeemer already built"] |
| 60 | + end |
| 61 | + |
| 62 | + style P2 fill:#fef3c7,stroke:#d97706,stroke-width:2px |
| 63 | + style P6 fill:#fee2e2,stroke:#b91c1c,stroke-width:2px |
| 64 | +`} /> |
| 65 | + |
| 66 | +The architecture resolves this by **deferring** redeemer construction: instead of building the redeemer immediately, store a **builder function** that will be invoked after coin selection completes. |
| 67 | + |
| 68 | +### Deferred Redeemer Construction |
| 69 | + |
| 70 | +<Mermaid chart={` |
| 71 | +sequenceDiagram |
| 72 | + participant User |
| 73 | + participant Builder |
| 74 | + participant CoinSelection |
| 75 | + participant Resolver |
| 76 | +
|
| 77 | + User->>Builder: Add script inputs with RedeemerBuilder |
| 78 | + Note over Builder: Store builder function<br/>(don't execute yet) |
| 79 | + |
| 80 | + User->>Builder: build() |
| 81 | + Builder->>CoinSelection: Run selection |
| 82 | + CoinSelection-->>Builder: Wallet UTxOs added |
| 83 | + |
| 84 | + Builder->>Resolver: All inputs now known |
| 85 | + Resolver->>Resolver: Sort inputs canonically |
| 86 | + Resolver->>Resolver: Compute index for each input |
| 87 | + Resolver->>Resolver: Call makeRedeemer(index, utxo) |
| 88 | + Resolver-->>Builder: Concrete redeemer returned |
| 89 | + |
| 90 | + Builder-->>User: Transaction ready |
| 91 | +`} /> |
| 92 | + |
| 93 | +When a user provides a `RedeemerBuilder` instead of a concrete redeemer: |
| 94 | + |
| 95 | +1. The builder stores a **deferred operation** (the intent without the final redeemer) |
| 96 | +2. During `build()`, coin selection runs and all inputs are collected |
| 97 | +3. Inputs are sorted canonically and indices are computed |
| 98 | +4. The builder function is called with both **index and UTxO data** |
| 99 | +5. The returned redeemer completes the operation |
| 100 | + |
| 101 | +## Index-Only vs Full-Context Callbacks |
| 102 | + |
| 103 | +Some transaction building approaches only provide indices to the redeemer builder callback. This becomes problematic when validators need UTxO data to construct the redeemer. For example, unique token minting where the asset name derives from the output reference being spent. |
| 104 | + |
| 105 | +**The Problem**: If the callback only receives an index, the UTxO data must be captured externally and correlated manually. This is error-prone and awkward. |
| 106 | + |
| 107 | +**The Solution**: Provide both index **and** UTxO to the callback. |
| 108 | + |
| 109 | +<Mermaid chart={` |
| 110 | +graph TD |
| 111 | + subgraph IndexOnly["Index-Only Approach"] |
| 112 | + L1["makeRedeemer(indices)"] |
| 113 | + L2[Only Index] |
| 114 | + L1 --> L2 |
| 115 | + L2 --> L3["No UTxO Data<br/>Must capture externally"] |
| 116 | + end |
| 117 | + |
| 118 | + subgraph IndexedInput["Full-Context Approach"] |
| 119 | + E1["makeRedeemer(indexedInputs)"] |
| 120 | + E2["{ index, utxo }"] |
| 121 | + E1 --> E2 |
| 122 | + E2 --> E3["Full Context<br/>Index + UTxO Data"] |
| 123 | + end |
| 124 | + |
| 125 | + style IndexOnly fill:#fee2e2,stroke:#b91c1c,stroke-width:2px |
| 126 | + style IndexedInput fill:#d1fae5,stroke:#059669,stroke-width:2px |
| 127 | + style L3 fill:#fecaca,stroke:#b91c1c,stroke-width:2px,color:#7f1d1d |
| 128 | + style E3 fill:#a7f3d0,stroke:#059669,stroke-width:2px,color:#064e3b |
| 129 | +`} /> |
| 130 | + |
| 131 | +The `IndexedInput` structure provides both: |
| 132 | +- **index**: Position in canonical input order (for the validator's O(1) lookup) |
| 133 | +- **utxo**: Full UTxO data (for constructing redeemers that depend on output references) |
| 134 | + |
| 135 | +## RedeemerBuilder Variants |
| 136 | + |
| 137 | +### Batch Variant |
| 138 | + |
| 139 | +For operations requiring indices of **multiple specified inputs**. The callback receives an array of `{ index, utxo }` pairs, maintaining the same order as the originally specified inputs. |
| 140 | + |
| 141 | +<Mermaid chart={` |
| 142 | +graph TD |
| 143 | + Start([Specify script inputs<br/>with RedeemerBuilder]) --> Store[Store deferred operation<br/>+ input list] |
| 144 | + |
| 145 | + Store --> Build["build() called"] |
| 146 | + Build --> CoinSelect[Coin Selection<br/>adds wallet UTxOs] |
| 147 | + CoinSelect --> SortInputs[Sort All Inputs<br/>Canonically] |
| 148 | + |
| 149 | + SortInputs --> ComputeIndices[Compute Index Map<br/>txHash+outputIndex → index] |
| 150 | + ComputeIndices --> LookupBatch[Lookup Indices for<br/>all specified inputs] |
| 151 | + |
| 152 | + LookupBatch --> CallMaker["Call makeRedeemer with<br/>array of { index, utxo }"] |
| 153 | + |
| 154 | + CallMaker --> ReturnRedeemer[Single Redeemer Returned<br/>with all indices] |
| 155 | + ReturnRedeemer --> CompleteOp[Complete operation<br/>with concrete redeemer] |
| 156 | + |
| 157 | + CompleteOp --> End([Transaction Built]) |
| 158 | + |
| 159 | + style Start fill:#ecf0f1,stroke:#34495e,stroke-width:3px,color:#000000 |
| 160 | + style End fill:#2ecc71,stroke:#27ae60,stroke-width:3px,color:#ffffff |
| 161 | + style CallMaker fill:#3b82f6,stroke:#1e3a8a,stroke-width:3px,color:#ffffff |
| 162 | +`} /> |
| 163 | + |
| 164 | +**Use Cases**: |
| 165 | +- Batch spending: redeemer contains list of `(inputIndex, outputIndex)` pairs |
| 166 | +- Multi-validator coordination: indices reference inputs across multiple scripts |
| 167 | +- Mint policies checking specific spend indices |
| 168 | + |
| 169 | +### Self Variant |
| 170 | + |
| 171 | +For operations where **each input needs its own redeemer with its own index**. The callback is invoked once per UTxO, receiving a single `{ index, utxo }` each time. |
| 172 | + |
| 173 | +<Mermaid chart={` |
| 174 | +graph TD |
| 175 | + Start([Specify script inputs<br/>with self RedeemerBuilder]) --> Store[Store deferred operation<br/>for each UTxO] |
| 176 | + |
| 177 | + Store --> Build["build() called"] |
| 178 | + Build --> CoinSelect[Coin Selection<br/>adds wallet UTxOs] |
| 179 | + CoinSelect --> SortInputs[Sort All Inputs<br/>Canonically] |
| 180 | + |
| 181 | + SortInputs --> ComputeIndices[Compute Index Map] |
| 182 | + |
| 183 | + ComputeIndices --> ForEach["For each UTxO:"] |
| 184 | + ForEach --> LookupIndex[Lookup this UTxO's Index] |
| 185 | + LookupIndex --> CallMaker["Call makeRedeemer with<br/>single { index, utxo }"] |
| 186 | + |
| 187 | + CallMaker --> AddInput[Add Input with<br/>this UTxO's Redeemer] |
| 188 | + AddInput --> MoreUtxos{More UTxOs?} |
| 189 | + MoreUtxos -->|Yes| ForEach |
| 190 | + MoreUtxos -->|No| End([Transaction Built]) |
| 191 | + |
| 192 | + style Start fill:#ecf0f1,stroke:#34495e,stroke-width:3px,color:#000000 |
| 193 | + style End fill:#2ecc71,stroke:#27ae60,stroke-width:3px,color:#ffffff |
| 194 | + style CallMaker fill:#3b82f6,stroke:#1e3a8a,stroke-width:3px,color:#ffffff |
| 195 | + style ForEach fill:#f59e0b,stroke:#92400e,stroke-width:3px,color:#ffffff |
| 196 | +`} /> |
| 197 | + |
| 198 | +**Use Cases**: |
| 199 | +- Simple spend validators that only need their own index |
| 200 | +- Validators that lookup their input via `inputs[redeemer]` |
| 201 | +- Unique token minting where asset name derives from output reference |
| 202 | + |
| 203 | +## Integration with Deferred Execution |
| 204 | + |
| 205 | +RedeemerBuilder integrates with the deferred execution model: |
| 206 | + |
| 207 | +<Mermaid chart={` |
| 208 | +sequenceDiagram |
| 209 | + participant User |
| 210 | + participant Builder |
| 211 | + participant State |
| 212 | + participant Resolver |
| 213 | +
|
| 214 | + User->>Builder: Add inputs with RedeemerBuilder |
| 215 | + Note over Builder: [1] Store deferred operation<br/>NO execution yet |
| 216 | + |
| 217 | + User->>Builder: Add outputs, metadata, etc. |
| 218 | + Note over Builder: [2] Store more operations |
| 219 | + |
| 220 | + User->>Builder: build() |
| 221 | + Builder->>State: [3] Create fresh state |
| 222 | + Builder->>State: [4] Execute operations<br/>(deferred ones wait) |
| 223 | + State->>State: [5] Coin selection runs<br/>wallet UTxOs added |
| 224 | + |
| 225 | + Builder->>Resolver: [6] Resolve all RedeemerBuilders |
| 226 | + Resolver->>Resolver: Sort all inputs canonically |
| 227 | + Resolver->>Resolver: Build index map |
| 228 | + |
| 229 | + loop For each deferred operation |
| 230 | + Resolver->>Resolver: Lookup indices for specified inputs |
| 231 | + Resolver->>Resolver: Call makeRedeemer({ index, utxo }) |
| 232 | + Resolver->>State: Complete operation with concrete redeemer |
| 233 | + end |
| 234 | + |
| 235 | + Builder-->>User: [7] Transaction ready for signing |
| 236 | +`} /> |
| 237 | + |
| 238 | +**Phase Integration:** |
| 239 | + |
| 240 | +1. **Operation Storage**: Adding inputs with `RedeemerBuilder` stores a deferred operation instead of executing immediately |
| 241 | +2. **Fresh State**: Each `build()` creates new state; redeemer resolution is independent per execution |
| 242 | +3. **Operation Execution**: Regular operations execute, accumulating inputs and outputs |
| 243 | +4. **Coin Selection**: Selection phase adds wallet UTxOs to cover requirements |
| 244 | +5. **Redeemer Resolution**: After selection, all inputs are known; resolve deferred operations |
| 245 | +6. **Index Computation**: Sort inputs canonically, build lookup table for indices |
| 246 | +7. **Builder Invocation**: Call each builder function with computed `{ index, utxo }` |
| 247 | + |
| 248 | +## Index Resolution Algorithm |
| 249 | + |
| 250 | +<Mermaid chart={` |
| 251 | +graph TD |
| 252 | + Start([Start Resolution]) --> CollectInputs[Collect All Inputs<br/>from Transaction State] |
| 253 | + CollectInputs --> Sort[Sort Canonically<br/>by txHash, outputIndex] |
| 254 | + |
| 255 | + Sort --> BuildMap[Build Index Lookup Table] |
| 256 | + BuildMap --> MapLogic["key = txHash + outputIndex<br/>value = position in sorted list"] |
| 257 | + |
| 258 | + MapLogic --> IterateBuilders[Iterate Deferred Operations] |
| 259 | + IterateBuilders --> CheckVariant{Builder Variant?} |
| 260 | + |
| 261 | + CheckVariant -->|batch| LookupMultiple[Lookup Index for<br/>each specified input] |
| 262 | + CheckVariant -->|self| LookupEach[For each UTxO<br/>lookup its index] |
| 263 | + |
| 264 | + LookupMultiple --> CallBatch["Call makeRedeemer with<br/>array of { index, utxo }"] |
| 265 | + LookupEach --> CallSelf["Call makeRedeemer with<br/>single { index, utxo }"] |
| 266 | + |
| 267 | + CallBatch --> UseRedeemer[Use Redeemer for<br/>all inputs together] |
| 268 | + CallSelf --> UseRedeemerPer[Use Redeemer for<br/>this specific input] |
| 269 | + |
| 270 | + UseRedeemer --> MoreBuilders{More Operations?} |
| 271 | + UseRedeemerPer --> MoreBuilders |
| 272 | + MoreBuilders -->|Yes| IterateBuilders |
| 273 | + MoreBuilders -->|No| End([Resolution Complete]) |
| 274 | + |
| 275 | + style Start fill:#ecf0f1,stroke:#34495e,stroke-width:3px,color:#000000 |
| 276 | + style End fill:#2ecc71,stroke:#27ae60,stroke-width:3px,color:#ffffff |
| 277 | + style CheckVariant fill:#fff3cd,stroke:#856404,stroke-width:2px,color:#000000 |
| 278 | + style CallBatch fill:#3b82f6,stroke:#1e3a8a,stroke-width:3px,color:#ffffff |
| 279 | + style CallSelf fill:#3b82f6,stroke:#1e3a8a,stroke-width:3px,color:#ffffff |
| 280 | +`} /> |
| 281 | + |
| 282 | +**Algorithm Steps:** |
| 283 | + |
| 284 | +1. **Collect**: Gather all inputs from transaction state (explicit inputs + coin selection additions) |
| 285 | +2. **Sort**: Apply canonical ordering, lexicographic by `(txHash, outputIndex)` |
| 286 | +3. **Index Map**: Create lookup table mapping each input's identifier to its position |
| 287 | +4. **Resolve Batch**: For `"batch"` variant, lookup all specified input indices, call builder once with array |
| 288 | +5. **Resolve Self**: For `"self"` variant, iterate inputs, call builder per UTxO with its index |
| 289 | + |
| 290 | +## Error Handling |
| 291 | + |
| 292 | +Resolution can fail if specified inputs are missing from the final transaction: |
| 293 | + |
| 294 | +**Missing Input Error**: A `RedeemerBuilder` references a UTxO that wasn't included in the final input set. This occurs if: |
| 295 | +- The UTxO was never added via `collectFrom` |
| 296 | +- The UTxO was filtered out during processing |
| 297 | +- The UTxO reference is malformed |
| 298 | + |
| 299 | +**Resolution After Selection Error**: If coin selection adds inputs that change the transaction balance, requiring another selection round, previously-resolved redeemers may have stale indices. The system detects this and fails with guidance to set a minimum fee. |
| 300 | + |
| 301 | +## Related Topics |
| 302 | + |
| 303 | +- [Deferred Execution](/docs/architecture/deferred-execution) - Program storage and fresh state model |
| 304 | +- [Transaction Flow](/docs/architecture/transaction-flow) - Build phases and state machine |
| 305 | +- [Coin Selection](/docs/architecture/coin-selection) - How wallet UTxOs are added |
| 306 | +- [Script Evaluation](/docs/architecture/script-evaluation) - ExUnits calculation after redeemer resolution |
0 commit comments