diff --git a/chain/gen/gen.go b/chain/gen/gen.go index 2e5f5e7f7..f0e884073 100644 --- a/chain/gen/gen.go +++ b/chain/gen/gen.go @@ -449,18 +449,19 @@ func (cg *ChainGen) NextTipSetFromMiners(base *types.TipSet, miners []address.Ad } func (cg *ChainGen) NextTipSetFromMinersWithMessagesAndNulls(base *types.TipSet, miners []address.Address, msgs [][]*types.SignedMessage, nulls abi.ChainEpoch) (*store.FullTipSet, error) { + ctx := context.TODO() var blks []*types.FullBlock for round := base.Height() + nulls + 1; len(blks) == 0; round++ { for mi, m := range miners { - bvals, et, ticket, err := cg.nextBlockProof(context.TODO(), base, m, round) + bvals, et, ticket, err := cg.nextBlockProof(ctx, base, m, round) if err != nil { return nil, xerrors.Errorf("next block proof: %w", err) } if et != nil { // TODO: maybe think about passing in more real parameters to this? - wpost, err := cg.eppProvs[m].ComputeProof(context.TODO(), nil, nil, round, network.Version0) + wpost, err := cg.eppProvs[m].ComputeProof(ctx, nil, nil, round, network.Version0) if err != nil { return nil, err } @@ -476,8 +477,18 @@ func (cg *ChainGen) NextTipSetFromMinersWithMessagesAndNulls(base *types.TipSet, } fts := store.NewFullTipSet(blks) - if err := cg.cs.PutTipSet(context.TODO(), fts.TipSet()); err != nil { - return nil, err + if err := cg.cs.PersistTipsets(ctx, []*types.TipSet{fts.TipSet()}); err != nil { + return nil, xerrors.Errorf("failed to persist tipset: %w", err) + } + + for _, blk := range blks { + if err := cg.cs.AddToTipSetTracker(ctx, blk.Header); err != nil { + return nil, xerrors.Errorf("failed to add to tipset tracker: %w", err) + } + } + + if err := cg.cs.RefreshHeaviestTipSet(ctx, fts.TipSet().Height()); err != nil { + return nil, xerrors.Errorf("failed to put tipset: %w", err) } cg.CurTipset = fts diff --git a/chain/store/checkpoint_test.go b/chain/store/checkpoint_test.go index bc2cb5e73..c5dff94a8 100644 --- a/chain/store/checkpoint_test.go +++ b/chain/store/checkpoint_test.go @@ -70,7 +70,7 @@ func TestChainCheckpoint(t *testing.T) { } // See if the chain will take the fork, it shouldn't. - err = cs.MaybeTakeHeavierTipSet(context.Background(), last) + err = cs.RefreshHeaviestTipSet(context.Background(), last.Height()) require.NoError(t, err) head = cs.GetHeaviestTipSet() require.True(t, head.Equals(checkpoint)) @@ -80,7 +80,7 @@ func TestChainCheckpoint(t *testing.T) { require.NoError(t, err) // Now switch to the other fork. - err = cs.MaybeTakeHeavierTipSet(context.Background(), last) + err = cs.RefreshHeaviestTipSet(context.Background(), last.Height()) require.NoError(t, err) head = cs.GetHeaviestTipSet() require.True(t, head.Equals(last)) diff --git a/chain/store/index_test.go b/chain/store/index_test.go index 63a1abad0..a3a4ad6ce 100644 --- a/chain/store/index_test.go +++ b/chain/store/index_test.go @@ -16,6 +16,7 @@ import ( "github.com/filecoin-project/lotus/chain/consensus/filcns" "github.com/filecoin-project/lotus/chain/gen" "github.com/filecoin-project/lotus/chain/store" + "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/types/mock" ) @@ -47,28 +48,29 @@ func TestIndexSeeks(t *testing.T) { } cur := mock.TipSet(gen) - if err := cs.PutTipSet(ctx, mock.TipSet(gen)); err != nil { - t.Fatal(err) - } + assert.NoError(t, cs.SetGenesis(ctx, gen)) // Put 113 blocks from genesis for i := 0; i < 113; i++ { - nextts := mock.TipSet(mock.MkBlock(cur, 1, 1)) - - if err := cs.PutTipSet(ctx, nextts); err != nil { - t.Fatal(err) - } + nextBlk := mock.MkBlock(cur, 1, 1) + nextts := mock.TipSet(nextBlk) + assert.NoError(t, cs.PersistTipsets(ctx, []*types.TipSet{nextts})) + assert.NoError(t, cs.AddToTipSetTracker(ctx, nextBlk)) cur = nextts } + assert.NoError(t, cs.RefreshHeaviestTipSet(ctx, cur.Height())) + // Put 50 null epochs + 1 block skip := mock.MkBlock(cur, 1, 1) skip.Height += 50 - skipts := mock.TipSet(skip) - if err := cs.PutTipSet(ctx, skipts); err != nil { + assert.NoError(t, cs.PersistTipsets(ctx, []*types.TipSet{skipts})) + assert.NoError(t, cs.AddToTipSetTracker(ctx, skip)) + + if err := cs.RefreshHeaviestTipSet(ctx, skip.Height); err != nil { t.Fatal(err) } diff --git a/chain/store/store.go b/chain/store/store.go index 88103ac48..a7075f4cc 100644 --- a/chain/store/store.go +++ b/chain/store/store.go @@ -367,49 +367,32 @@ func (cs *ChainStore) UnmarkBlockAsValidated(ctx context.Context, blkid cid.Cid) func (cs *ChainStore) SetGenesis(ctx context.Context, b *types.BlockHeader) error { ts, err := types.NewTipSet([]*types.BlockHeader{b}) if err != nil { - return err + return xerrors.Errorf("failed to construct genesis tipset: %w", err) } - if err := cs.PutTipSet(ctx, ts); err != nil { - return err + if err := cs.PersistTipsets(ctx, []*types.TipSet{ts}); err != nil { + return xerrors.Errorf("failed to persist genesis tipset: %w", err) + } + + if err := cs.AddToTipSetTracker(ctx, b); err != nil { + return xerrors.Errorf("failed to add genesis tipset to tracker: %w", err) + } + + if err := cs.RefreshHeaviestTipSet(ctx, ts.Height()); err != nil { + return xerrors.Errorf("failed to put genesis tipset: %w", err) } return cs.metadataDs.Put(ctx, dstore.NewKey("0"), b.Cid().Bytes()) } -func (cs *ChainStore) PutTipSet(ctx context.Context, ts *types.TipSet) error { - if err := cs.PersistTipsets(ctx, []*types.TipSet{ts}); err != nil { - return xerrors.Errorf("failed to persist tipset: %w", err) - } - - expanded, err := cs.expandTipset(ctx, ts.Blocks()[0]) - if err != nil { - return xerrors.Errorf("errored while expanding tipset: %w", err) - } - - if expanded.Key() != ts.Key() { - log.Debugf("expanded %s into %s\n", ts.Cids(), expanded.Cids()) - - tsBlk, err := expanded.Key().ToStorageBlock() - if err != nil { - return xerrors.Errorf("failed to get tipset key block: %w", err) - } - - if err = cs.chainLocalBlockstore.Put(ctx, tsBlk); err != nil { - return xerrors.Errorf("failed to put tipset key block: %w", err) - } - } - - if err := cs.MaybeTakeHeavierTipSet(ctx, expanded); err != nil { - return xerrors.Errorf("MaybeTakeHeavierTipSet failed in PutTipSet: %w", err) - } - return nil -} - -// 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 and does not exceed the maximum fork length. -func (cs *ChainStore) MaybeTakeHeavierTipSet(ctx context.Context, ts *types.TipSet) error { +// RefreshHeaviestTipSet receives a newTsHeight at which a new tipset might exist. It then: +// - "refreshes" the heaviest tipset that can be formed at its current heaviest height +// - if equivocation is detected among the miners of the current heaviest tipset, the head is immediately updated to the heaviest tipset that can be formed in a range of 5 epochs +// +// - forms the best tipset that can be formed at the _input_ height +// - compares the three tipset weights: "current" heaviest tipset, "refreshed" tipset, and best tipset at newTsHeight +// - updates "current" heaviest to the heaviest of those 3 tipsets (if an update is needed), assuming it doesn't violate the maximum fork rule +func (cs *ChainStore) RefreshHeaviestTipSet(ctx context.Context, newTsHeight abi.ChainEpoch) error { for { cs.heaviestLk.Lock() if len(cs.reorgCh) < reorgChBuf/2 { @@ -426,39 +409,88 @@ func (cs *ChainStore) MaybeTakeHeavierTipSet(ctx context.Context, ts *types.TipS defer cs.heaviestLk.Unlock() - if ts.Equals(cs.heaviest) { + heaviestWeight, err := cs.weight(ctx, cs.StateBlockstore(), cs.heaviest) + if err != nil { + return xerrors.Errorf("failed to calculate currentHeaviest's weight: %w", err) + } + + heaviestHeight := abi.ChainEpoch(0) + if cs.heaviest != nil { + heaviestHeight = cs.heaviest.Height() + } + + // Before we look at newTs, let's refresh best tipset at current head's height -- this is done to detect equivocation + newHeaviest, newHeaviestWeight, err := cs.FormHeaviestTipSetForHeight(ctx, heaviestHeight) + if err != nil { + return xerrors.Errorf("failed to reform head at same height: %w", err) + } + + // Equivocation has occurred! We need a new head NOW! + if newHeaviest == nil || newHeaviestWeight.LessThan(heaviestWeight) { + log.Warnf("chainstore heaviest tipset's weight SHRANK from %d (%s) to %d (%s) due to equivocation", heaviestWeight, cs.heaviest, newHeaviestWeight, newHeaviest) + // Unfortunately, we don't know what the right height to form a new heaviest tipset is. + // It is _probably_, but not _necessarily_, heaviestHeight. + // So, we need to explore a range of epochs, finding the heaviest tipset in that range. + // We thus try to form the heaviest tipset for 5 epochs above heaviestHeight (most of which will likely not exist), + // as well as for 5 below. + // This is slow, but we expect to almost-never be here (only if miners are equivocating, which carries a hefty penalty). + for i := heaviestHeight + 5; i > heaviestHeight-5; i-- { + possibleHeaviestTs, possibleHeaviestWeight, err := cs.FormHeaviestTipSetForHeight(ctx, i) + if err != nil { + return xerrors.Errorf("failed to produce head at height %d: %w", i, err) + } + + if possibleHeaviestWeight.GreaterThan(newHeaviestWeight) { + newHeaviestWeight = possibleHeaviestWeight + newHeaviest = possibleHeaviestTs + } + } + + if newHeaviest == nil { + return xerrors.Errorf("failed to refresh to a new valid tipset") + } + + errTake := cs.takeHeaviestTipSet(ctx, newHeaviest) + if errTake != nil { + return xerrors.Errorf("failed to take newHeaviest tipset as head: %w", err) + } + } + + // if the new height we were notified about isn't what we just refreshed at, see if we have a heavier tipset there + if newTsHeight != newHeaviest.Height() { + bestTs, bestTsWeight, err := cs.FormHeaviestTipSetForHeight(ctx, newTsHeight) + if err != nil { + return xerrors.Errorf("failed to form new heaviest tipset at height %d: %w", newTsHeight, err) + } + + heavier := bestTsWeight.GreaterThan(newHeaviestWeight) + if bestTsWeight.Equals(newHeaviestWeight) { + heavier = breakWeightTie(bestTs, newHeaviest) + } + + if heavier { + newHeaviest = bestTs + } + } + + // Everything's the same as before, exit early + if newHeaviest.Equals(cs.heaviest) { return nil } - w, err := cs.weight(ctx, cs.StateBlockstore(), ts) + // At this point, it MUST be true that newHeaviest is heavier than cs.heaviest -- update if fork allows + exceeds, err := cs.exceedsForkLength(ctx, cs.heaviest, newHeaviest) if err != nil { - return err + return xerrors.Errorf("failed to check fork length: %w", err) } - heaviestW, err := cs.weight(ctx, cs.StateBlockstore(), cs.heaviest) + + if exceeds { + return nil + } + + err = cs.takeHeaviestTipSet(ctx, newHeaviest) if err != nil { - return err - } - - heavier := w.GreaterThan(heaviestW) - if w.Equals(heaviestW) && !ts.Equals(cs.heaviest) { - log.Errorw("weight draw", "currTs", cs.heaviest, "ts", ts) - heavier = breakWeightTie(ts, cs.heaviest) - } - - if heavier { - // 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(ctx, cs.heaviest, ts) - if err != nil { - return err - } - if exceeds { - return nil - } - - return cs.takeHeaviestTipSet(ctx, ts) + return xerrors.Errorf("failed to take heaviest tipset: %w", err) } return nil @@ -655,6 +687,16 @@ func (cs *ChainStore) takeHeaviestTipSet(ctx context.Context, ts *types.TipSet) return err } + // write the tipsetkey block to the blockstore for EthAPI queries + tsBlk, err := ts.Key().ToStorageBlock() + if err != nil { + return xerrors.Errorf("failed to get tipset key block: %w", err) + } + + if err = cs.chainLocalBlockstore.Put(ctx, tsBlk); err != nil { + return xerrors.Errorf("failed to put tipset key block: %w", err) + } + if prevHeaviest != nil { // buf if len(cs.reorgCh) > 0 { log.Warnf("Reorg channel running behind, %d reorgs buffered", len(cs.reorgCh)) @@ -975,6 +1017,7 @@ func (cs *ChainStore) AddToTipSetTracker(ctx context.Context, b *types.BlockHead return nil } +// PersistTipsets writes the provided blocks and the TipSetKey objects to the blockstore func (cs *ChainStore) PersistTipsets(ctx context.Context, tipsets []*types.TipSet) error { toPersist := make([]*types.BlockHeader, 0, len(tipsets)*int(build.BlocksPerEpoch)) tsBlks := make([]block.Block, 0, len(tipsets)) @@ -1027,44 +1070,72 @@ func (cs *ChainStore) persistBlockHeaders(ctx context.Context, b ...*types.Block return err } -func (cs *ChainStore) expandTipset(ctx context.Context, b *types.BlockHeader) (*types.TipSet, error) { - // Hold lock for the whole function for now, if it becomes a problem we can - // fix pretty easily +// FormHeaviestTipSetForHeight looks up all valid blocks at a given height, and returns the heaviest tipset that can be made at that height +// It does not consider ANY blocks from miners that have "equivocated" (produced 2 blocks at the same height) +func (cs *ChainStore) FormHeaviestTipSetForHeight(ctx context.Context, height abi.ChainEpoch) (*types.TipSet, types.BigInt, error) { cs.tstLk.Lock() defer cs.tstLk.Unlock() - all := []*types.BlockHeader{b} - - tsets, ok := cs.tipsets[b.Height] + blockCids, ok := cs.tipsets[height] if !ok { - return types.NewTipSet(all) + return nil, types.NewInt(0), nil } - inclMiners := map[address.Address]cid.Cid{b.Miner: b.Cid()} - for _, bhc := range tsets { - if bhc == b.Cid() { - continue - } + // First, identify "bad" miners for the height + seenMiners := map[address.Address]struct{}{} + badMiners := map[address.Address]struct{}{} + blocks := make([]*types.BlockHeader, 0, len(blockCids)) + for _, bhc := range blockCids { h, err := cs.GetBlock(ctx, bhc) if err != nil { - return nil, xerrors.Errorf("failed to load block (%s) for tipset expansion: %w", bhc, err) + return nil, types.NewInt(0), xerrors.Errorf("failed to load block (%s) for tipset expansion: %w", bhc, err) } - if cid, found := inclMiners[h.Miner]; found { - log.Warnf("Have multiple blocks from miner %s at height %d in our tipset cache %s-%s", h.Miner, h.Height, h.Cid(), cid) + if _, seen := seenMiners[h.Miner]; seen { + badMiners[h.Miner] = struct{}{} continue } + seenMiners[h.Miner] = struct{}{} + blocks = append(blocks, h) + } - if types.CidArrsEqual(h.Parents, b.Parents) { - all = append(all, h) - inclMiners[h.Miner] = bhc + // Next, group by parent tipset + + formableTipsets := make(map[types.TipSetKey][]*types.BlockHeader, 0) + for _, h := range blocks { + if _, bad := badMiners[h.Miner]; bad { + continue + } + ptsk := types.NewTipSetKey(h.Parents...) + formableTipsets[ptsk] = append(formableTipsets[ptsk], h) + } + + maxWeight := types.NewInt(0) + var maxTs *types.TipSet + for _, headers := range formableTipsets { + ts, err := types.NewTipSet(headers) + if err != nil { + return nil, types.NewInt(0), xerrors.Errorf("unexpected error forming tipset: %w", err) + } + + weight, err := cs.Weight(ctx, ts) + if err != nil { + return nil, types.NewInt(0), xerrors.Errorf("failed to calculate weight: %w", err) + } + + heavier := weight.GreaterThan(maxWeight) + if weight.Equals(maxWeight) { + heavier = breakWeightTie(ts, maxTs) + } + + if heavier { + maxWeight = weight + maxTs = ts } } - // TODO: other validation...? - - return types.NewTipSet(all) + return maxTs, maxWeight, nil } func (cs *ChainStore) GetGenesis(ctx context.Context) (*types.BlockHeader, error) { diff --git a/chain/store/store_test.go b/chain/store/store_test.go index cea0fdc2a..14ba8cf0f 100644 --- a/chain/store/store_test.go +++ b/chain/store/store_test.go @@ -10,6 +10,7 @@ import ( "github.com/ipfs/go-datastore" "github.com/stretchr/testify/require" + "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/crypto" @@ -238,3 +239,170 @@ func TestChainExportImportFull(t *testing.T) { } } } + +func TestEquivocations(t *testing.T) { + ctx := context.Background() + cg, err := gen.NewGenerator() + if err != nil { + t.Fatal(err) + } + + var last *types.TipSet + for i := 0; i < 10; i++ { + ts, err := cg.NextTipSet() + if err != nil { + t.Fatal(err) + } + + last = ts.TipSet.TipSet() + } + + mTs, err := cg.NextTipSetFromMiners(last, []address.Address{last.Blocks()[0].Miner}, 0) + require.NoError(t, err) + require.Equal(t, 1, len(mTs.TipSet.TipSet().Cids())) + last = mTs.TipSet.TipSet() + + require.NotEmpty(t, last.Blocks()) + blk1 := *last.Blocks()[0] + + // quick check: asking to form tipset at latest height just returns head + bestHead, bestHeadWeight, err := cg.ChainStore().FormHeaviestTipSetForHeight(ctx, last.Height()) + require.NoError(t, err) + require.Equal(t, last.Key(), bestHead.Key()) + require.Contains(t, last.Cids(), blk1.Cid()) + expectedWeight, err := cg.ChainStore().Weight(ctx, bestHead) + require.NoError(t, err) + require.Equal(t, expectedWeight, bestHeadWeight) + + // add another block by a different miner -- it should get included in the best tipset + blk2 := blk1 + blk1Miner, err := address.IDFromAddress(blk2.Miner) + require.NoError(t, err) + blk2.Miner, err = address.NewIDAddress(blk1Miner + 50) + require.NoError(t, err) + addBlockToTracker(t, cg.ChainStore(), &blk2) + + bestHead, bestHeadWeight, err = cg.ChainStore().FormHeaviestTipSetForHeight(ctx, last.Height()) + require.NoError(t, err) + for _, blkCid := range last.Cids() { + require.Contains(t, bestHead.Cids(), blkCid) + } + require.Contains(t, bestHead.Cids(), blk2.Cid()) + expectedWeight, err = cg.ChainStore().Weight(ctx, bestHead) + require.NoError(t, err) + require.Equal(t, expectedWeight, bestHeadWeight) + + // add another block by a different miner, but on a different tipset -- it should NOT get included + blk3 := blk1 + blk3.Miner, err = address.NewIDAddress(blk1Miner + 100) + require.NoError(t, err) + blk1Parent, err := cg.ChainStore().GetBlock(ctx, blk3.Parents[0]) + require.NoError(t, err) + blk3.Parents = blk1Parent.Parents + addBlockToTracker(t, cg.ChainStore(), &blk3) + + bestHead, bestHeadWeight, err = cg.ChainStore().FormHeaviestTipSetForHeight(ctx, last.Height()) + require.NoError(t, err) + for _, blkCid := range last.Cids() { + require.Contains(t, bestHead.Cids(), blkCid) + } + require.Contains(t, bestHead.Cids(), blk2.Cid()) + require.NotContains(t, bestHead.Cids(), blk3.Cid()) + expectedWeight, err = cg.ChainStore().Weight(ctx, bestHead) + require.NoError(t, err) + require.Equal(t, expectedWeight, bestHeadWeight) + + // add another block by the same miner as blk1 -- it should NOT get included, and blk1 should be excluded too + blk4 := blk1 + blk4.Timestamp = blk1.Timestamp + 1 + addBlockToTracker(t, cg.ChainStore(), &blk4) + + bestHead, bestHeadWeight, err = cg.ChainStore().FormHeaviestTipSetForHeight(ctx, last.Height()) + require.NoError(t, err) + for _, blkCid := range last.Cids() { + if blkCid != blk1.Cid() { + require.Contains(t, bestHead.Cids(), blkCid) + } + } + require.NotContains(t, bestHead.Cids(), blk4.Cid()) + require.NotContains(t, bestHead.Cids(), blk1.Cid()) + expectedWeight, err = cg.ChainStore().Weight(ctx, bestHead) + require.NoError(t, err) + require.Equal(t, expectedWeight, bestHeadWeight) + + // check that after all of that, the chainstore's head has NOT changed + require.Equal(t, last.Key(), cg.ChainStore().GetHeaviestTipSet().Key()) + + // NOW, after all that, notify the chainstore to refresh its head + require.NoError(t, cg.ChainStore().RefreshHeaviestTipSet(ctx, blk1.Height+1)) + + originalHead := *last + newHead := cg.ChainStore().GetHeaviestTipSet() + // the newHead should be at the same height as the originalHead + require.Equal(t, originalHead.Height(), newHead.Height()) + // the newHead should NOT be the same as the originalHead + require.NotEqual(t, originalHead.Key(), newHead.Key()) + // specifically, it should not contain any blocks by blk1Miner + for _, b := range newHead.Blocks() { + require.NotEqual(t, blk1.Miner, b.Miner) + } + + // now have blk2's Miner equivocate too! this causes us to switch to a tipset with a different parent! + blk5 := blk2 + blk5.Timestamp = blk5.Timestamp + 1 + addBlockToTracker(t, cg.ChainStore(), &blk5) + + // notify the chainstore to refresh its head + require.NoError(t, cg.ChainStore().RefreshHeaviestTipSet(ctx, blk1.Height+1)) + newHead = cg.ChainStore().GetHeaviestTipSet() + // the newHead should still be at the same height as the originalHead + require.Equal(t, originalHead.Height(), newHead.Height()) + // BUT it should no longer have the same parents -- only blk3's miner is good, and they mined on a different tipset + require.Equal(t, 1, len(newHead.Blocks())) + require.Equal(t, blk3.Cid(), newHead.Cids()[0]) + require.NotEqual(t, originalHead.Parents(), newHead.Parents()) + + // now have blk3's Miner equivocate too! this causes us to switch to a previous epoch entirely :( + blk6 := blk3 + blk6.Timestamp = blk6.Timestamp + 1 + addBlockToTracker(t, cg.ChainStore(), &blk6) + + // trying to form a tipset at our previous height leads to emptiness + tryTs, tryTsWeight, err := cg.ChainStore().FormHeaviestTipSetForHeight(ctx, blk1.Height) + require.NoError(t, err) + require.Nil(t, tryTs) + require.True(t, tryTsWeight.IsZero()) + + // notify the chainstore to refresh its head + require.NoError(t, cg.ChainStore().RefreshHeaviestTipSet(ctx, blk1.Height+1)) + newHead = cg.ChainStore().GetHeaviestTipSet() + // the newHead should now be one epoch behind originalHead + require.Greater(t, originalHead.Height(), newHead.Height()) + + // next, we create a new tipset with only one block after many null rounds + headAfterNulls, err := cg.NextTipSetFromMiners(newHead, []address.Address{newHead.Blocks()[0].Miner}, 15) + require.NoError(t, err) + require.Equal(t, 1, len(headAfterNulls.TipSet.Blocks)) + + // now, we disqualify the block in this tipset because of equivocation + blkAfterNulls := headAfterNulls.TipSet.TipSet().Blocks()[0] + equivocatedBlkAfterNulls := *blkAfterNulls + equivocatedBlkAfterNulls.Timestamp = blkAfterNulls.Timestamp + 1 + addBlockToTracker(t, cg.ChainStore(), &equivocatedBlkAfterNulls) + + // try to form a tipset at this height -- it should be empty + tryTs2, tryTsWeight2, err := cg.ChainStore().FormHeaviestTipSetForHeight(ctx, blkAfterNulls.Height) + require.NoError(t, err) + require.Nil(t, tryTs2) + require.True(t, tryTsWeight2.IsZero()) + + // now we "notify" at this height -- it should fail, because we cannot refresh our head due to equivocation and nulls + require.ErrorContains(t, cg.ChainStore().RefreshHeaviestTipSet(ctx, blkAfterNulls.Height), "failed to refresh to a new valid tipset") +} + +func addBlockToTracker(t *testing.T, cs *store.ChainStore, blk *types.BlockHeader) { + blk2Ts, err := types.NewTipSet([]*types.BlockHeader{blk}) + require.NoError(t, err) + require.NoError(t, cs.PersistTipsets(context.TODO(), []*types.TipSet{blk2Ts})) + require.NoError(t, cs.AddToTipSetTracker(context.TODO(), blk)) +} diff --git a/chain/sync.go b/chain/sync.go index 7830a9771..6341deeeb 100644 --- a/chain/sync.go +++ b/chain/sync.go @@ -536,7 +536,7 @@ func (syncer *Syncer) Sync(ctx context.Context, maybeHead *types.TipSet) error { // At this point we have accepted and synced to the new `maybeHead` // (`StageSyncComplete`). - if err := syncer.store.PutTipSet(ctx, maybeHead); err != nil { + if err := syncer.store.RefreshHeaviestTipSet(ctx, maybeHead.Height()); err != nil { span.AddAttributes(trace.StringAttribute("put_error", err.Error())) span.SetStatus(trace.Status{ Code: 13, diff --git a/chain/sync_manager.go b/chain/sync_manager.go index 94017c276..3369c3b5a 100644 --- a/chain/sync_manager.go +++ b/chain/sync_manager.go @@ -92,6 +92,7 @@ type syncManager struct { var _ SyncManager = (*syncManager)(nil) type peerHead struct { + // Note: this doesn't _necessarily_ mean that p's head is ts, just that ts is a tipset that p sent to us p peer.ID ts *types.TipSet } diff --git a/chain/sync_test.go b/chain/sync_test.go index a86d42f17..ec960d7d0 100644 --- a/chain/sync_test.go +++ b/chain/sync_test.go @@ -311,7 +311,7 @@ func (tu *syncTestUtil) addSourceNode(gen int) { for _, lastB := range lastTs.Blocks { require.NoError(tu.t, cs.AddToTipSetTracker(context.Background(), lastB.Header)) } - err = cs.PutTipSet(tu.ctx, lastTs.TipSet()) + err = cs.RefreshHeaviestTipSet(tu.ctx, lastTs.TipSet().Height()) require.NoError(tu.t, err) tu.genesis = genesis