Tendermint 应用开发 - 全流程指南

Tendermint 应用开发 - 全流程指南

注: 本教程大部分内容根据官方文档翻译得到,并添加少部分讲解.

官方文档: https://docs.tendermint.com/

默认版本为: Tendermint v0.34.24

默认操作系统: Ubuntu Server 20.04 LTS

一. Tendermint概述

1.1 Tendermint 定义

Tendermint: Tendermint is software for securely and consistently replicating an application on many machines.

Tendermint是用于在许多机器上安全、一致地 复制 应用程序 的软件。

  • 安全性
    多达1/3的机器以任意方式出现故障,Tendermint也能正常工作。
  • 一致性
    每台无故障的机器都会看到相同的事务日志并计算相同的状态。

1.2 Tendermint 组成

Tendermint consists of two chief technical components: a blockchain consensus engine and a generic application interface

**Tendermint组成:**区块链共识引擎(Tendermint Core)+ 通用应用程序接口(ABCI)。

  1. **Tendermint Core: **Tendermint Core performs Byzantine Fault Tolerant (BFT) State Machine Replication (SMR) for arbitrary deterministic, finite state machines.

Tendermint Core对任意确定的 状态机 执行拜占庭容错(BFT)状态机复制(SMR)。

**有限状态机:**指的是 应用程序.

Tendermint Core另一种定义:

Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a **state transition machine **- written in any programming language - and securely replicates it on many machines.

  1. **ABCI(Application BlockChain Interface): **

Tendermint is able to decompose the blockchain design by offering a very simple API (i.e. the ABCI) between the application process and consensus process.

ABCI(Application Blockchain Interface)是Tendermint使用的一种接口协议规范,用于Tendermint Core(共识引擎)与应用程序之间的交互。

Tendermint能够通过在应用程序进程共识进程之间提供一个简单的API(即ABCI)来分解区块链设计。

在这里插入图片描述

1.3 Tendermint实现区块链应用, 组件的功能

  1. Tendermint Core:

    • 要在节点间传播区块和交易

    • 建立规范(不可变)的交易顺序

  2. Application:

    • 维护存放交易的数据库

    • 验证交易签名

    • 防止双花

    • 查询交易

  3. ABCI :

    • 提供一个接口规范, 使得开发者可以开发出可以与Tendermint Core交互的 应用程序.

1.4 Tendermint 共识流程概述

Tendermint是一种易于理解的、大部分是异步的拜占庭容错(BFT)共识协议。该协议遵循一个简单的状态机模型,如下所示:
在这里插入图片描述

**共识逻辑: **

在该协议中,参与者被称为验证者;他们轮流提出交易块并对其进行投票。每个高度上都有一个区块链中的区块被确认。如果一个区块无法被确认,协议将进入下一个轮次,新的验证者将为该高度提出一个新的区块。为了成功确认一个区块,需要进行两个阶段的投票,我们称之为pre-vote和pre-commit。当超过2/3的验证者在同一轮次中pre-commit同一个区块时,该区块被确认。

1.5 ABCI的消息类型

ABCI(Application Blockchain Interface)是Tendermint使用的一种接口协议规范,用于Tendermint Core(共识引擎)与应用程序之间的交互。Tendermint能够通过在应用程序进程共识进程之间提供一个简单的API(即ABCI)来分解区块链设计。

ABCI由三种主要的消息类型组成,这些消息从核心传递到应用程序,应用程序则回复相应的响应消息。这些消息在ABCI规范中进行了详细说明。

  1. DeliverTx: 每个区块链中的交易都通过这个消息传递给应用程序。应用程序需要使用DeliverTx消息对接收到的每个交易进行验证,包括验证当前状态、应用程序协议以及交易的加密凭证。

    1. CheckTx消息与DeliverTx类似,但仅用于验证交易。Tendermint Core的内存池首先使用CheckTx验证交易的有效性,只将有效的交易传递给其他节点。例如,应用程序可以检查交易中的递增序列号,如果序列号过旧,就在CheckTx阶段返回错误。或者,它们可能使用基于能力的系统,要求每个交易都需要重新获得能力。
  2. Commit消息用于计算对当前应用程序状态的加密承诺,并将其放入下一个区块头中。这具有一些便利的属性。对应用程序状态的不一致性现在会呈现为区块链分叉,从而捕捉到一整类编程错误。这还简化了安全轻量级客户端的开发,因为可以通过校验默克尔哈希证明与区块哈希进行对比,并验证区块哈希是否由一个法定人数签名。

