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
|
// Here extra is the "witness", a third block that shows the connection between A and B as
|
||||||
// A's sibling and B's parent.
|
// 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
|
// 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
|
var blockC types.BlockHeader
|
||||||
if len(extra) > 0 {
|
if len(extra) > 0 {
|
||||||
if decodeErr := blockC.UnmarshalCBOR(bytes.NewReader(extra)); decodeErr != nil {
|
if decodeErr := blockC.UnmarshalCBOR(bytes.NewReader(extra)); decodeErr != nil {
|
||||||
|
@ -37,6 +37,7 @@ import (
|
|||||||
lapi "github.com/filecoin-project/lotus/api"
|
lapi "github.com/filecoin-project/lotus/api"
|
||||||
"github.com/filecoin-project/lotus/build"
|
"github.com/filecoin-project/lotus/build"
|
||||||
"github.com/filecoin-project/lotus/chain/actors"
|
"github.com/filecoin-project/lotus/chain/actors"
|
||||||
|
"github.com/filecoin-project/lotus/chain/gen/slashfilter"
|
||||||
"github.com/filecoin-project/lotus/chain/types"
|
"github.com/filecoin-project/lotus/chain/types"
|
||||||
lcli "github.com/filecoin-project/lotus/cli"
|
lcli "github.com/filecoin-project/lotus/cli"
|
||||||
"github.com/filecoin-project/lotus/genesis"
|
"github.com/filecoin-project/lotus/genesis"
|
||||||
@ -445,7 +446,7 @@ func storageMinerInit(ctx context.Context, cctx *cli.Context, api lapi.FullNode,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
m := miner.NewMiner(api, epp, a)
|
m := miner.NewMiner(api, epp, a, slashfilter.New(mds))
|
||||||
{
|
{
|
||||||
if err := m.Start(ctx); err != nil {
|
if err := m.Start(ctx); err != nil {
|
||||||
return xerrors.Errorf("failed to start up genesis miner: %w", err)
|
return xerrors.Errorf("failed to start up genesis miner: %w", err)
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/filecoin-project/lotus/chain/gen/slashfilter"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ func randTimeOffset(width time.Duration) time.Duration {
|
|||||||
return val - (width / 2)
|
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)
|
arc, err := lru.NewARC(10000)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -60,6 +61,8 @@ func NewMiner(api api.FullNode, epp gen.WinningPoStProver, addr address.Address)
|
|||||||
|
|
||||||
return func(bool, error) {}, 0, nil
|
return func(bool, error) {}, 0, nil
|
||||||
},
|
},
|
||||||
|
|
||||||
|
sf: sf,
|
||||||
minedBlockHeights: arc,
|
minedBlockHeights: arc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -78,6 +81,7 @@ type Miner struct {
|
|||||||
|
|
||||||
lastWork *MiningBase
|
lastWork *MiningBase
|
||||||
|
|
||||||
|
sf *slashfilter.SlashFilter
|
||||||
minedBlockHeights *lru.ARCCache
|
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))
|
"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)
|
blkKey := fmt.Sprintf("%d", b.Header.Height)
|
||||||
if _, ok := m.minedBlockHeights.Get(blkKey); ok {
|
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)
|
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)
|
m.minedBlockHeights.Add(blkKey, true)
|
||||||
|
|
||||||
if err := m.api.SyncSubmitBlock(ctx, b); err != nil {
|
if err := m.api.SyncSubmitBlock(ctx, b); err != nil {
|
||||||
log.Errorf("failed to submit newly mined block: %s", err)
|
log.Errorf("failed to submit newly mined block: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package node
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/filecoin-project/lotus/chain/gen/slashfilter"
|
||||||
"github.com/filecoin-project/lotus/markets/dealfilter"
|
"github.com/filecoin-project/lotus/markets/dealfilter"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -214,6 +215,7 @@ func Online() Option {
|
|||||||
libp2p(),
|
libp2p(),
|
||||||
|
|
||||||
// common
|
// common
|
||||||
|
Override(new(*slashfilter.SlashFilter), modules.NewSlashFilter),
|
||||||
|
|
||||||
// Full node
|
// Full node
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package full
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"github.com/filecoin-project/lotus/chain/gen/slashfilter"
|
||||||
|
|
||||||
cid "github.com/ipfs/go-cid"
|
cid "github.com/ipfs/go-cid"
|
||||||
pubsub "github.com/libp2p/go-libp2p-pubsub"
|
pubsub "github.com/libp2p/go-libp2p-pubsub"
|
||||||
@ -18,6 +19,7 @@ import (
|
|||||||
type SyncAPI struct {
|
type SyncAPI struct {
|
||||||
fx.In
|
fx.In
|
||||||
|
|
||||||
|
SlashFilter *slashfilter.SlashFilter
|
||||||
Syncer *chain.Syncer
|
Syncer *chain.Syncer
|
||||||
PubSub *pubsub.PubSub
|
PubSub *pubsub.PubSub
|
||||||
NetName dtypes.NetworkName
|
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 {
|
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?
|
// TODO: should we have some sort of fast path to adding a local block?
|
||||||
bmsgs, err := a.Syncer.ChainStore().LoadMessagesFromCids(blk.BlsMessages)
|
bmsgs, err := a.Syncer.ChainStore().LoadMessagesFromCids(blk.BlsMessages)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -3,6 +3,7 @@ package modules
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"github.com/filecoin-project/lotus/chain/gen/slashfilter"
|
||||||
|
|
||||||
"github.com/ipfs/go-bitswap"
|
"github.com/ipfs/go-bitswap"
|
||||||
"github.com/ipfs/go-bitswap/network"
|
"github.com/ipfs/go-bitswap/network"
|
||||||
@ -167,3 +168,7 @@ func NewSyncer(lc fx.Lifecycle, sm *stmgr.StateManager, bsync *blocksync.BlockSy
|
|||||||
})
|
})
|
||||||
return syncer, nil
|
return syncer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewSlashFilter(ds dtypes.MetadataDS) *slashfilter.SlashFilter {
|
||||||
|
return slashfilter.New(ds)
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/filecoin-project/go-fil-markets/storagemarket/impl/funds"
|
"github.com/filecoin-project/go-fil-markets/storagemarket/impl/funds"
|
||||||
|
"github.com/filecoin-project/lotus/chain/gen/slashfilter"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -311,13 +312,13 @@ func StagingGraphsync(mctx helpers.MetricsCtx, lc fx.Lifecycle, ibs dtypes.Stagi
|
|||||||
return gs
|
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)
|
minerAddr, err := minerAddrFromDS(ds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
m := miner.NewMiner(api, epp, minerAddr)
|
m := miner.NewMiner(api, epp, minerAddr, sf)
|
||||||
|
|
||||||
lc.Append(fx.Hook{
|
lc.Append(fx.Hook{
|
||||||
OnStart: func(ctx context.Context) error {
|
OnStart: func(ctx context.Context) error {
|
||||||
|
@ -37,6 +37,8 @@ func badgerDs(path string) (datastore.Batching, error) {
|
|||||||
func levelDs(path string) (datastore.Batching, error) {
|
func levelDs(path string) (datastore.Batching, error) {
|
||||||
return levelds.NewDatastore(path, &levelds.Options{
|
return levelds.NewDatastore(path, &levelds.Options{
|
||||||
Compression: ldbopts.NoCompression,
|
Compression: ldbopts.NoCompression,
|
||||||
|
NoSync: false,
|
||||||
|
Strict: ldbopts.StrictAll,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user