-
Notifications
You must be signed in to change notification settings - Fork 9
btcwbackend: add neutrino+btcwallet wallet mode #228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
1cd3774
btcwbackend: add neutrino+btcwallet backend package
Roasbeef d959a7a
darepod: add btcwallet wallet type config and validation
Roasbeef 99eca2e
darepod: wire btcwbackend into daemon startup and RPC
Roasbeef 489c214
go.mod: promote neutrino and walletdb to direct dependencies
Roasbeef 1fe94fe
multi: address review findings from substrate review
Roasbeef 69e759c
multi: address iteration 2 review findings
Roasbeef ad45b8d
multi: extract walletcore shared package from lwwallet
Roasbeef 72f1d66
fixup! multi: extract walletcore shared package from lwwallet
Roasbeef af7f113
fixup! darepod: wire btcwbackend into daemon startup and RPC
Roasbeef 63a5f57
multi: fix walletdb API and expose bitcoind P2P port
sputn1ck e7f501d
multi: enable bitcoind P2P for neutrino and add sync wait
sputn1ck File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| package btcwbackend | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "log/slog" | ||
| "math" | ||
|
|
||
| "github.com/btcsuite/btcd/btcutil" | ||
| "github.com/btcsuite/btcd/chaincfg" | ||
| "github.com/btcsuite/btcd/chaincfg/chainhash" | ||
| "github.com/btcsuite/btcd/txscript" | ||
| "github.com/btcsuite/btcd/wire" | ||
| "github.com/btcsuite/btclog/v2" | ||
| "github.com/lightninglabs/darepo-client/wallet" | ||
| "github.com/lightninglabs/darepo-client/walletcore" | ||
| "github.com/lightninglabs/neutrino" | ||
| "github.com/lightningnetwork/lnd/blockcache" | ||
| "github.com/lightningnetwork/lnd/lnwallet/btcwallet" | ||
| ) | ||
|
|
||
| // BoardingBackendAdapter implements wallet.BoardingBackend by | ||
| // embedding walletcore.BoardingBackendBase for shared key derivation | ||
| // and script import, with neutrino providing block and transaction | ||
| // data. Unlike the lwwallet adapter which bypasses btcwallet for | ||
| // UTXO queries, this adapter uses btcwallet's native ListUnspent | ||
| // since neutrino's chain sync naturally detects matching outputs via | ||
| // compact block filters after address import. | ||
| type BoardingBackendAdapter struct { | ||
| // BoardingBackendBase provides shared DeriveNextKey, | ||
| // ImportTaprootScript, and address tracking. | ||
| walletcore.BoardingBackendBase | ||
|
|
||
| neutrinoCS *neutrino.ChainService | ||
| blockCache *blockcache.BlockCache | ||
|
|
||
| chainParams *chaincfg.Params | ||
| } | ||
|
|
||
| // NewBoardingBackendAdapter creates a new boarding backend adapter | ||
| // wrapping the given btcwallet instance with neutrino for chain data. | ||
| func NewBoardingBackendAdapter(btcw *btcwallet.BtcWallet, | ||
| neutrinoCS *neutrino.ChainService, | ||
| blockCache *blockcache.BlockCache, | ||
| chainParams *chaincfg.Params, coinType uint32, | ||
| logger btclog.Logger) *BoardingBackendAdapter { | ||
|
|
||
| return &BoardingBackendAdapter{ | ||
| BoardingBackendBase: walletcore.NewBoardingBackendBase( | ||
| btcw, coinType, logger, | ||
| ), | ||
| neutrinoCS: neutrinoCS, | ||
| blockCache: blockCache, | ||
| chainParams: chainParams, | ||
| } | ||
| } | ||
|
|
||
| // ListUnspent returns all UTXOs known to btcwallet with confirmation | ||
| // counts between minConfs and maxConfs. Unlike the lwwallet adapter | ||
| // which filters by imported boarding addresses, this returns all | ||
| // watched UTXOs directly from btcwallet. This avoids UTXO loss on | ||
| // restart since btcwallet persists its watch set independently of the | ||
| // in-memory importedAddrs map. | ||
| func (b *BoardingBackendAdapter) ListUnspent(ctx context.Context, | ||
| minConfs, maxConfs int32) ([]*wallet.Utxo, error) { | ||
|
|
||
| // Treat maxConfs of 0 as "no upper bound" so callers that | ||
| // omit the parameter don't accidentally filter everything. | ||
| if maxConfs == 0 { | ||
| maxConfs = math.MaxInt32 | ||
| } | ||
|
|
||
| // Query btcwallet for all unspent outputs. btcwallet already | ||
| // scopes results to watched scripts (including those imported | ||
| // via ImportTaprootScript), so no additional address filtering | ||
| // is needed. | ||
| results, err := b.BtcWallet.InternalWallet().ListUnspent( | ||
| minConfs, maxConfs, "", | ||
| ) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("list unspent: %w", err) | ||
| } | ||
|
|
||
| var utxos []*wallet.Utxo | ||
|
|
||
| for _, r := range results { | ||
| addr, err := btcutil.DecodeAddress( | ||
| r.Address, b.chainParams, | ||
| ) | ||
| if err != nil { | ||
| b.Log.WarnS(ctx, | ||
| "Failed to decode UTXO address", err, | ||
| slog.String("address", r.Address)) | ||
|
|
||
| continue | ||
| } | ||
|
|
||
| pkScript, err := txscript.PayToAddrScript(addr) | ||
| if err != nil { | ||
| b.Log.WarnS(ctx, | ||
| "Failed to create pkScript for address", | ||
| err, | ||
| slog.String("address", r.Address)) | ||
|
|
||
| continue | ||
| } | ||
|
|
||
| txid, err := chainhash.NewHashFromStr(r.TxID) | ||
| if err != nil { | ||
| b.Log.WarnS(ctx, | ||
| "Failed to parse UTXO txid", err, | ||
| slog.String("txid", r.TxID)) | ||
|
|
||
| continue | ||
| } | ||
|
|
||
| // Convert BTC amount to satoshis. | ||
| amount, err := btcutil.NewAmount(r.Amount) | ||
| if err != nil { | ||
| b.Log.WarnS(ctx, | ||
| "Failed to parse UTXO amount", err) | ||
|
|
||
| continue | ||
| } | ||
|
|
||
| utxos = append(utxos, &wallet.Utxo{ | ||
| Outpoint: wire.OutPoint{ | ||
| Hash: *txid, | ||
| Index: r.Vout, | ||
| }, | ||
| PkScript: pkScript, | ||
| Amount: amount, | ||
| Confirmations: int32(r.Confirmations), | ||
| }) | ||
| } | ||
|
|
||
| b.Log.DebugS(ctx, "ListUnspent called", | ||
| slog.Int("min_confs", int(minConfs)), | ||
| slog.Int("max_confs", int(maxConfs)), | ||
| slog.Int("utxo_count", len(utxos))) | ||
|
|
||
| return utxos, nil | ||
| } | ||
|
|
||
| // GetTransaction returns the full transaction and its confirmation | ||
| // block hash for the given txid. It fetches from btcwallet's | ||
| // transaction store, which is populated by neutrino's chain sync. | ||
| func (b *BoardingBackendAdapter) GetTransaction(ctx context.Context, | ||
| txid chainhash.Hash) (*wire.MsgTx, *chainhash.Hash, error) { | ||
|
|
||
| // Fetch the raw transaction from btcwallet's store. | ||
| tx, err := b.BtcWallet.FetchTx(txid) | ||
| if err != nil || tx == nil { | ||
| b.Log.DebugS(ctx, | ||
| "Transaction not in wallet store", | ||
| slog.String("txid", txid.String()), | ||
| ) | ||
|
|
||
| return nil, nil, fmt.Errorf( | ||
| "transaction %s not found in wallet", txid, | ||
| ) | ||
| } | ||
|
|
||
| // Look up the transaction details from btcwallet to get the | ||
| // block hash. | ||
| txDetails, err := b.BtcWallet.InternalWallet().GetTransaction( | ||
| txid, | ||
| ) | ||
| if err != nil { | ||
| // We have the tx but no details — return without block | ||
| // hash. | ||
| return tx, nil, nil | ||
| } | ||
|
|
||
| return tx, txDetails.BlockHash, nil | ||
| } | ||
|
|
||
| // GetBlock returns the full block for the given block hash via | ||
| // neutrino's P2P network. The block cache is used to avoid redundant | ||
| // fetches. | ||
| func (b *BoardingBackendAdapter) GetBlock(ctx context.Context, | ||
| blockHash chainhash.Hash) (*wire.MsgBlock, error) { | ||
|
|
||
| b.Log.DebugS(ctx, "Fetching block via neutrino", | ||
| slog.String("block_hash", blockHash.String())) | ||
|
|
||
| block, err := b.blockCache.GetBlock( | ||
| &blockHash, | ||
| func(hash *chainhash.Hash) (*wire.MsgBlock, error) { | ||
| blk, err := b.neutrinoCS.GetBlock(*hash) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return blk.MsgBlock(), nil | ||
| }, | ||
| ) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("get block: %w", err) | ||
| } | ||
|
|
||
| b.Log.DebugS(ctx, "Fetched block successfully", | ||
| slog.String("block_hash", blockHash.String()), | ||
| slog.Int("num_txs", len(block.Transactions))) | ||
|
|
||
| return block, nil | ||
| } | ||
|
|
||
| // Compile-time check that BoardingBackendAdapter implements | ||
| // wallet.BoardingBackend. | ||
| var _ wallet.BoardingBackend = (*BoardingBackendAdapter)(nil) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.