Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/block validation #2899

Merged
merged 11 commits into from
Jun 19, 2019
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()
}
66 changes: 37 additions & 29 deletions consensus/block_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"time"

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

Expand All @@ -27,56 +28,63 @@ type BlockSyntaxValidator interface {
ValidateSyntax(ctx context.Context, blk *types.Block) error
}

// BlockValidationClock defines an interface for fetching unix epoch time.
type BlockValidationClock interface {
EpochSeconds() uint64
}

// DefaultBlockValidationClock implements BlockValidationClock using the
// Go time package.
type DefaultBlockValidationClock struct{}

// NewDefaultBlockValidationClock returns a DefaultBlockValidationClock.
func NewDefaultBlockValidationClock() *DefaultBlockValidationClock {
return &DefaultBlockValidationClock{}
}

// EpochSeconds returns Unix time, the number of seconds elapsed since January 1, 1970 UTC.
// The result does not depend on location.
func (ebc *DefaultBlockValidationClock) EpochSeconds() uint64 {
return uint64(time.Now().Unix())
}

// DefaultBlockValidator implements the BlockValidator interface.
type DefaultBlockValidator struct {
clock BlockValidationClock
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) *DefaultBlockValidator {
func NewDefaultBlockValidator(blkTime time.Duration, c clock.Clock) *DefaultBlockValidator {
return &DefaultBlockValidator{
clock: NewDefaultBlockValidationClock(),
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 {
// TODO validate timestamp
// #2886
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 has nil StateRoot")
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 timestamp
// TODO validate block signature
// #2886
// TODO validate block signature: 1054
return nil
}

Expand Down
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))

}
2 changes: 2 additions & 0 deletions mining/block_generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ func (w *DefaultWorker) Generate(ctx context.Context,
Proof: proof,
StateRoot: newStateTreeCid,
Ticket: ticket,
// TODO when #2961 is resolved do the needful here.
Timestamp: types.Uint64(time.Now().Unix()),
}

