Merge pull request #10928 from storswiftlabs/slashfilter
Implement a tooling for slasher
This commit is contained in:
commit
24b958cffd
@ -26,20 +26,20 @@ func New(dstore ds.Batching) *SlashFilter {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *SlashFilter) MinedBlock(ctx context.Context, bh *types.BlockHeader, parentEpoch abi.ChainEpoch) error {
|
||||
func (f *SlashFilter) MinedBlock(ctx context.Context, bh *types.BlockHeader, parentEpoch abi.ChainEpoch) (cid.Cid, error) {
|
||||
epochKey := ds.NewKey(fmt.Sprintf("/%s/%d", bh.Miner, bh.Height))
|
||||
{
|
||||
// double-fork mining (2 blocks at one epoch)
|
||||
if err := checkFault(ctx, f.byEpoch, epochKey, bh, "double-fork mining faults"); err != nil {
|
||||
return err
|
||||
if witness, err := checkFault(ctx, f.byEpoch, epochKey, bh, "double-fork mining faults"); err != nil {
|
||||
return witness, xerrors.Errorf("check double-fork mining faults: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
parentsKey := ds.NewKey(fmt.Sprintf("/%s/%x", bh.Miner, types.NewTipSetKey(bh.Parents...).Bytes()))
|
||||
{
|
||||
// time-offset mining faults (2 blocks with the same parents)
|
||||
if err := checkFault(ctx, f.byParents, parentsKey, bh, "time-offset mining faults"); err != nil {
|
||||
return err
|
||||
if witness, err := checkFault(ctx, f.byParents, parentsKey, bh, "time-offset mining faults"); err != nil {
|
||||
return witness, xerrors.Errorf("check time-offset mining faults: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,19 +50,19 @@ func (f *SlashFilter) MinedBlock(ctx context.Context, bh *types.BlockHeader, par
|
||||
parentEpochKey := ds.NewKey(fmt.Sprintf("/%s/%d", bh.Miner, parentEpoch))
|
||||
have, err := f.byEpoch.Has(ctx, parentEpochKey)
|
||||
if err != nil {
|
||||
return err
|
||||
return cid.Undef, err
|
||||
}
|
||||
|
||||
if have {
|
||||
// If we had, make sure it's in our parent tipset
|
||||
cidb, err := f.byEpoch.Get(ctx, parentEpochKey)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("getting other block cid: %w", err)
|
||||
return cid.Undef, xerrors.Errorf("getting other block cid: %w", err)
|
||||
}
|
||||
|
||||
_, parent, err := cid.CidFromBytes(cidb)
|
||||
if err != nil {
|
||||
return err
|
||||
return cid.Undef, err
|
||||
}
|
||||
|
||||
var found bool
|
||||
@ -73,45 +73,45 @@ func (f *SlashFilter) MinedBlock(ctx context.Context, bh *types.BlockHeader, par
|
||||
}
|
||||
|
||||
if !found {
|
||||
return xerrors.Errorf("produced block would trigger 'parent-grinding fault' consensus fault; miner: %s; bh: %s, expected parent: %s", bh.Miner, bh.Cid(), parent)
|
||||
return parent, xerrors.Errorf("produced block would trigger 'parent-grinding fault' consensus fault; miner: %s; bh: %s, expected parent: %s", bh.Miner, bh.Cid(), parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := f.byParents.Put(ctx, parentsKey, bh.Cid().Bytes()); err != nil {
|
||||
return xerrors.Errorf("putting byEpoch entry: %w", err)
|
||||
return cid.Undef, xerrors.Errorf("putting byEpoch entry: %w", err)
|
||||
}
|
||||
|
||||
if err := f.byEpoch.Put(ctx, epochKey, bh.Cid().Bytes()); err != nil {
|
||||
return xerrors.Errorf("putting byEpoch entry: %w", err)
|
||||
return cid.Undef, xerrors.Errorf("putting byEpoch entry: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return cid.Undef, nil
|
||||
}
|
||||
|
||||
func checkFault(ctx context.Context, t ds.Datastore, key ds.Key, bh *types.BlockHeader, faultType string) error {
|
||||
func checkFault(ctx context.Context, t ds.Datastore, key ds.Key, bh *types.BlockHeader, faultType string) (cid.Cid, error) {
|
||||
fault, err := t.Has(ctx, key)
|
||||
if err != nil {
|
||||
return err
|
||||
return cid.Undef, xerrors.Errorf("failed to read from datastore: %w", err)
|
||||
}
|
||||
|
||||
if fault {
|
||||
cidb, err := t.Get(ctx, key)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("getting other block cid: %w", err)
|
||||
return cid.Undef, xerrors.Errorf("getting other block cid: %w", err)
|
||||
}
|
||||
|
||||
_, other, err := cid.CidFromBytes(cidb)
|
||||
if err != nil {
|
||||
return err
|
||||
return cid.Undef, err
|
||||
}
|
||||
|
||||
if other == bh.Cid() {
|
||||
return nil
|
||||
return cid.Undef, nil
|
||||
}
|
||||
|
||||
return xerrors.Errorf("produced block would trigger '%s' consensus fault; miner: %s; bh: %s, other: %s", faultType, bh.Miner, bh.Cid(), other)
|
||||
return other, xerrors.Errorf("produced block would trigger '%s' consensus fault; miner: %s; bh: %s, other: %s", faultType, bh.Miner, bh.Cid(), other)
|
||||
}
|
||||
|
||||
return nil
|
||||
return cid.Undef, nil
|
||||
}
|
||||
|
@ -16,9 +16,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/DataDog/zstd"
|
||||
"github.com/ipfs/go-cid"
|
||||
levelds "github.com/ipfs/go-ds-leveldb"
|
||||
metricsprom "github.com/ipfs/go-metrics-prometheus"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/multiformats/go-multiaddr"
|
||||
ldbopts "github.com/syndtr/goleveldb/leveldb/opt"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.opencensus.io/plugin/runmetrics"
|
||||
"go.opencensus.io/stats"
|
||||
@ -27,13 +30,19 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
"gopkg.in/cheggaaa/pb.v1"
|
||||
|
||||
"github.com/filecoin-project/go-address"
|
||||
cborutil "github.com/filecoin-project/go-cbor-util"
|
||||
"github.com/filecoin-project/go-jsonrpc"
|
||||
"github.com/filecoin-project/go-paramfetch"
|
||||
"github.com/filecoin-project/go-state-types/builtin"
|
||||
"github.com/filecoin-project/specs-actors/actors/builtin/miner"
|
||||
|
||||
lapi "github.com/filecoin-project/lotus/api"
|
||||
"github.com/filecoin-project/lotus/build"
|
||||
"github.com/filecoin-project/lotus/chain/actors"
|
||||
"github.com/filecoin-project/lotus/chain/consensus"
|
||||
"github.com/filecoin-project/lotus/chain/consensus/filcns"
|
||||
"github.com/filecoin-project/lotus/chain/gen/slashfilter"
|
||||
"github.com/filecoin-project/lotus/chain/index"
|
||||
"github.com/filecoin-project/lotus/chain/stmgr"
|
||||
"github.com/filecoin-project/lotus/chain/store"
|
||||
@ -160,6 +169,19 @@ var DaemonCmd = &cli.Command{
|
||||
Name: "restore-config",
|
||||
Usage: "config file to use when restoring from backup",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "slash-consensus",
|
||||
Usage: "Report consensus fault",
|
||||
Value: false,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "slasher-sender",
|
||||
Usage: "optionally specify the account to report consensus from",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "slashdb-dir",
|
||||
Value: "slash watch db dir path",
|
||||
},
|
||||
},
|
||||
Action: func(cctx *cli.Context) error {
|
||||
isLite := cctx.Bool("lite")
|
||||
@ -380,7 +402,14 @@ var DaemonCmd = &cli.Command{
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start json-rpc endpoint: %s", err)
|
||||
}
|
||||
|
||||
if cctx.IsSet("slash-consensus") && cctx.IsSet("slashdb-dir") {
|
||||
go func() {
|
||||
err := slashConsensus(api, cctx.String("slashdb-dir"), cctx.String("slasher-sender"))
|
||||
if err != nil {
|
||||
panic("slashConsensus error")
|
||||
}
|
||||
}()
|
||||
}
|
||||
// Monitor for shutdown.
|
||||
finishCh := node.MonitorShutdown(shutdownChan,
|
||||
node.ShutdownHandler{Component: "rpc server", StopFunc: rpcStopper},
|
||||
@ -574,3 +603,122 @@ func ImportChain(ctx context.Context, r repo.Repo, fname string, snapshot bool)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func slashConsensus(a lapi.FullNode, p string, from string) error {
|
||||
ctx := context.Background()
|
||||
var fromAddr address.Address
|
||||
|
||||
ds, err := levelds.NewDatastore(p, &levelds.Options{
|
||||
Compression: ldbopts.NoCompression,
|
||||
NoSync: false,
|
||||
Strict: ldbopts.StrictAll,
|
||||
ReadOnly: false,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("open leveldb: %w", err)
|
||||
}
|
||||
sf := slashfilter.New(ds)
|
||||
if from == "" {
|
||||
defaddr, err := a.WalletDefaultAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fromAddr = defaddr
|
||||
} else {
|
||||
addr, err := address.NewFromString(from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fromAddr = addr
|
||||
}
|
||||
|
||||
blocks, err := a.SyncIncomingBlocks(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("sync incoming blocks failed: %w", err)
|
||||
}
|
||||
for block := range blocks {
|
||||
log.Infof("deal with block: %d, %v, %s", block.Height, block.Miner, block.Cid())
|
||||
if otherBlock, extraBlock, err := slashFilterMinedBlock(ctx, sf, a, block); err != nil {
|
||||
if otherBlock == nil {
|
||||
continue
|
||||
}
|
||||
log.Errorf("<!!> SLASH FILTER ERROR: %s", err)
|
||||
bh1, err := cborutil.Dump(otherBlock)
|
||||
if err != nil {
|
||||
log.Errorf("could not dump otherblock:%s, err:%s", otherBlock.Cid(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
bh2, err := cborutil.Dump(block)
|
||||
if err != nil {
|
||||
log.Errorf("could not dump block:%s, err:%s", block.Cid(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
params := miner.ReportConsensusFaultParams{
|
||||
BlockHeader1: bh1,
|
||||
BlockHeader2: bh2,
|
||||
}
|
||||
if extraBlock != nil {
|
||||
be, err := cborutil.Dump(extraBlock)
|
||||
if err != nil {
|
||||
log.Errorf("could not dump block:%s, err:%s", block.Cid(), err)
|
||||
continue
|
||||
}
|
||||
params.BlockHeaderExtra = be
|
||||
}
|
||||
|
||||
enc, err := actors.SerializeParams(¶ms)
|
||||
if err != nil {
|
||||
log.Errorf("could not serialize declare faults parameters: %s", err)
|
||||
continue
|
||||
}
|
||||
message, err := a.MpoolPushMessage(ctx, &types.Message{
|
||||
To: block.Miner,
|
||||
From: fromAddr,
|
||||
Value: types.NewInt(0),
|
||||
Method: builtin.MethodsMiner.ReportConsensusFault,
|
||||
Params: enc,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
log.Errorf("ReportConsensusFault to messagepool error:%w", err)
|
||||
continue
|
||||
}
|
||||
log.Infof("ReportConsensusFault message CID:%s", message.Cid())
|
||||
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func slashFilterMinedBlock(ctx context.Context, sf *slashfilter.SlashFilter, a lapi.FullNode, blockB *types.BlockHeader) (*types.BlockHeader, *types.BlockHeader, error) {
|
||||
blockC, err := a.ChainGetBlock(ctx, blockB.Parents[0])
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("chain get block error:%s", err)
|
||||
}
|
||||
otherCid, err := sf.MinedBlock(ctx, blockB, blockC.Height)
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("slash filter check block error:%s", err)
|
||||
}
|
||||
if otherCid != cid.Undef {
|
||||
otherHeader, err := a.ChainGetBlock(ctx, otherCid)
|
||||
return otherHeader, nil, xerrors.Errorf("chain get other block error:%s", err)
|
||||
}
|
||||
blockA, err := a.ChainGetBlock(ctx, otherCid)
|
||||
|
||||
// (c) parent-grinding fault
|
||||
// Here extra is the "witness", a third block that shows the connection between A and B as
|
||||
// A's sibling and B's parent.
|
||||
// Specifically, since A is of lower height, it must be that B was mined omitting A from its tipset
|
||||
//
|
||||
// B
|
||||
// |
|
||||
// [A, C]
|
||||
if types.CidArrsEqual(blockA.Parents, blockC.Parents) && blockA.Height == blockC.Height &&
|
||||
types.CidArrsContains(blockB.Parents, blockC.Cid()) && !types.CidArrsContains(blockB.Parents, blockA.Cid()) {
|
||||
return blockA, blockC, xerrors.Errorf("chain get other block error:%s", err)
|
||||
}
|
||||
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
@ -74,6 +74,9 @@ OPTIONS:
|
||||
--api-max-req-size value maximum API request size accepted by the JSON RPC server (default: 0)
|
||||
--restore value restore from backup file
|
||||
--restore-config value config file to use when restoring from backup
|
||||
--slash-consensus Report consensus fault (default: false)
|
||||
--slasher-sender value optionally specify the account to report consensus from
|
||||
--slashdb-dir value (default: "slash watch db dir path")
|
||||
--help, -h show help
|
||||
```
|
||||
|
||||
|
@ -324,7 +324,7 @@ minerLoop:
|
||||
"block-time", btime, "time", build.Clock.Now(), "difference", build.Clock.Since(btime))
|
||||
}
|
||||
|
||||
if err := m.sf.MinedBlock(ctx, b.Header, base.TipSet.Height()+base.NullRounds); err != nil {
|
||||
if _, err = m.sf.MinedBlock(ctx, b.Header, base.TipSet.Height()+base.NullRounds); err != nil {
|
||||
log.Errorf("<!!> SLASH FILTER ERROR: %s", err)
|
||||
if os.Getenv("LOTUS_MINER_NO_SLASHFILTER") != "_yes_i_know_i_can_and_probably_will_lose_all_my_fil_and_power_" {
|
||||
continue
|
||||
|
@ -58,7 +58,7 @@ func (a *SyncAPI) SyncSubmitBlock(ctx context.Context, blk *types.BlockMsg) erro
|
||||
}
|
||||
|
||||
if a.SlashFilter != nil && os.Getenv("LOTUS_NO_SLASHFILTER") != "_yes_i_know_i_can_and_probably_will_lose_all_my_fil_and_power_" {
|
||||
if err := a.SlashFilter.MinedBlock(ctx, blk.Header, parent.Height); err != nil {
|
||||
if _, err = a.SlashFilter.MinedBlock(ctx, blk.Header, parent.Height); err != nil {
|
||||
log.Errorf("<!!> SLASH FILTER ERROR: %s", err)
|
||||
return xerrors.Errorf("<!!> SLASH FILTER ERROR: %w", err)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user