Skip to content

send_on_chain builds a 0-fee transaction → broadcast rejected with "min relay fee not met" #250

Description

@Jeezman

Client::send_on_chain produces a transaction that pays zero fee, so broadcasting it fails on any standard node:

sendrawtransaction RPC error: {"code":-26, "message": "min relay fee not met, 0 < 17"}

The let fee = Amount::from_sat(1_000) inside create_send_on_chain_transaction_inner is only used to overselect inputs during coin selection, it is never deducted from the transaction. The actual builder, create_unilateral_exit_transaction, sends every selected sat to the
destination + change, so sum(outputs) == sum(inputs) and the fee is 0. The sweep is effectively unusable on mainnet/signet today.

Environment

  • ark-client / ark-core v0.9.3 (also present in v0.9.0v0.9.2).
  • Any backend that enforces a min-relay-fee (i.e. all of them): sendrawtransaction / esplora POST /tx.

Reproduction

  1. Have one or more spendable boarding outputs / unilaterally-exited VTXOs.
  2. Call client.send_on_chain(addr, amount) with amount < total_selected(so a change output is produced — though it fails with or without change).
  3. The returned/broadcast transaction is rejected: min relay fee not met, 0 < N.

Root cause

create_send_on_chain_transaction_inner (ark-client) selects inputs with a 1000-sat margin, then hands the builder only to_amount:

// TODO: Do not use an arbitrary fee.
let fee = Amount::from_sat(1_000);
let (onchain_inputs, vtxo_inputs) = coin_select_for_onchain(self, to_amount + fee).await?;
// ...
let tx = create_unilateral_exit_transaction(
    to_address,
    to_amount,            // <-- the only amount the builder ever sees
    change_address,
    &onchain_inputs,
    &vtxo_inputs,
    sign,
)?;

create_unilateral_exit_transaction (ark-core, ark-core/src/unilateral_exit.rs) then computes change as everything that isn't to_amount:

let total_amount: Amount = onchain_inputs.iter().map(|o| o.amount)
    .chain(vtxo_inputs.iter().map(|v| v.witness_utxo.value))
    .sum();

// change = total - to_amount  (no fee term)
let change_amount = total_amount.checked_sub(to_amount).ok_or_else(/* ... */)?;

let mut output = vec![TxOut { value: to_amount, script_pubkey: to_address.script_pubkey() }];
if change_amount > Amount::ZERO {
    output.push(TxOut { value: change_amount, script_pubkey: change_address.script_pubkey() });
}

So outputs = to_amount + (total − to_amount) = total = inputs, and fee = inputs − outputs = 0. The 1000-sat fee used for selection is handed straight back to the user as change. There is no argument to create_unilateral_exit_transaction that reserves a fee, it is structurally a 0-fee builder.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions