') + ')', '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); } })(); })(); Fix two circular `Arc` references by TheBlueMatt · Pull Request #736 · lightningdevkit/ldk-node · GitHub
Skip to content

Fix two circular Arc references - #736

Merged
tnull merged 4 commits into
lightningdevkit:mainfrom
TheBlueMatt:2025-12-circular-arc-refs
Jan 13, 2026
Merged

Fix two circular Arc references#736
tnull merged 4 commits into
lightningdevkit:mainfrom
TheBlueMatt:2025-12-circular-arc-refs

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Contributor

Based on lightningdevkit/rust-lightning#4294, basically, but the first fix should probably get backported to 0.7 as it fixes the issue for RGS ldk-node instances even though we'll have to wait for an LDK update to fix the issue for P2P gossip instances.

@ldk-reviews-bot

ldk-reviews-bot commented Dec 31, 2025

Copy link
Copy Markdown

I've assigned @tnull as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@TheBlueMatt
TheBlueMattforce-pushed the 2025-12-circular-arc-refs branch from 58ab500 to 05d7a7dCompareDecember 31, 2025 20:19
@TheBlueMatt

Copy link
Copy Markdown
ContributorAuthor

Note that this is (obviously) blocked on landing the upstream LDK PR.

@TheBlueMatt
TheBlueMatt marked this pull request as draft December 31, 2025 20:21
@tnull

tnull commented Jan 8, 2026

Copy link
Copy Markdown
Collaborator

This needs a rebase now that lightningdevkit/rust-lightning#4294 and #743 landed.

When you bump the LDK dependency to 5236dba053a3f4f01cf0c32ce42b609a93738891, feel free to bump bitcoin-payment-instructions to tnull/bitcoin-payment-instructions@fdca6c6 which does the same.

`LiquiditySource` takes a reference to our `PeerManager` but the
`PeerManager` holds an indirect reference to the `LiquiditySource`.
As a result, after our `Node` instance is `stop`ped and the `Node`
`drop`ped, much of the node's memory will stick around, including
the `NetworkGraph`.
Here we fix this issue by using `Weak` pointers, though note that
there is another issue caused by LDK's gossip validation API.
In added logic to use the HRN resolver from
`bitcoin-payment-instructions`, we created a circular `Arc`
reference - the `LDKOnionMessageDNSSECHrnResolver` is used as a
handler for the `OnionMessenger` but we also set a
post-queue-action which holds a reference to the `PeerManager`.
As a result, after our `Node` instance is `stop`ped and the `Node`
`drop`ped, much of the node's memory will stick around, including
the `NetworkGraph`.
Here we fix this issue by using `Weak` pointers.
@TheBlueMatt
TheBlueMattforce-pushed the 2025-12-circular-arc-refs branch from 05d7a7d to 150b470CompareJanuary 8, 2026 14:50
@TheBlueMatt
TheBlueMatt marked this pull request as ready for review January 8, 2026 14:50
LDK's gossip validation API basically forced us to have a circular
`Arc` reference, leading to memory leaks after `drop`ping an
instance of `Node`. This is fixed upstream in LDK PR #4294 which we
update to here.
@TheBlueMatt
TheBlueMattforce-pushed the 2025-12-circular-arc-refs branch from 150b470 to 93c38bdCompareJanuary 8, 2026 14:51
@TheBlueMatt

Copy link
Copy Markdown
ContributorAuthor

Cleaned up and rebased. Note that a second arc circular ref slipped in in the interim. IMO the first commit should be backported as it by itself fixes Drop leaks at for non-P2P gossip sync nodes, which is better than nothing.

@TheBlueMatt
TheBlueMattforce-pushed the 2025-12-circular-arc-refs branch 4 times, most recently from 4e1df76 to 97a750fCompareJanuary 8, 2026 15:24
@TheBlueMattTheBlueMatt mentioned this pull request Jan 8, 2026
@tnull

tnull commented Jan 8, 2026

Copy link
Copy Markdown
Collaborator

Cleaned up and rebased. Note that a second arc circular ref slipped in in the interim. IMO the first commit should be backported as it by itself fixes Drop leaks at for non-P2P gossip sync nodes, which is better than nothing.

Thanks, will take a look. CI failure is unrelated (see #748).

@tnulltnull left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly looks good, two questions regarding the cycle tests commit.

Comment thread.github/workflows/cln-integration.yml Outdated
Comment threadsrc/lib.rs Outdated
})
}

#[cfg(cycle_tests)]

@tnulltnullJan 9, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than leaking internals and adding all that boilerplate which only makes sense in the cycle_tests context, could we just add a LeakChecker object (probably living in src/leak_checker.rs) taking a number of weak references and holding it as the last field in Node. This LeakChecker could then check all counts are 0 in its drop implementation? Seems like that would a) encapsulate the checks more cleanly b) could also be easily used to check that more (most?) of the main object Arcs aren't leaking on Node::drop?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, yea, that's cleaner. I didn't bother putting it in a separate file cause its literally 10 lines of code without comments and that seemed kinda absurd. let me know if you prefer a file.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, keeping it in the same file is fine for if you so prefer, but I'm confused why we need the TestNode still then? See #736 (comment)

@TheBlueMatt
TheBlueMattforce-pushed the 2025-12-circular-arc-refs branch from b3462c8 to 56832fbCompareJanuary 10, 2026 23:29
Comment threadtests/common/mod.rs Outdated
Err(e) if e == NodeError::NotRunning => {},
Err(e) => panic!("{e:?}"),
}
leak_checks.assert_no_leaks();

@tnulltnullJan 12, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused: why can we not do this in Node::drop and avoid all the TestNode-related changes now? If we add a _leak_checker field as the last field in the struct Node, Rust's drop order should ensure it's always dropped last, i.e., when all other references should be gone, no?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm, I hadn't considered that, nifty idea.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made this change, though sadly note that we still need/want TestNode cause we have to make sure that we stop the node before dropping it, which we don't always do in tests.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cause we have to make sure that we stop the node before dropping it

Not sure I'm following. I guess explicitly stopping in tests might be preferable, but why do we have "to make sure we stop before drop?

In particular, why did you remove the Drop implementation from Node? I don't think we want to do that, as we need to try to do everything to cleanly shutdown, even if users don't explicitly call stop.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooops, uhhhh, cause I'm blind :). I reverted the pile of TestNode changes to just leave the checker.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooops, uhhhh, cause I'm blind :). I reverted the pile of TestNode changes to just leave the checker.

Cool, seems you might still need to push these changes though :)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol oops. done

@tnulltnull left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, feel free to squash the fixups!

Due to two circular `Arc` references, after `stop`ping and
`drop`ping the `Node` instance the bulk of ldk-node's memory (in
the form of the `NetworkGraph`) would hang around. Here we add a
test for this in our integration tests, checking if the
`NetworkGraph` (as a proxy for other objects held in reference by
the `PeerManager`) hangs around after `Node`s are `drop`ped.
@TheBlueMatt
TheBlueMattforce-pushed the 2025-12-circular-arc-refs branch from 340f7de to 6a32f36CompareJanuary 12, 2026 15:42
@TheBlueMatt

Copy link
Copy Markdown
ContributorAuthor

Cool, squashed without further changes.

@tnull
tnull merged commit a1651fc into lightningdevkit:mainJan 13, 2026
17 of 18 checks passed
Sign up for freeto 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.

3 participants

@TheBlueMatt@ldk-reviews-bot@tnull