Introduce

Yu is an easy-to-use and highly customizable blockchain framework developed in golang. Most of the existing blockchains (Public Blockchain and Consortium Blockchain) can be developed with yu.

Why do this?

The current blockchain framework either has a high threshold for getting started, or the degree of customization is very low, or the community is inactive and has stopped maintenance. So I developed it myself This blockchain framework. The purpose is to allow developers to get started with the core of blockchain development more easily, quickly and deeply.

What functions can Yu customize

  • Customized consensus algorithms, including but not limited to pow, pos, poa, pbft, etc.
  • Custom block verification logic, transaction verification logic, rules for packaging transactions from the transaction pool.
  • Insert custom actions at various stages of the block generation cycle, including controlling when transactions are executed.
  • Custom on-chain transactions (ie Writing, called transactions in Ethereum) and queries
  • Custom chain structure (including fork logic) and transaction pool logic
  • Can be developed in multiple programming languages (not yet finished)
  • You can use [dependency injection] to manage each business module (Tripod)

Quick Start

Install

go get github.com/yu-org/yu

Source code link: https://github.com/yu-org/yu/blob/main/apps/quickstart/chain.go

Develop an on-chain Writing and Reading

package main

import (
	"github.com/yu-org/yu/apps/poa"
	"github.com/yu-org/yu/core/context"
	"github.com/yu-org/yu/core/startup"
	"github.com/yu-org/yu/core/tripod"
	"net/http"
)

type QuickStart struct {
	*tripod.Tripod
}

func NewQuickStart() *QuickStart {
	tri := &QuickStart{
		tripod.NewTripod(),
	}
	// 此处需要手动将自定义的 Writing 注册到 tripod 中,
	tri.SetWritings(tri.WriteA)
	// 此处需要手动将自定义的 Reading 注册到 tripod 中
	tri.SetReadings(tri.ReadA)
	return tri
}

type WriteRequest struct {
	Key   string `json:"key"`
	Value string `json:"value"`
}

// Here is a custom development of a Writing.   
// The Writing will be consensus and executed across the entire network.
func (q *QuickStart) WriteA(ctx *context.WriteContext) error {
	// set this Writing lei cost (lei and gas are synonymous)
	ctx.SetLei(100)

	req := new(WriteRequest)
	err := ctx.BindJson(req)
	if err != nil {
		return err
	}
	// Store data in on-chain state.
	q.Set([]byte(req.Key), []byte(req.Value))
	// Emit an event.
	ctx.EmitStringEvent("execute success")
	return nil
}

type ReadRequest struct {
	Key string `json:"key"`
}

type ReadResponse struct {
	Value string `json:"value"`
}

// Here is a custom development of a Reading
func (q *QuickStart) ReadA(ctx *context.ReadContext) {
	req := new(ReadRequest)
	err := ctx.BindJson(req)
	if err != nil {
		ctx.Err(http.StatusBadRequest, err)
		return
	}
	value, err := q.Get([]byte(req.Key))
	if err != nil {
		ctx.ErrOk(err)
		return
	}
	ctx.JsonOk(ReadResponse{Value: string(value)})
}

// Load tripods and start up the chain
func main() {
    // default poa tripod config
    poaCfg := poa.DefaultCfg(0)
    // default yu config
    yuCfg := startup.InitDefaultKernelConfig()

    poaTri := poa.NewPoa(poaCfg)
    qsTri := NewQuickStart()
	startup.InitDefaultKernel(yuCfg).
		WithTripods(poaTri, qsTri).
		Startup()
}

Run

go build -o yu-example

./yu-example  
 

At this point, a blockchain has been activated. In the future, more blockchain nodes can be added to build a blockchain network

Fundamental

Process overview

image

When a transaction is initiated from the client to the chain, it will first be checked by txpool, and only after the check is passed will it be put into the transaction pool and broadcast to other blockchain nodes. Receive transactions broadcast from other nodes in the P2P network, and put them into txpool after checking (no longer broadcast)

land runs, starts generating blocks, and performs a series of processing on blocks: including mining blocks, broadcasting, validating blocks from other nodes, executing intra-block transactions, storing blocks on the chain, etc. . During this process There are so many things that can be freely controlled, and you can use it to implement any consensus algorithm, sharding method, block time, package transaction method, choose when to execute transactions, etc. you want. with blockchain, The interaction of txdb, state is also in this process.

After each block goes through all the custom tripod logic in land, it will enter the next block stage, generate the next block and process it, and the cycle goes on and on.

Components

Core components

