package store import ( "context" "os" "strconv" lru "github.com/hashicorp/golang-lru" "golang.org/x/xerrors" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/lotus/chain/types" ) var DefaultChainIndexCacheSize = 32 << 10 func init() { if s := os.Getenv("LOTUS_CHAIN_INDEX_CACHE"); s != "" { lcic, err := strconv.Atoi(s) if err != nil { log.Errorf("failed to parse 'LOTUS_CHAIN_INDEX_CACHE' env var: %s", err) } DefaultChainIndexCacheSize = lcic } } type ChainIndex struct { skipCache *lru.ARCCache loadTipSet loadTipSetFunc skipLength abi.ChainEpoch } type loadTipSetFunc func(context.Context, types.TipSetKey) (*types.TipSet, error) func NewChainIndex(lts loadTipSetFunc) *ChainIndex { sc, _ := lru.NewARC(DefaultChainIndexCacheSize) return &ChainIndex{ skipCache: sc, loadTipSet: lts, skipLength: 20, } } type lbEntry struct { ts *types.TipSet parentHeight abi.ChainEpoch targetHeight abi.ChainEpoch target types.TipSetKey } func (ci *ChainIndex) GetTipsetByHeight(ctx context.Context, from *types.TipSet, to abi.ChainEpoch) (*types.TipSet, error) { if from.Height()-to <= ci.skipLength { return ci.walkBack(ctx, from, to) } rounded, err := ci.roundDown(ctx, from) if err != nil { return nil, err } cur := rounded.Key() for { cval, ok := ci.skipCache.Get(cur) if !ok { fc, err := ci.fillCache(ctx, cur) if err != nil { return nil, err } cval = fc } lbe := cval.(*lbEntry) if lbe.ts.Height() == to || lbe.parentHeight < to { return lbe.ts, nil } else if to > lbe.targetHeight { return ci.walkBack(ctx, lbe.ts, to) } cur = lbe.target } } func (ci *ChainIndex) GetTipsetByHeightWithoutCache(ctx context.Context, from *types.TipSet, to abi.ChainEpoch) (*types.TipSet, error) { return ci.walkBack(ctx, from, to) } func (ci *ChainIndex) fillCache(ctx context.Context, tsk types.TipSetKey) (*lbEntry, error) { ts, err := ci.loadTipSet(ctx, tsk) if err != nil { return nil, err } if ts.Height() == 0 { return &lbEntry{ ts: ts, parentHeight: 0, }, nil } // will either be equal to ts.Height, or at least > ts.Parent.Height() rheight := ci.roundHeight(ts.Height()) parent, err := ci.loadTipSet(ctx, ts.Parents()) if err != nil { return nil, err } rheight -= ci.skipLength if rheight < 0 { rheight = 0 } var skipTarget *types.TipSet if parent.Height() < rheight { skipTarget = parent } else { skipTarget, err = ci.walkBack(ctx, parent, rheight) if err != nil { return nil, xerrors.Errorf("fillCache walkback: %w", err) } } lbe := &lbEntry{ ts: ts, parentHeight: parent.Height(), targetHeight: skipTarget.Height(), target: skipTarget.Key(), } ci.skipCache.Add(tsk, lbe) return lbe, nil } // floors to nearest skipLength multiple func (ci *ChainIndex) roundHeight(h abi.ChainEpoch) abi.ChainEpoch { return (h / ci.skipLength) * ci.skipLength } func (ci *ChainIndex) roundDown(ctx context.Context, ts *types.TipSet) (*types.TipSet, error) { target := ci.roundHeight(ts.Height()) rounded, err := ci.walkBack(ctx, ts, target) if err != nil { return nil, err } return rounded, nil } func (ci *ChainIndex) walkBack(ctx context.Context, from *types.TipSet, to abi.ChainEpoch) (*types.TipSet, error) { if to > from.Height() { return nil, xerrors.Errorf("looking for tipset with height greater than start point") } if to == from.Height() { return from, nil } ts := from for { pts, err := ci.loadTipSet(ctx, ts.Parents()) if err != nil { return nil, err } if to > pts.Height() { // in case pts is lower than the epoch we're looking for (null blocks) // return a tipset above that height return ts, nil } if to == pts.Height() { return pts, nil } ts = pts } }