From 0c0b1583e1fc591d2c2d9496c08e02c7b17f775e Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Wed, 30 Oct 2024 20:47:35 -0700 Subject: [PATCH] [WIP] test mining on a forked chain --- chain/checkpoint.go | 28 ++++++++++------ itests/checkpoint_test.go | 61 ++++++++++++++++++++++++++++++++++ itests/kit/ensemble_presets.go | 22 ++++++++++++ miner/miner.go | 6 ++++ 4 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 itests/checkpoint_test.go diff --git a/chain/checkpoint.go b/chain/checkpoint.go index 766af95b42c..f703299e11e 100644 --- a/chain/checkpoint.go +++ b/chain/checkpoint.go @@ -13,6 +13,12 @@ func (syncer *Syncer) SyncCheckpoint(ctx context.Context, tsk types.TipSetKey) e return xerrors.Errorf("called with empty tsk") } + // XXX: I think this change here is correct (we won't have a tipset unless we have all parents) but: + // 1. I'm not sure. + // 2. The sync code is dumb and should look at what we have. Actually, it does look at what + // we have... then it seems to ignore it? + // + // Without this change, the test fails because we can't sync the chain we already have! ts, err := syncer.ChainStore().LoadTipSet(ctx, tsk) if err != nil { tss, err := syncer.Exchange.GetBlocks(ctx, tsk, 1) @@ -22,18 +28,18 @@ func (syncer *Syncer) SyncCheckpoint(ctx context.Context, tsk types.TipSetKey) e return xerrors.Errorf("expected 1 tipset, got %d", len(tss)) } ts = tss[0] - } - hts := syncer.ChainStore().GetHeaviestTipSet() - if !hts.Equals(ts) { - if anc, err := syncer.store.IsAncestorOf(ctx, ts, hts); err != nil { - return xerrors.Errorf("failed to walk the chain when checkpointing: %w", err) - } else if !anc { - if err := syncer.collectChain(ctx, ts, hts, true); err != nil { - return xerrors.Errorf("failed to collect chain for checkpoint: %w", err) - } - } // else new checkpoint is on the current chain, we definitely have the tipsets. - } // else current head, no need to switch. + hts := syncer.ChainStore().GetHeaviestTipSet() + if !hts.Equals(ts) { + if anc, err := syncer.store.IsAncestorOf(ctx, ts, hts); err != nil { + return xerrors.Errorf("failed to walk the chain when checkpointing: %w", err) + } else if !anc { + if err := syncer.collectChain(ctx, ts, hts, true); err != nil { + return xerrors.Errorf("failed to collect chain for checkpoint: %w", err) + } + } // else new checkpoint is on the current chain, we definitely have the tipsets. + } // else current head, no need to switch. + } if err := syncer.ChainStore().SetCheckpoint(ctx, ts); err != nil { return xerrors.Errorf("failed to set the chain checkpoint: %w", err) diff --git a/itests/checkpoint_test.go b/itests/checkpoint_test.go new file mode 100644 index 00000000000..0d1b9aec461 --- /dev/null +++ b/itests/checkpoint_test.go @@ -0,0 +1,61 @@ +package itests + +import ( + "context" + "testing" + "time" + + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/itests/kit" + "github.com/stretchr/testify/require" +) + +func TestCheckpointFork(t *testing.T) { + ctx := context.Background() + + blocktime := 100 * time.Millisecond + // Create three miners. + miners := make([]*kit.TestMiner, 3) + n1, ens := kit.EnsembleOneMany(t, + miners, + kit.MockProofs(), + kit.ThroughRPC(), + ) + + // Start 2 of them. + ens.InterconnectAll().BeginMining(blocktime, miners[:2]...) + + { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + n1.WaitTillChain(ctx, kit.HeightAtLeast(abi.ChainEpoch(5))) + cancel() + } + + // Wait till both participate in a single tipset. + var target *types.TipSet + { + // find the first tipset where two miners mine a block. + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + target = n1.WaitTillChain(ctx, func(ts *types.TipSet) bool { + return len(ts.Blocks()) == 2 + }) + cancel() + } + + // Wait till we've moved on from that tipset. + targetHeight := target.Height() + 10 + n1.WaitTillChain(ctx, kit.HeightAtLeast(targetHeight)) + + // Forcibly sync to this fork tipset. + forkTs, err := types.NewTipSet(target.Blocks()[:1]) + require.NoError(t, err) + require.NoError(t, n1.SyncCheckpoint(ctx, forkTs.Key())) + ens.BeginMining(blocktime, miners[2]) + + // See if we can start making progress again! + newHead := n1.WaitTillChain(ctx, kit.HeightAtLeast(targetHeight)) + forkTs2, err := n1.ChainGetTipSetByHeight(ctx, forkTs.Height(), newHead.Key()) + require.NoError(t, err) + require.True(t, forkTs.Equals(forkTs2)) +} diff --git a/itests/kit/ensemble_presets.go b/itests/kit/ensemble_presets.go index c3c17d4d96d..37cb9f4d652 100644 --- a/itests/kit/ensemble_presets.go +++ b/itests/kit/ensemble_presets.go @@ -77,6 +77,28 @@ func EnsembleOneTwo(t *testing.T, opts ...interface{}) (*TestFullNode, *TestMine return &full, &one, &two, ens } +// EnsembleOneMany and starts an Ensemble with one full node and fills the miner slice with miners. +// It does not interconnect nodes nor does it begin mining. +// +// This function supports passing both ensemble and node functional options. +// Functional options are applied to all nodes. +func EnsembleOneMany(t *testing.T, miners []*TestMiner, opts ...interface{}) (*TestFullNode, *Ensemble) { + opts = append(opts, WithAllSubsystems()) + + eopts, nopts := siftOptions(t, opts) + + var full TestFullNode + ens := NewEnsemble(t, eopts...).FullNode(&full, nopts...) + for i := range miners { + var m TestMiner + ens.Miner(&m, &full, nopts...) + miners[i] = &m + } + ens.Start() + + return &full, ens +} + func siftOptions(t *testing.T, opts []interface{}) (eopts []EnsembleOpt, nopts []NodeOpt) { for _, v := range opts { switch o := v.(type) { diff --git a/miner/miner.go b/miner/miner.go index 785cc3a2c9a..bec096a61b8 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -348,11 +348,15 @@ minerLoop: if err != nil { log.Errorf(" SLASH FILTER ERRORED: %s", err) // Continue here, because it's _probably_ wiser to not submit this block + base.NullRounds++ + // XXX: WAIT? continue } if fault { log.Errorf(" SLASH FILTER DETECTED FAULT due to blocks %s and %s", b.Header.Cid(), witness) + base.NullRounds++ + // XXX: WAIT? continue } } @@ -360,6 +364,8 @@ minerLoop: // Check for blocks created at the same height. if _, ok := m.minedBlockHeights.Get(b.Header.Height); ok { log.Warnw("Created a block at the same height as another block we've created", "height", b.Header.Height, "miner", b.Header.Miner, "parents", b.Header.Parents) + base.NullRounds++ + // XXX: WAIT? continue }