The following blockchain, txdb, txpool are all interfaces, with built-in default implementations, if developers have special needs, they can re-implement the interfaces themselves:

  • blockchain: Chain structure, responsible for storing block data and organizing chain structure and fork logic.
  • txdb: The database of yu, which stores the specific transaction data in all blocks, event and error after the transaction is executed.
  • txpool: The transaction pool is responsible for validating and caching transactions sent from outside.
  • state: Storage state, which stores the state after each transaction(Writing) is executed, such as account balance.
  • tripod: The basic minimum unit that runs the blockchain and allows developers to customize logic. Developers can customize multiple tripods and load their ordered combinations into land for the framework to call.

Low-level components

  • store: The storage layer supports various forms of storage such as kv, sql, and fs, and they are all interfaces. Developers can specify the required storage engine (for example, kv currently has two storage engines, badger and boltdb) Currently the storage in state is implemented using boltdb, while the storage in blockchain and txdb is implemented using sqlite.
  • p2p: A peer-to-peer transport network, used to discover nodes in the network and propagate transactions and blocks, etc.
  • keypair: Asymmetric encryption algorithm for public and private keys, currently supports sr25519 and ed25519. This component is in the form of an interface and can extend other encryption algorithms by itself.

image

Blockchain

The blockchain component is the persistent storage of the blockchain structure, and the chain structure maintained by the entire network. Note: This structure does not store specific transaction data and transaction Writing results. This part of the data is stored in txdb.

Interface

Code is here
blockchain implements the storage and processing logic of the basic chain structure. The following are the interfaces of the core functions. Developers can customize their own chain storage methods and processing logic if required:

func ConvergeType() ConvergeType

ConvergeType() returns the convergence type of the chain (that is, in what form the block was finalized) As we all know, in the blockchain, multiple nodes may produce blocks at the same moment, thus forming a fork. If it does not converge, the system will not know which chain is the main chain. Therefore, convergence is required, and the most common methods of convergence are:

  • Longest sub-chain (Longest): The longest branch will be considered as the main chain, and this method will be rolled back.

  • Heaviest: The fork with the largest number of sub-chains will be considered as the main chain, and this method will be rolled back.

  • Finalize: The block or chain is voted through voting and other methods to finally finalize a fork to become the main chain. Under normal circumstances, the finalized block cannot be rolled back.

func GetGenesis() (*CompactBlock, error)
func SetGenesis(b *CompactBlock) error

Obtain the genesis block from the chain and store the genesis block into the chain.

func AppendBlock(b *CompactBlock) error
func GetBlock(blockHash Hash) (*CompactBlock, error)
func UpdateBlock(b *CompactBlock) error

AppendBlock(block)adds the block to the end of the chain
GetBlock(blockHash)gets a specific block based on the block hash
UpdateBlock(block)updates block data with block hash as index

func Children(prevBlockHash Hash) ([]*CompactBlock, error)  
func Finalize(blockHash Hash) error  
func GetEndBlock() (*CompactBlock, error)
func LastFinalized() (*CompactBlock, error)

Children(blockHash) gets all its subblocks based on a block hash
Finalize(blockHash) finals consensus finalizes a block
GetEndBlock() gets the last unfinalized block
LastFinalized() gets the last finalized block

txdb

txdb stores specific transaction data in all blocks, event and error after the transaction is executed.

Interface

Code is here
The following are the interfaces of the core functions. Developers can customize their own txdb if required.

func GetTxn(txnHash Hash) (*SignedTxn, error)
func SetTxn(stxn *SignedTxn) error

GetTxn(txnHash) gets transaction data based on the hash value of the transaction
SetTxn(stxn) stores transaction data in the txdb (indexed by transaction hash))

func GetTxns(blockHash Hash) ([]*SignedTxn, error)
func SetTxns(blockHash Hash, txns []*SignedTxn) error

GetTxns(blockHash) gets all transaction data in the block according to the block hash
SetTxns(blockHash, txns) stores all transaction data in a block into txdb(Indexed by transaction hash)

func GetEvents(blockHash Hash) ([]*Event, error)
func SetEvents(events []*Event) error

GetEvents(blockHash) gets all events in the block according to the block hash
SetEvents(events) stores all event in txdb (indexed by auto-incrementing ID)

func GetErrors(blockHash Hash) ([]*Error, error)
func SetError(err *Error) error

GetErrors(blockHash) gets all error in the block according to the block hash
SetError(err) stores all error in txdb (indexed by auto-incrementing ID)

Txpool

Code is here
The following are the interfaces of the core functions. Developers can customize their own trading pools if required.

