From 56cbbbaf4c638264cbf3ceb5cf16161b6adf2bc3 Mon Sep 17 00:00:00 2001 From: Jimmy Hu Date: Tue, 23 Oct 2018 09:43:24 +0800 Subject: core: prepare empty block if null block is confirmed by BA. (#231) --- core/consensus.go | 41 ++++++++++++++++++++++++++++++++------ core/lattice-data.go | 34 +++++++++++++++++++++++++++++++ core/lattice-data_test.go | 12 +++++++++++ core/lattice.go | 28 +++++++++++++++++++++++--- core/test/blocks-generator.go | 18 ++++++++++++----- core/test/blocks-generator_test.go | 21 ++++++++++--------- core/types/block.go | 5 +++++ 7 files changed, 136 insertions(+), 23 deletions(-) (limited to 'core') diff --git a/core/consensus.go b/core/consensus.go index 76fa3b7..a0de742 100644 --- a/core/consensus.go +++ b/core/consensus.go @@ -93,19 +93,35 @@ func (recv *consensusBAReceiver) ProposeBlock() common.Hash { func (recv *consensusBAReceiver) ConfirmBlock( hash common.Hash, votes map[types.NodeID]*types.Vote) { - block, exist := recv.consensus.baModules[recv.chainID]. - findCandidateBlock(hash) - if !exist { - recv.consensus.logger.Error("Unknown block confirmed", "hash", hash) - return + var block *types.Block + if (hash == common.Hash{}) { + recv.consensus.logger.Info("Empty block is confirmed", + "position", recv.agreementModule.agreementID()) + var err error + block, err = recv.consensus.proposeEmptyBlock(recv.chainID) + if err != nil { + recv.consensus.logger.Error("Propose empty block failed", "error", err) + return + } + } else { + var exist bool + block, exist = recv.consensus.baModules[recv.chainID]. + findCandidateBlock(hash) + if !exist { + recv.consensus.logger.Error("Unknown block confirmed", "hash", hash) + return + } } recv.consensus.ccModule.registerBlock(block) voteList := make([]types.Vote, 0, len(votes)) for _, vote := range votes { + if vote.BlockHash != hash { + continue + } voteList = append(voteList, *vote) } result := &types.AgreementResult{ - BlockHash: hash, + BlockHash: block.Hash, Position: block.Position, Votes: voteList, } @@ -642,6 +658,19 @@ func (con *Consensus) proposeBlock(chainID uint32, round uint64) *types.Block { return block } +func (con *Consensus) proposeEmptyBlock( + chainID uint32) (*types.Block, error) { + block := &types.Block{ + Position: types.Position{ + ChainID: chainID, + }, + } + if err := con.lattice.PrepareEmptyBlock(block); err != nil { + return nil, err + } + return block, nil +} + // ProcessVote is the entry point to submit ont vote to a Consensus instance. func (con *Consensus) ProcessVote(vote *types.Vote) (err error) { v := vote.Clone() diff --git a/core/lattice-data.go b/core/lattice-data.go index 50fa1d3..31604a6 100644 --- a/core/lattice-data.go +++ b/core/lattice-data.go @@ -412,6 +412,40 @@ func (data *latticeData) prepareBlock(b *types.Block) error { return nil } +// prepareEmptyBlock helps to setup fields of block based on its ChainID. +// including: +// - Acks only acking its parent +// - Timestamp with parent.Timestamp + minBlockProposeInterval +// - ParentHash and Height from parent block. If there is no valid parent block +// (ex. Newly added chain or bootstrap ), these fields would be setup as +// genesis block. +func (data *latticeData) prepareEmptyBlock(b *types.Block) { + // emptyBlock has no proposer. + b.ProposerID = types.NodeID{} + var acks common.Hashes + // Reset fields to make sure we got these information from parent block. + b.Position.Height = 0 + b.Position.Round = 0 + b.ParentHash = common.Hash{} + b.Timestamp = time.Time{} + // Decide valid timestamp range. + homeChain := data.chains[b.Position.ChainID] + if homeChain.tip != nil { + chainTip := homeChain.tip + b.ParentHash = chainTip.Hash + chainTipConfig := data.getConfig(chainTip.Position.Round) + if chainTip.Timestamp.After(chainTipConfig.roundEndTime) { + b.Position.Round = chainTip.Position.Round + 1 + } else { + b.Position.Round = chainTip.Position.Round + } + b.Position.Height = chainTip.Position.Height + 1 + b.Timestamp = chainTip.Timestamp.Add(chainTipConfig.minBlockTimeInterval) + acks = append(acks, chainTip.Hash) + } + b.Acks = common.NewSortedHashes(acks) +} + // TODO(mission): make more abstraction for this method. // nextHeight returns the next height for the chain. func (data *latticeData) nextPosition(chainID uint32) types.Position { diff --git a/core/lattice-data_test.go b/core/lattice-data_test.go index 263474c..64767e1 100644 --- a/core/lattice-data_test.go +++ b/core/lattice-data_test.go @@ -577,6 +577,18 @@ func (s *LatticeDataTestSuite) TestNextPosition() { s.Equal(data.nextPosition(0), types.Position{ChainID: 0, Height: 0}) } +func (s *LatticeDataTestSuite) TestPrepareEmptyBlock() { + data, _ := s.genTestCase1() + b := &types.Block{ + Position: types.Position{ + ChainID: 0, + }, + } + data.prepareEmptyBlock(b) + s.True(b.IsEmpty()) + s.Equal(uint64(4), b.Position.Height) +} + func (s *LatticeDataTestSuite) TestNumChainsChange() { // This test case verify the behavior when NumChains // changes. We only reply on methods of latticeData diff --git a/core/lattice.go b/core/lattice.go index 73fdcb0..984203d 100644 --- a/core/lattice.go +++ b/core/lattice.go @@ -92,15 +92,37 @@ func (s *Lattice) PrepareBlock( return } +// PrepareEmptyBlock setup block's field based on current lattice status. +func (s *Lattice) PrepareEmptyBlock(b *types.Block) (err error) { + s.lock.RLock() + defer s.lock.RUnlock() + s.data.prepareEmptyBlock(b) + if b.Hash, err = hashBlock(b); err != nil { + return + } + return +} + // SanityCheck check if a block is valid. // If checkRelation is true, it also checks with current lattice status. // // If some acking blocks don't exists, Lattice would help to cache this block // and retry when lattice updated in Lattice.ProcessBlock. func (s *Lattice) SanityCheck(b *types.Block, checkRelation bool) (err error) { - // Verify block's signature. - if err = s.authModule.VerifyBlock(b); err != nil { - return + if b.IsEmpty() { + // Only need to verify block's hash. + var hash common.Hash + if hash, err = hashBlock(b); err != nil { + return + } + if b.Hash != hash { + return ErrInvalidBlock + } + } else { + // Verify block's signature. + if err = s.authModule.VerifyBlock(b); err != nil { + return + } } // Make sure acks are sorted. for i := range b.Acks { diff --git a/core/test/blocks-generator.go b/core/test/blocks-generator.go index 393e3a3..62c777e 100644 --- a/core/test/blocks-generator.go +++ b/core/test/blocks-generator.go @@ -190,26 +190,34 @@ func (ns *nodeSetStatus) proposeBlock( blockHeight = status.tip.Position.Height + 1 acks = append(acks, parentHash) } + // 10% of chance to produce empty block. + empty := ns.randGen.Float32() < 0.1 && blockHeight > 0 chainID := ns.proposerChain[proposerID] newBlock := &types.Block{ - ProposerID: proposerID, ParentHash: parentHash, Position: types.Position{ Round: ns.round, Height: blockHeight, ChainID: chainID, }, - Acks: common.NewSortedHashes(acks), Timestamp: status.getNextBlockTime(ns.timePicker), } + if empty { + newBlock.Acks = common.NewSortedHashes(common.Hashes{parentHash}) + } else { + newBlock.ProposerID = proposerID + newBlock.Acks = common.NewSortedHashes(acks) + } var err error newBlock.Hash, err = ns.hashBlock(newBlock) if err != nil { return nil, err } - newBlock.Signature, err = status.prvKey.Sign(newBlock.Hash) - if err != nil { - return nil, err + if !empty { + newBlock.Signature, err = status.prvKey.Sign(newBlock.Hash) + if err != nil { + return nil, err + } } status.blocks = append(status.blocks, newBlock) status.tip = newBlock diff --git a/core/test/blocks-generator_test.go b/core/test/blocks-generator_test.go index fafbd6c..075cc9f 100644 --- a/core/test/blocks-generator_test.go +++ b/core/test/blocks-generator_test.go @@ -51,7 +51,7 @@ func (s *BlocksGeneratorTestSuite) TestGenerate() { // Load all blocks in that database for further checking. iter, err := db.GetAll() req.NoError(err) - blocksByNode := make(map[types.NodeID][]*types.Block) + blocksByChain := make(map[uint32][]*types.Block) blocksByHash := make(map[common.Hash]*types.Block) for { block, err := iter.Next() @@ -62,11 +62,13 @@ func (s *BlocksGeneratorTestSuite) TestGenerate() { // TODO(mission): Make sure each block is correctly signed once // we have a way to access core.hashBlock. req.NotEqual(block.Hash, common.Hash{}) - req.NotEmpty(block.Signature) + if !block.IsEmpty() { + req.NotEmpty(block.Signature) + } req.Equal(block.Position.Round, uint64(1)) - blocksByNode[block.ProposerID] = - append(blocksByNode[block.ProposerID], &block) - sort.Sort(types.ByPosition(blocksByNode[block.ProposerID])) + blocksByChain[block.Position.ChainID] = + append(blocksByChain[block.Position.ChainID], &block) + sort.Sort(types.ByPosition(blocksByChain[block.Position.ChainID])) blocksByHash[block.Hash] = &block } // Make sure these two rules are hold for these blocks: @@ -77,8 +79,8 @@ func (s *BlocksGeneratorTestSuite) TestGenerate() { // previous block. // - The last block of each chain should pass endTime. // - No Acks in genesis bloc - for _, blocks := range blocksByNode { - lastAckingHeights := map[types.NodeID]uint64{} + for _, blocks := range blocksByChain { + lastAckingHeights := map[uint32]uint64{} req.NotEmpty(blocks) // Check genesis block. genesisBlock := blocks[0] @@ -95,11 +97,12 @@ func (s *BlocksGeneratorTestSuite) TestGenerate() { ackedBlock := blocksByHash[ack] req.NotNil(ackedBlock) prevAckingHeight, exists := - lastAckingHeights[ackedBlock.ProposerID] + lastAckingHeights[ackedBlock.Position.ChainID] if exists { s.True(prevAckingHeight < ackedBlock.Position.Height) } - lastAckingHeights[ackedBlock.ProposerID] = ackedBlock.Position.Height + lastAckingHeights[ackedBlock.Position.ChainID] = + ackedBlock.Position.Height // Block Height should always incremental by 1. // // Because we iterate blocks slice from 1, diff --git a/core/types/block.go b/core/types/block.go index c8ee3c6..9de4673 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -257,6 +257,11 @@ func (b *Block) IsFinalized() bool { return b.Finalization.Height != 0 } +// IsEmpty checks if the block is an 'empty block'. +func (b *Block) IsEmpty() bool { + return b.ProposerID.Hash == common.Hash{} +} + // IsAcking checks if a block acking another by it's hash. func (b *Block) IsAcking(hash common.Hash) bool { idx := sort.Search(len(b.Acks), func(i int) bool { -- cgit v1.2.3