Consensus filter

This commit is contained in:
Łukasz Magiera 2020-08-06 03:14:13 +02:00
parent 6b8f27264b
commit f8b8ecc0c3
9 changed files with 153 additions and 5 deletions

View File

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

View File

@ -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 {

View File

@ -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)

View File

@ -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)
}

View File

@ -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

View File

@ -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 {

View File

@ -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)
}

View File

@ -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 {

View File

@ -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,
})
}