Skip to content

Commit 908352f

Browse files
committed
feat: add redeemer index
1 parent c26391a commit 908352f

File tree

17 files changed

+1301
-96
lines changed

17 files changed

+1301
-96
lines changed

docs/content/docs/architecture/index.mdx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,15 @@ Architectural patterns provide consistency:
169169
<Card title="Deferred Execution" href="/docs/architecture/deferred-execution">
170170
Immutable builder pattern, program-as-value semantics, and composition
171171
</Card>
172+
<Card title="Coin Selection" href="/docs/architecture/coin-selection">
173+
UTxO selection algorithms for covering transaction requirements
174+
</Card>
175+
<Card title="Redeemer Indexing" href="/docs/architecture/redeemer-indexing">
176+
Deferred redeemer construction for Plutus script index optimization
177+
</Card>
178+
<Card title="Script Evaluation" href="/docs/architecture/script-evaluation">
179+
Plutus validator execution and ExUnits calculation
180+
</Card>
172181
<Card title="Provider Layer" href="/docs/architecture/provider-layer">
173182
Blockchain access abstraction across Blockfrost, Kupmios, Maestro, Koios
174183
</Card>

docs/content/docs/architecture/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"index",
55
"transaction-flow",
66
"coin-selection",
7+
"redeemer-indexing",
78
"script-evaluation",
89
"unfrack-optimization",
910
"devnet",
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
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

Comments
 (0)