Skip to content

Head closed with sub-min-ADA (dust) outputs can never be fanned out — stuck in Closed with H39 #2759

Description

@v0d1ch

Context & versions

  • hydra-node on master .
  • Head configured with L2 --ledger-protocol-parameters where utxoCostPerByte = 0 (the zero-fee setup suggested by the tutorial). The L2 ledger consequently accepts outputs below L1 min-ADA, e.g. token-only or tiny-lovelace outputs.
  • L1 with standard protocol parameters (coinsPerUTxOByte = 4310).

Steps to reproduce

  1. Open a head with utxoCostPerByte = 0 in the L2 ledger parameters.
  2. Submit an L2 NewTx creating an output below L1 min-ADA (e.g. 100 000 lovelace) — the snapshot confirms fine.
  3. Close the head, wait for the contestation deadline, then Fanout.

Actual behavior

Every fanout attempt fails locally with ScriptFailedInWallet and head validator error H39 FanoutUTxOHashMismatch — the transaction is never submitted. The head stays in Closed indefinitely and funds are locked: a sub-min-ADA output can never exist on L1, so not even partial fanout can finalize the head, and the head tokens plus ada overhead remain locked as well.

ScriptFailedInWallet, redeemerPtr = ConwaySpending (AsIx 0)
CekError: The machine terminated because of an error ... ["H39","PT5"]

The root cause has two layers:

  1. An output below L1 min-ADA cannot be an output of any valid L1 transaction, so a head closed in this state is unfannable by construction.
  2. It surfaces as a confusing H39 instead of an honest OutputTooSmallUTxO because of the internal wallet: coverFee_ applies ensureMinCoinTxOut to all transaction outputs before running script estimation (Hydra.Chain.Direct.Wallet.coverFee_), silently bumping the dust output to min-ADA (100 000 → ~849 070 for a simple payment address). The bumped output no longer matches the accumulator commitment that the parties signed in the snapshot and that is stored in the closed datum, so the fanout membership check fails on every retry.

Expected behavior

  • The node prevents accidentally closing a head whose UTxO cannot be fanned out on L1: SafeClose refuses to close (the API schema already advertises a pre-close safety check, but it currently only covers non-ADA assets), and plain Close at least emits a warning listing the offending outputs. The checks must run against L1 protocol parameters: per-output min-ADA, closing value covered by the head output, per-output size.
  • The internal wallet does not silently mutate outputs of hash-committed protocol transactions — failing loudly would surface the real error (OutputTooSmallUTxO) instead of a misleading script failure.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

Status
Todo 📋

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions