Consensus filter
This commit is contained in:
parent
6b8f27264b
commit
f8b8ecc0c3
112
chain/gen/slashfilter/slashfilter.go
Normal file
112
chain/gen/slashfilter/slashfilter.go
Normal 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
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user