一个应用程序可以与多个ABCI socket连接。Tendermint Core会创建三个ABCI连接到应用程序:一个用于验证内存池中的交易广播,一个用于共识引擎运行块提案,还有一个用于查询应用程序状态。以下图表描述了消息通过ABCI的流程。
在这里插入图片描述

1.6 Tendermint 技术概览图

TODO

在这里插入图片描述

1.7 Tendermint 质量保证( 性能测试)

可参见: https://docs.tendermint.com/v0.34/qa/#

二. Tendermint 部署与启动

[注]:

  1. 官方文档中,提供一个快速安装的脚本,但是该脚本中使用的go版本为1.10,且仅供一个全新的 Ubuntu 16.04 实例, 不便于我们后续学习和部署, 所以不使用.
  2. 从二进制文件安装, 仅能安装官方编译好的二进制文件, 我们无法进行二次开发, 所以也不使用.

2.1 从源代码安装Tendermint

2.1.1安装Go语言环境
  1. 安装Go

参考: https://go.dev/doc/install

  1. 设置环境变量

    echo export GOPATH=\"\$HOME/go\" >> ~/.bashrc
    echo export PATH=\"\$PATH:\$GOPATH/bin\" >> ~/.bashrc
    

    [注]:

    后续make build的时候,可能会出现报错, 这是go代理 和 本地网络等多种原因造成的,可以尝试:

    # 设置go 中国代理:
    unset GOPROXY
    export GOPROXY=https://goproxy.cn
    # 取消git 代理
    git config --global --unset http.proxy
    git config --global --unset https.proxy
    # 取消本机的http代理和https代理
    unset http_proxy
    unset https_proxy
    # 最后重启机器再次尝试
    
2.1.2 获取Tendermint 源码
git clone -b v0.34.24 https://github.com/tendermint/tendermint.git
2.1.3 编译
cd tendermint
make build
2.1.4 测试是否安装成功
tendermint version

2.2 启动Tendermint

  1. 单节点启动
tendermint init
tendermint node --proxy_app=kvstore

[注]:

如果报错为无法初始化, 认证信息已经存在, 那么可能是之前初始化过, 需要:

rm -rf ~/.tendermint

后续的应用开发中, 会修改该配置文件的位置, 注意每次启动所对应的配置文件.

  1. 单节点测试
curl -s 'localhost:26657/broadcast_tx_commit?tx="cdd=hello"'
curl -s 'localhost:26657/abci_query?data="cdd"'
curl -s 'localhost:26657/broadcast_tx_commit?tx="cdd=world"'
curl -s 'localhost:26657/abci_query?data="xyz"'

2.3 编译CLevelDB支持

TODO

https://docs.tendermint.com/v0.34/introduction/install.html

2.4 多机部署

TODO

三. 创建一个Tendermint 内置应用程序 示例

通过遵循本指南,您将创建一个名为kvstore的Tendermint Core项目,这是一个(非常)简单的分布式BFT键值存储。

内置应用程序外部应用程序 对比:

  1. 内置应用程序: 在与Tendermint Core 相同的进程中 运行 应用程序 将为您提供 尽可能好的性能。
  2. 外部应用程序: 对于外部应用程序,必须通过TCP、Unix域套接字或gRPC与Tendermint Core进行通信。

3.1 创建一个应用程序项目

  1. 创建项目
mkdir tendermint-app
cd tendermint-app
  1. 创建包

创建一个app.go文件, 你可以将其放置在main包下

mkdir main
cd main
  1. 创建应用程序代码文件
vim app.go

app.go:

package main

import (
    "bytes"
    "github.com/dgraph-io/badger"
	abcitypes "github.com/tendermint/tendermint/abci/types"
    
)

type KVStoreApplication struct {
	db           *badger.DB
	currentBatch *badger.Txn
}

var _ abcitypes.Application = (*KVStoreApplication)(nil)

func NewKVStoreApplication(db *badger.DB) *KVStoreApplication {
	return &KVStoreApplication{
		db: db,
	}
}

func (KVStoreApplication) Info(req abcitypes.RequestInfo) abcitypes.ResponseInfo {
	return abcitypes.ResponseInfo{}
}

