Conversation
A broadcast with no clean answer was written `Failure` with no hash: invisible to the verifier forever, though the tx may have landed. `sendTransaction` now derives the hash locally (sha256 of the bytes it broadcasts, as `matchTxInBlock` does), so every outcome carries one. It also stops flattening errors: dedup is success, a deterministic CheckTx rejection is `rejected`, timeouts and resets are unknown. `ExecuteTransaction` anchors hash + heights BEFORE broadcasting; an unknown outcome stays `pending` for the chain to settle.
- executeTransaction now throws a retryable failure when the node gave no definitive answer, so Temporal re-sends the same bytes on a policy scoped to that activity; a sdk code 32 is handed to the verifier instead. - Both flows catch the exhausted retry: the anchored flow records the last error, the legacy flow anchors the row so a relaunch can never re-send as attempt 1 and trust a rejection while the stake is on chain. - sendTransaction classifies refused/unresolvable connections as neverSent (walking undici's cause chain), drops the dead TimeoutError branch, and the attempt number defaults to null outside an activity context. - The failure type lives in a Node-free leaf module and a bundle smoke test guards it: importing it from the activities module broke the workflow bundle. Activity, DAL and classifier tests added.
PR description — fixes #339SummaryA broadcast that returned no clean answer was recorded as a permanent The broadcaster no longer decides the outcome. It anchors the transaction (hash + heights), sends Design
Broadcast retriesA tx signed through Soothe carries no
The blocker that had to be fixed firstThe anchor could not have worked as intended. Both consumers were affected, so Measured against the real function with a real Without this, the change would have moved the defect rather than removed it: from "wrongly marked Also included
Correction to the issue textThe issue says the middleman never got the #308 broadcast/verify split. It got half — the Tests
|
A broadcast with no clean answer was written
Failurewith no hash: invisible to the verifier forever, though the tx may have landed.sendTransactionnow derives the hash locally (sha256 of the bytes it broadcasts, asmatchTxInBlockdoes), so every outcome carries one.It also stops flattening errors: dedup is success, a deterministic CheckTx rejection is
rejected, a refused or unresolvable connection isneverSent(nothing reached the node), and everything else is unknown.ExecuteTransactionanchors hash + heights BEFORE broadcasting; an unknown outcome stayspendingfor the chain to settle.Unknown outcomes are retried, not parked.
executeTransactionreturns only a definitive answer: success, a CheckTx rejection, or a sdk code 32 (sequence already consumed, which a re-send cannot change and the verifier settles by hash or by the sequence rule). Anything else — node unreachable, connection dropped mid-request, mempool full — throws a retryable failure so Temporal re-broadcasts the same bytes, on a retry policy scoped to that one activity (5s → 30s backoff, 5 attempts, ~3.5 min worst case). This closes the gap for Soothe-signed transactions, which carry no timeoutHeight and would otherwise sit pending, keys held, until the signer's sequence was consumed by some other tx. Once the policy is spent, both flows catch the failure: the anchored flow records the last error on the row, and the legacy flow (pre-upgrade histories) anchors the row in that run — never failing the run, since a relaunched run with no hash would re-send as attempt 1 and could trust a rejection while the stake is on chain. An outage longer than the retry window still leaves a Soothe-signed row waiting on the sequence rule; an alert for pending rows with no timeout is a follow-up.parseSignerAndSequencedecoded hex payloads as base64 and returned nulls for every transaction, so no middleman transaction could reach a failure verdict; fixed and tested both ways.The workflow bundle is now guarded by a test that builds it: a runtime import from the activities module would pull drizzle/pg/node:crypto into Temporal's sandbox and crash the worker at start-up, which nothing else in CI can see.
Closes #339
Not done, flagged for follow-up:
A Soothe-signed tx carries no
timeoutHeight, so a broadcast that never reached a mempool canonly be failed once the signer's sequence is consumed by another tx — for an idle delegator,
never. The retries here shrink the window to ~3.5 min but do not close it. Alerting on those
rows (never auto-failing them) is #351.