summary
Tendermint can be simply understood as a modular blockchain development framework, which supports developers to customize their own blockchain without considering the implementation of consensus algorithm and P2P network.
Because in Tendermint, the consensus engine and P2P network are encapsulated in the Tendermint Core and interact with the application layer through ABCI. Therefore, when developing with Tendermint, we only need to implement the ABCI interface to quickly develop a blockchain application.
KVStore case
KVStore official document address: https://docs.tendermint.com/master/tutorials/go-built-in.html
KVStore application
package main import ( "bytes" abcitypes "github.com/tendermint/tendermint/abci/types" "github.com/dgraph-io/badger" ) //Implement abci interface var _ abcitypes.Application = (*KVStoreApplication)(nil) //Defines the structure of the KVStore program type KVStoreApplication struct { db *badger.DB currentBatch *badger.Txn } // Create an ABCI APP func NewKVStoreApplication(db *badger.DB) *KVStoreApplication { return &KVStoreApplication{ db: db, } } // Check whether the transaction meets your requirements. When 0 is returned, it represents a valid transaction func (app *KVStoreApplication) isValid(tx []byte) (code uint32) { // Format verification. If the format is not k=v, the return code is 1 parts := bytes.Split(tx, []byte("=")) if len(parts) != 2 { return 1 } key, value := parts[0], parts[1] //Check whether the same KV exists err := app.db.View(func(txn *badger.Txn) error { item, err := txn.Get(key) if err != nil && err != badger.ErrKeyNotFound { return err } if err == nil { return item.Value(func(val []byte) error { if bytes.Equal(val, value) { code = 2 } return nil }) } return nil }) if err != nil { panic(err) } return code } func (app *KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock { app.currentBatch = app.db.NewTransaction(true) return abcitypes.ResponseBeginBlock{} } //When a new transaction is added to the Tendermint Core, it will require the application to check (verify format, signature, etc.) and pass only when 0 is returned func (app KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx { code := app.isValid(req.Tx) return abcitypes.ResponseCheckTx{Code: code, GasUsed: 1} } //Here we create a batch, which will store the transactions of the block. func (app *KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx { code := app.isValid(req.Tx) if code != 0 { return abcitypes.ResponseDeliverTx{Code: code} } parts := bytes.Split(req.Tx, []byte("=")) key, value := parts[0], parts[1] err := app.currentBatch.Set(key, value) if err != nil { panic(err) } return abcitypes.ResponseDeliverTx{Code: 0} } func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit { // Commit transactions to the database. This function will be called when Tendermint core commits blocks app.currentBatch.Commit() return abcitypes.ResponseCommit{Data: []byte{}} } func (app *KVStoreApplication) Query(reqQuery abcitypes.RequestQuery) (resQuery abcitypes.ResponseQuery) { resQuery.Key = reqQuery.Data err := app.db.View(func(txn *badger.Txn) error { item, err := txn.Get(reqQuery.Data) if err != nil && err != badger.ErrKeyNotFound { return err } if err == badger.ErrKeyNotFound { resQuery.Log = "does not exist" } else { return item.Value(func(val []byte) error { resQuery.Log = "exists" resQuery.Value = val return nil }) } return nil }) if err != nil { panic(err) } return } func (KVStoreApplication) Info(req abcitypes.RequestInfo) abcitypes.ResponseInfo { return abcitypes.ResponseInfo{} } func (KVStoreApplication) InitChain(req abcitypes.RequestInitChain) abcitypes.ResponseInitChain { return abcitypes.ResponseInitChain{} } func (KVStoreApplication) EndBlock(req abcitypes.RequestEndBlock) abcitypes.ResponseEndBlock { return abcitypes.ResponseEndBlock{} } func (KVStoreApplication) ListSnapshots(abcitypes.RequestListSnapshots) abcitypes.ResponseListSnapshots { return abcitypes.ResponseListSnapshots{} } func (KVStoreApplication) OfferSnapshot(abcitypes.RequestOfferSnapshot) abcitypes.ResponseOfferSnapshot { return abcitypes.ResponseOfferSnapshot{} } func (KVStoreApplication) LoadSnapshotChunk(abcitypes.RequestLoadSnapshotChunk) abcitypes.ResponseLoadSnapshotChunk { return abcitypes.ResponseLoadSnapshotChunk{} } func (KVStoreApplication) ApplySnapshotChunk(abcitypes.RequestApplySnapshotChunk) abcitypes.ResponseApplySnapshotChunk { return abcitypes.ResponseApplySnapshotChunk{} }
Tendermint Core
package main import ( "flag" "fmt" "os" "os/signal" "path/filepath" "syscall" "github.com/dgraph-io/badger" "github.com/spf13/viper" abci "github.com/tendermint/tendermint/abci/types" cfg "github.com/tendermint/tendermint/config" tmflags "github.com/tendermint/tendermint/libs/cli/flags" "github.com/tendermint/tendermint/libs/log" nm "github.com/tendermint/tendermint/node" "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/privval" "github.com/tendermint/tendermint/proxy" ) var configFile string // Set profile path func init() { flag.StringVar(&configFile, "config", "$HOME/.tendermint/config/config.toml", "Path to config.toml") } func main() { //Initialize the Badger database and create an application instance db, err := badger.Open(badger.DefaultOptions("/tmp/badger")) if err != nil { fmt.Fprintf(os.Stderr, "failed to open badger db: %v", err) os.Exit(1) } defer db.Close() // Create ABCI APP app := NewKVStoreApplication(db) flag.Parse() // Create a Tendermint Core Node instance node, err := newTendermint(app, configFile) if err != nil { fmt.Fprintf(os.Stderr, "%v", err) os.Exit(2) } // Open node node.Start() // Close the node when the program exits defer func() { node.Stop() node.Wait() }() // Exit program c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) <-c os.Exit(0) } //Create a local node func newTendermint(app abci.Application, configFile string) (*nm.Node, error) { // Read the default Validator configuration config := cfg.DefaultValidatorConfig() // Set the path of the configuration file config.RootDir = filepath.Dir(filepath.Dir(configFile)) // The viper tool is used here, // Official documents: https://github.com/spf13/viper viper.SetConfigFile(configFile) if err := viper.ReadInConfig(); err != nil { return nil, fmt.Errorf("viper failed to read config file: %w", err) } if err := viper.Unmarshal(config); err != nil { return nil, fmt.Errorf("viper failed to unmarshal config: %w", err) } if err := config.ValidateBasic(); err != nil { return nil, fmt.Errorf("config is invalid: %w", err) } // Create log logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) var err error logger, err = tmflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel) if err != nil { return nil, fmt.Errorf("failed to parse log level: %w", err) } // Read configuration file pv, _ := privval.LoadFilePV( config.PrivValidatorKeyFile(), config.PrivValidatorStateFile(), ) nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile()) if err != nil { return nil, fmt.Errorf("failed to load node's key: %w", err) } // Create from node profile node, err := nm.NewNode( config, pv, nodeKey, proxy.NewLocalClientCreator(app), nm.DefaultGenesisDocProviderFunc(config), nm.DefaultDBProvider, nm.DefaultMetricsProvider(config.Instrumentation), logger) if err != nil { return nil, fmt.Errorf("failed to create new Tendermint node: %w", err) } return node, nil }
Program workflow
1. Call broadcast through RPC_ tx_ Commit, submit the transaction, that is, User Input in the figure. Transactions are first stored in the MemPool cache.
2. The transaction pool calls CheckTx to verify the validity of the transaction. If it passes the verification, it will be put into the trading pool.
3. The Proposer selected from the Validator packages transactions and forms blocks in the transaction pool.
4. First round of balloting. The Proposer broadcasts blocks through the mission protocol. Validators of the whole network verify blocks. Pass the verification and agree to pre vote; An empty block is generated if it fails or times out. Then each Validator broadcasts
5. Second round of balloting. All validators collect the voting information of other validators. If more than 2 / 3 of the nodes agree to pre vote, they will vote for pre commit. If less than 2 / 3 of the nodes or timeout, an empty block will continue to be generated.
6. All validators broadcast their own voting results and collect the voting results of other validators. If no more than two-thirds of the nodes receive voting information, vote pre commit or timeout. Then do not submit the block. If there are more than two-thirds of pre commit, submit the block. Finally, the block height is increased by 1.
last
Here is just a simple case to dredge the Tendermint workflow. If you have any questions, you can come to the group for communication and discussion. There are also many videos and books in the group that can be downloaded by yourself.
Finally, I recommend a official account of a big guy. Welcome to pay attention to it: block chain technology stack.