Skip to content

fix(utxos): validate, dedupe and bound chain-resolved utxoRefs - #439

Open
QSchlegel wants to merge 1 commit into
preprodfrom
claude/daily-2026-11-10-utxo-ref-bounds
Open

QSchlegel wants to merge 1 commit into
preprodfrom
claude/daily-2026-11-10-utxo-ref-bounds

Conversation

@QSchlegel

Copy link
Copy Markdown
Collaborator

What

Hardens resolveUtxoRefsFromChain — the helper behind utxoRefs on 8 v1 endpoints (botStakeCertificate, botDRepCertificate, proxySetup, proxySpend, proxyCleanup, proxyDRepCertificate, proxyVote) — and the single-ref resolver in proxyUtxos.ts (collateralRef, proxyUtxoRefs).

Why

  1. Unvalidated hash in the Blockfrost URL. Mesh's BlockfrostProvider.fetchUTxOs requests txs/${hash}/utxos. Any string with a non-negative outputIndex was passed through, so a txHash like ../accounts/stake… sent a request with our project key to a different Blockfrost endpoint. Now txHash must be 64 hex characters (lowercased) before any chain call.
  2. Duplicate refs. The same txHash#index twice was added to the builder twice, so its value was counted twice. It now returns 400 with utxoRefs[i] repeats ….
  3. Unbounded upstream calls. There was one serial Blockfrost call per ref and no cap. A 200 KB body could name about 2,000 refs. The new cap is MAX_UTXO_REFS = 455 (16 KiB / 36 bytes per input), which is more than one transaction could ever spend. Refs that share a transaction now use one fetch, because Blockfrost returns every output of the tx whether or not an index is passed (Mesh filters afterwards).

All errors stay 400. Every ref is validated before the first upstream call.

Behaviour change for clients

  • A non-hex or short txHash is now rejected. It was already unusable upstream.
  • A repeated ref, or more than 455 refs, now gets a 400. The proxy endpoints treat utxoRefs as a pool to select from, so a client offering more than 455 candidates would now be refused. The CI scenarios and bot-ref build refs from freeUtxos, so their hashes are 64 lowercase hex and the refs are distinct.

src/pages/api/v1/README.md documents the rules.

Tests

  • resolveUtxoRefsFromChain.test.ts is rewritten as a spec: 13 tests covering parsing, path-like hashes, the cap, validate-before-fetch, duplicates in different case, fetch-once-per-tx, canonical lowercase, and the three 400 paths.
  • proxyUtxos.test.ts: a new test checks that a crafted hash never reaches the provider. Fixtures now use 64-hex hashes.
  • npx tsc --noEmit: clean.
  • node scripts/run-tests.mjs: 185 suites, 2363 passed and 2 skipped (preprod baseline was 2352), plus 13 suites with 140 passed.
  • No UI or tRPC changes, so the build and bundle budget were not re-run. The preprod build today was within budget: shared JS 193.7 KB gz.
  • A separate cold review found no correctness or security regressions. Its wording note on the cap is addressed.

Not done / follow-up

  • proxyUtxoRefs in proxySpend and proxyCleanup still resolves one call per ref, with no cap or duplicate check. It now only gets the hash check.
  • txs/{hash}/utxos also returns spent outputs. Neither the old code nor the new code detects them before submission, and the "not found or already spent" message has always overstated that. Fixing it needs a separate consumed_by_tx check.
  • I did not exercise this against live Blockfrost.

Generated by Claude Code

resolveUtxoRefsFromChain (8 v1 endpoints) and the single-ref proxy resolver
interpolated caller-supplied txHash values straight into the Blockfrost URL
(`txs/<hash>/utxos`), accepted the same ref twice, and issued one serial
Blockfrost call per ref with no upper bound.

- txHash must be 64 hex (canonicalised to lowercase) before any chain call,
  so a value like `../accounts/...` can no longer steer our project key to a
  different Blockfrost endpoint.
- A repeated ref is a 400 instead of a doubled builder input.
- At most 455 refs (16 KiB / 36 bytes per input): more than one transaction
  could ever spend; previously a 200 KB body could trigger ~2,000 calls.
- Refs sharing a transaction are resolved with one fetch; Blockfrost returns
  every output of the tx whether or not an index is passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rkSXFUyVC6wzduGmfdTVc
@vercel

vercel Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
multisig Ready Ready Preview Nov 10, 2026 6:07am UTC

Request Review

This branch was successfully deployed

1 active deployment
Preview — 19c80ced Deployed Nov 10, 2026 by vercel[bot]
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