// Package neutrino is a compact-filter (BIP157/BIP158) light client: it syncs // and verifies the block header chain, the compact filter header chain, and // the filters themselves, then matches wallet scripts locally. // // The transport is a net.Conn, so the same client runs over TCP to a trusted // node or over the Nostr relay tunnel to a bridge. The store is an interface: // memory and append-only files are provided, and the wasm build can supply an // IndexedDB implementation without touching the sync logic. package neutrino import ( "bufio" "encoding/binary" "errors" "fmt" "io" "os" "path/filepath" "sort" "sync" "git.smesh.lol/nostrino/pkg/btcwire" "git.smesh.lol/nostrino/pkg/crypto/chainhash" ) // Anchor is the trusted starting point of a store. Headers are verified from // Height+1 onward, and the filter headers from FilterHeight+1. A genesis // anchor has FilterHeight -1 because the first filter header is not known // until it has been downloaded; a checkpoint pins both values. type Anchor struct { Height uint32 BlockHash chainhash.Hash FilterHeight int32 // -1 when no filter header is trusted yet FilterHeader chainhash.Hash } // Store is the durable state a light client needs. Headers and filter headers // are contiguous from the anchor; filters may start at a later height, because // a swap wallet usually scans only recent history. type Store interface { // Anchor returns the trusted starting point. A store with no anchor has // not been initialised. Anchor() (Anchor, bool) // SetAnchor installs the trusted starting point. It must be called before // headers or filter headers are appended. SetAnchor(anchor Anchor) error // HeaderTip returns the highest header height and its hash. With no // downloaded headers it is the anchor. HeaderTip() (uint32, chainhash.Hash, bool) // Header returns the header at height. Header(height uint32) (btcwire.BlockHeader, bool) // PutHeaders appends a contiguous run. start must be the anchor height // plus one for an empty store, otherwise the previous tip plus one. PutHeaders(start uint32, headers []btcwire.BlockHeader) error // FilterHeaderTip returns the highest filter header height and its value. // With none downloaded it is the anchor's filter header, when it has one. FilterHeaderTip() (uint32, chainhash.Hash, bool) // FilterHeader returns the filter header at height. FilterHeader(height uint32) (chainhash.Hash, bool) // FilterHash returns the hash of the compact filter at height, as // committed to by the filter header chain. FilterHash(height uint32) (chainhash.Hash, bool) // PutFilterHeaders appends a contiguous run of filter hashes and their // rolling filter headers. The two slices are parallel. PutFilterHeaders(start uint32, filterHashes, filterHeaders []chainhash.Hash) error // FilterTip returns the highest stored filter height. FilterTip() (uint32, bool) // Filter returns the compact filter at height. Filter(height uint32) ([]byte, bool) // PutFilter stores one verified filter. PutFilter(height uint32, blockHash, filterHash chainhash.Hash, filter []byte) error // Truncate removes every header, filter header and filter at or above // from. It is the reorg primitive. from must be above the anchor. Truncate(from uint32) error } // Memory is an in-memory Store. It is useful for tests and for a client that // deliberately keeps no state. type Memory struct { mu sync.RWMutex anchor Anchor hasAnchor bool headers map[uint32]btcwire.BlockHeader headerTip uint32 hasHeader bool fheaders map[uint32]chainhash.Hash fhashes map[uint32]chainhash.Hash fheaderTip uint32 hasFHeader bool filters map[uint32][]byte filterTip uint32 hasFilter bool } // NewMemory returns an empty memory store. func NewMemory() *Memory { return &Memory{ headers: make(map[uint32]btcwire.BlockHeader), fheaders: make(map[uint32]chainhash.Hash), fhashes: make(map[uint32]chainhash.Hash), filters: make(map[uint32][]byte), } } func (m *Memory) Anchor() (Anchor, bool) { m.mu.RLock() defer m.mu.RUnlock() return m.anchor, m.hasAnchor } func (m *Memory) SetAnchor(anchor Anchor) error { m.mu.Lock() defer m.mu.Unlock() return m.setAnchorLocked(anchor) } func (m *Memory) setAnchorLocked(anchor Anchor) error { if anchor.BlockHash == (chainhash.Hash{}) { return errors.New("neutrino: anchor has no block hash") } if anchor.FilterHeight < -1 { return errors.New("neutrino: anchor filter height below -1") } if anchor.FilterHeight >= 0 && anchor.FilterHeader == (chainhash.Hash{}) { return errors.New("neutrino: anchor has no filter header") } if m.hasAnchor { return errors.New("neutrino: anchor already set") } if hdr, ok := m.headers[anchor.Height]; ok && hdr.BlockHash() != anchor.BlockHash { return errors.New("neutrino: anchor block hash does not match the stored header") } if anchor.FilterHeight >= 0 { if fh, ok := m.fheaders[uint32(anchor.FilterHeight)]; ok && fh != anchor.FilterHeader { return errors.New("neutrino: anchor filter header does not match the stored filter header") } } m.anchor = anchor m.hasAnchor = true if anchor.FilterHeight >= 0 { m.fheaders[uint32(anchor.FilterHeight)] = anchor.FilterHeader if !m.hasFHeader || uint32(anchor.FilterHeight) > m.fheaderTip { m.fheaderTip = uint32(anchor.FilterHeight) m.hasFHeader = true } } return nil } func (m *Memory) HeaderTip() (uint32, chainhash.Hash, bool) { m.mu.RLock() defer m.mu.RUnlock() if m.hasHeader { for h := m.headerTip; ; h-- { if hdr, ok := m.headers[h]; ok { return h, hdr.BlockHash(), true } if h == 0 { break } } } if m.hasAnchor { return m.anchor.Height, m.anchor.BlockHash, true } return 0, chainhash.Hash{}, false } func (m *Memory) Header(height uint32) (btcwire.BlockHeader, bool) { m.mu.RLock() defer m.mu.RUnlock() h, ok := m.headers[height] return h, ok } func (m *Memory) PutHeaders(start uint32, headers []btcwire.BlockHeader) error { m.mu.Lock() defer m.mu.Unlock() if len(headers) == 0 { return nil } if m.hasHeader { _, tipHash, _ := m.headerTipLocked() if start != m.headerTip+1 { return fmt.Errorf("neutrino: header batch starts at %d, want %d", start, m.headerTip+1) } if headers[0].PrevBlock != tipHash { return fmt.Errorf("neutrino: header %d does not link to the tip", start) } } else if m.hasAnchor { switch { case start == m.anchor.Height+1: if headers[0].PrevBlock != m.anchor.BlockHash { return fmt.Errorf("neutrino: header %d does not link to the anchor", start) } case start == 0 && m.anchor.Height == 0: // Legacy stores keep the genesis header itself. if headers[0].BlockHash() != m.anchor.BlockHash { return fmt.Errorf("neutrino: genesis header does not match the anchor") } if headers[0].PrevBlock != (chainhash.Hash{}) { return fmt.Errorf("neutrino: genesis header has a previous block") } default: return fmt.Errorf("neutrino: first header batch starts at %d, want %d", start, m.anchor.Height+1) } } else if start != 0 { return fmt.Errorf("neutrino: first header batch starts at %d, want 0", start) } prev := headers[0].PrevBlock for i, h := range headers { if i > 0 && h.PrevBlock != prev { return fmt.Errorf("neutrino: header %d does not link to header %d", start+uint32(i), start+uint32(i)-1) } m.headers[start+uint32(i)] = h prev = h.BlockHash() } m.headerTip = start + uint32(len(headers)) - 1 m.hasHeader = true return nil } func (m *Memory) headerTipLocked() (uint32, chainhash.Hash, bool) { if !m.hasHeader { if m.hasAnchor { return m.anchor.Height, m.anchor.BlockHash, true } return 0, chainhash.Hash{}, false } h, ok := m.headers[m.headerTip] return m.headerTip, h.BlockHash(), ok } func (m *Memory) FilterHeaderTip() (uint32, chainhash.Hash, bool) { m.mu.RLock() defer m.mu.RUnlock() if m.hasFHeader { return m.fheaderTip, m.fheaders[m.fheaderTip], true } if m.hasAnchor && m.anchor.FilterHeight >= 0 { return uint32(m.anchor.FilterHeight), m.anchor.FilterHeader, true } return 0, chainhash.Hash{}, false } func (m *Memory) FilterHeader(height uint32) (chainhash.Hash, bool) { m.mu.RLock() defer m.mu.RUnlock() h, ok := m.fheaders[height] return h, ok } func (m *Memory) FilterHash(height uint32) (chainhash.Hash, bool) { m.mu.RLock() defer m.mu.RUnlock() h, ok := m.fhashes[height] return h, ok } func (m *Memory) PutFilterHeaders(start uint32, filterHashes, headers []chainhash.Hash) error { m.mu.Lock() defer m.mu.Unlock() if len(headers) != len(filterHashes) { return errors.New("neutrino: filter hash and filter header counts differ") } if len(headers) == 0 { return nil } prev := chainhash.Hash{} if m.hasFHeader { if start != m.fheaderTip+1 { return fmt.Errorf("neutrino: filter header batch starts at %d, want %d", start, m.fheaderTip+1) } prev = m.fheaders[m.fheaderTip] } else if m.hasAnchor && m.anchor.FilterHeight >= 0 { if start != uint32(m.anchor.FilterHeight)+1 { return fmt.Errorf("neutrino: first filter header batch starts at %d, want %d", start, uint32(m.anchor.FilterHeight)+1) } prev = m.anchor.FilterHeader } else if start != 0 { return fmt.Errorf("neutrino: first filter header batch starts at %d, want 0", start) } for i, h := range headers { want := btcwire.FilterHeader(filterHashes[i], prev) if h != want { return fmt.Errorf("neutrino: filter header %d does not commit to its filter hash", start+uint32(i)) } m.fhashes[start+uint32(i)] = filterHashes[i] m.fheaders[start+uint32(i)] = h prev = h } m.fheaderTip = start + uint32(len(headers)) - 1 m.hasFHeader = true return nil } func (m *Memory) FilterTip() (uint32, bool) { m.mu.RLock() defer m.mu.RUnlock() return m.filterTip, m.hasFilter } func (m *Memory) Filter(height uint32) ([]byte, bool) { m.mu.RLock() defer m.mu.RUnlock() f, ok := m.filters[height] return f, ok } func (m *Memory) PutFilter(height uint32, blockHash, filterHash chainhash.Hash, filter []byte) error { m.mu.Lock() defer m.mu.Unlock() hdr, ok := m.headers[height] if !ok { if m.hasAnchor && height == m.anchor.Height+1 { // The first filter after a checkpoint still needs its header; // callers store headers before filters. return fmt.Errorf("neutrino: filter %d has no header", height) } return fmt.Errorf("neutrino: filter %d has no header", height) } if hdr.BlockHash() != blockHash { return fmt.Errorf("neutrino: filter %d block hash mismatch", height) } if chainhash.DoubleHashH(filter) != filterHash { return fmt.Errorf("neutrino: filter %d hash mismatch", height) } if _, exists := m.filters[height]; exists { return fmt.Errorf("neutrino: filter %d already stored", height) } cp := make([]byte, len(filter)) copy(cp, filter) m.filters[height] = cp if !m.hasFilter || height > m.filterTip { m.filterTip = height m.hasFilter = true } return nil } // HeaderHeights lists the stored header heights in ascending order. func (m *Memory) HeaderHeights() []uint32 { m.mu.RLock() defer m.mu.RUnlock() out := make([]uint32, 0, len(m.headers)) for h := range m.headers { out = append(out, h) } sort.Slice(out, func(i, j int) bool { return out[i] < out[j] }) return out } // FilterHeaderHeights lists the stored filter header heights. func (m *Memory) FilterHeaderHeights() []uint32 { m.mu.RLock() defer m.mu.RUnlock() out := make([]uint32, 0, len(m.fheaders)) for h := range m.fheaders { out = append(out, h) } sort.Slice(out, func(i, j int) bool { return out[i] < out[j] }) return out } // FilterHeights lists the stored filter heights. func (m *Memory) FilterHeights() []uint32 { m.mu.RLock() defer m.mu.RUnlock() out := make([]uint32, 0, len(m.filters)) for h := range m.filters { out = append(out, h) } sort.Slice(out, func(i, j int) bool { return out[i] < out[j] }) return out } // Truncate removes headers, filter headers and filters at or above from. func (m *Memory) Truncate(from uint32) error { m.mu.Lock() defer m.mu.Unlock() if m.hasAnchor && from <= m.anchor.Height { return fmt.Errorf("neutrino: truncate at %d is at or below the anchor %d", from, m.anchor.Height) } for h := range m.headers { if h >= from { delete(m.headers, h) } } m.recomputeHeaderTipLocked() for h := range m.fheaders { if h >= from { delete(m.fheaders, h) delete(m.fhashes, h) } } m.recomputeFilterHeaderTipLocked() for h := range m.filters { if h >= from { delete(m.filters, h) } } m.recomputeFilterTipLocked() return nil } func (m *Memory) recomputeHeaderTipLocked() { m.hasHeader = false for h := range m.headers { if !m.hasHeader || h > m.headerTip { m.headerTip = h m.hasHeader = true } } } func (m *Memory) recomputeFilterHeaderTipLocked() { m.hasFHeader = false for h := range m.fheaders { if !m.hasFHeader || h > m.fheaderTip { m.fheaderTip = h m.hasFHeader = true } } } func (m *Memory) recomputeFilterTipLocked() { m.hasFilter = false for h := range m.filters { if !m.hasFilter || h > m.filterTip { m.filterTip = h m.hasFilter = true } } } // FileStore persists the same state as Memory in three append-only files. // Each record carries its height, so a partial tail from a crash is detected // and truncated on open rather than silently misread. type FileStore struct { *Memory dir string mu sync.Mutex headers *os.File fheaders *os.File filters *os.File } const ( headerRecordSize = 4 + btcwire.BlockHeaderSize fheaderRecordSize = 4 + 2*chainhash.HashSize filterRecordPrefix = 4 + 2*chainhash.HashSize + 4 anchorRecordSize = 4 + 1 + 4 + chainhash.HashSize + 4 + chainhash.HashSize anchorMagic = "NSTA" anchorVersion = 1 ) // OpenFileStore opens or creates a store in dir. func OpenFileStore(dir string) (*FileStore, error) { if err := os.MkdirAll(dir, 0o700); err != nil { return nil, fmt.Errorf("neutrino: store dir: %w", err) } s := &FileStore{Memory: NewMemory(), dir: dir} var err error if s.headers, err = os.OpenFile(filepath.Join(dir, "headers.bin"), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o600); err != nil { return nil, err } if s.fheaders, err = os.OpenFile(filepath.Join(dir, "cfheaders.bin"), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o600); err != nil { return nil, err } if s.filters, err = os.OpenFile(filepath.Join(dir, "filters.bin"), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o600); err != nil { return nil, err } if err := s.loadAnchor(); err != nil { return nil, err } if err := s.loadHeaders(); err != nil { return nil, err } if err := s.loadFilterHeaders(); err != nil { return nil, err } if err := s.loadFilters(); err != nil { return nil, err } // An older store has no anchor file. Its headers start at genesis, so the // anchor is the genesis header and the filter chain starts from zero. if _, ok := s.Memory.Anchor(); !ok { if hdr, ok := s.Memory.Header(0); ok { if err := s.SetAnchor(Anchor{Height: 0, BlockHash: hdr.BlockHash(), FilterHeight: -1}); err != nil { return nil, err } } } return s, nil } func (s *FileStore) anchorPath() string { return filepath.Join(s.dir, "anchor.bin") } func (s *FileStore) loadAnchor() error { body, err := os.ReadFile(s.anchorPath()) if err != nil { if os.IsNotExist(err) { return nil } return fmt.Errorf("neutrino: read anchor: %w", err) } if len(body) != anchorRecordSize { return fmt.Errorf("neutrino: anchor file has %d bytes, want %d", len(body), anchorRecordSize) } if string(body[:4]) != anchorMagic { return errors.New("neutrino: anchor file has a bad magic") } if body[4] != anchorVersion { return fmt.Errorf("neutrino: anchor file version %d", body[4]) } var a Anchor a.Height = binary.LittleEndian.Uint32(body[5:9]) copy(a.BlockHash[:], body[9:9+chainhash.HashSize]) off := 9 + chainhash.HashSize a.FilterHeight = int32(binary.LittleEndian.Uint32(body[off : off+4])) off += 4 copy(a.FilterHeader[:], body[off:off+chainhash.HashSize]) if err := s.Memory.SetAnchor(a); err != nil { return fmt.Errorf("neutrino: load anchor: %w", err) } return nil } // SetAnchor installs and persists the trusted starting point. func (s *FileStore) SetAnchor(a Anchor) error { if err := s.Memory.SetAnchor(a); err != nil { return err } var body [anchorRecordSize]byte copy(body[:4], anchorMagic) body[4] = anchorVersion binary.LittleEndian.PutUint32(body[5:9], a.Height) copy(body[9:9+chainhash.HashSize], a.BlockHash[:]) off := 9 + chainhash.HashSize binary.LittleEndian.PutUint32(body[off:off+4], uint32(a.FilterHeight)) off += 4 copy(body[off:off+chainhash.HashSize], a.FilterHeader[:]) s.mu.Lock() defer s.mu.Unlock() tmp := s.anchorPath() + ".tmp" if err := os.WriteFile(tmp, body[:], 0o600); err != nil { return fmt.Errorf("neutrino: write anchor: %w", err) } if err := os.Rename(tmp, s.anchorPath()); err != nil { return fmt.Errorf("neutrino: install anchor: %w", err) } return nil } func (s *FileStore) loadHeaders() error { path := s.dir + "/headers.bin" f, err := os.OpenFile(path, os.O_RDWR, 0) if err != nil { return err } defer f.Close() br := bufio.NewReaderSize(f, 1<<20) anchor, hasAnchor := s.Memory.Anchor() expect := uint32(0) if hasAnchor && anchor.Height > 0 { expect = anchor.Height + 1 } first := true valid := int64(0) for { var rec [headerRecordSize]byte if _, err := io.ReadFull(br, rec[:]); err != nil { if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { return truncateTo(f, path, valid) } return err } height := binary.LittleEndian.Uint32(rec[:4]) if first { first = false switch { case !hasAnchor: if height != 0 { return fmt.Errorf("neutrino: header file starts at %d, want 0", height) } expect = 0 case anchor.Height == 0 && height == 0: expect = 0 // legacy store keeps the genesis header case height == anchor.Height+1: expect = height default: return fmt.Errorf("neutrino: header file starts at %d, want %d", height, anchor.Height+1) } } if height != expect { return fmt.Errorf("neutrino: header file has height %d, want %d", height, expect) } var h btcwire.BlockHeader if err := h.Deserialize(rec[4:]); err != nil { return err } if expect > 0 { prev, ok := s.Memory.Header(expect - 1) if !ok || prev.BlockHash() != h.PrevBlock { return fmt.Errorf("neutrino: header file breaks linkage at %d", expect) } } if err := s.Memory.PutHeaders(expect, []btcwire.BlockHeader{h}); err != nil { return err } expect++ valid += headerRecordSize } } func (s *FileStore) loadFilterHeaders() error { path := s.dir + "/cfheaders.bin" f, err := os.OpenFile(path, os.O_RDWR, 0) if err != nil { return err } defer f.Close() br := bufio.NewReaderSize(f, 1<<20) expect := uint32(0) if a, ok := s.Memory.Anchor(); ok && a.FilterHeight >= 0 { expect = uint32(a.FilterHeight) + 1 } valid := int64(0) for { var rec [fheaderRecordSize]byte if _, err := io.ReadFull(br, rec[:]); err != nil { if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { return truncateTo(f, path, valid) } return err } height := binary.LittleEndian.Uint32(rec[:4]) if height != expect { return fmt.Errorf("neutrino: filter header file has height %d, want %d", height, expect) } var fh, h chainhash.Hash copy(fh[:], rec[4:36]) copy(h[:], rec[36:68]) if err := s.Memory.PutFilterHeaders(height, []chainhash.Hash{fh}, []chainhash.Hash{h}); err != nil { return err } expect++ valid += fheaderRecordSize } } func (s *FileStore) loadFilters() error { path := s.dir + "/filters.bin" f, err := os.OpenFile(path, os.O_RDWR, 0) if err != nil { return err } defer f.Close() br := bufio.NewReaderSize(f, 1<<20) valid := int64(0) for { var prefix [filterRecordPrefix]byte if _, err := io.ReadFull(br, prefix[:]); err != nil { if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { return truncateTo(f, path, valid) } return err } height := binary.LittleEndian.Uint32(prefix[:4]) var blockHash, filterHash chainhash.Hash copy(blockHash[:], prefix[4:36]) copy(filterHash[:], prefix[36:68]) n := binary.LittleEndian.Uint32(prefix[68:72]) if n > 4<<20 { return fmt.Errorf("neutrino: filter file record %d declares %d bytes", height, n) } filter := make([]byte, n) if _, err := io.ReadFull(br, filter); err != nil { if errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) { return truncateTo(f, path, valid) } return err } if err := s.Memory.PutFilter(height, blockHash, filterHash, filter); err != nil { return err } valid += int64(filterRecordPrefix) + int64(n) } } // truncateTo removes a partial record left by a crash, so the next append // starts at a record boundary. func truncateTo(f *os.File, path string, valid int64) error { info, err := f.Stat() if err != nil { return err } if info.Size() == valid { return nil } if err := f.Truncate(valid); err != nil { return fmt.Errorf("neutrino: truncate %s: %w", path, err) } return nil } func (s *FileStore) PutHeaders(start uint32, headers []btcwire.BlockHeader) error { if err := s.Memory.PutHeaders(start, headers); err != nil { return err } s.mu.Lock() defer s.mu.Unlock() var rec [headerRecordSize]byte for i, h := range headers { binary.LittleEndian.PutUint32(rec[:4], start+uint32(i)) raw := h.Serialize() copy(rec[4:], raw[:]) if _, err := s.headers.Write(rec[:]); err != nil { return err } } return s.headers.Sync() } func (s *FileStore) PutFilterHeaders(start uint32, filterHashes, headers []chainhash.Hash) error { if err := s.Memory.PutFilterHeaders(start, filterHashes, headers); err != nil { return err } s.mu.Lock() defer s.mu.Unlock() var rec [fheaderRecordSize]byte for i, h := range headers { binary.LittleEndian.PutUint32(rec[:4], start+uint32(i)) copy(rec[4:36], filterHashes[i][:]) copy(rec[36:68], h[:]) if _, err := s.fheaders.Write(rec[:]); err != nil { return err } } return s.fheaders.Sync() } func (s *FileStore) PutFilter(height uint32, blockHash, filterHash chainhash.Hash, filter []byte) error { if err := s.Memory.PutFilter(height, blockHash, filterHash, filter); err != nil { return err } s.mu.Lock() defer s.mu.Unlock() var prefix [filterRecordPrefix]byte binary.LittleEndian.PutUint32(prefix[:4], height) copy(prefix[4:36], blockHash[:]) copy(prefix[36:68], filterHash[:]) binary.LittleEndian.PutUint32(prefix[68:72], uint32(len(filter))) if _, err := s.filters.Write(prefix[:]); err != nil { return err } if _, err := s.filters.Write(filter); err != nil { return err } return s.filters.Sync() } // Truncate removes the in-memory state and rewrites the three files. Reorgs // are rare and a rewrite is much simpler than recording a truncation point in // an append-only format. func (s *FileStore) Truncate(from uint32) error { if err := s.Memory.Truncate(from); err != nil { return err } s.mu.Lock() defer s.mu.Unlock() if err := s.rewriteHeadersLocked(); err != nil { return err } if err := s.rewriteFilterHeadersLocked(); err != nil { return err } return s.rewriteFiltersLocked() } func (s *FileStore) rewriteHeadersLocked() error { heights := s.Memory.HeaderHeights() f, err := s.rewriteFile(s.headers, "headers.bin", func(f *os.File) error { var rec [headerRecordSize]byte for _, h := range heights { hdr, ok := s.Memory.Header(h) if !ok { continue } binary.LittleEndian.PutUint32(rec[:4], h) raw := hdr.Serialize() copy(rec[4:], raw[:]) if _, err := f.Write(rec[:]); err != nil { return err } } return nil }) if err != nil { return err } s.headers = f return nil } func (s *FileStore) rewriteFilterHeadersLocked() error { anchor, hasAnchor := s.Memory.Anchor() f, err := s.rewriteFile(s.fheaders, "cfheaders.bin", func(f *os.File) error { var rec [fheaderRecordSize]byte for _, h := range s.Memory.FilterHeaderHeights() { if hasAnchor && h <= uint32(anchor.FilterHeight) { continue } fh, ok := s.Memory.FilterHash(h) if !ok { continue } hd, ok := s.Memory.FilterHeader(h) if !ok { continue } binary.LittleEndian.PutUint32(rec[:4], h) copy(rec[4:36], fh[:]) copy(rec[36:68], hd[:]) if _, err := f.Write(rec[:]); err != nil { return err } } return nil }) if err != nil { return err } s.fheaders = f return nil } func (s *FileStore) rewriteFiltersLocked() error { f, err := s.rewriteFile(s.filters, "filters.bin", func(f *os.File) error { var prefix [filterRecordPrefix]byte for _, h := range s.Memory.FilterHeights() { data, ok := s.Memory.Filter(h) if !ok { continue } hdr, ok := s.Memory.Header(h) if !ok { return fmt.Errorf("neutrino: filter %d has no header", h) } fh, ok := s.Memory.FilterHash(h) if !ok { return fmt.Errorf("neutrino: filter %d has no filter hash", h) } bh := hdr.BlockHash() binary.LittleEndian.PutUint32(prefix[:4], h) copy(prefix[4:36], bh[:]) copy(prefix[36:68], fh[:]) binary.LittleEndian.PutUint32(prefix[68:72], uint32(len(data))) if _, err := f.Write(prefix[:]); err != nil { return err } if _, err := f.Write(data); err != nil { return err } } return nil }) if err != nil { return err } s.filters = f return nil } func (s *FileStore) rewriteFile(old *os.File, name string, write func(*os.File) error) (*os.File, error) { path := filepath.Join(s.dir, name) tmp := path + ".tmp" f, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) if err != nil { return nil, err } if err := write(f); err != nil { f.Close() os.Remove(tmp) return nil, err } if err := f.Sync(); err != nil { f.Close() os.Remove(tmp) return nil, err } if err := f.Close(); err != nil { os.Remove(tmp) return nil, err } if old != nil { old.Close() } if err := os.Rename(tmp, path); err != nil { return nil, err } return os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o600) } // Close closes the open files. func (s *FileStore) Close() error { s.mu.Lock() defer s.mu.Unlock() var first error for _, f := range []*os.File{s.headers, s.fheaders, s.filters} { if f != nil { if err := f.Close(); err != nil && first == nil { first = err } } } return first }