-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6107 from filecoin-project/feat/checkpoint-sync
feat: allow checkpointing to forks
- Loading branch information
Showing
6 changed files
with
267 additions
and
87 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,81 +1,57 @@ | ||
package chain | ||
|
||
import ( | ||
"encoding/json" | ||
"context" | ||
|
||
"github.com/filecoin-project/lotus/chain/types" | ||
|
||
"github.com/filecoin-project/lotus/node/modules/dtypes" | ||
"github.com/ipfs/go-datastore" | ||
"golang.org/x/xerrors" | ||
) | ||
|
||
var CheckpointKey = datastore.NewKey("/chain/checks") | ||
|
||
func loadCheckpoint(ds dtypes.MetadataDS) (types.TipSetKey, error) { | ||
haveChks, err := ds.Has(CheckpointKey) | ||
if err != nil { | ||
return types.EmptyTSK, err | ||
} | ||
|
||
if !haveChks { | ||
return types.EmptyTSK, nil | ||
func (syncer *Syncer) SyncCheckpoint(ctx context.Context, tsk types.TipSetKey) error { | ||
if tsk == types.EmptyTSK { | ||
return xerrors.Errorf("called with empty tsk") | ||
} | ||
|
||
tskBytes, err := ds.Get(CheckpointKey) | ||
ts, err := syncer.ChainStore().LoadTipSet(tsk) | ||
if err != nil { | ||
return types.EmptyTSK, err | ||
tss, err := syncer.Exchange.GetBlocks(ctx, tsk, 1) | ||
if err != nil { | ||
return xerrors.Errorf("failed to fetch tipset: %w", err) | ||
} else if len(tss) != 1 { | ||
return xerrors.Errorf("expected 1 tipset, got %d", len(tss)) | ||
} | ||
ts = tss[0] | ||
} | ||
|
||
var tsk types.TipSetKey | ||
err = json.Unmarshal(tskBytes, &tsk) | ||
if err != nil { | ||
return types.EmptyTSK, err | ||
if err := syncer.switchChain(ctx, ts); err != nil { | ||
return xerrors.Errorf("failed to switch chain when syncing checkpoint: %w", err) | ||
} | ||
|
||
return tsk, err | ||
} | ||
|
||
func (syncer *Syncer) SetCheckpoint(tsk types.TipSetKey) error { | ||
if tsk == types.EmptyTSK { | ||
return xerrors.Errorf("called with empty tsk") | ||
if err := syncer.ChainStore().SetCheckpoint(ts); err != nil { | ||
return xerrors.Errorf("failed to set the chain checkpoint: %w", err) | ||
} | ||
|
||
syncer.checkptLk.Lock() | ||
defer syncer.checkptLk.Unlock() | ||
|
||
ts, err := syncer.ChainStore().LoadTipSet(tsk) | ||
if err != nil { | ||
return xerrors.Errorf("cannot find tipset: %w", err) | ||
} | ||
return nil | ||
} | ||
|
||
func (syncer *Syncer) switchChain(ctx context.Context, ts *types.TipSet) error { | ||
hts := syncer.ChainStore().GetHeaviestTipSet() | ||
anc, err := syncer.ChainStore().IsAncestorOf(ts, hts) | ||
if err != nil { | ||
return xerrors.Errorf("cannot determine whether checkpoint tipset is in main-chain: %w", err) | ||
if hts.Equals(ts) { | ||
return nil | ||
} | ||
|
||
if !hts.Equals(ts) && !anc { | ||
return xerrors.Errorf("cannot mark tipset as checkpoint, since it isn't in the main-chain: %w", err) | ||
if anc, err := syncer.store.IsAncestorOf(ts, hts); err == nil && anc { | ||
return nil | ||
} | ||
|
||
tskBytes, err := json.Marshal(tsk) | ||
if err != nil { | ||
return err | ||
// Otherwise, sync the chain and set the head. | ||
if err := syncer.collectChain(ctx, ts, hts, true); err != nil { | ||
return xerrors.Errorf("failed to collect chain for checkpoint: %w", err) | ||
} | ||
|
||
err = syncer.ds.Put(CheckpointKey, tskBytes) | ||
if err != nil { | ||
return err | ||
if err := syncer.ChainStore().SetHead(ts); err != nil { | ||
return xerrors.Errorf("failed to set the chain head: %w", err) | ||
} | ||
|
||
syncer.checkpt = tsk | ||
|
||
return nil | ||
} | ||
|
||
func (syncer *Syncer) GetCheckpoint() types.TipSetKey { | ||
syncer.checkptLk.Lock() | ||
defer syncer.checkptLk.Unlock() | ||
return syncer.checkpt | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package store_test | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/filecoin-project/lotus/chain/gen" | ||
) | ||
|
||
func TestChainCheckpoint(t *testing.T) { | ||
cg, err := gen.NewGenerator() | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
// Let the first miner mine some blocks. | ||
last := cg.CurTipset.TipSet() | ||
for i := 0; i < 4; i++ { | ||
ts, err := cg.NextTipSetFromMiners(last, cg.Miners[:1]) | ||
require.NoError(t, err) | ||
|
||
last = ts.TipSet.TipSet() | ||
} | ||
|
||
cs := cg.ChainStore() | ||
|
||
checkpoint := last | ||
checkpointParents, err := cs.GetTipSetFromKey(checkpoint.Parents()) | ||
require.NoError(t, err) | ||
|
||
// Set the head to the block before the checkpoint. | ||
err = cs.SetHead(checkpointParents) | ||
require.NoError(t, err) | ||
|
||
// Verify it worked. | ||
head := cs.GetHeaviestTipSet() | ||
require.True(t, head.Equals(checkpointParents)) | ||
|
||
// Try to set the checkpoint in the future, it should fail. | ||
err = cs.SetCheckpoint(checkpoint) | ||
require.Error(t, err) | ||
|
||
// Then move the head back. | ||
err = cs.SetHead(checkpoint) | ||
require.NoError(t, err) | ||
|
||
// Verify it worked. | ||
head = cs.GetHeaviestTipSet() | ||
require.True(t, head.Equals(checkpoint)) | ||
|
||
// And checkpoint it. | ||
err = cs.SetCheckpoint(checkpoint) | ||
require.NoError(t, err) | ||
|
||
// Let the second miner miner mine a fork | ||
last = checkpointParents | ||
for i := 0; i < 4; i++ { | ||
ts, err := cg.NextTipSetFromMiners(last, cg.Miners[1:]) | ||
require.NoError(t, err) | ||
|
||
last = ts.TipSet.TipSet() | ||
} | ||
|
||
// See if the chain will take the fork, it shouldn't. | ||
err = cs.MaybeTakeHeavierTipSet(context.Background(), last) | ||
require.NoError(t, err) | ||
head = cs.GetHeaviestTipSet() | ||
require.True(t, head.Equals(checkpoint)) | ||
|
||
// Remove the checkpoint. | ||
err = cs.RemoveCheckpoint() | ||
require.NoError(t, err) | ||
|
||
// Now switch to the other fork. | ||
err = cs.MaybeTakeHeavierTipSet(context.Background(), last) | ||
require.NoError(t, err) | ||
head = cs.GetHeaviestTipSet() | ||
require.True(t, head.Equals(last)) | ||
|
||
// Setting a checkpoint on the other fork should fail. | ||
err = cs.SetCheckpoint(checkpoint) | ||
require.Error(t, err) | ||
|
||
// Setting a checkpoint on this fork should succeed. | ||
err = cs.SetCheckpoint(checkpointParents) | ||
require.NoError(t, err) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.