for i, msg := range res.PermanentFailures {
Expand Down
4 changes: 3 additions & 1 deletion node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"github.com/filecoin-project/go-filecoin/actor/builtin"
"github.com/filecoin-project/go-filecoin/address"
"github.com/filecoin-project/go-filecoin/chain"
"github.com/filecoin-project/go-filecoin/clock"
"github.com/filecoin-project/go-filecoin/config"
"github.com/filecoin-project/go-filecoin/consensus"
"github.com/filecoin-project/go-filecoin/core"
Expand Down Expand Up @@ -382,7 +383,8 @@ func (nc *Config) Build(ctx context.Context) (*Node, error) {
pingService := ping.NewPingService(peerHost)

// setup block validation
blkValid := consensus.NewDefaultBlockValidator(nc.BlockTime)
// TODO when #2961 is resolved do the needful here.
blkValid := consensus.NewDefaultBlockValidator(nc.BlockTime, clock.NewSystemClock())

// set up bitswap
nwork := bsnet.NewFromIpfsHost(peerHost, router)
Expand Down
29 changes: 29 additions & 0 deletions testhelpers/clock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package testhelpers

import (
"time"
)

// FakeSystemClock returns a mocked clock implementation that may be manually
// set for testing things related to time.
type FakeSystemClock struct {
now time.Time
}

// NewFakeSystemClock returns a mocked clock implementation that may be manually
// set for testing things related to time.
func NewFakeSystemClock(n time.Time) *FakeSystemClock {
return &FakeSystemClock{
now: n,
}
}

// Now returns the current value of the FakeSystemClock.
func (mc *FakeSystemClock) Now() time.Time {
return mc.now
}

// Set sets the current time value of the FakeSystemClock.
func (mc *FakeSystemClock) Set(t time.Time) {
mc.now = t
}
14 changes: 14 additions & 0 deletions types/tipset.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,20 @@ func (ts TipSet) MinTicket() (Signature, error) {
return ts.blocks[0].Ticket, nil
}

// MinTimestamp returns the smallest timestamp of all blocks in the tipset.
func (ts TipSet) MinTimestamp() (Uint64, error) {
frrist marked this conversation as resolved.
Show resolved Hide resolved
if len(ts.blocks) == 0 {
return 0, errUndefTipSet
}
min := ts.blocks[0].Timestamp
for i := 1; i < len(ts.blocks); i++ {
if ts.blocks[i].Timestamp < min {
min = ts.blocks[i].Timestamp
}
}
return min, nil
}

// Height returns the height of a tipset.
func (ts TipSet) Height() (uint64, error) {
if len(ts.blocks) == 0 {
Expand Down
18 changes: 12 additions & 6 deletions types/tipset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func init() {
mockSignerForTest, _ = NewMockSignersAndKeyInfo(2)
}

func block(t *testing.T, ticket []byte, height int, parentCid cid.Cid, parentWeight uint64, msg string) *Block {
func block(t *testing.T, ticket []byte, height int, parentCid cid.Cid, parentWeight, timestamp uint64, msg string) *Block {
addrGetter := address.NewForTestGetter()

m1 := NewMessage(mockSignerForTest.Addresses[0], addrGetter(), 0, NewAttoFILFromFIL(10), "hello", []byte(msg))
Expand All @@ -45,6 +45,7 @@ func block(t *testing.T, ticket []byte, height int, parentCid cid.Cid, parentWei
Messages: []*SignedMessage{sm1},
StateRoot: SomeCid(),
MessageReceipts: []*MessageReceipt{{ExitCode: 1, Return: [][]byte{ret}}},
Timestamp: Uint64(timestamp),
}
}

Expand All @@ -68,8 +69,8 @@ func TestTipSet(t *testing.T) {
})

t.Run("order breaks ties with CID", func(t *testing.T) {
b1 := block(t, []byte{1}, 1, cid1, parentWeight, "1")
b2 := block(t, []byte{1}, 1, cid1, parentWeight, "2")
b1 := block(t, []byte{1}, 1, cid1, parentWeight, 1, "1")
b2 := block(t, []byte{1}, 1, cid1, parentWeight, 2, "2")

ts := RequireNewTipSet(t, b1, b2)
if bytes.Compare(b1.Cid().Bytes(), b2.Cid().Bytes()) < 0 {
Expand Down Expand Up @@ -121,6 +122,11 @@ func TestTipSet(t *testing.T) {
assert.Equal(t, b1.Ticket, tsTicket)
})

t.Run("min timestamp", func(t *testing.T) {
tsTime, _ := RequireNewTipSet(t, b1, b2, b3).MinTimestamp()
assert.Equal(t, b1.Timestamp, tsTime)
})
frrist marked this conversation as resolved.
Show resolved Hide resolved

t.Run("equality", func(t *testing.T) {
ts1a := RequireNewTipSet(t, b3, b2, b1)
ts1b := RequireNewTipSet(t, b1, b2, b3)
Expand Down Expand Up @@ -198,8 +204,8 @@ func TestTipSet(t *testing.T) {

// Test methods: String, ToSortedCidSet, ToSlice, MinTicket, Height, NewTipSet, Equals
func makeTestBlocks(t *testing.T) (*Block, *Block, *Block) {
b1 := block(t, []byte{1}, 1, cid1, parentWeight, "1")
b2 := block(t, []byte{2}, 1, cid1, parentWeight, "2")
b3 := block(t, []byte{3}, 1, cid1, parentWeight, "3")
b1 := block(t, []byte{1}, 1, cid1, parentWeight, 1, "1")
b2 := block(t, []byte{2}, 1, cid1, parentWeight, 2, "2")
b3 := block(t, []byte{3}, 1, cid1, parentWeight, 3, "3")
return b1, b2, b3
}