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
- Sync an Electrum-backed regtest wallet against a chain at height 160,623.
- Point the same wallet at a different regtest chain near height 1,114.
- Call
send_to_address.
- The wallet creates a transaction with
nLockTime=160623 and returns its Txid.
- 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.
Summary
OnchainPayment::send_to_addressandsend_all_to_addressreturn aTxidafter 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
Txidas a successful on-chain send.Reproduced against
0.7.0-rc.63(c9a41458a646324c6c8728b4de8329ad4d52ac8d). The same queue-based result contract remains onmain(03ccac798aeb907c6dda367fa4c8af6e3eaa54fa).Reproduction
send_to_address.nLockTime=160623and returns itsTxid.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_addressinvokes the synchronousBroadcasterInterface, computes the transaction id, and returns it:TransactionBroadcaster::broadcast_transactionsonly enqueues cloned transactions withtry_send.ChainSource::continuously_process_broadcast_queuelater 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_blockingreturns a nestedResult, but the current match treats every successfully joined task as success without inspecting the innertransaction_broadcastresult:Here
Ok(_)includesOk(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-finalreaches the caller or an equivalent transaction-keyed event before the caller is expected to present success.Accepted transactions retain the current
Txidbehavior. Asynchronous rebroadcasting for LDK-managed transactions can remain independent of the explicit user-send result.Consumer evidence
sendToAddress/sendAllToAddressand immediately creates sent activity plus the success screen from the returnedTxid.Txid.