Skip to content

Number spent inputs in ledger order - #14

Open
carbolymer wants to merge 2 commits into
mgalazyn/feature/gated-migrationsfrom
mgalazyn/fix/spent-input-index
Open

carbolymer wants to merge 2 commits into
mgalazyn/feature/gated-migrationsfrom
mgalazyn/fix/spent-input-index

Conversation

@carbolymer

@carbolymer carbolymer commented Nov 11, 2026 •

Copy link
Copy Markdown

Stacking

This PR stacks on #16 (gated migrations, #15). Its base is that PR's branch, mgalazyn/feature/gated-migrations, so GitHub shows only this PR's 2 commits: "Number spent inputs in ledger order" and "Repair spent input indexes recorded before the fix". The second is a data repair that uses the new Repair migration kind.

Problem

For each spent output, Kupo records the index of the spending input (spent_at.input_index) and the spend redeemer (spent_at.redeemer). Since v2.10.0, both are wrong for any transaction that spends two or more inputs. The ledger numbers inputs by their position in the ascending input set, so the smallest input is index 0 and its redeemer is the one for pointer 0. Kupo numbered them from the other end, and looked the redeemer up with that same wrong index.

Take a transaction with inputs a < b, where R0 is the redeemer for a and R1 the redeemer for b:

input ledger Kupo before this fix
a index 0, R0 index 1, R1
b index 1, R1 index 0, R0

So clients get the wrong index and also the redeemer of a different input. With N inputs, only the middle input of an odd N is correct. Single-input transactions are not affected. Both chain producers are affected: the direct node connection and Ogmios.

Cause

matchBlock (src/Kupo/Data/Pattern.hs) numbers the inputs with a foldr that carries the counter in its accumulator. foldr reaches the last element first, so the largest input gets 0 and the smallest gets N-1. This came in with 76013ce (upstream CardanoSolutions/kupo, 2025-01-02), first released in v2.10.

Fix

Number the inputs left to right with foldrWithIndex from Kupo.Prelude. The index and the redeemer lookup then both follow the ledger.

Existing data

The fix alone does not change rows already stored, so the second commit adds a repair, the Repair migration v2.12.0/002.

On the first start of the fixed binary, Kupo records a cutoff slot and logs a warning at every start while the repair is pending. Spends recorded before the cutoff may be wrong. To repair them, stop Kupo and run:

kupo db migrate --workdir DIR --confirm-complete-index

The flag confirms that the index matches * and was synchronised from origin. Only such an index holds all the inputs of every transaction. Any other index needs a resync.

The repair works in batches of 21600-slot windows, and skips windows without spends. For each spending transaction, it ranks the stored inputs by output reference, which is the ledger order, and compares that with the stored indexes:

  • Indexes that count down from the last input are renumbered, and the redeemers are swapped to match.
  • Groups already in ledger order are left alone. Kupo records these again after a rollback below the cutoff.
  • Other groups are counted as incomplete and reported, because the index lacks some of their inputs.

The final log line counts the renumbered and the incomplete transactions.

Known limitations:

  • Kupo does not index genesis outputs, so a transaction that spends one is incomplete even in a * index, and some of these may be renumbered wrongly.
  • Spender groups in a database that was ever pruned cannot be verified (an interrupted prune can leave a partial group that looks complete), so run the repair only on a database that was never pruned, or resynchronise.
  • A database upgraded from Kupo 2.9 or older never had these fields filled, so the repair is recorded as skipped there.
  • Repair by re-fetching blocks from the node is not implemented.

The CHANGELOG.md entry under 2.12.0 "Changed" describes this.

Testing

  • New regression test matchBlock in test/Test/Kupo/Data/PatternSpec.hs. It builds a transaction that spends two inputs listed as [hi, lo], and checks that lo gets index 0 with redeemer 0 and hi gets index 1 with redeemer 1. It fails without the fix.
  • New tests cover the repair.
  • The unit suite passes with 409 examples, 395 of them from the base PR, and the build is warning-free.
  • End to end with the built binary, on a database made by the binary from before the fix: the repair was pending, then applied, and the second run reported it up to date.

Related

Upstream master has the same code. The open upstream PR CardanoSolutions#210 describes the same right-to-left fold, for redeemers only.

@carbolymer
carbolymer force-pushed the mgalazyn/fix/spent-input-index branch from af6fdcf to 3131e78 Compare November 11, 2026 13:56
@carbolymer
carbolymer changed the base branch from master to mgalazyn/feature/gated-migrations November 12, 2026 10:09
@carbolymer
carbolymer force-pushed the mgalazyn/fix/spent-input-index branch from 3131e78 to 7b93ea6 Compare November 12, 2026 11:13
@carbolymer
carbolymer force-pushed the mgalazyn/feature/gated-migrations branch 2 times, most recently from 1aaa00c to 804b7de Compare November 13, 2026 10:30
@carbolymer
carbolymer force-pushed the mgalazyn/fix/spent-input-index branch from 7b93ea6 to c6d8711 Compare November 13, 2026 10:30
@carbolymer
carbolymer marked this pull request as ready for review November 13, 2026 12:32
@carbolymer carbolymer self-assigned this Nov 13, 2026
Since v2.10.0 (76013ce), spent_at.input_index and the spend
redeemer were numbered from the last input, so the largest input
got index 0.
The ledger numbers spend redeemers by position in the ascending
input set.

Number the inputs from the first one, so the index and the redeemer
match the ledger.
Rows indexed before this fix keep the old values until a resync.
@carbolymer
carbolymer force-pushed the mgalazyn/fix/spent-input-index branch from c6d8711 to 6fe0e6f Compare November 13, 2026 12:46
@carbolymer
carbolymer force-pushed the mgalazyn/feature/gated-migrations branch from 804b7de to 3fd8a76 Compare November 13, 2026 12:46
Register v2.12.0/002, a data repair for the spends that Kupo recorded
up to the cutoff slot, with their inputs numbered from the last one.
kupo db migrate runs it when the patterns include * and the operator
confirms with --confirm-complete-index that the index was synchronised
from origin. The repair names v2.10.0/001, which added spent_by, as the
origin of its data, so an upgrade from v2.9 or older skips it.

The repair reads the spends in windows of 21600 slots, and skips the
windows without any. It ranks the inputs of each spending transaction
by output reference, which is the ledger order. When the stored indexes
run from the last input, it numbers the inputs in ledger order and
gives each input the redeemer of its pointer. It leaves alone the
transactions already in ledger order, which Kupo records again after a
rollback below the cutoff, and those that it cannot verify: their rows
do not decode, or their indexes match neither order, for example
because the index lacks some of their inputs. The final log line counts
the renumbered and the unverified transactions.

An interrupted prune can leave only the last inputs of a transaction,
which look like a complete set. The precondition message and the
documentation therefore ask for an index that was never pruned.
@carbolymer
carbolymer force-pushed the mgalazyn/fix/spent-input-index branch from 6fe0e6f to b11a40e Compare November 13, 2026 12:59

@Jimbo4350 Jimbo4350 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yikes. LGTM, one comment.

, toPolicy = \_ _ -> ()
}
let patterns' = Set.singleton (MatchAny IncludingBootstrap)
let got = Map.lookup (txId, getPointSlotNo pt) $ consumed $ matchBlock codecs patterns' blk

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you improve this property to generate varying numbers of inputs to test against?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants