Case analysis of Tendermint KVStore

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.

Keywords: Blockchain

Added by aperantos on Thu, 17 Feb 2022 19:23:57 +0200