func BaseCheck(*SignedTxn) error
func TripodsCheck(stxn *SignedTxn) error
func NecessaryCheck(stxn *SignedTxn) error

BaseCheck(): when inserts transaction data into the transaction pool, it will do some basic checks. The basic checks are: whether the transaction data is too large, whether the transaction pool capacity has reached the upper limit, whether the signature in the transaction is legal, whether there is Transactions are repeated. The first three will return an error if the check fails. When the transaction is repeated, the message will be ignored but no error will be returned.

TripodsCheck(): After the basic check of TripodsChecks, the tripods check will be performed, which is the transaction check rule defined by the developer.

NecessaryCheck() represents a necessity check, which only checks whether the transaction data is too large, whether the signature is valid, and the tripods check. After the node receives the block from the P2P network, the synchronization Check the transaction data in the block, and directly store it in txdb after checking.

func Insert(txn *SignedTxn) error

Insert() means inserting transaction data into the transaction pool. This function has two types of checks, BaseCheck and TripodsCheck. Only after passing the check will it be inserted into the transaction pool, otherwise an error will be returned.

func Pack(numLimit uint64) ([]*SignedTxn, error)
func PackFor(numLimit uint64, filter func(*SignedTxn) error) ([]*SignedTxn, error)

Pack(numLimit) means to pack a maximum of numLimit transactions from the transaction pool. The packaged transactions will be marked, but will not be deleted from the transaction pool immediately.
PackFor(numLimit, filter) has one more filter function than Pack. All packed transactions must pass the filter of filter. If filter returns an error, the transaction will not be packed.

func GetTxn(hash Hash) (*SignedTxn, error)
func Reset(*Block) error

GetTxn(hash) gets transaction data according to the transaction hash value
Reset(*Block) clears the transactions in the given block from the transaction pool as they are already packed.

Tripod

Description

tripod is the core of the entire yu framework, and developers define their own business logic by implementing its interface. It consists of three parts:

  1. Custom block verification rules and transaction checking logic.
  2. Control the life cycle of blocks to achieve deep customization development. The life cycle has four phases: initChain, startBlock, endBlock, finalizeBlock. where only initChain It is called only at the time of the genesis block, and the last three are called every block. (Personalized consensus algorithms can be implemented with the block life cycle)
  3. Customize Transaction(Writing), Query(Query) and P2P Request Handling (p2pHandler)

Interface

Code is here

func Name() string
func SetChainEnv(env *ChainEnv)

Name() returns the name of tripod
SetChainEnv(*ChainEnv) loads ChainEnv into tripod, which is convenient for subsequent calls. (ChainEnv is introduced in context and chainEnv)

func CheckTxn(*SignedTxn) error
func VerifyBlock(block *Block) bool

checkTxn() is a developer-defined transaction check logic. As mentioned in the previous chapter, when an external transaction is inserted into txpool, the two steps of BaseCheck and TripodsCheck will be executed. And TripodsCheck is defined here, each tripod can define its own transaction check logic.

VerifyBlock() is a developer-defined block verification logic. Every time the blockchain receives a block from external broadcast, it needs to be verified before it can be processed. Different chains have different verifications for blocks. test method, so Developers can customize the validation logic by themselves. Each tripod can define its own transaction checking logic.

func InitChain() 
func StartBlock(block *Block) 
func EndBlock(block *Block)
func FinalizeBlock(block *Block)

InitChain() will only be called once when the blockchain is started, and it is specially used to define and process the genesis block.
StartBlock() defines the logic of the stage when the block is generated.
EndBlock() defines the logic at the end of the block, where the transaction is generally executed and the block is stored in the blockchain.
FinalizeBlock() defines the logic of the block's finalize phase (i.e. final consensus). If the blockchain is a finalize type chain, you need to customize this part of the logic, otherwise it is not necessary.

Dependency Injection

We may develop many Tripods in yu, and there may be dependencies between different Tripods. For example, when a developer develops a Tripod named staking, it may need to rely on the asset Tripod. In order to facilitate developers to manage Tripod dependencies more conveniently. yu provides the function of dependency injection.

type Staking struct {
	*tripod.Tripod
	asset *asset.Asset `tripod:"asset"`
}

As shown above, it is known that we already have an Asset Tripod. Now we develop the pledge module, which needs to rely on the assets in the asset Some functions, then we only need to declare the asset variable in the Staking structure and add a tag (tripod:"asset"). This is a fixed syntax tripod:<TripodName>, after adding, we just need to start yu normally:

    startup.SyncAndStartup(Staking.New(), asset.NewAsset())

Simple as above. SyncAndStartup() is a necessary step to start yu, you only need to call it once in your code, Startup() will automatically load these Tripods and complete dependency injection at runtime.

land

land is used to load all tripods for the framework to call.

land internal flow chart

image

State

state is the state storage on the chain. For example, after A initiates an on-chain transfer to B, the account balance information of A and B will be stored. Below we refer to all operations on the state store in each transaction as a transaction.
Transactions in state are atomic. For example, there are 3 operations that modify the state in a transaction, then these 3 operations can either be modified successfully, or none of them are successful.
eg:

func (a *A) DoTest(ctx *context.WriteContext) error {
    a.State.Set(a, []byte("yu"), []byte("yu")) // 1. Mute state

    err := DoOther()
    if err != nil {
        return err
    }

    a.State.Set(a, []byte("qi"), []byte("qi")) // 2. Mute state
}

In the above code, there are two Mute state operations. If the code executes successfully in the first place, but returns an error in DoOther(), then the modify state in the second place cannot be completed. at this time The framework will also cancel the first state modification to ensure atomicity.

Currently, only the storage form of kvdb is supported internally. The state is stored in blocks, and the transactions in each block are executed in sequence and stored in the database.

Code is here

func StartBlock(blockHash)

StartBlock() is called during the start block phase of the blockchain operation to tell state the current block hash.

func SetCanRead(blockHash)

SetCanRead() sets the state in the block that can currently be read. After a block transaction is executed and stored in the chain, it may not be allowed to be read immediately, especially the finalize type chain, which requires The result state of the Writing in the block can not be read by the outside world until the block is finalized. Of course non-finalized chains can be read as soon as the block is stored in the chain.

func Commit() (Hash, error)

Commit() commits all transactions in the block and returns the stateroot of the state tree under the block.

func Discard()

Discard() discards current transaction.

func DiscardAll()

DiscardAll() discards all transactions in the current block.

func NextTxn()

NextTxn() indicates that the current transaction is completed and the next transaction is started.

P2P network

p2p is the network foundation of the blockchain. For a customizable blockchain framework, yu allows developers to freely use the functions of the P2P network, and developers can call it in ChainEnv. The underlying implementation of this interface comes from libp2p

Interface

Code is here ,the core functions are as follows:

    LocalID() peer.ID
    GetBootNodes() []peer.ID
    ConnectBootNodes() error

LocalID() returns local p2p ID
GetBootNodes() returns the p2p IDs of the bootnodes nodes in the p2p network, bootnodes are configured in the configuration file.
ConnectBootNodes() connects these bootnodes

    AddTopic(topicName string)
    PubP2P(topic string, msg []byte) error
    SubP2P(topic string) ([]byte, error)

AddTopic() adds a topic to the P2P network, which is used as a topic for subscription publishing.
PubP2P() publishes messages into the topic
SubP2P() subscribes messages from the topic

    RequestPeer(peerID peer.ID, code int, request []byte) (response []byte, err error)
    SetHandlers(handlers map[int]dev.P2pHandler)

RequestPeer() sends request data to a specific p2p node, code is the request category, which can be customized. (except 100 and 101, these two are predefined code of the framework, 100 is used when P2P node connection comparison data; 101 is used when synchronizing transaction data)
SetHandlers() is set to handle requests from the P2P network, each code corresponds to a handler, the interface of handler is here , developers can customize the logic here.

Development Manual

This chapter will go into the details of how to develop a blockchain with yu. i.e. how to build your own tripod.

The default path of the configuration file is yu_conf/kernel.toml. If you need to configure it separately, please use -k to specify the path of the configuration file.
Of course, if you don't want to configure the file yourself, you can also use -dc to enable the system default configuration. However, it should be noted that the default configuration of the system does not connect to any bootnodes.

Config File

The format of the yu configuration file is .toml.
The configuration content is divided into the following parts, code is here

  • Node Config
  • each Component Config
  • P2P Config

The default configuration file path for yu is yu_conf/kernel.toml. Of course, if you don't want to fill in the configuration file manually, you can also enable the command line as in [Quick Start] (2. Quick Start.md) -dc=true to take advantage of the default configuration. The source address of the default configuration is here

节点配置

