Skip to main content

Selective fanout

When a head is closed and ready to fan out, the plain Fanout command distributes the whole UTxO set, splitting it across transactions as needed. With a selective (partial) fanout you instead choose exactly which UTXO to distribute, and in which order — paying the on-chain cost only for what you care about, and leaving the rest in the head to fan out later.

This how-to assumes we are in a similar situation as in the Getting started or Testnet tutorial: a head has been Closed and the node has emitted a ReadyToFanout message. For how this works on-chain, see Partial fanout.

Open a WebSocket session if you haven't already:

websocat "ws://127.0.0.1:4001?history=no"

First, find out what is still in the head. The confirmed snapshot UTxO is available over HTTP:

curl --fail-with-body -sS http://127.0.0.1:4001/snapshot/utxo -o fanout-utxo.json
jq . fanout-utxo.json

A PartialFanout command supplies the selected outputs under utxoToFanout. For example, generate a command for the first output in the response:

jq -ce '
if length == 0 then error("No outputs to fan out")
else {tag: "PartialFanout", utxoToFanout: (to_entries | .[:1] | from_entries)}
end' fanout-utxo.json

Paste the generated JSON into the WebSocket session. To choose a different subset, edit utxoToFanout, preserving each selected output exactly.

The node submits the corresponding layer 1 transaction (automatically splitting it across several transactions if the selection is too large to fit in one) and reports intermediate progress with HeadPartiallyFannedOut:

Example HeadPartiallyFannedOut
{
"tag": "HeadPartiallyFannedOut",
"headId": "...",
"distributedUTxO": { /* what this step put back on layer 1 */ },
"remainingUTxO": { /* what is still in the head */ },
"fanoutMode": "AwaitingFanoutSelection"
}

fanoutMode tells you whether the node is still draining on its own (AutoFanningOut, e.g. after a plain Fanout) or is waiting for your next selection (AwaitingFanoutSelection) — so a client can show the right prompt instead of guessing. Use remainingUTxO to choose your next selection. Keep issuing PartialFanout commands — selecting the entire remaining set when you want to finish — until the head is drained. On the final step the node automatically submits the transaction that burns the head tokens and emits HeadIsFinalized with the complete distributed finalizedUTxO map. If the entire head fits in one final transaction, there may be no intermediate HeadPartiallyFannedOut event.

Selecting the entire remaining set can take the automatic-drain path. Use the reported fanoutMode and finalization event rather than assuming every selection ends in AwaitingFanoutSelection.

Selective fanout is sticky

Once you send the first PartialFanout for a head, the plain Fanout command is no longer accepted for it (you will receive a CommandFailed). Continue with PartialFanout commands until the head is empty.

Multiple parties

Any party can drive the next step. After one party's PartialFanout lands, the others simply observe it and wait — they will not automatically drain the remainder. So you can start a selective fanout on one node and continue selecting from another (useful if the initiating node goes offline), driving each step sequentially.

Driver liveness

Only the node that issued a command advances the fanout. In particular, a plain Fanout puts the initiating node into an automatic-drain mode where it keeps posting the remaining steps by itself; the other nodes only observe and wait. If that node goes offline mid-fanout, the remainder is not drained automatically — any other party must resume it by issuing PartialFanout commands (selecting the remaining set) until the head is empty.

If a step is rolled back on chain, the driving node re-posts the next fanout transaction to resume. As with all rollback handling, this assumes the rolled-back transactions eventually re-appear; a deeply divergent rollback may require re-issuing a PartialFanout.

To confirm, query the wallet on layer 1. Use the address file from the testnet tutorial and keep CARDANO_NODE_SOCKET_PATH and CARDANO_NODE_NETWORK_ID set for that network:

WALLET_ADDR=$(cat credentials/alice-funds.addr)
cardano-cli query utxo \
--address "$WALLET_ADDR" \
--output-json | jq