Skip to content

Commit 0332b69

Browse files
committed
docs: update arch docs
1 parent 908352f commit 0332b69

File tree

2 files changed

+88
-229
lines changed

2 files changed

+88
-229
lines changed
Lines changed: 87 additions & 228 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,90 @@
11
---
22
title: Redeemer Indexing
3-
description: Deferred redeemer construction for Plutus script index optimization
3+
description: Deferred redeemer construction for efficient validator input lookup
44
---
55

66
import { Mermaid } from "@/components/mdx/mermaid"
77

88
## Abstract
99

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.
10+
Plutus validators can receive input indices via redeemers for O(1) array lookup instead of O(n) traversal. On-chain traversal is expensive in execution units, so passing indices allows validators to directly access only the inputs they care about.
1111

12-
## Purpose and Scope
12+
The challenge: Cardano currently enforces canonical sorting of inputs, and indices depend on this final sorted order. When coin selection or other operations add inputs to the transaction, they insert into the sorted order and shift existing indices.
1313

14-
**Purpose**: Enable Plutus validators to use input indices efficiently by deferring redeemer construction until final input ordering is determined.
14+
The solution: **defer redeemer construction** until all inputs are known and sorted.
1515

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
16+
> **Note**: [CIP-128](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0128) proposes removing the canonical ordering requirement, allowing user-controlled input order. Once implemented, you could place contract inputs at known positions (e.g., indices 0, 1, 2) and append wallet inputs after, making indices predictable without deferred construction.
2217
23-
**Out of Scope**:
24-
- Redeemer encoding/serialization
25-
- Script evaluation and execution units
26-
- Plutus validator implementation patterns (on-chain concerns)
18+
## Why Index-Based Lookup Matters
2719

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.
20+
Cardano transactions sort inputs lexicographically by `(txHash, outputIndex)`. A validator receives all transaction inputs in this canonical order. Without indices, finding relevant inputs requires traversing the entire list.
3321

3422
<Mermaid chart={`
3523
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]
24+
subgraph Traversal["Without Indices: O(n)"]
25+
T1[For each input] --> T2[Check if relevant] --> T3[Process if match]
3826
end
3927
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]
28+
subgraph Direct["With Indices: O(1)"]
29+
D1[Redeemer contains indices] --> D2["Access inputs[i] directly"] --> D3[Process immediately]
4230
end
43-
44-
style Naive fill:#fee2e2,stroke:#b91c1c,stroke-width:2px
45-
style Optimized fill:#d1fae5,stroke:#059669,stroke-width:2px
4631
`} />
4732

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.
33+
Consider a transaction with 15 inputs: 5 contract UTxOs and 10 wallet UTxOs for fees. A naive validator would check all 15 inputs to find the 5 it cares about. With indices in the redeemer, it directly accesses `inputs[3]`, `inputs[7]`, `inputs[12]`, etc.
4934

50-
### The Circular Dependency
35+
## The Stake Validator Pattern
36+
37+
A common pattern uses a stake validator as the main coordinator. Spend validators are "dumb" and only verify that the stake validator ran. The stake validator receives all relevant input indices and performs the actual business logic.
5138

5239
<Mermaid chart={`
5340
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"]
41+
subgraph Transaction
42+
SW[Stake Validator Withdrawal]
43+
S1[Spend Input 1]
44+
S2[Spend Input 2]
45+
S3[Spend Input 3]
46+
W1[Wallet Input]
47+
W2[Wallet Input]
6048
end
6149
62-
style P2 fill:#fef3c7,stroke:#d97706,stroke-width:2px
63-
style P6 fill:#fee2e2,stroke:#b91c1c,stroke-width:2px
50+
subgraph Validation
51+
SW -->|"redeemer: [0, 2, 4]"| Logic[Business Logic]
52+
Logic -->|"inputs[0]"| S1
53+
Logic -->|"inputs[2]"| S2
54+
Logic -->|"inputs[4]"| S3
55+
S1 -->|check withdrawal exists| SW
56+
S2 -->|check withdrawal exists| SW
57+
S3 -->|check withdrawal exists| SW
58+
end
59+
`} />
60+
61+
The stake validator:
62+
1. Receives indices of contract inputs in its redeemer
63+
2. Directly accesses those inputs via index
64+
3. Validates business logic for all contract inputs in one execution
65+
66+
The spend validators:
67+
1. Simply verify the stake validator ran (withdrawal exists in transaction)
68+
2. Delegate all logic to the stake validator
69+
70+
This pattern reduces total execution cost since the stake validator runs once instead of N spend validators running independently.
71+
72+
## The Circular Dependency
73+
74+
The problem: input indices depend on the final sorted order of **all** inputs. But coin selection adds wallet UTxOs after the user specifies script inputs. The indices change.
75+
76+
<Mermaid chart={`
77+
graph LR
78+
P1["User specifies<br/>script inputs"] --> P2["Needs indices<br/>for redeemer"]
79+
P2 -.->|"depends on"| P4
80+
P3["Transaction built"] --> P4["Coin selection<br/>adds wallet UTxOs"]
81+
P4 --> P5["Inputs sorted<br/>indices known"]
82+
P5 --> P6["Too late!<br/>Redeemer built"]
6483
`} />
6584

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.
85+
## Deferred Construction
6786

