Uh oh!
There was an error while loading. Please reload this page.
') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })();
There was an error while loading. Please reload this page.
SignerProvider callbacks #1010
Summary
<WalletKeysManager as SignerProvider>::get_shutdown_scriptpubkey(andget_destination_script) callRuntime::block_on(wallet.get_new_address()), which awaits the wallet persister lock and store I/O. LDK invokes these sync callbacks on runtime worker threads from inside the event handler (e.g.,ChannelManager::do_accept_inbound_channel→ChannelContext::new_for_inbound_channel), while holding the per-peer channelMutex. If theblock_onfuture cannot complete without another runtime task being polled, and the remaining worker cores get captured by tasks blocking synchronously on the same channel locks, the runtime deadlocks permanently.This is the same class as #978 (fixed by #980) but a different entry point: #980 stopped holding the sync BDK wallet lock across store awaits and isolated store I/O onto dedicated runtimes, but the
SignerProvidercallbacks still block a worker on wallet persistence while holding LDK channel locks — which lets unrelated runtime tasks wedge the scheduler through those locks.Observed deadlock (thread samples)
Hit 2 of 2 full
cargo test --test integration_tests_rustruns (macOS, 2026-07-30), striking whichever test was unlucky under whole-suite load —open_channel_variants_reserve_funds_for_anchor_peersin one run,split_underpaid_bolt11_paymentin the other. Both use#[tokio::test(flavor = "multi_thread", worker_threads = 1)], so the node runs on the test's single-worker runtime. Not reproducible standalone without suite load (see deterministic repro below).macOS
sampleshows the identical three-party cycle in both hangs:Thread A — background processor task (holds the per-peer channel
Mutex, parked forever):Thread B — the fresh thread that received Thread A's core via the
block_in_placehandoff (captures the core, blocked without handoff):Party 3 — the future Thread A waits on.
Wallet::get_new_addressawaits the wallet persistertokio::sync::Mutexand thenpersist_changeset(store I/O). Under load that mutex is routinely held by another runtime task (wallet sync /apply_update,block_connected, etc.). That task is queued on the runtime — but the only core is captive on Thread B, so it can never run to release the lock, so Thread A is never woken, so the peerMutexis never released, so Thread B never unblocks. No party can make progress; the hang is permanent, and even tokio timers stop firing (no worker polls the timer wheel).worker_threads = 1is the minimal trigger, but any runtime deadlocks once N cores are simultaneously captured by tasks blocking synchronously on locks held by parkedblock_oncallers.Deterministic reproduction
Gate a node's
bdk_wallet-namespaceKVStore::writes (in-memory store whose wallet writes await an already-heldRwLock) and have a peer open a channel to it on aworker_threads = 1runtime. The acceptor's event handler entersget_shutdown_scriptpubkey→block_on→ parks on the gated write while holding the peerMutex; thelightning-net-tokioconnection task then captures the handed-off core inmaybe_generate_initial_closing_signed; timers die; the node never emitsChannelPending. A thread sample of the wedged test process shows exactly the same stacks as the organic hangs.(Regression test included in the fix PR:
channel_open_completes_while_wallet_persistence_is_stalled.)Proposed fix
Deriving a fresh address does not need to wait for persistence — only the reveal must be ordered. Make the sync
SignerProvidercallbacks non-blocking:take_staged()— the same ordering every other persist path uses, so out-of-orderlast_revealedregressions are impossible.Semantics change: a persistence failure no longer fails the channel open (previously
Err(())from the callback). If the node crashes before the flush lands, the revealed index is re-revealed after restart, potentially reusing the address; BDK's keychain lookahead still detects funds sent to it. This matches the durability LDK's ownKeysManagerprovides for shutdown scripts (none — it derives a static script and persists nothing).Related bridges not covered here
OnchainPayment::new_addressandLSPS1Liquidity::request_channelstillblock_on(get_new_address()), but they are public sync API entry points invoked on user threads and do not hold LDK locks. They are the known/accepted sync-bridge pattern (cf. theStoreRuntimedocs), though they remain susceptible if users call them from runtime context.<Wallet as Listen>::block_connectedalso bridges viablock_onwhile holding the wallet persister lock (bitcoind chain source only). It does not hold LDK channel locks, so it cannot form this cycle by itself, but it can be one of the parked parties in a multi-worker capture.SignerProvider/ChangeDestinationSourcecallbacks in LDK would remove the need for the bridge entirely.Provenance
Introduced by #919 (commit 5ac2ed1, "Use BDK's async wallet persister"): before it,
get_new_addresspersisted through BDK's sync persister with no runtime involvement, so the syncSignerProvidercallbacks could never park on the runtime. #919 bridged the still-sync wallet APIs viaRuntime::block_on(flagged in its commit message as a stopgap). #980 later reshaped the bridge (moving theblock_oninto the callbacks themselves) while fixing #978, but this cycle runs through the LDK peerMutexrather than the wallet lock, so it survived.Environment
main@ abbed3e, LDK0.3.0+git(lightningdevkit/rust-lightning@506cb91f)cargo testwhole-suite load