') + ')', '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); } })(); })(); Add support for sourcing chain data from Electrum by tnull · Pull Request #486 · lightningdevkit/ldk-node · GitHub
Skip to content

Add support for sourcing chain data from Electrum - #486

Merged
tnull merged 13 commits into
lightningdevkit:mainfrom
tnull:2025-02-add-electrum-support
Apr 16, 2025
Merged

Add support for sourcing chain data from Electrum#486
tnull merged 13 commits into
lightningdevkit:mainfrom
tnull:2025-02-add-electrum-support

Conversation

@tnull

@tnulltnull commented Mar 17, 2025

Copy link
Copy Markdown
Collaborator

Closes#196.

So far, we support Esplora and Bitcoind RPC chain sources. In this PR we add Electrum support based on the blocking rust-electrum-client, and it's bdk_electrum and lightning-transaction-sync counterparts.

Due to the blocking nature of rust-electrum-client and as it directly connects to the server upon Client::new (see bitcoindevkit/rust-electrum-client#166), we ended up wrapping the runtime-specific behavior in an ElectrumRuntimeClient object that is initialized and dropped in Node::start and stop, respectively.

One thing missing that we still need to consider is how we'd reestablish connections to the remote after they have been lost for one reason or another. IMO, that behavior should live in rust-electrum-client to avoid all users having to duplicate it, so it's pending resolution of bitcoindevkit/rust-electrum-client#165

As we did with bitcoind-RPC, Electrum support is tested by adding another full_cycle integration test.

@tnulltnull added this to the 0.5 milestone Mar 17, 2025
@tnull
tnullforce-pushed the 2025-02-add-electrum-support branch from b386ee7 to 9352b2eCompareMarch 17, 2025 14:17
@tnull
tnull requested a review from jkczyzMarch 17, 2025 14:51
@tnull
tnullforce-pushed the 2025-02-add-electrum-support branch 3 times, most recently from 568d10d to 678cf91CompareMarch 18, 2025 12:50

@elnoshelnosh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm familiarizing myself with the codebase but I tried running a node locally with the changes here pointing to an electrum server and basic operations are working. I just have a couple of questions, if I may, to get a better understanding.

Not strictly related to the PR but maybe I can sneak it in, why is it that for bitcoin core rpc and electrum they are in their separate bitcoind_rpc.rs and electrum.rs files but for esplora it is all in the mod.rs? I see that that the EsploraAsyncClient is directly used without a wrapper but wanted to see what were the reasons for it (:

Comment threadsrc/chain/electrum.rs
Comment threadtests/integration_tests_rust.rs
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Not strictly related to the PR but maybe I can sneak it in, why is it that for bitcoin core rpc and electrum they are in their separate bitcoind_rpc.rs and electrum.rs files but for esplora it is all in the mod.rs?

Hmm, for one it has historic reasons, but also bitcoind RPC and electrum both ended up needing additional helper objects/types that made sense to split out to dedicated sub-modules. While we could eventually refactor the Esplora part so more code lives in a dedicated esplora.rs file, there is no strong necessity for it right now I think.

I see that that the EsploraAsyncClient is directly used without a wrapper but wanted to see what were the reasons for it (:

Right, that's essentially the main reason why it still lives in mod.rs. For electrum we ended up needing a wrapper that is created on runtime, and for bitcoind RPC we implemented a specific wrapper around RpcClient that handles the JSON parsing, essentially.

@tnull
tnullforce-pushed the 2025-02-add-electrum-support branch 3 times, most recently from 9608e66 to 3142c41CompareMarch 19, 2025 12:54
Comment threadsrc/chain/mod.rs Outdated
@tnull
tnullforce-pushed the 2025-02-add-electrum-support branch 4 times, most recently from 466ab9f to e39b1f2CompareMarch 27, 2025 08:47
@tnull

tnull commented Mar 27, 2025

Copy link
Copy Markdown
CollaboratorAuthor

Blocked on rust-bitcoin/corepc#111 for now.

@tnull
tnullforce-pushed the 2025-02-add-electrum-support branch 2 times, most recently from 0d401fe to dda6bc7CompareMarch 27, 2025 10:50
@jkczyz
jkczyz removed their request for review March 27, 2025 18:42
@tnull
tnull requested a review from joostjagerMarch 27, 2025 19:04
@tnull
tnullforce-pushed the 2025-02-add-electrum-support branch from dda6bc7 to edb4b10CompareMarch 28, 2025 08:55
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Rebased on main after #508 landed.

@joostjagerjoostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The question that I asked myself most during review is whether the way this PR deals with the runtime and start/stop is really the best option for now.

Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/builder.rs
Comment threadsrc/chain/mod.rs Outdated
Comment threadsrc/chain/mod.rs Outdated
Comment threadsrc/chain/mod.rs Outdated
Comment threadsrc/chain/electrum.rs
Comment threadsrc/chain/electrum.rs
Comment threadsrc/chain/mod.rs Outdated
Comment threadsrc/chain/mod.rs Outdated
Comment threadsrc/chain/electrum.rs Outdated
@tnull
tnullforce-pushed the 2025-02-add-electrum-support branch from edb4b10 to 97f4bd9CompareMarch 28, 2025 16:21
@tnull
tnullforce-pushed the 2025-02-add-electrum-support branch from 96ee279 to cb5dc5bCompareApril 3, 2025 12:51
@tnull

tnull commented Apr 3, 2025

Copy link
Copy Markdown
CollaboratorAuthor

Squashed fixups without further changes.

@joostjagerjoostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nearly there. Just the one question about the double timeouts.

Comment threadsrc/chain/mod.rs
Comment threadsrc/chain/mod.rs Outdated
Comment threadsrc/chain/electrum.rs

let spawn_fut = self.runtime.spawn_blocking(move || electrum_client.batch_call(&batch));

let timeout_fut = tokio::time::timeout(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I see you also set retries now. I saw that it was 1 previously (the default). Did the earlier test with the wifi disconnect pass because that one retry happened after you already reconnected wifi?

If I understand this correctly, setting the timeout on the electrum client itself is essential to not end up with a potential graveyard of stuck threads? This might be important to add as a comment.

Also curious to hear why you think that retrying over and over again when a previous request is still stuck is a good idea? It will eventually exhaust the thread pool, right? And then stop independent processes within LDK too?

The original reason for the double timeout is because esplora-client turned out to be not trustworthy. But, as suggested above, isn't electrum different because it isn't an async library and it is unlikely for the timeout to fail? Looking at the sources, it seems that the timeout is set on a pretty low level for all calls.

.. we need to bump the version, while making sure `electrsd` and CLN CI
are using the same version. As the `blockstream/bitcoind` version hasn't
reached v28 yet, we opt for v27.2 everywhere for now.
@tnull
tnullforce-pushed the 2025-02-add-electrum-support branch from cb5dc5b to db756c6CompareApril 4, 2025 08:21
@tnull

tnull commented Apr 4, 2025

Copy link
Copy Markdown
CollaboratorAuthor

The corerpc-node just shipped the needed fix as part of the 0.7 minor release, now we just need RCasatta/electrsd#100 to land as part of an electrsd release to go forward with this.

@tnulltnull mentioned this pull request Apr 7, 2025

@enigbeenigbe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I have taken another look at this PR and now understand why you apply pending filter entries to the pending transaction and output vectors. The refactor that introduced ElectrumRuntimeStatus made it easier to reason about it as a state machine with the attendant events, actions, and state or internal transitions.

state machine

From the snippet below, when we start the Node with the tokio runtime, it indicates that other background tasks do not get started before the ChainSource has started:

pubfn start_with_runtime(&self,runtime:Arc<tokio::runtime::Runtime>) -> Result<(),Error>{
... // Start up any runtime-dependant chain sources (e.g. Electrum)self.chain_source.start(Arc::clone(&runtime)).map_err(|e| {log_error!(self.logger,"Failed to start chain syncing: {}", e);
e
})?;
...
// Other background tasks

This suggests that it is unlikely that Confirmables would register transactions and outputs prior the chain source starting and, in extension, prior to the runtime client being available (ElectrumRuntimeStatus::Started). Are there scenarios I am missing where this could happen? I'd appreciate any further clarification.

Otherwise, the PR looks good to me. Reviewing this has been quite educational. Thank you!

@tnull

tnull commented Apr 10, 2025

Copy link
Copy Markdown
CollaboratorAuthor

I have taken another look at this PR and now understand why you apply pending filter entries to the pending transaction and output vectors.

Thank you for the review!

This suggests that it is unlikely that Confirmables would register transactions and outputs prior the chain source starting and, in extension, prior to the runtime client being available (ElectrumRuntimeStatus::Started). Are there scenarios I am missing where this could happen? I'd appreciate any further clarification.

Unfortunately, the ChannelMonitors register the transactions/outputs they're interested in when ChainMonitor::watch_channel is called. See:
https://github.com/lightningdevkit/rust-lightning/blob/85185d863871fefa5243fa9e38f3037b5db3eac6/lightning/src/chain/chainmonitor.rs#L790

and

https://github.com/lightningdevkit/rust-lightning/blob/85185d863871fefa5243fa9e38f3037b5db3eac6/lightning/src/chain/channelmonitor.rs#L1776

In turn we currently call this upon initialization:

chain_monitor.watch_channel(funding_outpoint, channel_monitor).map_err(|e| {

not at runtime/during Node::start. This is one of the reasons we decided to soon refactor this so that we'd intialize most LDK objects only at start, not in Builder::build. Another (actually even more important) reason to do this is that we'd then be able to first retrieve the current best block before initializing the ChannelManager and other objects for the first time, avoiding the currently necessary resyncing from genesis (cf. #415).

@tnull
tnullforce-pushed the 2025-02-add-electrum-support branch 3 times, most recently from 37d667c to 24d0874CompareApril 15, 2025 10:01
@tnull
tnull requested a review from joostjagerApril 15, 2025 12:24
tnull added 12 commits April 15, 2025 18:14
We upgrade our tests to use `electrum-client` v0.22 and `electrsd` v0.31
to ensure compatibility with `bdk_electrum` were about to start using.
By default `rustls`, our TLS implementation of choice, uses `aws-lc-rs`
which requires `bindgen` on some platforms. To allow building with
`aws-lc-rs` on Android, we here install the `bindgen-cli` tool before
running the bindings generation script in CI.
We here setup the basic API and structure for the
`ChainSource::Electrum`.
Currently, we won't have a `Runtime` available when initializing
`ChainSource::Electrum` in `Builder::build`. We therefore isolate any
runtime-specific behavior into an `ElectrumRuntimeClient`.
This might change in the future, but for now we do need this workaround.
Currently, we won't have a `Runtime` available when initializing
`ChainSource::Electrum`. We therefore isolate any runtime-specific
behavior into the `ElectrumRuntimeStatus`.
Here, we implement `Filter` for `ElectrumRuntimeClient`, but we need to
cache the registrations as they might happen prior to
`ElectrumRuntimeClient` becoming available.
.. as we do with `BitcoindRpc`, we now test `Electrum` support by
running the `full_cycle` test in CI.
@tnull
tnullforce-pushed the 2025-02-add-electrum-support branch from 24d0874 to 5cdd2e3CompareApril 15, 2025 16:14
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Squashed fixups without further changes.

@joostjagerjoostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!

Still interested to see if conversion to a more object-oriented style for the backends is straight-forward or not.

@tnull
tnull merged commit d840bbd into lightningdevkit:mainApr 16, 2025
@tnulltnull mentioned this pull request Apr 17, 2025
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.

Add Electrum support

4 participants

@tnull@joostjager@enigbe@elnosh