// Package chainsource makes a light client answer on a chaindata channel. // // The wallet never touches the client: it sends a frame and this adapter drives the client. // That is what keeps the two sides independent of each other's code, and it is why this // adapter is the only place that knows both. package chainsource import ( "context" "git.smesh.lol/nostrino/pkg/chaindata" "git.smesh.lol/nostrino/pkg/neutrino" ) // Nostrino is a chaindata.Source over a BIP157 light client and its store. // // One value per chain. The client is not safe for two concurrent syncs, so the adapter // serialises them: a wallet that asked for two would be two questions about one chain. type Nostrino struct { client *neutrino.Client store neutrino.Store sync chan struct{} } // New wraps a client. func New(client *neutrino.Client) (*Nostrino, error) { if client == nil { return nil, errNoClient } if client.Store() == nil { return nil, errNoStore } return &Nostrino{client: client, store: client.Store(), sync: make(chan struct{}, 1)}, nil } // errNoClient is returned for a nil client. var errNoClient = errorString("chainsource: no light client") // errNoStore is returned for a client with no store. var errNoStore = errorString("chainsource: the light client has no store") // errorString is a plain error, kept here so the package needs no other dependency. type errorString string func (e errorString) Error() string { return string(e) } // Position reports the store's verified tips. // // It reads the store rather than the peer: a tip the client has not verified is not a // position this wallet may act on. func (n *Nostrino) Position(chaindata.Context) (chaindata.Position, error) { position := chaindata.Position{} if tip, _, ok := n.store.HeaderTip(); ok { position.Tip = tip } if filters, ok := n.store.FilterTip(); ok { position.Filters = filters } return position, nil } // Sync advances the chain from a height. // // from is a lower bound, not a demand: the client resumes from its own verified tip, because // re-downloading verified filters is work with no answer in it. func (n *Nostrino) Sync(ctx chaindata.Context, from uint32) (chaindata.Position, error) { release, ok := n.lock(ctx) if !ok { return chaindata.Position{}, errorString("chainsource: cancelled before the sync began") } defer release() runCtx, stop := bridge(ctx) defer stop() if _, err := n.client.SyncFrom(runCtx, from, 0); err != nil { return chaindata.Position{}, err } return n.Position(ctx) } // Match returns the heights whose stored filters mention any of the scripts. // // The scan is the client's own local matching: the peer never learns the script set. func (n *Nostrino) Match(ctx chaindata.Context, scripts [][]byte) ([]uint32, error) { if len(scripts) == 0 { return nil, errorString("chainsource: a match needs at least one script") } release, ok := n.lock(ctx) if !ok { return nil, errorString("chainsource: cancelled before the match began") } defer release() return n.client.MatchScripts(0, 0, scripts) } // Filters returns a contiguous run of stored filters. // // The range is served from the client's own verified store, and the whole run is one slice // of filters: the caller gets a batch, not a round trip per block. A height whose filter is // not stored is a gap, and a gap is an error rather than a shorter answer, because a wallet // that silently skipped a block would miss its own money. // // Block returns the raw block at height. func (n *Nostrino) Block(ctx chaindata.Context, height uint32) ([]byte, error) { release, ok := n.lock(ctx) if !ok { return nil, errorString("chainsource: cancelled before the block fetch began") } defer release() requestCtx, done := bridge(ctx) defer done() raw, err := n.client.Block(requestCtx, height) if err != nil { return nil, errorString("chainsource: " + err.Error()) } return raw, nil } // Broadcast sends one raw transaction to the peer this client follows. // // It is the write half of the channel: a wallet that builds and signs locally hands the bytes // here, and the light client's tunnel carries them to the node. The peer does not acknowledge a // tx message, so a nil error means the frame reached the peer. func (n *Nostrino) Broadcast(ctx chaindata.Context, raw []byte) error { release, ok := n.lock(ctx) if !ok { return errorString("chainsource: cancelled before the broadcast began") } defer release() requestCtx, done := bridge(ctx) defer done() if err := n.client.Broadcast(requestCtx, raw); err != nil { return errorString("chainsource: " + err.Error()) } return nil } func (n *Nostrino) Filters( ctx chaindata.Context, from, count uint32, ) ([]chaindata.Filter, error) { if count == 0 || count > chaindata.MaxFilters { return nil, errorString("chainsource: a filter read is 1 to 2000 filters") } release, ok := n.lock(ctx) if !ok { return nil, errorString("chainsource: cancelled before the read began") } defer release() out := make([]chaindata.Filter, 0, count) for offset := uint32(0); offset < count; offset++ { height := from + offset body, ok := n.store.Filter(height) if !ok { return nil, errorString("chainsource: no filter at height " + itoa(height)) } filter := chaindata.Filter{Height: height, Filter: body} if header, ok := n.store.Header(height); ok { hash := header.BlockHash() filter.BlockHash = hash[:] } out = append(out, filter) } return out, nil } // lock takes the one sync slot, or gives up when the request is cancelled. // // The cancellation is checked before the slot is offered, not raced with it: when both cases // of a select are ready Go picks one at random, so a cancelled request that also found the // slot free would start its work half the time. A cancelled request must never begin. func (n *Nostrino) lock(ctx chaindata.Context) (func(), bool) { select { case <-ctx.Done(): return nil, false default: } select { case n.sync <- struct{}{}: return func() { <-n.sync }, true case <-ctx.Done(): return nil, false } } // bridge turns a chaindata cancellation into a context cancellation. // // It is one goroutine per request, which is the price of an interface that carries a channel // instead of a context.Context: the client's loops select on the derived context, so the sync // stops at its next read rather than at its next check of a flag. func bridge(ctx chaindata.Context) (context.Context, func()) { runCtx, cancel := context.WithCancel(context.Background()) stopped := make(chan struct{}) go func() { defer close(stopped) select { case <-ctx.Done(): cancel() case <-runCtx.Done(): } }() return runCtx, func() { cancel() <-stopped } } // itoa renders a height without pulling in formatting. func itoa(value uint32) string { if value == 0 { return "0" } var digits []byte for value > 0 { digits = append([]byte{byte('0' + value%10)}, digits...) value /= 10 } return string(digits) }