Skip to content

Commit

Permalink
[WIP] test mining on a forked chain
Browse files Browse the repository at this point in the history
  • Loading branch information
Stebalien committed Oct 31, 2024
1 parent e1a0572 commit 0c0b158
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 11 deletions.
28 changes: 17 additions & 11 deletions chain/checkpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
61 changes: 61 additions & 0 deletions itests/checkpoint_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
22 changes: 22 additions & 0 deletions itests/kit/ensemble_presets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions miner/miner.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,18 +348,24 @@ 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
}
}

// 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
}

Expand Down

0 comments on commit 0c0b158

Please sign in to comment.