🔧 AI Nachrichten Major AI platforms go down in unprecedented simultaneous outage(03.09.2026 um 17:34 Uhr)
🔧 AI Nachrichten ChatGPT, Claude, and Grok Down? Users Report Widespread Outages(03.09.2026 um 19:14 Uhr)
🔧 AI Nachrichten OpenAI Launches GPT-6 Astra, Says We May Have Entered the AGI Era(03.09.2026 um 22:08 Uhr)
🔧 AI Nachrichten Claude Comes to CarPlay as Fifth Major AI Chatbot App(05.09.2026 um 05:31 Uhr)
🔧 AI Nachrichten OpenAI’s GPT-6 Astra Is AGI, Says NVIDIA CEO Jensen Huang(07.09.2026 um 06:31 Uhr)
🔧 AI Nachrichten Blame AI companies for Mac mini and Mac Studio shortage(31.08.2026 um 10:32 Uhr)
🔧 AI Nachrichten Major AI platforms go down in unprecedented simultaneous outage(03.09.2026 um 17:34 Uhr)
🔧 AI Nachrichten ChatGPT, Claude, and Grok Down? Users Report Widespread Outages(03.09.2026 um 19:14 Uhr)
🔧 AI Nachrichten OpenAI Launches GPT-6 Astra, Says We May Have Entered the AGI Era(03.09.2026 um 22:08 Uhr)
🔧 AI Nachrichten Claude Comes to CarPlay as Fifth Major AI Chatbot App(05.09.2026 um 05:31 Uhr)
🔧 AI Nachrichten OpenAI’s GPT-6 Astra Is AGI, Says NVIDIA CEO Jensen Huang(07.09.2026 um 06:31 Uhr)
🔧 AI Nachrichten Blame AI companies for Mac mini and Mac Studio shortage(31.08.2026 um 10:32 Uhr)

🔧 Programmierung 🕛 kürzlich 6 Min Lesezeit
0

When the exit code lies: fork retractions, lost transactions, and trusting the chain in midnight-node

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

Your transaction tool reports FAILED_TO_FINALIZE. Your CI marks the job red. Your retry logic fires. But the block explorer says the transaction finalized 22 seconds after you sent it.



The exit code lied.



This is a debugging story from for 's watch stream, which emits status events: InBestBlock, NoLongerInBestBlock, InFinalizedBlock, and so on. The sender's logic was:




  1. Wait for the tx to appear in a best block.

  2. Then wait (with a timeout) for InFinalizedBlock.

  3. Timeout expires → report FAILED_TO_FINALIZE, exit non-zero.



Here's the failure mode from the issue, reconstructed from logs:




CODE
t+0s    tx submitted, InBestBlock (block A)
t+6s fork: block A retracted → NoLongerInBestBlock
t+8s tx re-included in block B on the winning branch
t+22s block B finalized ✅ tx is permanently on chain
t+60s watcher's finalization timeout expires
→ exit: FAILED_TO_FINALIZE ❌






Two things went wrong. First, NoLongerInBestBlock was being silently swallowed — the logs showed nothing at the moment the interesting thing happened. Second, after the retraction, the watch stream never surfaced the re-inclusion; the watcher waited out its timeout on a dead branch while the transaction quietly finalized elsewhere.






Why a wrong exit code is worse than a crash



If this tool only fed dashboards, a spurious failure would cost an engineer an eyebrow raise. But callers make decisions based on exit codes. The sharpest edge in our case: reward-claim transactions with at-most-once semantics. A wrapper script sees the non-zero exit, assumes the claim never landed, and retries — resubmitting an operation that already succeeded. The exit code isn't diagnostics; it's an API, and it was returning wrong answers.



This is a general lesson worth internalizing: any tool that reports transaction outcomes is part of someone's correctness argument. Treat its outputs with the same rigor as consensus code.






The fix: the chain gets the last word



The watch stream is a convenience, not a source of truth. The source of truth is the finalized chain, and it's sitting right there behind an RPC. So on watch timeout, the sender now scans finalized blocks, newest first, looking for the extrinsic hash:




