Skip to content

On-chain send returns before backend broadcast result #112

Description

@ovitrif

Summary

OnchainPayment::send_to_address and send_all_to_address return a Txid after placing the transaction in an asynchronous broadcast queue, before the configured backend attempts the broadcast. Backend rejection and timeout results are only logged, so mobile callers cannot distinguish local transaction submission from backend acceptance.

This causes synonymdev/bitkit-ios#717 and synonymdev/bitkit-android#1211: both apps immediately present the returned Txid as a successful on-chain send.

Reproduced against 0.7.0-rc.63 (c9a41458a646324c6c8728b4de8329ad4d52ac8d). The same queue-based result contract remains on main (03ccac798aeb907c6dda367fa4c8af6e3eaa54fa).

Reproduction

  1. Sync an Electrum-backed regtest wallet against a chain at height 160,623.
  2. Point the same wallet at a different regtest chain near height 1,114.
  3. Call send_to_address.
  4. The wallet creates a transaction with nLockTime=160623 and returns its Txid.
  5. The backend rejects the transaction as non-final; it never enters the mempool.

Observed result: the caller receives Ok(txid) and both mobile apps show success even though the backend dropped the transaction.

Root cause

Wallet::send_to_address invokes the synchronous BroadcasterInterface, computes the transaction id, and returns it:

self.broadcaster.broadcast_transactions(&[&tx]);
let txid = tx.compute_txid();
Ok(txid)

TransactionBroadcaster::broadcast_transactions only enqueues cloned transactions with try_send. ChainSource::continuously_process_broadcast_queue later submits each package to Electrum, Esplora, or bitcoind. Those backend results cannot reach the original caller.

The Electrum path also misreports backend rejection as success. spawn_blocking returns a nested Result, but the current match treats every successfully joined task as success without inspecting the inner transaction_broadcast result:

Ok(res) => match res {
    Ok(_) => log_trace!(..., "Successfully broadcast transaction {}", txid),
    Err(e) => ...
}

Here Ok(_) includes Ok(Err(electrum_rejection)) from the joined task.

Expected behavior

The on-chain send API exposes an outcome that distinguishes local creation/submission from backend acceptance. A deterministic backend rejection such as non-final reaches the caller or an equivalent transaction-keyed event before the caller is expected to present success.

Accepted transactions retain the current Txid behavior. Asynchronous rebroadcasting for LDK-managed transactions can remain independent of the explicit user-send result.

Consumer evidence

  • iOS calls sendToAddress / sendAllToAddress and immediately creates sent activity plus the success screen from the returned Txid.
  • Android calls the same rc.63 binding and likewise completes the send flow from the returned Txid.
  • Neither binding exposes the raw transaction or a backend broadcast-result event, so the apps cannot safely recover the missing result without duplicating chain-source behavior.

Activity

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions