aboutsummaryrefslogblamecommitdiffstats
path: root/core/test/blocks-generator.go
blob: eacc43677f41eff696dc40efc09001b37a8d5eb3 (plain) (tree)
1
2
3
4
5
6
7
8


                                                         

                                                                               


                                                                              



                                                                              












                                                                           
                                                                 
                                                                       

                                                                          


                                                                     


                                                                         



                                                            
                                             
                            
                        
                                       
                                                

 
                                                        
 

                                                          


                                        


                                    
                                                                 



                                                           
                                                        





                                                                              

                                              



                 
                                                
                            


                                                  
                                 
                                    

                                 

 


                                                                    




                                                      
                                                           
                                                                        
                 
                                                                 
                                              
         
                              


                                             
                                    

                                                                               


         
                                                      
                                  
                                             
                                             
 
                                            
                                                  
                                                





                                                      

                                                   
                                        
 
                              



                                                                            

                                                




                                                                                  

                                                                          

                         
                                     
                                                                   
                                                        
                        
                                              



                                                       
                                        



              


                                                         
                                 
                                                   

                                       
                                   


                                                                     

                                                                        



                                       
                                         

                                                            
                  
                                                        
                                                  
         

                                        
                                                             

                 




                                                   



                                                            
                                                       
                            




                                                              
                                                              




                                                                      
                                                                           






                                               

                                                         

                                                       

 


                                                                   
                                                                      

                                                        




                                                            

                                                    


                                                

                                        
                                                 
 

                                                 

                                

                                       









                                                                             
                                                         
                                     

                        
                                        
                                
                                                                    
 



                                        

                                                                  


                                              
         







                                                                  
         
                                                        

                                                                           



                                                               


             
                                                                    
                                               
                                                              





                                       
                                                           
                                                





                                                           
                                         


                                                                                

                              








                                           
// Copyright 2018 The dexon-consensus-core Authors
// This file is part of the dexon-consensus-core library.
//
// The dexon-consensus-core library is free software: you can redistribute it
// and/or modify it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation, either version 3 of the License,
// or (at your option) any later version.
//
// The dexon-consensus-core library is distributed in the hope that it will be
// useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
// General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the dexon-consensus-core library. If not, see
// <http://www.gnu.org/licenses/>.

package test

import (
    "errors"
    "math"
    "math/rand"
    "time"

    "github.com/dexon-foundation/dexon-consensus-core/common"
    "github.com/dexon-foundation/dexon-consensus-core/core/blockdb"
    "github.com/dexon-foundation/dexon-consensus-core/core/crypto"
    "github.com/dexon-foundation/dexon-consensus-core/core/crypto/eth"
    "github.com/dexon-foundation/dexon-consensus-core/core/types"
)

// TODO(mission): blocks generator should generate blocks based on chain,
//                not nodes.

// ErrParentNotAcked would be raised when some block doesn't
// ack its parent block.
var ErrParentNotAcked = errors.New("parent is not acked")

// nodeStatus is a state holder for each node
// during generating blocks.
type nodeStatus struct {
    blocks           []*types.Block
    lastAckingHeight map[types.NodeID]uint64
}

type hashBlockFn func(*types.Block) (common.Hash, error)

// getAckedBlockHash would randomly pick one block between
// last acked one to current head.
func (vs *nodeStatus) getAckedBlockHash(
    ackedNID types.NodeID,
    ackedNode *nodeStatus,
    randGen *rand.Rand) (
    hash common.Hash, ok bool) {

    baseAckingHeight, exists := vs.lastAckingHeight[ackedNID]
    if exists {
        // Do not ack the same block(height) twice.
        baseAckingHeight++
    }
    totalBlockCount := uint64(len(ackedNode.blocks))
    if totalBlockCount <= baseAckingHeight {
        // There is no new block to ack.
        return
    }
    ackableRange := totalBlockCount - baseAckingHeight
    height := uint64((randGen.Uint64() % ackableRange) + baseAckingHeight)
    vs.lastAckingHeight[ackedNID] = height
    hash = ackedNode.blocks[height].Hash
    ok = true
    return
}

// nodeSetStatus is a state holder for all nodes
// during generating blocks.
type nodeSetStatus struct {
    status        map[types.NodeID]*nodeStatus
    proposerChain map[types.NodeID]uint32
    timestamps    []time.Time
    nodeIDs       []types.NodeID
    randGen       *rand.Rand
    hashBlock     hashBlockFn
}

func newNodeSetStatus(
    nIDs []types.NodeID, hashBlock hashBlockFn) *nodeSetStatus {

    status := make(map[types.NodeID]*nodeStatus)
    timestamps := make([]time.Time, 0, len(nIDs))
    proposerChain := make(map[types.NodeID]uint32)
    for i, nID := range nIDs {
        status[nID] = &nodeStatus{
            blocks:           []*types.Block{},
            lastAckingHeight: make(map[types.NodeID]uint64),
        }
        timestamps = append(timestamps, time.Now().UTC())
        proposerChain[nID] = uint32(i)
    }
    return &nodeSetStatus{
        status:        status,
        proposerChain: proposerChain,
        timestamps:    timestamps,
        nodeIDs:       nIDs,
        randGen:       rand.New(rand.NewSource(time.Now().UnixNano())),
        hashBlock:     hashBlock,
    }
}

// findIncompleteNodes is a helper to check which node
// doesn't generate enough blocks.
func (vs *nodeSetStatus) findIncompleteNodes(
    blockNum int) (nIDs []types.NodeID) {

    for nID, status := range vs.status {
        if len(status.blocks) < blockNum {
            nIDs = append(nIDs, nID)
        }
    }
    return
}

// prepareAcksForNewBlock collects acks for one block.
func (vs *nodeSetStatus) prepareAcksForNewBlock(
    proposerID types.NodeID, ackingCount int) (
    acks common.Hashes, err error) {

    acks = common.Hashes{}
    if len(vs.status[proposerID].blocks) == 0 {
        // The 'Acks' filed of genesis blocks would always be empty.
        return
    }
    // Pick nodeIDs to be acked.
    ackingNIDs := map[types.NodeID]struct{}{
        proposerID: struct{}{}, // Acking parent block is always required.
    }
    if ackingCount > 0 {
        ackingCount-- // We would always include ack to parent block.
    }
    for _, i := range vs.randGen.Perm(len(vs.nodeIDs))[:ackingCount] {
        ackingNIDs[vs.nodeIDs[i]] = struct{}{}
    }
    // Generate acks.
    for nID := range ackingNIDs {
        ack, ok := vs.status[proposerID].getAckedBlockHash(
            nID, vs.status[nID], vs.randGen)
        if !ok {
            if nID == proposerID {
                err = ErrParentNotAcked
            }
            continue
        }
        acks = append(acks, ack)
    }
    return
}

// proposeBlock propose new block and update node status.
func (vs *nodeSetStatus) proposeBlock(
    proposerID types.NodeID,
    prvKey crypto.PrivateKey,
    acks common.Hashes) (*types.Block, error) {

    status := vs.status[proposerID]
    parentHash := common.Hash{}
    if len(status.blocks) > 0 {
        parentHash = status.blocks[len(status.blocks)-1].Hash
    }
    chainID := vs.proposerChain[proposerID]
    vs.timestamps[chainID] = vs.timestamps[chainID].Add(time.Second)

    newBlock := &types.Block{
        ProposerID: proposerID,
        ParentHash: parentHash,
        Position: types.Position{
            Height:  uint64(len(status.blocks)),
            ChainID: chainID,
        },
        Acks:      common.NewSortedHashes(acks),
        Timestamp: vs.timestamps[chainID],
    }
    for i, nID := range vs.nodeIDs {
        if nID == proposerID {
            newBlock.Position.ChainID = uint32(i)
        }
    }
    var err error
    newBlock.Hash, err = vs.hashBlock(newBlock)
    if err != nil {
        return nil, err
    }
    newBlock.Signature, err = prvKey.Sign(newBlock.Hash)
    if err != nil {
        return nil, err
    }
    status.blocks = append(status.blocks, newBlock)
    return newBlock, nil
}

// normalAckingCountGenerator would randomly pick acking count
// by a normal distribution.
func normalAckingCountGenerator(
    chainNum uint32, mean, deviation float64) func() int {

    return func() int {
        var expected float64
        for {
            expected = rand.NormFloat64()*deviation + mean
            if expected >= 0 && expected <= float64(chainNum) {
                break
            }
        }
        return int(math.Ceil(expected))
    }
}

// MaxAckingCountGenerator return generator which returns
// fixed maximum acking count.
func MaxAckingCountGenerator(count uint32) func() int {
    return func() int { return int(count) }
}

// generateNodePicker is a function generator, which would generate
// a function to randomly pick one node ID from a slice of node ID.
func generateNodePicker() func([]types.NodeID) types.NodeID {
    privateRand := rand.New(rand.NewSource(time.Now().UnixNano()))
    return func(nIDs []types.NodeID) types.NodeID {
        return nIDs[privateRand.Intn(len(nIDs))]
    }
}

// BlocksGenerator could generate blocks forming valid DAGs.
type BlocksGenerator struct {
    nodePicker func([]types.NodeID) types.NodeID
    hashBlock  hashBlockFn
}

// NewBlocksGenerator constructs BlockGenerator.
func NewBlocksGenerator(nodePicker func(
    []types.NodeID) types.NodeID,
    hashBlock hashBlockFn) *BlocksGenerator {

    if nodePicker == nil {
        nodePicker = generateNodePicker()
    }
    return &BlocksGenerator{
        nodePicker: nodePicker,
        hashBlock:  hashBlock,
    }
}

// Generate is the entry point to generate blocks. The caller is responsible
// to provide a function to generate count of acked block for each new block.
// The prototype of ackingCountGenerator is a function returning 'int'.
// For example, if you need to generate a group of blocks and each of them
// has maximum 2 acks.
//   func () int { return 2 }
// The default ackingCountGenerator would randomly pick a number based on
// the nodeCount you provided with a normal distribution.
func (gen *BlocksGenerator) Generate(
    chainNum uint32,
    blockNum int,
    ackingCountGenerator func() int,
    writer blockdb.Writer) (
    nodePrvKeys map[types.NodeID]crypto.PrivateKey, err error) {

    var (
        prvKey crypto.PrivateKey
        nodes  = types.NodeIDs{}
    )
    if ackingCountGenerator == nil {
        ackingCountGenerator = normalAckingCountGenerator(
            chainNum,
            float64(chainNum/2),
            float64(chainNum/4+1))
    }
    nodePrvKeys = map[types.NodeID]crypto.PrivateKey{}
    for i := uint32(0); i < chainNum; i++ {
        if prvKey, err = eth.NewPrivateKey(); err != nil {
            return
        }
        id := types.NewNodeID(prvKey.PublicKey())
        nodes = append(nodes, id)
        nodePrvKeys[id] = prvKey
    }
    status := newNodeSetStatus(nodes, gen.hashBlock)

    // We would record the smallest height of block that could be acked
    // from each node's point-of-view.
    toAck := make(map[types.NodeID]map[types.NodeID]uint64)
    for _, nID := range nodes {
        toAck[nID] = make(map[types.NodeID]uint64)
    }

    for {
        // Find nodes that doesn't propose enough blocks and
        // pick one from them randomly.
        notYet := status.findIncompleteNodes(blockNum)
        if len(notYet) == 0 {
            break
        }

        // Propose a new block.
        var (
            proposerID = gen.nodePicker(notYet)
            acks       common.Hashes
        )
        acks, err = status.prepareAcksForNewBlock(
            proposerID, ackingCountGenerator())
        if err != nil {
            return
        }
        var newBlock *types.Block
        if newBlock, err = status.proposeBlock(
            proposerID, nodePrvKeys[proposerID], acks); err != nil {

            return
        }

        // Persist block to db.
        err = writer.Put(*newBlock)
        if err != nil {
            return
        }
    }
    return
}