Skip to content

Commit

Permalink
Patch/block validation 0.2.3 (#2958)
Browse files Browse the repository at this point in the history
* add block validation interfaces
* plumb block validation interfaces to test and sync
* move block syntax validation to fetcher
* add timestamp to block
* move blocktime onto consensus and plumb
* move the blocktime default to the consensus package and add plumbing and porcelain cals to access it.
* add MinTimestamp to tipset and test
* add clock package and block validation
  • Loading branch information
frrist authored Jun 19, 2019
1 parent fcf56ac commit 1ccea38
Show file tree
Hide file tree
Showing 34 changed files with 517 additions and 124 deletions.
2 changes: 1 addition & 1 deletion chain/default_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func initStoreTest(ctx context.Context, t *testing.T) {
r := repo.NewInMemoryRepo()
bs := bstore.NewBlockstore(r.Datastore())
cst := hamt.NewCborStore()
con := consensus.NewExpected(cst, bs, th.NewTestProcessor(), powerTable, genCid, proofs.NewFakeVerifier(true, nil))
con := consensus.NewExpected(cst, bs, th.NewTestProcessor(), th.NewFakeBlockValidator(), powerTable, genCid, proofs.NewFakeVerifier(true, nil), th.BlockTimeTest)
initSyncTest(t, con, initGenesis, cst, bs, r)
requireSetTestChain(t, con, true)
}
Expand Down
10 changes: 7 additions & 3 deletions chain/default_syncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,8 @@ func (syncer *DefaultSyncer) collectChain(ctx context.Context, tipsetCids types.
return nil, err
}

ts, err := syncer.consensus.NewValidTipSet(ctx, blks)
ts, err := types.NewTipSet(blks...)
if err != nil {
syncer.badTipSets.Add(tsKey)
syncer.badTipSets.AddChain(chain)
return nil, err
}

Expand Down Expand Up @@ -195,6 +193,12 @@ func (syncer *DefaultSyncer) syncOne(ctx context.Context, parent, next types.Tip
return nil
}

for _, b := range next.ToSlice() {
if err := syncer.consensus.ValidateSemantic(ctx, b, &parent); err != nil {
return err
}
}

// Lookup parent state. It is guaranteed by the syncer that it is in
// the chainStore.
st, err := syncer.tipSetState(ctx, parent.ToSortedCidSet())
Expand Down
10 changes: 5 additions & 5 deletions chain/default_syncer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func loadSyncerFromRepo(t *testing.T, r repo.Repo) (*chain.DefaultSyncer, *th.Te
bs := bstore.NewBlockstore(r.Datastore())
cst := hamt.NewCborStore()
verifier := proofs.NewFakeVerifier(true, nil)
con := consensus.NewExpected(cst, bs, th.NewTestProcessor(), powerTable, genCid, verifier)
con := consensus.NewExpected(cst, bs, th.NewTestProcessor(), th.NewFakeBlockValidator(), powerTable, genCid, verifier, th.BlockTimeTest)

calcGenBlk, err := initGenesis(cst, bs) // flushes state
require.NoError(t, err)
Expand All @@ -210,7 +210,7 @@ func initSyncTestDefault(t *testing.T) (*chain.DefaultSyncer, chain.Store, repo.
bs := bstore.NewBlockstore(r.Datastore())
cst := hamt.NewCborStore()
verifier := proofs.NewFakeVerifier(true, nil)
con := consensus.NewExpected(cst, bs, processor, powerTable, genCid, verifier)
con := consensus.NewExpected(cst, bs, processor, th.NewFakeBlockValidator(), powerTable, genCid, verifier, th.BlockTimeTest)
requireSetTestChain(t, con, false)
return initSyncTest(t, con, initGenesis, cst, bs, r)
}
Expand All @@ -223,7 +223,7 @@ func initSyncTestWithPowerTable(t *testing.T, powerTable consensus.PowerTableVie
bs := bstore.NewBlockstore(r.Datastore())
cst := hamt.NewCborStore()
verifier := proofs.NewFakeVerifier(true, nil)
con := consensus.NewExpected(cst, bs, processor, powerTable, genCid, verifier)
con := consensus.NewExpected(cst, bs, processor, th.NewFakeBlockValidator(), powerTable, genCid, verifier, th.BlockTimeTest)
requireSetTestChain(t, con, false)
sync, testchain, _, fetcher := initSyncTest(t, con, initGenesis, cst, bs, r)
return sync, testchain, con, fetcher
Expand Down Expand Up @@ -932,7 +932,7 @@ func TestTipSetWeightDeep(t *testing.T) {
chainStore := chain.NewDefaultStore(r.ChainDatastore(), cst, calcGenBlk.Cid())

verifier := proofs.NewFakeVerifier(true, nil)
con := consensus.NewExpected(cst, bs, th.NewTestProcessor(), &th.TestView{}, calcGenBlk.Cid(), verifier)
con := consensus.NewExpected(cst, bs, th.NewTestProcessor(), th.NewFakeBlockValidator(), &th.TestView{}, calcGenBlk.Cid(), verifier, th.BlockTimeTest)

// Initialize stores to contain genesis block and state
calcGenTS := th.RequireNewTipSet(t, &calcGenBlk)
Expand All @@ -951,7 +951,7 @@ func TestTipSetWeightDeep(t *testing.T) {

// Now sync the chainStore with consensus using a MarketView.
verifier = proofs.NewFakeVerifier(true, nil)
con = consensus.NewExpected(cst, bs, th.NewTestProcessor(), &consensus.MarketView{}, calcGenBlk.Cid(), verifier)
con = consensus.NewExpected(cst, bs, th.NewTestProcessor(), th.NewFakeBlockValidator(), &consensus.MarketView{}, calcGenBlk.Cid(), verifier, th.BlockTimeTest)
syncer := chain.NewDefaultSyncer(cst, con, chainStore, blockSource)
baseTS := requireHeadTipset(t, chainStore) // this is the last block of the bootstrapping chain creating miners
require.Equal(t, 1, len(baseTS))
Expand Down
24 changes: 24 additions & 0 deletions clock/clock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package clock

import (
"time"
)

// Clock defines an interface for fetching time that may be used instead of the
// time module.
type Clock interface {
Now() time.Time
}

// SystemClock delegates calls to the time package.
type SystemClock struct{}

// NewSystemClock returns a SystemClock that delegates calls to the time package.
func NewSystemClock() *SystemClock {
return &SystemClock{}
}

// Now returns the current local time.
func (bc *SystemClock) Now() time.Time {
return time.Now()
}
4 changes: 2 additions & 2 deletions commands/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"github.com/pkg/errors"

"github.com/filecoin-project/go-filecoin/config"
"github.com/filecoin-project/go-filecoin/mining"
"github.com/filecoin-project/go-filecoin/consensus"
"github.com/filecoin-project/go-filecoin/node"
"github.com/filecoin-project/go-filecoin/paths"
"github.com/filecoin-project/go-filecoin/repo"
Expand All @@ -38,7 +38,7 @@ var daemonCmd = &cmds.Command{
cmdkit.BoolOption(OfflineMode, "start the node without networking"),
cmdkit.BoolOption(ELStdout),
cmdkit.BoolOption(IsRelay, "advertise and allow filecoin network traffic to be relayed through this node"),
cmdkit.StringOption(BlockTime, "time a node waits before trying to mine the next block").WithDefault(mining.DefaultBlockTime.String()),
cmdkit.StringOption(BlockTime, "time a node waits before trying to mine the next block").WithDefault(consensus.DefaultBlockTime.String()),
},
Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error {
return daemonRun(req, re, env)
Expand Down
6 changes: 5 additions & 1 deletion commands/schema/filecoin_block.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@
"string",
"null"
]
},
"timestamp": {
"type": "string"
}
},
"required": [
Expand All @@ -74,7 +77,8 @@
"parents",
"proof",
"stateRoot",
"ticket"
"ticket",
"timestamp"
],
"type": "object"
},
Expand Down
95 changes: 95 additions & 0 deletions consensus/block_validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package consensus

import (
"context"
"fmt"
"time"

"github.com/filecoin-project/go-filecoin/clock"
"github.com/filecoin-project/go-filecoin/types"
)

// BlockValidator defines an interface used to validate a blocks syntax and
// semantics.
type BlockValidator interface {
BlockSemanticValidator
BlockSyntaxValidator
}

// BlockSemanticValidator defines an interface used to validate a blocks
// semantics.
type BlockSemanticValidator interface {
ValidateSemantic(ctx context.Context, child *types.Block, parents *types.TipSet) error
}

// BlockSyntaxValidator defines an interface used to validate a blocks
// syntax.
type BlockSyntaxValidator interface {
ValidateSyntax(ctx context.Context, blk *types.Block) error
}

// DefaultBlockValidator implements the BlockValidator interface.
type DefaultBlockValidator struct {
clock.Clock
blockTime time.Duration
}

// NewDefaultBlockValidator returns a new DefaultBlockValidator. It uses `blkTime`
// to validate blocks and uses the DefaultBlockValidationClock.
func NewDefaultBlockValidator(blkTime time.Duration, c clock.Clock) *DefaultBlockValidator {
return &DefaultBlockValidator{
Clock: c,
blockTime: blkTime,
}
}

// ValidateSemantic validates a block is correctly derived from its parent.
func (dv *DefaultBlockValidator) ValidateSemantic(ctx context.Context, child *types.Block, parents *types.TipSet) error {
pmin, err := parents.MinTimestamp()
if err != nil {
return err
}

ph, err := parents.Height()
if err != nil {
return err
}

if uint64(child.Height) <= ph {
return fmt.Errorf("block %s has invalid height %d", child.Cid().String(), child.Height)
}

// check that child is appropriately delayed from its parents including
// null blocks.
// TODO replace check on height when #2222 lands
limit := uint64(pmin) + uint64(dv.BlockTime().Seconds())*(uint64(child.Height)-ph)
if uint64(child.Timestamp) < limit {
return fmt.Errorf("block %s with timestamp %d generated too far past parent, expected timestamp < %d", child.Cid().String(), child.Timestamp, limit)
}
return nil
}

// ValidateSyntax validates a single block is correctly formed.
func (dv *DefaultBlockValidator) ValidateSyntax(ctx context.Context, blk *types.Block) error {
now := uint64(dv.Now().Unix())
if uint64(blk.Timestamp) > now {
return fmt.Errorf("block %s with timestamp %d generate in future at time %d", blk.Cid().String(), blk.Timestamp, now)
}
if !blk.StateRoot.Defined() {
return fmt.Errorf("block %s has nil StateRoot", blk.Cid().String())
}
if blk.Miner.Empty() {
return fmt.Errorf("block %s has nil miner address", blk.Miner.String())
}
if len(blk.Ticket) == 0 {
return fmt.Errorf("block %s has nil ticket", blk.Cid().String())
}
// TODO validate block signature: 1054
return nil
}

// BlockTime returns the block time the DefaultBlockValidator uses to validate
/// blocks against.
func (dv *DefaultBlockValidator) BlockTime() time.Duration {
return dv.blockTime
}
135 changes: 135 additions & 0 deletions consensus/block_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package consensus_test

import (
"context"
"testing"
"time"

"github.com/ipfs/go-cid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/filecoin-project/go-filecoin/address"
"github.com/filecoin-project/go-filecoin/consensus"
th "github.com/filecoin-project/go-filecoin/testhelpers"
tf "github.com/filecoin-project/go-filecoin/testhelpers/testflags"
"github.com/filecoin-project/go-filecoin/types"
)

func TestBlockValidSemantic(t *testing.T) {
tf.UnitTest(t)

blockTime := consensus.DefaultBlockTime
ts := time.Unix(1234567890, 0)
mclock := th.NewFakeSystemClock(ts)
ctx := context.Background()

validator := consensus.NewDefaultBlockValidator(blockTime, mclock)

t.Run("reject block with same height as parents", func(t *testing.T) {
// passes with valid height
c := &types.Block{Height: 2, Timestamp: types.Uint64(ts.Add(blockTime).Unix())}
p := &types.Block{Height: 1, Timestamp: types.Uint64(ts.Unix())}
parents := consensus.RequireNewTipSet(require.New(t), p)
require.NoError(t, validator.ValidateSemantic(ctx, c, &parents))

// invalidate parent by matching child height
p = &types.Block{Height: 2, Timestamp: types.Uint64(ts.Unix())}
parents = consensus.RequireNewTipSet(require.New(t), p)

err := validator.ValidateSemantic(ctx, c, &parents)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid height")

})

t.Run("reject block mined too soon after parent", func(t *testing.T) {
// Passes with correct timestamp
c := &types.Block{Height: 2, Timestamp: types.Uint64(ts.Add(blockTime).Unix())}
p := &types.Block{Height: 1, Timestamp: types.Uint64(ts.Unix())}
parents := consensus.RequireNewTipSet(require.New(t), p)
require.NoError(t, validator.ValidateSemantic(ctx, c, &parents))

// fails with invalid timestamp
c = &types.Block{Height: 2, Timestamp: types.Uint64(ts.Unix())}
err := validator.ValidateSemantic(ctx, c, &parents)
assert.Error(t, err)
assert.Contains(t, err.Error(), "too far")

})

t.Run("reject block mined too soon after parent with one null block", func(t *testing.T) {
// Passes with correct timestamp
c := &types.Block{Height: 3, Timestamp: types.Uint64(ts.Add(2 * blockTime).Unix())}
p := &types.Block{Height: 1, Timestamp: types.Uint64(ts.Unix())}
parents := consensus.RequireNewTipSet(require.New(t), p)
err := validator.ValidateSemantic(ctx, c, &parents)
require.NoError(t, err)

// fail when nul block calc is off by one blocktime
c = &types.Block{Height: 3, Timestamp: types.Uint64(ts.Add(blockTime).Unix())}
err = validator.ValidateSemantic(ctx, c, &parents)
assert.Error(t, err)
assert.Contains(t, err.Error(), "too far")

// fail with same timestamp as parent
c = &types.Block{Height: 3, Timestamp: types.Uint64(ts.Unix())}
err = validator.ValidateSemantic(ctx, c, &parents)
assert.Error(t, err)
assert.Contains(t, err.Error(), "too far")

})
}

func TestBlockValidSyntax(t *testing.T) {
tf.UnitTest(t)

blockTime := consensus.DefaultBlockTime
ts := time.Unix(1234567890, 0)
mclock := th.NewFakeSystemClock(ts)

ctx := context.Background()

validator := consensus.NewDefaultBlockValidator(blockTime, mclock)

validTs := types.Uint64(ts.Unix())
validSt := types.NewCidForTestGetter()()
validAd := address.NewForTestGetter()()
validTi := []byte{1}
// create a valid block
blk := &types.Block{
Timestamp: validTs,
StateRoot: validSt,
Miner: validAd,
Ticket: validTi,
}
require.NoError(t, validator.ValidateSyntax(ctx, blk))

// below we will invalidate each part of the block, assert that it fails
// validation, then revalidate the block

// invalidate timestamp
blk.Timestamp = types.Uint64(ts.Add(time.Second).Unix())
require.Error(t, validator.ValidateSyntax(ctx, blk))
blk.Timestamp = validTs
require.NoError(t, validator.ValidateSyntax(ctx, blk))

// invalidate statetooy
blk.StateRoot = cid.Undef
require.Error(t, validator.ValidateSyntax(ctx, blk))
blk.StateRoot = validSt
require.NoError(t, validator.ValidateSyntax(ctx, blk))

// invalidate miner address
blk.Miner = address.Undef
require.Error(t, validator.ValidateSyntax(ctx, blk))
blk.Miner = validAd
require.NoError(t, validator.ValidateSyntax(ctx, blk))

// invalidate ticket
blk.Ticket = []byte{}
require.Error(t, validator.ValidateSyntax(ctx, blk))
blk.Ticket = validTi
require.NoError(t, validator.ValidateSyntax(ctx, blk))

}
Loading

0 comments on commit 1ccea38

Please sign in to comment.