68-
### Deferred Redeemer Construction
87+
Instead of building the redeemer immediately, store a **builder function** that will be invoked after coin selection:
6988

7089
<Mermaid chart={`
7190
sequenceDiagram
@@ -74,233 +93,73 @@ sequenceDiagram
7493
participant CoinSelection
7594
participant Resolver
7695
77-
User->>Builder: Add script inputs with RedeemerBuilder
78-
Note over Builder: Store builder function<br/>(don't execute yet)
79-
96+
User->>Builder: Specify inputs with redeemer function
97+
Note over Builder: Store function, don't execute
98+
8099
User->>Builder: build()
81100
Builder->>CoinSelection: Run selection
82101
CoinSelection-->>Builder: Wallet UTxOs added
83-
102+
84103
Builder->>Resolver: All inputs now known
85104
Resolver->>Resolver: Sort inputs canonically
86-
Resolver->>Resolver: Compute index for each input
87-
Resolver->>Resolver: Call makeRedeemer(index, utxo)
105+
Resolver->>Resolver: Compute indices
106+
Resolver->>Resolver: Invoke redeemer function with indices
88107
Resolver-->>Builder: Concrete redeemer returned
89-
108+
90109
Builder-->>User: Transaction ready
91110
`} />
92111

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.
112+
## Three Modes
104113

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.
114+
### Batch Mode
106115

107-
**The Solution**: Provide both index **and** UTxO to the callback.
116+
For validators that need indices of multiple inputs. The function receives all indexed inputs and returns a single redeemer.
108117

109118
<Mermaid chart={`
110119
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
120+
B1[Specify inputs to track] --> B2[Function called once after sorting] --> B3["Receives all { index, utxo } pairs"] --> B4[Returns redeemer with indices list]
129121
`} />
130122

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)
123+
Primary use case: stake validator coordinator receiving list of contract input indices.
134124

135-
## RedeemerBuilder Variants
125+
### Self Mode
136126

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.
127+
For spend validators that need their own index. The function is called once per script UTxO.
140128

141129
<Mermaid chart={`
142130
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
131+
SE1[Each script UTxO] --> SE2[Function called per UTxO] --> SE3["Receives own { index, utxo }"] --> SE4[Returns redeemer with own index]
162132
`} />
163133

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
134+
Use case: spend validator that looks up its own input by index for validation.
168135

169-
### Self Variant
136+
### Static Mode
170137

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.
138+
For redeemers that don't need indices. The data is used directly.
172139

173140
<Mermaid chart={`
174141
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
142+
S1[Concrete redeemer data] --> S2[No computation needed] --> S3[Used directly]
280143
`} />
281144

282-
**Algorithm Steps:**
145+
## Indexed Input
283146

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
147+
Both Batch and Self modes receive indexed inputs containing:
289148

290-
## Error Handling
149+
- **Index**: Final 0-based position in canonically sorted transaction inputs
150+
- **UTxO**: The original UTxO data
291151

292-
Resolution can fail if specified inputs are missing from the final transaction:
152+
The UTxO is included because redeemers may need output reference data, not just the index.
293153

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
154+
## When to Use Each Mode
298155

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.
156+
| Mode | Use Case |
157+
|------|----------|
158+
| **Batch** | Stake validator coordinator with list of contract input indices |
159+
| **Self** | Spend validator needing its own index |
160+
| **Static** | Redeemer doesn't depend on indices |
300161

301162
## Related Topics
302163

303164
- [Deferred Execution](/docs/architecture/deferred-execution) - Program storage and fresh state model
304165
- [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

Comments
 (0)