func (KVStoreApplication) SetOption(req abcitypes.RequestSetOption) abcitypes.ResponseSetOption {
	return abcitypes.ResponseSetOption{}
}

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) isValid(tx []byte) (code uint32) {
	// check format
	parts := bytes.Split(tx, []byte("="))
	if len(parts) != 2 {
		return 1
	}

	key, value := parts[0], parts[1]

	// check if the same key=value already 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) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx {
	code := app.isValid(req.Tx)
	return abcitypes.ResponseCheckTx{Code: code, GasWanted: 1}
}

func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit {
	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) InitChain(req abcitypes.RequestInitChain) abcitypes.ResponseInitChain {
	return abcitypes.ResponseInitChain{}
}

func (app *KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock {
	app.currentBatch = app.db.NewTransaction(true)
	return abcitypes.ResponseBeginBlock{}
}

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{}
}

在1.5小结将结果ABCI的消息类型, 现在将针对每种消息类型分类解释每个方法.

现在,我将解释每个方法,解释何时调用它,并添加所需的业务逻辑。

3.2 CheckTx

当一个新的事务被添加到Tendermint Core时,它会要求应用程序对其进行检查(验证格式、签名等)。

在app.go中, CheckTx 方法用于检查该事务是否合法, 该方法调用isValid方法分析参数req是否合法.

import "bytes"

func (app *KVStoreApplication) isValid(tx []byte) (code uint32) {
	// check format
	parts := bytes.Split(tx, []byte("="))
	if len(parts) != 2 {
		return 1
	}

	key, value := parts[0], parts[1]

	// check if the same key=value already 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) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx {
	code := app.isValid(req.Tx)
	return abcitypes.ResponseCheckTx{Code: code, GasWanted: 1}
}

isValid方法核心思想:

如果事务没有{字节}={字节}的形式,我们返回1个代码。当已经存在相同的键=值(相同的键和值)时,我们返回2个代码。对于其他代码,我们返回一个零代码,表示它们是有效的。

请注意,任何具有非零代码的东西都将被Tendermint Core视为无效(-1、100等)。

对于底层键值存储,我们将使用badger,这是一个可嵌入、持久且快速的键值(KV)数据库。

import "github.com/dgraph-io/badger"

type KVStoreApplication struct {
	db           *badger.DB
	currentBatch *badger.Txn
}

func NewKVStoreApplication(db *badger.DB) *KVStoreApplication {
	return &KVStoreApplication{
		db: db,
	}
}

3.3 BeginBlock -> DeliverTx -> EndBlock -> Commit

当Tendermint Core决定出块时,它将分三部分传送到应用程序:BeginBlock,DeliverTx,最后是EndBlock。

func (app *KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock {
	app.currentBatch = app.db.NewTransaction(true)
	return abcitypes.ResponseBeginBlock{}
}
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}
}

如果事务的格式不正确,或者已经存在相同的key=value,我们将再次返回非零代码。否则,我们将其添加到当前批次中。

在当前设计中,区块可能包括不正确的交易(通过CheckTx但未通过DeliverTx的交易或提案人直接包括的交易)。这样做是出于性能原因。

请注意,我们不能在DeliverTx内提交事务,因为在这种情况下,可以并行调用的Query将返回不一致的数据(即,即使实际块尚未提交,它也会报告某些值已经存在)。

Commit指示应用程序保持新状态。

func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit {
	app.currentBatch.Commit()
	return abcitypes.ResponseCommit{Data: []byte{}}
}

3.4 Query

当客户端想要知道某个特定的键/值何时存在时,它将调用Tendermint Core RPC/abci_query端点,后者将依次调用应用程序的query方法。

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
}

3.5 在一个进程中, 启动一个应用程序和一个Tendermint Core 实例

创建一个main.go文件,将以下代码放入.

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

func init() {
	flag.StringVar(&configFile, "config", "$HOME/.tendermint/config/config.toml", "Path to config.toml")
}

func main() {
	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()
	app := NewKVStoreApplication(db)

	flag.Parse()

	node, err := newTendermint(app, configFile)
	if err != nil {
		fmt.Fprintf(os.Stderr, "%v", err)
		os.Exit(2)
	}

	node.Start()
	defer func() {
		node.Stop()
		node.Wait()
	}()

	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
	<-c
	os.Exit(0)
}

