From f8b8ecc0c30be3cab73e64c57a9beb524806a22e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Thu, 6 Aug 2020 03:14:13 +0200 Subject: [PATCH] Consensus filter --- chain/gen/slashfilter/slashfilter.go | 112 +++++++++++++++++++++++++++ chain/vm/syscalls.go | 4 + cmd/lotus-storage-miner/init.go | 3 +- miner/miner.go | 13 +++- node/builder.go | 2 + node/impl/full/sync.go | 12 +++ node/modules/chain.go | 5 ++ node/modules/storageminer.go | 5 +- node/repo/fsrepo_ds.go | 2 + 9 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 chain/gen/slashfilter/slashfilter.go diff --git a/chain/gen/slashfilter/slashfilter.go b/chain/gen/slashfilter/slashfilter.go new file mode 100644 index 000000000..e5a0de63e --- /dev/null +++ b/chain/gen/slashfilter/slashfilter.go @@ -0,0 +1,112 @@ +package slashfilter + +import ( + "fmt" + + "golang.org/x/xerrors" + + "github.com/ipfs/go-cid" + ds "github.com/ipfs/go-datastore" + "github.com/ipfs/go-datastore/namespace" + + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/specs-actors/actors/abi" +) + +type SlashFilter struct { + byEpoch ds.Datastore // double-fork mining faults, parent-grinding fault + byParents ds.Datastore // time-offset mining faults +} + +func New(dstore ds.Batching) *SlashFilter { + return &SlashFilter{ + byEpoch: namespace.Wrap(dstore, ds.NewKey("/slashfilter/epoch")), + byParents: namespace.Wrap(dstore, ds.NewKey("/slashfilter/parents")), + } +} + +func (f *SlashFilter) MinedBlock(bh *types.BlockHeader, parentEpoch abi.ChainEpoch) error { + epochKey := ds.NewKey(fmt.Sprintf("/%s/%d", bh.Miner, bh.Height)) + { + // double-fork mining (2 blocks at one epoch) + if err := checkFault(f.byEpoch, epochKey, bh, "double-fork mining faults"); err != nil { + return 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(f.byParents, parentsKey, bh, "time-offset mining faults"); err != nil { + return err + } + } + + { + // parent-grinding fault (didn't mine on top of our own block) + + // First check if we have mined a block on the parent epoch + parentEpochKey := ds.NewKey(fmt.Sprintf("/%s/%d", bh.Miner, parentEpoch)) + have, err := f.byEpoch.Has(parentEpochKey) + if err != nil { + return err + } + + if have { + // If we had, make sure it's in our parent tipset + cidb, err := f.byEpoch.Get(parentEpochKey) + if err != nil { + return xerrors.Errorf("getting other block cid: %w", err) + } + + _, parent, err := cid.CidFromBytes(cidb) + if err != nil { + return err + } + + var found bool + for _, c := range bh.Parents { + if c.Equals(parent) { + found = true + } + } + + if !found { + return xerrors.Errorf("produced block would trigger 'parent-grinding fault' consensus fault; bh: %s, expected parent: %s", bh.Cid(), parent) + } + } + } + + if err := f.byParents.Put(parentsKey, bh.Cid().Bytes()); err != nil { + return xerrors.Errorf("putting byEpoch entry: %w", err) + } + + if err := f.byEpoch.Put(epochKey, bh.Cid().Bytes()); err != nil { + return xerrors.Errorf("putting byEpoch entry: %w", err) + } + + return nil +} + +func checkFault(t ds.Datastore, key ds.Key, bh *types.BlockHeader, faultType string) error { + fault, err := t.Has(key) + if err != nil { + return err + } + + if fault { + cidb, err := t.Get(key) + if err != nil { + return xerrors.Errorf("getting other block cid: %w", err) + } + + _, other, err := cid.CidFromBytes(cidb) + if err != nil { + return err + } + + return xerrors.Errorf("produced block would trigger '%s' consensus fault; bh: %s, other: %s", faultType, bh.Cid(), other) + } + + return nil +} diff --git a/chain/vm/syscalls.go b/chain/vm/syscalls.go index d25c947d6..06bf345f8 100644 --- a/chain/vm/syscalls.go +++ b/chain/vm/syscalls.go @@ -139,6 +139,10 @@ func (ss *syscallShim) VerifyConsensusFault(a, b, extra []byte) (*runtime.Consen // 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] var blockC types.BlockHeader if len(extra) > 0 { if decodeErr := blockC.UnmarshalCBOR(bytes.NewReader(extra)); decodeErr != nil { diff --git a/cmd/lotus-storage-miner/init.go b/cmd/lotus-storage-miner/init.go index adc537c8b..a1d75ac14 100644 --- a/cmd/lotus-storage-miner/init.go +++ b/cmd/lotus-storage-miner/init.go @@ -37,6 +37,7 @@ import ( 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/gen/slashfilter" "github.com/filecoin-project/lotus/chain/types" lcli "github.com/filecoin-project/lotus/cli" "github.com/filecoin-project/lotus/genesis" @@ -445,7 +446,7 @@ func storageMinerInit(ctx context.Context, cctx *cli.Context, api lapi.FullNode, return err } - m := miner.NewMiner(api, epp, a) + m := miner.NewMiner(api, epp, a, slashfilter.New(mds)) { if err := m.Start(ctx); err != nil { return xerrors.Errorf("failed to start up genesis miner: %w", err) diff --git a/miner/miner.go b/miner/miner.go index 49d6217c0..59110f384 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -6,6 +6,7 @@ import ( "crypto/rand" "encoding/binary" "fmt" + "github.com/filecoin-project/lotus/chain/gen/slashfilter" "sync" "time" @@ -39,7 +40,7 @@ func randTimeOffset(width time.Duration) time.Duration { return val - (width / 2) } -func NewMiner(api api.FullNode, epp gen.WinningPoStProver, addr address.Address) *Miner { +func NewMiner(api api.FullNode, epp gen.WinningPoStProver, addr address.Address, sf *slashfilter.SlashFilter) *Miner { arc, err := lru.NewARC(10000) if err != nil { panic(err) @@ -60,6 +61,8 @@ func NewMiner(api api.FullNode, epp gen.WinningPoStProver, addr address.Address) return func(bool, error) {}, 0, nil }, + + sf: sf, minedBlockHeights: arc, } } @@ -78,6 +81,7 @@ type Miner struct { lastWork *MiningBase + sf *slashfilter.SlashFilter minedBlockHeights *lru.ARCCache } @@ -197,7 +201,11 @@ func (m *Miner) mine(ctx context.Context) { "block-time", btime, "time", build.Clock.Now(), "difference", build.Clock.Since(btime)) } - // TODO: should do better 'anti slash' protection here + if err := m.sf.MinedBlock(b.Header, base.TipSet.Height()+base.NullRounds); err != nil { + log.Errorf(" SLASH FILTER ERROR: %s", err) + continue + } + blkKey := fmt.Sprintf("%d", b.Header.Height) if _, ok := m.minedBlockHeights.Get(blkKey); 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) @@ -205,6 +213,7 @@ func (m *Miner) mine(ctx context.Context) { } m.minedBlockHeights.Add(blkKey, true) + if err := m.api.SyncSubmitBlock(ctx, b); err != nil { log.Errorf("failed to submit newly mined block: %s", err) } diff --git a/node/builder.go b/node/builder.go index 171c4a96e..aef0088ac 100644 --- a/node/builder.go +++ b/node/builder.go @@ -3,6 +3,7 @@ package node import ( "context" "errors" + "github.com/filecoin-project/lotus/chain/gen/slashfilter" "github.com/filecoin-project/lotus/markets/dealfilter" "time" @@ -214,6 +215,7 @@ func Online() Option { libp2p(), // common + Override(new(*slashfilter.SlashFilter), modules.NewSlashFilter), // Full node diff --git a/node/impl/full/sync.go b/node/impl/full/sync.go index c7c9baaaf..e9a15319e 100644 --- a/node/impl/full/sync.go +++ b/node/impl/full/sync.go @@ -2,6 +2,7 @@ package full import ( "context" + "github.com/filecoin-project/lotus/chain/gen/slashfilter" cid "github.com/ipfs/go-cid" pubsub "github.com/libp2p/go-libp2p-pubsub" @@ -18,6 +19,7 @@ import ( type SyncAPI struct { fx.In + SlashFilter *slashfilter.SlashFilter Syncer *chain.Syncer PubSub *pubsub.PubSub NetName dtypes.NetworkName @@ -44,6 +46,16 @@ func (a *SyncAPI) SyncState(ctx context.Context) (*api.SyncState, error) { } func (a *SyncAPI) SyncSubmitBlock(ctx context.Context, blk *types.BlockMsg) error { + parent, err := a.Syncer.ChainStore().GetBlock(blk.Header.Parents[0]) + if err != nil { + return xerrors.Errorf("loading parent block: %w", err) + } + + if err := a.SlashFilter.MinedBlock(blk.Header, parent.Height); err != nil { + log.Errorf(" SLASH FILTER ERROR: %s", err) + return xerrors.Errorf(" SLASH FILTER ERROR: %w", err) + } + // TODO: should we have some sort of fast path to adding a local block? bmsgs, err := a.Syncer.ChainStore().LoadMessagesFromCids(blk.BlsMessages) if err != nil { diff --git a/node/modules/chain.go b/node/modules/chain.go index 23f2b43fa..ac16a738c 100644 --- a/node/modules/chain.go +++ b/node/modules/chain.go @@ -3,6 +3,7 @@ package modules import ( "bytes" "context" + "github.com/filecoin-project/lotus/chain/gen/slashfilter" "github.com/ipfs/go-bitswap" "github.com/ipfs/go-bitswap/network" @@ -167,3 +168,7 @@ func NewSyncer(lc fx.Lifecycle, sm *stmgr.StateManager, bsync *blocksync.BlockSy }) return syncer, nil } + +func NewSlashFilter(ds dtypes.MetadataDS) *slashfilter.SlashFilter { + return slashfilter.New(ds) +} diff --git a/node/modules/storageminer.go b/node/modules/storageminer.go index 03a7efb9d..90013864e 100644 --- a/node/modules/storageminer.go +++ b/node/modules/storageminer.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "github.com/filecoin-project/go-fil-markets/storagemarket/impl/funds" + "github.com/filecoin-project/lotus/chain/gen/slashfilter" "net/http" "time" @@ -311,13 +312,13 @@ func StagingGraphsync(mctx helpers.MetricsCtx, lc fx.Lifecycle, ibs dtypes.Stagi return gs } -func SetupBlockProducer(lc fx.Lifecycle, ds dtypes.MetadataDS, api lapi.FullNode, epp gen.WinningPoStProver) (*miner.Miner, error) { +func SetupBlockProducer(lc fx.Lifecycle, ds dtypes.MetadataDS, api lapi.FullNode, epp gen.WinningPoStProver, sf *slashfilter.SlashFilter) (*miner.Miner, error) { minerAddr, err := minerAddrFromDS(ds) if err != nil { return nil, err } - m := miner.NewMiner(api, epp, minerAddr) + m := miner.NewMiner(api, epp, minerAddr, sf) lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { diff --git a/node/repo/fsrepo_ds.go b/node/repo/fsrepo_ds.go index 7075bda79..9da7210cc 100644 --- a/node/repo/fsrepo_ds.go +++ b/node/repo/fsrepo_ds.go @@ -37,6 +37,8 @@ func badgerDs(path string) (datastore.Batching, error) { func levelDs(path string) (datastore.Batching, error) { return levelds.NewDatastore(path, &levelds.Options{ Compression: ldbopts.NoCompression, + NoSync: false, + Strict: ldbopts.StrictAll, }) }