diff --git a/clock/clock.go b/clock/clock.go new file mode 100644 index 0000000000..3e18997af7 --- /dev/null +++ b/clock/clock.go @@ -0,0 +1,53 @@ +package clock + +import ( + "sync" + "time" +) + +// TODO this is the bare min, not wasting time to get killed in review. + +// Clock is a thing +type Clock interface { + Now() time.Time +} + +// BlockClock is a thing +type BlockClock struct{} + +// NewBlockClock is a thing +func NewBlockClock() *BlockClock { + return &BlockClock{} +} + +// Now is a thing +func (bc *BlockClock) Now() time.Time { + return time.Now() +} + +// MockClock is a thing +type MockClock struct { + nowMu sync.Mutex + now time.Time +} + +// NewMockClock is a thing +func NewMockClock(n time.Time) *MockClock { + return &MockClock{ + now: n, + } +} + +// Now is a thing +func (mc *MockClock) Now() time.Time { + mc.nowMu.Lock() + defer mc.nowMu.Unlock() + return mc.now +} + +// Set is a thing +func (mc *MockClock) Set(t time.Time) { + mc.nowMu.Lock() + defer mc.nowMu.Unlock() + mc.now = t +} diff --git a/consensus/block_validation.go b/consensus/block_validation.go index 5a54c060fc..6b2ddc189f 100644 --- a/consensus/block_validation.go +++ b/consensus/block_validation.go @@ -5,9 +5,19 @@ import ( "fmt" "time" + "github.com/pkg/errors" + + "github.com/filecoin-project/go-filecoin/clock" "github.com/filecoin-project/go-filecoin/types" ) +var ( + // ErrTooSoon is returned when a block is generated too soon after its parent. + ErrTooSoon = errors.New("block was generated too soon") + // ErrInvalidHeight is returned when a block has an invliad height. + ErrInvalidHeight = errors.New("block has invalid height") +) + // BlockValidator defines an interface used to validate a blocks syntax and // semantics. type BlockValidator interface { @@ -27,56 +37,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 ErrInvalidHeight + } + + // 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 errors.Wrapf(ErrTooSoon, "limit: %d, childTs: %d", limit, child.Timestamp) + return ErrTooSoon + } return nil } // ValidateSyntax validates a single block is correctly formed. func (dv *DefaultBlockValidator) ValidateSyntax(ctx context.Context, blk *types.Block) error { + if uint64(blk.Timestamp) > uint64(dv.Now().Unix()) { + return fmt.Errorf("block generate in future") + } if !blk.StateRoot.Defined() { return fmt.Errorf("block has nil StateRoot") } - // TODO validate timestamp - // TODO validate block signature - // #2886 + if blk.Miner.Empty() { + return fmt.Errorf("block has nil miner address") + } + if len(blk.Ticket) == 0 { + return fmt.Errorf("block has nil ticket") + } + // TODO vlidate block signature: 1054 return nil } diff --git a/consensus/block_validation_test.go b/consensus/block_validation_test.go new file mode 100644 index 0000000000..985aa4f137 --- /dev/null +++ b/consensus/block_validation_test.go @@ -0,0 +1,143 @@ +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/clock" + "github.com/filecoin-project/go-filecoin/consensus" + tf "github.com/filecoin-project/go-filecoin/testhelpers/testflags" + "github.com/filecoin-project/go-filecoin/types" +) + +// to block time converts a time to block timestamp +func tbt(t time.Time) types.Uint64 { + return types.Uint64(t.Unix()) +} + +func TestBlockValidSemantic(t *testing.T) { + tf.UnitTest(t) + + blockTime := consensus.DefaultBlockTime + ts := time.Now() + mclock := clock.NewMockClock(ts) + ctx := context.Background() + + validator := consensus.NewDefaultBlockValidator(blockTime, mclock) + //panic(fmt.Sprintf("ts: %d, addedTo: %d", tbt(ts), tbt(ts.Add(blockTime)))) + + t.Run("reject block with same height as parents", func(t *testing.T) { + c := &types.Block{Height: 1, Timestamp: tbt(ts)} + p := &types.Block{Height: 1, Timestamp: tbt(ts)} + parents, err := types.NewTipSet(p) + require.NoError(t, err) + + err = validator.ValidateSemantic(ctx, c, &parents) + assert.Equal(t, consensus.ErrInvalidHeight, err) + }) + + t.Run("reject block mined too soon after parent", func(t *testing.T) { + c := &types.Block{Height: 2, Timestamp: tbt(ts)} + p := &types.Block{Height: 1, Timestamp: tbt(ts)} + parents, err := types.NewTipSet(p) + require.NoError(t, err) + + err = validator.ValidateSemantic(ctx, c, &parents) + assert.Equal(t, consensus.ErrTooSoon, err) + }) + + t.Run("reject block mined too soon after parent with one null block", func(t *testing.T) { + c := &types.Block{Height: 3, Timestamp: tbt(ts)} + p := &types.Block{Height: 1, Timestamp: tbt(ts)} + parents, err := types.NewTipSet(p) + require.NoError(t, err) + + err = validator.ValidateSemantic(ctx, c, &parents) + assert.Equal(t, consensus.ErrTooSoon, err) + }) + + t.Run("accept block mined one block time after parent", func(t *testing.T) { + c := &types.Block{Height: 2, Timestamp: tbt(ts.Add(blockTime))} + p := &types.Block{Height: 1, Timestamp: tbt(ts)} + parents, err := types.NewTipSet(p) + require.NoError(t, err) + + err = validator.ValidateSemantic(ctx, c, &parents) + assert.NoError(t, err) + }) + + t.Run("accept block mined one block time after parent with one null block", func(t *testing.T) { + c := &types.Block{Height: 3, Timestamp: tbt(ts.Add(2 * blockTime))} + p := &types.Block{Height: 1, Timestamp: tbt(ts)} + parents, err := types.NewTipSet(p) + require.NoError(t, err) + + err = validator.ValidateSemantic(ctx, c, &parents) + assert.NoError(t, err) + }) + +} + +func TestBlockValidSyntax(t *testing.T) { + tf.UnitTest(t) + + blockTime := consensus.DefaultBlockTime + + loc, _ := time.LoadLocation("America/New_York") + ts := time.Date(2019, time.April, 1, 0, 0, 0, 0, loc) + mclock := clock.NewMockClock(ts) + + ctx := context.Background() + + validator := consensus.NewDefaultBlockValidator(blockTime, mclock) + + t.Run("reject block generated in future", func(t *testing.T) { + blk := &types.Block{ + Timestamp: types.Uint64(ts.Add(time.Second * 1).Unix()), + } + assert.Error(t, validator.ValidateSyntax(ctx, blk)) + }) + + t.Run("reject block with undef StateRoot", func(t *testing.T) { + blk := &types.Block{ + StateRoot: cid.Undef, + } + assert.Error(t, validator.ValidateSyntax(ctx, blk)) + }) + + t.Run("reject block with undef miner address", func(t *testing.T) { + blk := &types.Block{ + Miner: address.Undef, + } + assert.Error(t, validator.ValidateSyntax(ctx, blk)) + }) + + t.Run("reject block with empty ticket", func(t *testing.T) { + blk := &types.Block{ + Ticket: []byte{}, + } + assert.Error(t, validator.ValidateSyntax(ctx, blk)) + }) + + // Impossible due to implementation + /* + t.Run("reject block with negative parent weight", func(t *testing.T) { + blk := &types.Block{ + ParentWeight: -1, + } + assert.Error(t, validator.ValidateSyntax(ctx, blk)) + }) + t.Run("reject block with negative height", func(t *testing.T) { + blk := &types.Block{ + Height: -1, + } + assert.Error(t, validator.ValidateSyntax(ctx, blk)) + }) + */ +} diff --git a/mining/block_generate.go b/mining/block_generate.go index 2278541f36..1cdeb3ef46 100644 --- a/mining/block_generate.go +++ b/mining/block_generate.go @@ -86,6 +86,7 @@ func (w *DefaultWorker) Generate(ctx context.Context, Proof: proof, StateRoot: newStateTreeCid, Ticket: ticket, + Timestamp: types.Uint64(time.Now().Unix()), } for i, msg := range res.PermanentFailures { diff --git a/node/node.go b/node/node.go index f2ea98585b..4c2dd34c0e 100644 --- a/node/node.go +++ b/node/node.go @@ -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" @@ -378,7 +379,7 @@ func (nc *Config) Build(ctx context.Context) (*Node, error) { pingService := ping.NewPingService(peerHost) // setup block validation - blkValid := consensus.NewDefaultBlockValidator(nc.BlockTime) + blkValid := consensus.NewDefaultBlockValidator(nc.BlockTime, clock.NewBlockClock()) // set up bitswap nwork := bsnet.NewFromIpfsHost(peerHost, router) diff --git a/types/tipset.go b/types/tipset.go index cd59fbe255..677393ce6e 100644 --- a/types/tipset.go +++ b/types/tipset.go @@ -119,6 +119,7 @@ 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) { if len(ts.blocks) == 0 { return 0, errUndefTipSet