Skip to content

fix(pool): keep the node's rejection reason when a broadcast fails - #8

Open
criptoworld8484 wants to merge 1 commit into
masterfrom
fix/broadcast-error-reason
Open

fix(pool): keep the node's rejection reason when a broadcast fails#8
criptoworld8484 wants to merge 1 commit into
masterfrom
fix/broadcast-error-reason

Conversation

@criptoworld8484

Copy link
Copy Markdown
Owner

Qué pasó

Probando el fallback de la 0.3.18 en el nodo (Umbrel testnet4): se para Fulcrum, una tx programada por fecha/hora vence… y el dashboard dice "Failed to broadcast transaction". Parecía que el fallback a Bitcoin Core no funcionaba.

No era eso. Los logs del nodo muestran que el fallback hizo exactamente su trabajo: source=BitcoinCore indexer_up=false core_up=true, la altura siguió avanzando (144078 → 144085) con Fulcrum parado, y el scheduler marcó la tx vencida y la intentó difundir a su hora.

Lo que fallaba era la tx: gasta los dos mismos UTXOs que otra tx del pool que ya se había confirmado doce minutos antes. Es un doble gasto, y la red lo rechaza siempre — bad-txns-inputs-missingorspent, tanto por Core como por Fulcrum cuando volvió.

El bug real

El motivo del rechazo no llegaba a ninguna parte. Todos los backends envuelven el error del nodo en un .context(), así que e.to_string() se queda solo con nuestra línea de contexto y tira la causa. Eso tiene dos consecuencias:

  1. Cosmética pero cara: el usuario (y yo, depurando) no puede distinguir "tu tx es un doble gasto" de "el workaround está roto". Es justo la confusión que provocó este informe.
  2. Funcional: is_retriable_broadcast_error() decide si reintentar buscando non-final, locktime o too-long-mempool-chain en el mensaje… que son palabras que viven en la causa descartada. Estaba ciega: una tx rechazada por non-final, que el nodo aceptaría un bloque más tarde, se marcaba como failed definitiva en vez de reintentarse.

El arreglo

Renderizar el error con {:#} (cadena completa) en vez de {}, de modo que la causa llegue tanto a la decisión de reintento como al error_message que ve el usuario.

Dos tests nuevos fijan el contrato: con la cadena completa un non-final se reconoce como reintentable, y un bad-txns-inputs-missingorspent no. 43 tests pasan.

🤖 Generated with Claude Code

A failed broadcast was recorded as the bare context line ("Failed to broadcast
transaction"): every backend wraps the node's reason in a `.context()`, so
`e.to_string()` threw away the only part that says *why*.

Two consequences, one cosmetic and one not:

- The dashboard showed a message that explains nothing. Debugging a real
  incident on the node, "Failed to broadcast transaction" was indistinguishable
  from "the Bitcoin Core fallback is broken" — the tx was in fact a double spend
  (bad-txns-inputs-missingorspent), which no backend would ever accept.
- is_retriable_broadcast_error() matches on "non-final", "locktime" and
  "too-long-mempool-chain", all of which live in the discarded source. It was
  therefore blind: a tx the node would accept one block later got marked
  permanently failed instead of being retried.

Render with `{:#}` so the whole error chain reaches both the retry decision and
the user.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant