Skip to content

Comments

Add support for esplora backend#10538

Open
niteshbalusu11 wants to merge 56 commits intolightningnetwork:masterfrom
niteshbalusu11:esplora-support
Open

Add support for esplora backend#10538
niteshbalusu11 wants to merge 56 commits intolightningnetwork:masterfrom
niteshbalusu11:esplora-support

Conversation

@niteshbalusu11
Copy link

Change Description

Description of change / link to associated issue.

Disclaimer: This PR was heavily written by AI but I spent a lot of time code reviewing and testing it

Motivation

I have been working on Blixt Wallet with @hsjoberg for a long time now and we have been using Neutrino as our backend, although neutrino is great for privacy, the UX of it with syncing issues has been a pain for a lot of our users especially for users in countries where internet connectivity is not very good. We even deployed btcd nodes in 4 continents to reduce latency for users but that hasn't helped either.

The PR introduces a new backend for LND based on esplora so you can connect to any public esplora endpoint such as mempool.space or blockstream.info. However, it is recommended that wallet devs run their own instance of esplora because the public endpoints are heavily rate limited.

Details

The core components of this PR are:

  • esplora/client.go: HTTP client for Esplora REST API with automatic retries and block polling for new blocks.
  • esplora/chainclient.go: Implements chain.Interface for btcwallet integration, handling address watching, transaction notifications, and block processing
  • esplora/fee_estimator.go: Implements chainfee.Estimator interface with cached fee estimates and automatic background updates
  • chainntnfs/esploranotify/: Chain notifier implementation for confirmation and spend notifications
  • routing/chainview/esplora.go: Filtered chain view for UTXO tracking

Wallet recovery:

  • When it comes to wallet recovery, neutrino is efficient with sending data across the wire with compact blocks but esplora doesn't do that. To get around this, Evan had the idea of adding a Gap limit which I implemented which significantly improves the performance during recoveries.
  • Configurable gap limit (default: 20) and address batch size (default: 10) for concurrent queries
  • For large address sets, automatically switches to block-based scanning which fetches block transactions and scans them locally which is much more efficient than per-address API queries

Block notification handling:

  • Since esplora is HTTP only, we use polling at configurable intervals (default 10s)

Configuration

[esplora]
esplora.url              - API endpoint URL
esplora.requesttimeout   - HTTP request timeout (default: 30s)
esplora.maxretries       - Retry count for failed requests (default: 3)
esplora.pollinterval     - Block polling interval (default: 10s)
esplora.usegaplimit      - Enable gap limit optimization (default: true)
esplora.gaplimit         - Consecutive unused addresses before stopping (default: 20)
esplora.addressbatchsize - Concurrent address queries (default: 10)

Steps to Test

  • I have made a repo for end to end testing of various important scenarios that you can run for testing the backend:
    https://github.com/niteshbalusu11/lnd-esplora-testing
  • The Readme of the repo should have all the details needed for testing.
  • There are unit tests in the esplora directory for important components.
    Also, @kaloudis helped a lot with testing this with Zeus on testnet.

Pull Request Checklist

Testing

  • Your PR passes all CI checks.
  • Tests covering the positive and negative (error paths) are included.
  • Bug fixes contain tests triggering the bug to prevent regressions.

Code Style and Documentation

📝 Please see our Contribution Guidelines for further guidance.

Refactor `handleNewHeader` to sequentially process blocks, ensuring
complete block notification and watched address tracking. Key changes:
- Process blocks from last known height to current
- Fetch and validate each block header
- Separate reorg detection from block processing
- Improve logging and error handling
- Ensure consistent block caching and notifications
This change adds REST API capabilities to the Electrum backend,
enabling:
- Fetching full block information
- Retrieving transaction details
- Finding transaction indices within blocks
- Validating channel-related transactions

Key changes include:
- Added RESTClient to electrum package
- Updated ElectrumNotifier and ChainClient to support REST URL
- Modified configuration to require REST URL in Electrum mode
- Implemented methods for block and transaction retrieval
This change adds support for fetching the actual pkScript for Taproot
outputs when the script cannot be derived directly from the witness. It
retrieves the full funding transaction to extract the correct output
script for historical spend detection.
Ensure proper synchronization and order of operations when updating
transaction notifier during block connected events. The height is now
updated first, and confirmation/spend checks run concurrently to prevent
potential race conditions.
This commit improves the fee estimation logic by:
- Adding a method to find the most appropriate cached fee
- Implementing fee clamping to ensure minimum fee thresholds
- Handling edge cases like missing targets and exceeding max block
  target
Improve block transaction scanning by:
- Increasing max concurrent block fetches
- Using /block/:hash/txs endpoint for more efficient pre-fetching
- Reducing API calls by processing addresses directly from API response
- Minimizing duplicate transaction processing
- Fetching only matched raw transactions at the end
The commit introduces an advanced gap limit scanning method for more
efficient wallet recovery in the Esplora chain client. Key improvements
include:

- Configurable gap limit optimization
- Ability to control address batch size
- Dramatically faster wallet recovery by stopping scan after consecutive
  unused addresses
- Parallel address querying for improved performance
@gemini-code-assist
Copy link

Summary of Changes

Hello @niteshbalusu11, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request integrates a new Esplora backend for LND, providing a robust and efficient alternative to the existing Neutrino backend. The primary motivation is to enhance the user experience, particularly for users facing unreliable internet connections, by leveraging Esplora's HTTP-based API for blockchain interactions. The implementation covers essential services like block and transaction notifications, fee estimation, and wallet synchronization, with specific optimizations for faster and more reliable wallet recovery processes.

Highlights

  • New Esplora Backend Integration: Introduces Esplora as a new, lightweight HTTP-based blockchain backend for LND, offering an alternative to Neutrino for improved user experience, especially in environments with poor internet connectivity.
  • Comprehensive Chain Interaction: Implements core blockchain functionalities including an Esplora HTTP client, a chain notifier for confirmation and spend notifications, a fee estimator with cached updates, and a chain client for btcwallet integration.
  • Optimized Wallet Recovery: Features a configurable 'Gap Limit' (default: 20) and address batching (default: 10) to significantly enhance wallet recovery performance. For large address sets, it automatically switches to more efficient block-based scanning.
  • HTTP-Based Block Notifications: Addresses the HTTP-only nature of Esplora by implementing a polling mechanism (default: 10s interval) for new block notifications, ensuring timely updates.
  • Configuration Options: Provides a dedicated [esplora] section in the configuration for customizing URL, request timeouts, retry counts, polling intervals, and wallet recovery parameters.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces significant new functionality by adding support for Esplora as a chain backend. This is a valuable addition, allowing lnd to operate in a lightweight mode without a local full node. The implementation is comprehensive, touching configuration, chain notification, chain view, and wallet logic, and includes thoughtful optimizations like gap-limit scanning for wallet recovery. My review focuses on potential bugs related to error handling, performance optimizations for sorting and data fetching, and adherence to the project's coding style. Overall, this is a great contribution.

Comment on lines +12 to +13
// createNewNotifier creates a new instance of the EsploraNotifier from a
// config.

Choose a reason for hiding this comment

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

medium

According to the repository's style guide, function comments should start with the function's name. This comment should be updated to // CreateNewNotifier....

Suggested change
// createNewNotifier creates a new instance of the EsploraNotifier from a
// config.
// CreateNewNotifier creates a new instance of the EsploraNotifier from a
// config.
References
  1. Function comments must begin with the function name. (link)

Comment on lines 272 to 348
func (e *EsploraFilteredChainView) filterBlockTransactions(
blockHeight uint32) []*wire.MsgTx {

e.filterMtx.RLock()
if len(e.chainFilter) == 0 {
e.filterMtx.RUnlock()
return nil
}

// Copy the current filter to avoid holding the lock during API calls.
watchedOutpoints := make(map[wire.OutPoint][]byte)
for op, script := range e.chainFilter {
watchedOutpoints[op] = script
}
e.filterMtx.RUnlock()

var filteredTxns []*wire.MsgTx
spentOutpoints := make([]wire.OutPoint, 0)

ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()

// For each watched outpoint, check if it was spent using the outspend
// endpoint.
for outpoint := range watchedOutpoints {
outSpend, err := e.client.GetTxOutSpend(
ctx, outpoint.Hash.String(), outpoint.Index,
)
if err != nil {
log.Debugf("Failed to check outspend for %v: %v",
outpoint, err)
continue
}

if !outSpend.Spent {
continue
}

// Check if the spend is confirmed and at this block height.
if !outSpend.Status.Confirmed {
continue
}

if uint32(outSpend.Status.BlockHeight) != blockHeight {
continue
}

// Fetch the spending transaction.
tx, err := e.client.GetRawTransactionMsgTx(ctx, outSpend.TxID)
if err != nil {
log.Debugf("Failed to get spending tx %s: %v",
outSpend.TxID, err)
continue
}

filteredTxns = append(filteredTxns, tx)
spentOutpoints = append(spentOutpoints, outpoint)
}

// Remove spent outpoints from the filter.
if len(spentOutpoints) > 0 {
e.filterMtx.Lock()
for _, op := range spentOutpoints {
pkScript := e.chainFilter[op]
delete(e.chainFilter, op)

// Also remove from scripthash mapping.
if pkScript != nil {
sh := esplora.ScripthashFromScript(pkScript)
delete(e.scripthashToOutpoint, sh)
}
}
e.filterMtx.Unlock()
}

return filteredTxns
}

Choose a reason for hiding this comment

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

medium

The current implementation of filterBlockTransactions iterates over all watched outpoints and makes a separate API call (GetTxOutSpend) for each one. For a node with many channels, this can result in a very large number of API calls per block, which is inefficient and may lead to being rate-limited by public Esplora servers.

A more efficient approach would be to fetch all transactions in the block with a single client.GetBlockTxs call (which handles pagination internally) and then iterate through the transactions and their inputs locally to check for spends of watched outpoints. This would significantly reduce the number of API requests.

Copy link
Author

Choose a reason for hiding this comment

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

Made this more efficient.

@yyforyongyu
Copy link
Member

Approved CI run

@lightninglabs-deploy
Copy link
Collaborator

@niteshbalusu11, remember to re-request review from reviewers when ready

Comment on lines +401 to +402
if i < c.cfg.MaxRetries {
time.Sleep(time.Duration(i+1) * 100 * time.Millisecond)
Copy link
Contributor

Choose a reason for hiding this comment

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

it would be nice to turn the retry backoff from a linear to an exponential backoff, either configurable or by default. As is this is too aggressive for most backends.

Good to see that the max retry count is configurable though.

Copy link
Contributor

Choose a reason for hiding this comment

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

also, I believe this call is only catching transport failures (no response at all). we should also try to catch non-200 errors eg. 429 (rate limiting) and 503 (service not available) in a block below this

Comment on lines +401 to +402
if i < c.cfg.MaxRetries {
time.Sleep(time.Duration(i+1) * 100 * time.Millisecond)
Copy link
Contributor

Choose a reason for hiding this comment

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

also, I believe this call is only catching transport failures (no response at all). we should also try to catch non-200 errors eg. 429 (rate limiting) and 503 (service not available) in a block below this

Sign up for free to 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.

4 participants