源码样式:

    // 0: local-node 
    //1: master-worker
    RunMode RunMode `toml:"run_mode"`
    // serve http port
    HttpPort string `toml:"http_port"`
    // serve websocket port
    WsPort string `toml:"ws_port"`
    // log out level:
    // panic, fatal, error, warn, info, debug, trace
    LogLevel string `toml:"log_level"`
    
    LeiLimit uint64 `toml:"lei_limit"`
  • Runmode: the node operation mode, 0 is for single-node operation, 1 is for master-worker operation (master-worker is not currently supported)
  • HttpPort: http listening address, port configuration for serving external calls.
  • WsPort: the websocket listening address, the port configuration for serving external calls.
  • LogLevel: print the level of the log, from top to bottom, there are panic, fatal, error, warn, info, debug, trace
  • LeiLimit:the upper limit of lei that can be consumed per block

配置示例:

run_mode = 0

http_port = "7998"

ws_port = "8998"

log_level = "info"

lei_limit = 50000

各组件配置

blockchainconfig codes:

type BlockchainConf struct {
	ChainDB         SqlDbConf `toml:"chain_db"`
}
  • ChainDB: The sql database used to store the blockchain.

txdbconfig codes:

type txdbConf struct {
	BaseDB SqlDbConf `toml:"base_db"`
}
  • BaseDB: The sql database used to store txdb.

txpoolconfig codes:

type TxpoolConf struct {
	PoolSize   uint64    `toml:"pool_size"`
	TxnMaxSize int       `toml:"txn_max_size"`
}
  • PoolSize: The size of the transaction pool, if it exceeds this size, subsequent transactions will not be put into the transaction pool.
  • TxnMaxSize:The data size of the maximum transaction, if a transaction exceeds this configuration, it will not be put into the transaction pool.

stateconfig codes:

type StateConf struct {
	KV StateKvConf `toml:"kv"`
}

At present, state only supports kvdb storage method, and more storage forms will be opened in the future

Config Example:

[block_chain.chain_db]
sql_db_type = "sqlite"
dsn = "chain.db"



[yu_db.base_db]
sql_db_type = "sqlite"
dsn = "txdb.db"

[txpool]
pool_size = 2048
txn_max_size = 1024000

[state.kv.index_db]
kv_type = "bolt"
path = "./state_index.db"

[state.kv.node_base]
kv_type = "bolt"
path = "./state_base.db"

p2p Config

Config codes:

    // For listening from blockchain network.
	P2pListenAddrs []string `toml:"p2p_listen_addrs"`
	// To connect other hosts as a p2p network.
	Bootnodes []string `toml:"bootnodes"`

	ProtocolID string `toml:"protocol_id"`
	// 0: RSA
	// 1: Ed25519
	// 2: Secp256k1
	// 3: ECDSA
	NodeKeyType int `toml:"node_key_type"`

	NodeKeyRandSeed int64 `toml:"node_key_rand_seed"`

	NodeKey string `toml:"node_key"`

	// Only RSA has this param.
	NodeKeyBits int `toml:"node_key_bits"`
	// When use param 'NodeKey', 'NodeKeyFile' will not work.
	NodeKeyFile string `toml:"node_key_file"`
  • P2pListenAddrs: Indicates the address of the P2P protocol that the blockchain node listens to
  • Bootnodes: Link other blockchain nodes in the P2P network to synchronize historical blocks
  • ProtocolID: Customized protocol name in P2P network
  • NodeKeyType: The encryption type of the key of the P2P node, which is used to generate the key of the p2p address.
  • Four encryption algorithms are currently supported:
    • 0: RSA
    • 1: Ed25519
    • 2: Secp256k1
    • 3: ECDSA
  • NodeKeyRandSeed: Generate the seed of the key of the p2p node, which is used to generate the key of the P2P address. When this item is not empty, there is no need to configure NodeKey.
  • NodeKey:The key of the P2P address, used to generate the p2p address. When NodeKeyRandSeed is not empty, this item can be left blank.
  • NodeKeyBits: The bits of the key. This configuration is only required for the encryption algorithm RSA. Other types of encryption algorithms do not need to configure this parameter.
  • NodeKeyFile: Read the key file of the p2p address. Only when NodeKey is empty, the system will read this configuration item.

Config Example:


p2p_listen_addrs = ["/ip4/127.0.0.1/tcp/8887"]

protocol_id = "yu"

node_key_type = 1
node_key_rand_seed = 1

Context and ChainEnv

In Quick Start, we have seen how to customize an Writing, there are two parameters here, one is context, the other is ChainEnv, they represent two different the use of.