func newTendermint(app abci.Application, configFile string) (*nm.Node, error) {
 // read config
 config := cfg.DefaultConfig()
 config.RootDir = filepath.Dir(filepath.Dir(configFile))
 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 logger
 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 private validator
 pv := privval.LoadFilePV(
  config.PrivValidatorKeyFile(),
  config.PrivValidatorStateFile(),
 )

 // read node key
 nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile())
 if err != nil {
  return nil, fmt.Errorf("failed to load node's key: %w", err)
 }

 // create node
 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
}

3.5.1 代码拆分讲解
  1. 首先,我们初始化Badger数据库并创建一个应用程序实例:
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()
app := NewKVStoreApplication(db)

如果是Windows, 则需要设置:

db, err := badger.Open(badger.DefaultOptions("/tmp/badger").WithTruncate(true))

对于Windows用户来说,重新启动此应用程序将导致badger抛出错误,因为它需要截断值日志。可以通过将truncate选项设置为true来避免这种情况.

  1. 然后我们创建一个Tendermint核心节点实例:
flag.Parse()

node, err := newTendermint(app, configFile)
if err != nil {
	fmt.Fprintf(os.Stderr, "%v", err)
	os.Exit(2)
}

  1. 最后,我们启动节点, 并添加一些信号处理,以便在接收到SIGTERM或Ctrl-C时优雅地停止它。
node.Start()
defer func() {
	node.Stop()
	node.Wait()
}()

c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
<-c
os.Exit(0)

3.5.2 newTendermint的具体流程

其中,newTendermint的具体流程如下:

①. 读取配置文件

viper(打开新窗口)用于读取配置,我们稍后将使用tendermint-init命令生成该配置。

 // read config
 config := cfg.DefaultConfig()
 config.RootDir = filepath.Dir(filepath.Dir(configFile))
 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 logger
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)
}

③. 读取验证器

我们使用FilePV,它是一个私有验证器(即签署共识消息的东西)。通常,您会使用SignerRemote连接到外部HSM

// read private validator
pv := privval.LoadFilePV(
	config.PrivValidatorKeyFile(),
	config.PrivValidatorStateFile(),
)

④.读取nodeKey

nodeKey用于识别p2p网络中的节点。

 // read node key
 nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile())
 if err != nil {
  return nil, fmt.Errorf("failed to load node's key: %w", err)
 }

⑤.创建节点

NewNode需要一些东西,包括配置文件、专用验证器、节点密钥和其他一些东西,才能构建完整的节点。

请注意,我们在这里使用proxy.NewLocalClientCreator来创建本地客户端,而不是通过套接字或gRPC进行通信的客户端。

// create node
 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

3.6 编译和启动 应用程序

  1. 获取依赖包, 将所有依赖打包进项目
cd tendermint-app
go mod init
go get github.com/tendermint/tendermint/@v0.34.24
go mod tidy
go mod vendor
  1. 编译
cd main
go build

编译成功后,会输出 main 的可执行文件.

  1. 初始化tendermint 配置文件
rm -rf /tmp/example
TMHOME="/tmp/example" tendermint init
  1. 启动
./main -config "/tmp/example/config/config.toml"

在这里插入图片描述

  1. 发送交易,测试
curl -s 'localhost:26657/broadcast_tx_commit?tx="cdd=hello"'
curl -s 'localhost:26657/abci_query?data="cdd"'
curl -s 'localhost:26657/broadcast_tx_commit?tx="cdd=world"'
curl -s 'localhost:26657/abci_query?data="xyz"'

在这里插入图片描述

  1. 具体的更多的ABCI规范:

https://github.com/tendermint/tendermint/tree/v0.34.x/spec/abci/

  • Tendermint Core 保持三个连接:
    • 内存池连接: 检查交易在提交前是否应转接;只使用 CheckTx
    • 共识连接: 用于执行已提交的交易。对于每个块的消息序列 - BeginBlock, [DeliverTx, ...], EndBlock, Commit
    • 查询连接: 查询应用程式状态;只使用 QueryInfo

零. TODO

  1. 编译CLevelDB支持

  2. 多机部署

  3. 编写自己的应用程序代码逻辑, 部署执行测试.

  4. Docker-compose 部署 test network

  5. 将mysql 挂到应用下面,尝试编写逻辑。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值