mirror of
https://github.com/filecoin-project/lotus.git
synced 2025-08-24 01:08:42 +08:00

Assert that EC finalized tipset height is never negative in v2 APIs Fix an edge case where if the head epoch is below the chain finality policy (defaulting to 900) EC finalized head height would become negative. Fixes: #13167
159 lines
5.1 KiB
Go
159 lines
5.1 KiB
Go
package full
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"go.uber.org/fx"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/filecoin-project/go-f3"
|
|
"github.com/filecoin-project/go-state-types/abi"
|
|
|
|
"github.com/filecoin-project/lotus/api"
|
|
"github.com/filecoin-project/lotus/build/buildconstants"
|
|
"github.com/filecoin-project/lotus/chain/actors/policy"
|
|
"github.com/filecoin-project/lotus/chain/lf3"
|
|
"github.com/filecoin-project/lotus/chain/store"
|
|
"github.com/filecoin-project/lotus/chain/types"
|
|
)
|
|
|
|
var _ ChainModuleAPIv2 = (*ChainModuleV2)(nil)
|
|
|
|
type ChainModuleAPIv2 interface {
|
|
ChainGetTipSet(context.Context, types.TipSetSelector) (*types.TipSet, error)
|
|
}
|
|
|
|
type ChainModuleV2 struct {
|
|
Chain *store.ChainStore
|
|
F3 lf3.F3Backend `optional:"true"`
|
|
|
|
fx.In
|
|
}
|
|
|
|
func (cm *ChainModuleV2) ChainGetTipSet(ctx context.Context, selector types.TipSetSelector) (*types.TipSet, error) {
|
|
if err := selector.Validate(); err != nil {
|
|
return nil, xerrors.Errorf("validating selector: %w", err)
|
|
}
|
|
|
|
// Get tipset by key.
|
|
if selector.Key != nil {
|
|
return cm.Chain.GetTipSetFromKey(ctx, *selector.Key)
|
|
}
|
|
|
|
// Get tipset by height.
|
|
if selector.Height != nil {
|
|
anchor, err := cm.getTipSetByAnchor(ctx, selector.Height.Anchor)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("getting anchor from tipset: %w", err)
|
|
}
|
|
return cm.Chain.GetTipsetByHeight(ctx, *selector.Height.At, anchor, selector.Height.Previous)
|
|
}
|
|
|
|
// Get tipset by tag, either latest or finalized.
|
|
if selector.Tag != nil {
|
|
return cm.getTipSetByTag(ctx, *selector.Tag)
|
|
}
|
|
|
|
return nil, xerrors.Errorf("no tipset found for selector")
|
|
}
|
|
|
|
func (cm *ChainModuleV2) getTipSetByTag(ctx context.Context, tag types.TipSetTag) (*types.TipSet, error) {
|
|
switch tag {
|
|
case types.TipSetTags.Latest:
|
|
return cm.Chain.GetHeaviestTipSet(), nil
|
|
case types.TipSetTags.Finalized:
|
|
return cm.getLatestFinalizedTipset(ctx)
|
|
case types.TipSetTags.Safe:
|
|
return cm.getLatestSafeTipSet(ctx)
|
|
default:
|
|
return nil, xerrors.Errorf("unknown tipset tag: %s", tag)
|
|
}
|
|
}
|
|
|
|
func (cm *ChainModuleV2) getLatestSafeTipSet(ctx context.Context) (*types.TipSet, error) {
|
|
finalized, err := cm.getLatestFinalizedTipset(ctx)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("getting latest finalized tipset: %w", err)
|
|
}
|
|
heaviest := cm.Chain.GetHeaviestTipSet()
|
|
if heaviest == nil {
|
|
return nil, xerrors.Errorf("no known heaviest tipset")
|
|
}
|
|
safeHeight := max(0, heaviest.Height()-buildconstants.SafeHeightDistance)
|
|
if finalized != nil && finalized.Height() >= safeHeight {
|
|
return finalized, nil
|
|
}
|
|
return cm.Chain.GetTipsetByHeight(ctx, safeHeight, heaviest, true)
|
|
}
|
|
|
|
func (cm *ChainModuleV2) getLatestFinalizedTipset(ctx context.Context) (*types.TipSet, error) {
|
|
if cm.F3 == nil {
|
|
// F3 is disabled; fall back to EC finality.
|
|
return cm.getECFinalized(ctx)
|
|
}
|
|
|
|
cert, err := cm.F3.GetLatestCert(ctx)
|
|
if err != nil {
|
|
if errors.Is(err, f3.ErrF3NotRunning) || errors.Is(err, api.ErrF3NotReady) {
|
|
// Only fall back to EC finality if F3 isn't running or not ready.
|
|
log.Debugw("F3 not running or not ready, falling back to EC finality", "err", err)
|
|
return cm.getECFinalized(ctx)
|
|
}
|
|
return nil, err
|
|
}
|
|
if cert == nil {
|
|
// No latest certificate. Fall back to EC finality.
|
|
return cm.getECFinalized(ctx)
|
|
}
|
|
|
|
// Extract the finalized tipeset from the certificate.
|
|
latestF3FinalizedTipSet := cert.ECChain.Head()
|
|
|
|
// Fall back to EC finality if the latest F3 finalized tipset is older than EC finality.
|
|
latestF3FinalizedEpoch := abi.ChainEpoch(latestF3FinalizedTipSet.Epoch)
|
|
head := cm.Chain.GetHeaviestTipSet()
|
|
if head == nil {
|
|
return nil, xerrors.New("no known heaviest tipset")
|
|
}
|
|
if head.Height()-latestF3FinalizedEpoch > policy.ChainFinality {
|
|
log.Debugw("Latest F3 finalized tipset is older than EC finality, falling back to EC finality", "headEpoch", head.Height(), "latestF3FinalizedEpoch", latestF3FinalizedEpoch)
|
|
return cm.getECFinalized(ctx)
|
|
}
|
|
|
|
// All good, load the latest F3 finalized tipset.
|
|
tsk, err := types.TipSetKeyFromBytes(latestF3FinalizedTipSet.Key)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("decoding latest f3 cert tipset key: %w", err)
|
|
}
|
|
ts, err := cm.Chain.LoadTipSet(ctx, tsk)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("loading latest f3 cert tipset %s: %w", tsk, err)
|
|
}
|
|
return ts, nil
|
|
}
|
|
|
|
func (cm *ChainModuleV2) getTipSetByAnchor(ctx context.Context, anchor *types.TipSetAnchor) (*types.TipSet, error) {
|
|
switch {
|
|
case anchor == nil:
|
|
// No anchor specified. Fall back to finalized tipset.
|
|
return cm.getTipSetByTag(ctx, types.TipSetTags.Finalized)
|
|
case anchor.Key == nil && anchor.Tag == nil:
|
|
// Anchor is zero-valued. Fall back to heaviest tipset.
|
|
return cm.Chain.GetHeaviestTipSet(), nil
|
|
case anchor.Key != nil:
|
|
// Get tipset at the specified key.
|
|
return cm.Chain.GetTipSetFromKey(ctx, *anchor.Key)
|
|
case anchor.Tag != nil:
|
|
return cm.getTipSetByTag(ctx, *anchor.Tag)
|
|
default:
|
|
return nil, xerrors.Errorf("invalid anchor: %v", anchor)
|
|
}
|
|
}
|
|
|
|
func (cm *ChainModuleV2) getECFinalized(ctx context.Context) (*types.TipSet, error) {
|
|
head := cm.Chain.GetHeaviestTipSet()
|
|
finalizedHeight := max(0, head.Height()-policy.ChainFinality)
|
|
return cm.Chain.GetTipsetByHeight(ctx, finalizedHeight, head, true)
|
|
}
|