Context represents contextual information for interactions with off-chain. It includes caller address, passed parameterseventerror

  • The parameters passed in by the client caller (that is, the person who initiated the transaction) will be serialized in the form of json and stored in context in the form of string -> interface{} map. We use context to get the cryptographic address of the client caller and the parameters passed in by the caller. from context When getting parameters, you can use any go basic type to get parameters. If the parameter type is custom, you need to obtain interface through context.Get(paramName) and then convert it yourself. The types of parameters that can be directly obtained are:
   common.Hash([32]byte)
   common.Address([20]byte)
   string
   []byte
   bool 
   int, int8, int16, int32, int64
   uint, uint8, uint16, uint32, uint64
   float32, float64
  • At the same time, if you need to return event out of the chain, you also need to complete it through context (ctx.EmitEvent([]byte)).

ChainEnv contains all environment components related to all chains, including blockchain, txdb, txpool, state, Execute, P2pNetwork. Components that can be directly manipulated by developers to the bottom layer of the framework.

Writing and Reading

Under normal circumstances, developers will use more Writing and Reading, because these are basically external read and write interfaces at the business level. So in this section we look at how in yu use them. The code is here

Defined as follows:

type (
    Writing func(ctx *WriteContext) error

    Reading func(ctx *ReadContext) error
	
	P2pHandler func([]byte) ([]byte, error)
)
  • Writing is called transaction in other blockchains such as Ethereum. In the final analysis, it is a write operation to the state on the chain, so such an operation will be synchronized to the entire network. When you need to modify the state on the chain, you need to call chainEnv.KVDB to modify the state. Note that the first parameter of the set, get, delete functions in chainEnv.kvdb must be the current tripod Pointer, such as e.State.Set(e, ..., ...) in Quick Start
    currentBlock is the block that the current transaction is in.
    ctx.EmitEvent() is called when event needs to be sent off-chain.

  • Reading is an on-chain query. It does not modify the on-chain state, so the operation will not be synchronized to the entire network.
    Status query calls env.KVDB.Get() or env.KVDB.GetByBlockHash(). The former is to query the status that the final consensus has been reached on the main chain; the latter is to query the status of a specific block. historical state.
    respObj is used to return the queried status value to the off-chain.

  • P2pHandler is to define and process requests from the P2P network. For example, when a node needs to broadcast or send a processing request to a specific node, this function can be customized to complete the custom function.

  • Inject into Tripod:When building all our Writing and Reading, we need to call tripod.SetWritings() to inject all Writing into tripod, And call tripod.SetReadings() to inject all Reading into tripod.

  • Load into land:The framework provides a SyncAndStartup entry as long as the startup Load all the tripods we built by calling SyncAndStartup(...Tripod), thereby telling the framework which tripods we have customized.

About defaultTripod

