Litecoin MWEB Light Client implementing LIP-0006 syncing
MWEB sync requires:
- Block headers (for sampling and verification)
- P2P connections to Litecoin nodes (to download MWEB data)
Different Litecoin backends have headers but may not have P2P support.
MwebSync handles MWEB P2P internally using neutrino/query. Backends only need to provide:
- Access to block headers
- Access to P2P peers (MwebSync does the actual MWEB queries)
- Backend-Agnostic: Works with any block header source (neutrino, Electrum, full node RPC, etc.)
- Minimal Interface Surface: Only 3 core interfaces to implement
go get github.com/ltcsuite/mwebsyncpackage main
import (
"context"
"log"
"github.com/ltcsuite/ltcd/chaincfg"
"github.com/ltcsuite/mwebsync"
)
func main() {
// Implement the required interfaces for your backend
headerProvider := NewYourHeaderProvider() // Provides block headers
peerQuerier := NewYourPeerQuerier() // Handles P2P queries
syncNotifier := NewYourSyncNotifier() // Signals when headers are synced
coinDB := NewYourCoinDatabase() // Stores MWEB UTXOs
// Create the syncer
syncer, err := mwebsync.New(&mwebsync.Config{
HeaderProvider: headerProvider,
PeerQuerier: peerQuerier,
SyncNotifier: syncNotifier,
CoinDatabase: coinDB,
ChainParams: &chaincfg.MainNetParams,
OnUtxosAdded: func(leafset *mweb.Leafset, utxos []*wire.MwebNetUtxo) {
log.Printf("Received %d new MWEB UTXOs", len(utxos))
// Process UTXOs for your wallet
},
})
if err != nil {
log.Fatal(err)
}
// Start syncing
if err := syncer.Start(); err != nil {
log.Fatal(err)
}
// Wait for sync to complete
<-context.Background().Done()
// Gracefully stop
syncer.Stop()
}MwebSync requires 3 simple interfaces from external backends:
Supplies block headers (however your backend gets them).
type BlockHeaderProvider interface {
ChainTip() (*wire.BlockHeader, uint32, error)
FetchHeaderByHeight(height uint32) (*wire.BlockHeader, error)
}Provides access to connected Litecoin P2P peers. MwebSync handles all MWEB queries internally using neutrino/query.
type PeerProvider interface {
// Gives access to connected peers
ConnectedPeers() (<-chan query.Peer, func(), error)
// Bans misbehaving peers
BanPeer(peerAddr string, reason banman.Reason) error
// Queries all peers (for tip verification)
QueryAllPeers(queryMsg wire.Message,
checkResponse func(sp query.Peer, resp wire.Message,
quit chan<- struct{}, peerQuit chan<- struct{}))
}Different backends:
- Neutrino: Wraps ChainService to reuse existing peer connections
- Electrum: Creates P2P connection manager (Electrum servers don't support MWEB)
Signals when block headers are synced. MWEB sync waits for block headers to be ready before starting.
type SyncStateNotifier interface {
WaitForHeadersSync(ctx context.Context) error
GetHeaderTip() uint32
}Stores MWEB coins and related data. This interface is identical to github.com/ltcsuite/neutrino/mwebdb.CoinDatabase and can use the same implementation.
type CoinDatabase interface {
GetRollbackHeight() (uint32, error)
PutRollbackHeight(uint32) error
ClearRollbackHeight(uint32) error
GetLeavesAtHeight() (map[uint32]uint64, error)
PutLeavesAtHeight(map[uint32]uint64) error
RollbackLeavesAtHeight(uint32) error
GetLeafset() (*mweb.Leafset, error)
PutLeafsetAndPurge(*mweb.Leafset, []uint64) error
PutCoins([]*wire.MwebNetUtxo) error
FetchCoin(*chainhash.Hash) (*wire.MwebOutput, error)
FetchLeaves([]uint64) ([]*wire.MwebNetUtxo, error)
PurgeCoins() error
}The sync process follows these phases:
- Wait Phase: Wait for block headers to sync
- Rollback Phase: Handle chain reorganizations
- Header Sampling: Build height→leaf-count index (stratified sampling)
- Tip Verification: Get and verify current MWEB state (header + leafset)
- UTXO Differential Sync: Download only changed UTXOs
- Finalization: Update database and notify listeners
- Idle Phase: Wait for new blocks
MwebSync uses a two-pass sampling strategy to build an efficient height→leaf-count index:
- Coarse pass: Every 100th block from MWEB activation to tip
- Fine pass: Every block in the last 4000 blocks
This index is used for "UTXO dating" - estimating when a UTXO was created based on its leaf index.
Instead of downloading all UTXOs on every sync, MwebSync compares the old and new leafsets (bitmaps) to identify:
- Added leaves: New UTXOs to download
- Removed leaves: Spent UTXOs to purge
This minimizes bandwidth and processing requirements.
Neutrino can wrap its existing components:
type NeutrinoPeerProvider struct {
chainService *neutrino.ChainService
}
func (n *NeutrinoPeerProvider) ConnectedPeers() (<-chan query.Peer, func(), error) {
// Return channel from chainService.Peers()
}
func (n *NeutrinoPeerProvider) BanPeer(addr string, reason banman.Reason) error {
// Use chainService's BanPeer
}
func (n *NeutrinoPeerProvider) QueryAllPeers(...) {
// Use chainService's queryAllPeers function
}Electrum needs to create a P2P connection manager since Electrum servers don't support MWEB:
type ElectrumPeerProvider struct {
peerManager *ltcp2p.PeerManager // Simple P2P manager
}
func (e *ElectrumPeerProvider) ConnectedPeers() (<-chan query.Peer, func(), error) {
// Return peers from P2P manager
}- Header Verification:
mweb.VerifyHeader()ensures MWEB headers are valid - Leafset Verification:
mweb.VerifyLeafset()ensures leafsets match headers - UTXO Verification:
mweb.VerifyUtxos()ensures UTXOs are valid against the MMR root - Peer Banning: Malicious peers are banned automatically
No, you can reuse the existing implementation from github.com/ltcsuite/neutrino/mwebdb.
MwebSync detects rollbacks automatically and rewinds MWEB state accordingly:
- Rollbacks > 10 blocks: Purge all MWEB data and resync
- Rollbacks ≤ 10 blocks: Roll back to the rollback height
MwebSync focuses on confirmed transactions. Mempool handling is left to the wallet implementation via the OnUtxosAdded callback.