From 69eb2163d26a9ce0a0294c5a38e1aad0586b41fe Mon Sep 17 00:00:00 2001 From: Lucas Molas Date: Mon, 18 Jan 2021 12:34:25 -0300 Subject: [PATCH] fix(sync): enforce fork len when changing head (#5244) --- chain/store/store.go | 72 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/chain/store/store.go b/chain/store/store.go index 2ca09dfe9..28c427b2b 100644 --- a/chain/store/store.go +++ b/chain/store/store.go @@ -363,7 +363,7 @@ func (cs *ChainStore) PutTipSet(ctx context.Context, ts *types.TipSet) error { // MaybeTakeHeavierTipSet evaluates the incoming tipset and locks it in our // internal state as our new head, if and only if it is heavier than the current -// head. +// head and does not exceed the maximum fork length. func (cs *ChainStore) MaybeTakeHeavierTipSet(ctx context.Context, ts *types.TipSet) error { cs.heaviestLk.Lock() defer cs.heaviestLk.Unlock() @@ -380,6 +380,15 @@ func (cs *ChainStore) MaybeTakeHeavierTipSet(ctx context.Context, ts *types.TipS // TODO: don't do this for initial sync. Now that we don't have a // difference between 'bootstrap sync' and 'caught up' sync, we need // some other heuristic. + + exceeds, err := cs.exceedsForkLength(cs.heaviest, ts) + if err != nil { + return err + } + if exceeds { + return nil + } + return cs.takeHeaviestTipSet(ctx, ts) } else if w.Equals(heaviestW) && !ts.Equals(cs.heaviest) { log.Errorw("weight draw", "currTs", cs.heaviest, "ts", ts) @@ -387,6 +396,67 @@ func (cs *ChainStore) MaybeTakeHeavierTipSet(ctx context.Context, ts *types.TipS return nil } +// Check if the two tipsets have a fork length above `ForkLengthThreshold`. +// `synced` is the head of the chain we are currently synced to and `external` +// is the incoming tipset potentially belonging to a forked chain. It assumes +// the external chain has already been validated and available in the ChainStore. +// The "fast forward" case is covered in this logic as a valid fork of length 0. +// +// FIXME: We may want to replace some of the logic in `syncFork()` with this. +// `syncFork()` counts the length on both sides of the fork at the moment (we +// need to settle on that) but here we just enforce it on the `synced` side. +func (cs *ChainStore) exceedsForkLength(synced, external *types.TipSet) (bool, error) { + if synced == nil || external == nil { + // FIXME: If `cs.heaviest` is nil we should just bypass the entire + // `MaybeTakeHeavierTipSet` logic (instead of each of the called + // functions having to handle the nil case on their own). + return false, nil + } + + var err error + // `forkLength`: number of tipsets we need to walk back from the our `synced` + // chain to the common ancestor with the new `external` head in order to + // adopt the fork. + for forkLength := 0; forkLength < int(build.ForkLengthThreshold); forkLength++ { + // First walk back as many tipsets in the external chain to match the + // `synced` height to compare them. If we go past the `synced` height + // the subsequent match will fail but it will still be useful to get + // closer to the `synced` head parent's height in the next loop. + for external.Height() > synced.Height() { + if external.Height() == 0 { + // We reached the genesis of the external chain without a match; + // this is considered a fork outside the allowed limit (of "infinite" + // length). + return true, nil + } + external, err = cs.LoadTipSet(external.Parents()) + if err != nil { + return false, xerrors.Errorf("failed to load parent tipset in external chain: %w", err) + } + } + + // Now check if we arrived at the common ancestor. + if synced.Equals(external) { + return false, nil + } + + // If we didn't, go back *one* tipset on the `synced` side (incrementing + // the `forkLength`). + if synced.Height() == 0 { + // Same check as the `external` side, if we reach the start (genesis) + // there is no common ancestor. + return true, nil + } + synced, err = cs.LoadTipSet(synced.Parents()) + if err != nil { + return false, xerrors.Errorf("failed to load parent tipset in synced chain: %w", err) + } + } + + // We traversed the fork length allowed without finding a common ancestor. + return true, nil +} + // ForceHeadSilent forces a chain head tipset without triggering a reorg // operation. //