CODE
pub async fn find_in_finalized_chain(
client: &MidnightNodeClient,
extrinsic_hash_hex: &str,
max_depth: u32,
) -> Option<String> {
let mut hash = client.rpc.chain_get_finalized_head().await.ok()?;

for _ in 0..max_depth {
let block = match client.rpc.chain_get_block(Some(hash)).await {
Ok(Some(b)) => b,
_ => return None,
};

for ext in &block.block.extrinsics {
let ext_hash =
format!("0x{}", hex::encode(sp_crypto_hashing::blake2_256(&ext.0)));
if ext_hash == extrinsic_hash_hex {
return Some(hash_to_str(hash));
}
}

if block.block.header.number == 0 {
return None;
}
hash = block.block.header.parent_hash;
}
None
}






A few design notes:





  • Bounded depth (64 blocks below the finalized head — comfortably past any plausible retraction-to-finalization window at 6-second blocks). An unbounded walk to genesis on a wrong hash would turn one bug into a different one.


  • Extrinsic identity is just a hash. A Substrate extrinsic's hash is the blake2-256 of its encoded bytes, so the scan needs no indexer, no storage queries — fetch blocks, hash extrinsics, compare.


  • Fail open, loudly. RPC errors during the scan log a warning and return None — the sender then reports the timeout as before. The fallback can only upgrade a false failure into a truthful success, never mask a real one.



With the scan in place, the outcome reporting becomes honest:




CODE
let message = if finalized_block_hash.is_some() {
if finalized.is_some() { "FINALIZED" } else { "FINALIZED_AFTER_RETRACTION" }
} else {
"FAILED_TO_FINALIZE"
};






FINALIZED_AFTER_RETRACTION exits zero — because the transaction is on chain — but it's a distinct message, so operators can see how often forks are eating their watch streams. And the retraction itself is no longer swallowed: the moment NoLongerInBestBlock arrives, the sender logs tx retracted from best block; watching for re-inclusion.



The timeouts also became configurable (MN_SEND_BEST_BLOCK_TIMEOUT / MN_SEND_FINALIZED_TIMEOUT, in seconds) for slow or fault-injected environments — as environment variables rather than CLI flags, partly to keep the change surface away from other in-flight CLI work.






Testing what you can, admitting what you can't



Here's the honest part: you cannot deterministically force a fork retraction in CI. Retractions emerge from validator timing races; no RPC call produces one on demand. A test suite that claims to cover the retraction path end-to-end would be lying the same way the exit code was.



What you can do is split the fix so the untestable part is trivially small (a timeout branch calling one function) and the testable part carries the logic. The e2e test for the scan uses a trick worth stealing: every Substrate block already contains a transaction you didn't send — the timestamp inherent that the block author injects. So the test:




  1. runs against a real node, grabs a finalized block, and takes its timestamp-inherent extrinsic;

  2. hashes those bytes and asserts find_in_finalized_chain locates that extrinsic at exactly that block (positive case, using an extrinsic the test never had to submit);

  3. asserts a fabricated hash comes back None after the bounded walk (negative case).



No fault injection, no mocked RPC — the scan is exercised against genuinely finalized blocks, and the only unverified wiring is a five-line match.






Takeaways





  • Watch streams are best-effort; finality is a fact. When a stream and the chain can disagree, reconcile against the chain before reporting failure.


  • Exit codes are API. If a caller might retry based on your answer, a false negative is a correctness bug, not a cosmetic one.


  • Never swallow the interesting event. The silent NoLongerInBestBlock was the difference between a 5-minute diagnosis and a mystery.


  • Bound your fallbacks. A recovery path with no depth limit is a new incident waiting to happen.


  • Be honest about test coverage. Shrink the untestable wiring instead of pretending the test forces the failure.



The fix is open for review at and your tooling watches transactions, go check what your code does with NoLongerInBestBlock. There's a decent chance the answer is "nothing," and now you know why that matters.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
3 Quellen
GPT-6 Astra Release Today? OpenAI’s Next Major AI Model Is Almost Here
1 Quelle
Apple accuses OpenAI of destroying evidence as trade-secrets fight intensifies
1 Quelle
Major AI platforms go down in unprecedented simultaneous outage