[defaultTripod](https://github.com/yu-org/yu/blob/master/tripod/ default_tripod.go) It can help you write a lot less code, you need to put it in the first member variable of the tripod structure of your custom implementation (you must omit the variable name to achieve the effect of inheriting defaultTripod). as follows:

type Example struct {
	*tripod.Tripod
}

Block Lifecycle

Previous has introduced the concept of block cycle in tripod in detail. By customizing the block cycle, we can implement many deep functions, such as consensus algorithms. Below we use Take pow consensus as an example, the original code address here

  • Init blockchain,build a genesis block.
    func (p *Pow) InitChain() error {
        // build a genesis block and append chain.
        chain := p.env.Chain
        gensisBlock := &types.Block{
            Header: &types.Header{},
        }
        err := chain.SetGenesis(gensisBlock)
        if err != nil {
            return err
        }
        // Start a gorotine to pull data from other nodes in the P2P network in real time
        go func() {
            for {
                msg, err := p.env.P2pNetwork.SubP2P(StartBlockTopic)
                if err != nil {
                    logrus.Error("subscribe message from P2P error: ", err)
                    continue
                }
                p.msgChan <- msg
            }
        }()
        return nil
    }
  • Block starts,build a new block
func (p *Pow) StartBlock(block *Block) error {
    pool := p.env.Pool
    // If a block from another node is received before the local node produces a block, the mining will be skipped after verification and the block will be directly stored in the chain.
    // If there is none or the block is invalid, the local node starts mining.
    if p.UseBlocksFromP2P(block) {
        logrus.Infof("--------USE P2P block(%s)", block.Hash.String())
        return nil
    }
	
    // Pack transactions from txpool.
    txns, err := pool.Pack(p.packLimit)
	if err != nil {
        return err
    }
	
    ......
    // Start to mining.
    nonce, hash, err := spow.Run(block, p.target, p.targetBits)
    if err != nil {
        return err
    }
	
    ......
    // After mining, the block is broadcast to the P2P network.
    return p.env.P2pNetwork.PubP2P(StartBlockTopic, rawBlockByt)
}
  • End block
func (*Pow) EndBlock(block *Block) error {
    chain := p.env.Chain
    pool := p.env.Pool
    
    // Execute the transactions of the block.
    err := p.env.Execute(block)
    if err != nil {
        return err
    }
    
    // Store blocks in the chain for persistence.
    err = chain.AppendBlock(block)
    if err != nil {
        return err
    }
    
    return pool.Reset(block)
}

  • Final consensus, finalizing the block: Since the pow consensus generally does not require the finalization stage of the final consensus, because they have the longest sub-chain or the heaviest sub-chain to prove the computing power. So here we don't need to implement FinalizeBlock().
func (*Pow) FinalizeBlock(_ *Block) error {
	return nil
}

So far, a pow consensus mechanism based on sha256 is completed.

Build a blockchain network

When we have completed the configuration file, we can start building the blockchain network by ourselves.
In this chapter, we choose to compile example to start the blockchain node, in this example Loaded with pow consensus and asset module. Of course, we can also customize tripod to replace pow and asset or add more tripod.
First we use the configuration file to start the first blockchain node locally:

INFO[0000] register Writing(Transfer) into Tripod(asset)
INFO[0000] register Writing(CreateAccount) into Tripod(asset)
INFO[0000] register Query(QueryBalance) into Tripod(asset)
INFO[2021-07-25 11:11:09] I am /ip4/127.0.0.1/tcp/8887/p2p/12D3KooWHHzSeKaY8xuZVzkLbKFfvNgPPeKhFBGrMbNzbm5akpqu
INFO[2021-07-25 11:11:11] start block...................
INFO[2021-07-25 11:11:11] prev-block hash is (0x0000000000000000000000000000000000000000000000000000000000000000), height is (0)
INFO[2021-07-25 11:11:11] [[[Mining a new Block!!!]]]
655111533297e8df7319ce54a0ed74f643940831f388211a1fe985960c82fe75
INFO[2021-07-25 11:11:11] append block(1)
INFO[2021-07-25 11:11:13] start block...................
INFO[2021-07-25 11:11:13] prev-block hash is (0x0000337defe7b4412c872bd42a6d21626ea6a57c3d695379efb8b447d67a1bbd), height is (1)
INFO[2021-07-25 11:11:13] [[[Mining a new Block!!!]]]
c64e251d8617a96b80a1d8ff5b74cceef20fa9750bdbc7d095f87791ce01fc88
INFO[2021-07-25 11:11:13] append block(2)
INFO[2021-07-25 11:11:15] start block...................
INFO[2021-07-25 11:11:15] prev-block hash is (0x00005ecc8b50fbc01d32330c5cdd1574a1f0ead527a94f54f326ecaad0473ffb), height is (2)
INFO[2021-07-25 11:11:15] [[[Mining a new Block!!!]]]
819da2117ca553f098a1b16c1ad95f2e10498a5df855f5980d8a54c183b302e2
b062934ef239c48553a43591d451878a1f31abe0a72400f18d7daf4055e22625
INFO[2021-07-25 11:11:15] append block(3)

Note: In the information output above, the string of addresses after I am is the p2p address generated by node1 itself, and node 2 and node 3 need to connect to this address to build a blockchain network.

Then we write the configuration file in another path to deploy the second node and start it:

INFO[0000] register Writing(Transfer) into Tripod(asset)
INFO[0000] register Writing(CreateAccount) into Tripod(asset)
INFO[0000] register Query(QueryBalance) into Tripod(asset)
INFO[2021-07-25 11:11:17] I am /ip4/127.0.0.1/tcp/8886/p2p/12D3KooWEsGugZfk1eWUGf4oUXksvfA9QoCX8etf745SVEyiZFXQ
INFO[2021-07-25 11:11:17] start to sync history from other node
INFO[2021-07-25 11:11:17] fetch history blocks from (1) to (3)
INFO[2021-07-25 11:11:19] start block...................
INFO[2021-07-25 11:11:19] prev-block hash is (0x000046787b43397f7aa4bd1b59ecc9a25e0691086db15ef1613c0e7eeefe3c9a), height is (3)
INFO[2021-07-25 11:11:19] USE P2P block(0x00007536afbeaf2ed6e84c449b7b17836280490711b85e676b77c21d75d31f22)
INFO[2021-07-25 11:11:19] append block(4)
INFO[2021-07-25 11:11:21] start block...................
INFO[2021-07-25 11:11:21] prev-block hash is (0x00007536afbeaf2ed6e84c449b7b17836280490711b85e676b77c21d75d31f22), height is (4)
INFO[2021-07-25 11:11:21] USE P2P block(0x0000ebd7752fc71f0cc27740763c27ec9db87dd91fd781f07316744c98196806)
INFO[2021-07-25 11:11:21] append block(5)

In the output of the above figure, fetch history blocks from (1) to (3) means that when node 2 starts, the history blocks from height 1 to 3 are obtained from node 1

Finally, we write the configuration file in the third path to deploy the second node and start it:

INFO[0000] register Writing(Transfer) into Tripod(asset)
INFO[0000] register Writing(CreateAccount) into Tripod(asset)
INFO[0000] register Query(QueryBalance) into Tripod(asset)
INFO[2021-07-25 11:11:24] I am /ip4/127.0.0.1/tcp/8885/p2p/12D3KooWGcd7wei9ndo1uSZKap3s8X2E3kxN674xGo8prJUBSezD
INFO[2021-07-25 11:11:24] start to sync history from other node
INFO[2021-07-25 11:11:24] fetch history blocks from (1) to (7)
INFO[2021-07-25 11:11:26] start block...................
INFO[2021-07-25 11:11:26] prev-block hash is (0x00002b56e478552f309b208e90cae288f8f809e2ccefd788730d55ea5e39e1e1), height is (7)
INFO[2021-07-25 11:11:26] USE P2P block(0x0000f86034bf5e2fe3cd308d08ad0dab1b61c41e7f722c489c14e1215acd0990)
INFO[2021-07-25 11:11:26] append block(8)
INFO[2021-07-25 11:11:28] start block...................
INFO[2021-07-25 11:11:28] prev-block hash is (0x0000f86034bf5e2fe3cd308d08ad0dab1b61c41e7f722c489c14e1215acd0990), height is (8)
INFO[2021-07-25 11:11:28] USE P2P block(0x000018f47c74dafca4a54596d5284fb320050f1cd3817a87864074cb602e2d15)
INFO[2021-07-25 11:11:28] append block(9)

In the output of the above figure, fetch history blocks from (1) to (7) indicates that when node 3 starts, the history blocks from height 1 to 7 are obtained from node 1

Node1 Config

run_mode = 0
http_port = "7999"
ws_port = "8999"
log_level = "info"
log_output = "yu.log"
lei_limit = 50000
timeout = 60

[p2p]
p2p_listen_addrs = ["/ip4/127.0.0.1/tcp/8887"]
protocol_id = "yu"
node_key_type = 1
node_key_rand_seed = 1

[kvdb]
kv_type = "bolt"
path = "./yu.db"

[block_chain.chain_db]
sql_db_type = "sqlite"
dsn = "chain.db"

[txpool]
pool_size = 2048
txn_max_size = 1024000

Node2 Config

run_mode = 0
http_port = "7998"
ws_port = "8998"
log_level = "info"
log_output = "yu.log"
lei_limit = 50000
timeout = 60

[p2p]
p2p_listen_addrs = ["/ip4/127.0.0.1/tcp/8886"]
# This configuration is the P2P address printed out when node 1 starts up
bootnodes = ["/ip4/127.0.0.1/tcp/8887/p2p/12D3KooWHHzSeKaY8xuZVzkLbKFfvNgPPeKhFBGrMbNzbm5akpqu"]
protocol_id = "yu"
node_key_type = 1

[kvdb]
kv_type = "bolt"
path = "./yu.db"

[block_chain.chain_db]
sql_db_type = "sqlite"
dsn = "chain.db"

[txpool]
pool_size = 2048
txn_max_size = 1024000

Node3 Config

run_mode = 0
http_port = "3998"
ws_port = "3998"
log_level = "info"
log_output = "yu.log"
lei_limit = 50000
timeout = 60

[p2p]
p2p_listen_addrs = ["/ip4/127.0.0.1/tcp/8885"]
# This configuration is the P2P address printed out when node 1 starts up
bootnodes = ["/ip4/127.0.0.1/tcp/8887/p2p/12D3KooWHHzSeKaY8xuZVzkLbKFfvNgPPeKhFBGrMbNzbm5akpqu"]
protocol_id = "yu"
node_key_type = 1

[block_chain.chain_db]
sql_db_type = "sqlite"
dsn = "chain.db"

[kvdb]
kv_type = "bolt"
path = "./yu.db"

[txpool]
pool_size = 2048
txn_max_size = 1024000