11. SPV轻节点部署和使用文档

11.1. 概述

11.1.1. SPV轻节点概述

SPV轻节点在spvlight两种模式下,支持独立部署和作为组件集成的方式使用:

  • 独立部署,单独一个进程。在spv模式下,通过同步区块头和交易hash可对外提供交易存在性及有效性证明服务;在light模式下,可同步和查询区块和同组织内的交易数据。

  • 作为组件集成进其他项目,与其他项目在一个进程中。在spv模式下,调用启动以获取业务链的数据,可提供交易存在性及有效性证明功能;在light模式下,可同步和查询区块和同组织内的交易数据,并支持用户注册回调函数,在提交区块后将被执行。

了解SPV轻节点设计方案,请点击如下链接:
SPV轻节点设计文档

11.2. SPV轻节点独立部署流程

11.2.1. 源码下载

  • 下载chainmaker-spv源码到本地

$ git clone --recursive https://git.chainmaker.org.cn/chainmaker/chainmaker-spv.git

11.2.2. 生成物料

  • 进入chainmaker-spv/scripts目录,执行prepare.sh脚本,将编译生成spv二进制文件,并生成spv所需要的配置文件,存于chainmaker-spv/build/release路径中。

# 进入脚本目录
$ cd chainmaker-spv/scripts

# 查看目录结构
$ tree 
.
├── local              # 该文件下的脚本,在生成物料时,将被拷贝到chainmaker-spv/build/release/bin目录下
│   ├── start.sh
│   └── stop.sh
├── prepare.sh        # 编译生成spv二进制并生成配置文件模板的脚本
├── start.sh          # 启动SPV的脚本
└── stop.sh           # 停止SPV的脚本    

# 编译生成spv二进制文件并生成配置文件模板
$ ./prepare.sh

# 查看生成的二进制文件和配置文件
$ tree -L 3 ../build/release
../build/release
├── bin
│   ├── spv       # 二进制文件
│   ├── start.sh  # 启动脚本
│   └── stop.sh   # 停止脚本
└── config                                 
    ├── chainmaker_sdk_config_chain1.yml    # 链ID为chain1的SDK配置文件
    ├── chainmaker_sdk_config_chain2.yml    # 链ID为chain2的SDK配置文件
    ├── crypto-config                       # ChainMaker-go-sdk所需证书配置文件
    │   ├── wx-org1.chainmaker.org
    │   ├── wx-org2.chainmaker.org
    │   ├── wx-org3.chainmaker.org
    │   └── wx-org4.chainmaker.org
    └── spv_config.yml                      # SPV配置文件                   

11.2.3. 修改配置文件

  • 从SPV轻节点节点所要链接的远端ChainMaker链,拷贝节点证书配置文件crypto-config,更新SPV轻节点项目chainmaker-spv/build/release/config路径下的crypto-config文件。

目前SPV轻节点只支持作为ChainMaker的轻节点,通常crypto-configchainmaker-go/build路径下。

  • 修改chainmaker-spv/build/release/config路径下的SPV配置文件spv_config.yml

在chains中配置SPV轻节点信息,只支持chainmaker_light,chainmaker_spvfabric_spv三种模式,可支持多链。需配置的远端链信息包括链ID、同步链最新区块高度时间间隔、链SDK配置文件路径。
在grpc中配置SPV提供交易存在性和有效性服务的grpc地址/端口。 在web中配置查询区块信息、交易信息、以及SPV轻节点同步的区块高度等服务的web地址/端口。 在storage中配置SPV的存储模块,目前多链共用同一存储模块。
在log中配置SPV的log信息,目前多链共用日志模块,多链以chainID区分。
注意:SPV配置文件中的路径是基于bin文件夹下spv二进制文件的相对路径,也可以使用绝对路径。

# 链配置
chains:
  # 类型  仅支持(chainmaker_light,chainmaker_spv,fabric_spv)
  - chain_type: "chainmaker_light"
    # 链ID
    chain_id: "chain1"
    # 同步配置,同步链中节点区块最新高度信息的时间间隔,单位:毫秒
    sync_chainInfo_interval: 10000
    # sdk配置文件路径
    sdk_config_path: "../config/chainmaker_config/chainmaker_sdk_config_chain1.yml"

# - chain_type: "chainmaker_spv"
#   # 链ID
#   chain_id: "chain2"
#   # 同步配置,同步链中节点区块最新高度信息的时间间隔,单位:毫秒
#   sync_chainInfo_interval: 10000
#   # sdk配置文件路径
#   sdk_config_path: "../config/chainmaker_config/chainmaker_sdk_config_chain2.yml"

# - chain_type: "fabric_spv"
#   chain_id: "mychannel"
#   sync_chainInfo_interval: 10000
#   sdk_config_path: "../config/fabric_config/fabric_sdk_config_org1.yaml"
#   fabric_extra_config:    # fabric特有的配置项,其他类型的链不需要配置
#     user: "User1"    # 用户名
#     peers:           # 节点列表
#       - peer: "peer0.org1.example.com"
#       - peer: "peer1.org1.example.com"
#       - peer: "peer0.org2.example.com"
#       - peer: "peer1.org2.example.com"

# grpc配置
grpc:
  # grpc监听网卡地址
  address: 127.0.0.1
  # grpc监听端口
  port: 12308

# web配置
web:
  # web服务监听网卡地址
  address: 127.0.0.1
  # web监听端口
  port: 8080

# 存储配置,用于配置当前SPV对区块头和交易哈希的存储记录
storage:
  # 存储采用的类型,当前仅支持leveldb类型
  provider: "leveldb"
  # 存储采用leveldb的情况下,对应leveldb的详细配置
  leveldb:
    # leveldb的存储路径
    store_path: "../data/spv_db"
    # leveldb的写入Buffer大小,单位:M
    write_buffer_size: 4
    # leveldb的布隆过滤器的bit长度
    bloom_filter_bits: 10

# 日志配置,用于配置日志的打印
log:
  system:
    # 日志打印级别
    log_level: "INFO"
    # 日志文件路径
    file_path: "../log/spv.log"
    # 日志最长保存时间,单位:天
    max_age: 365
    # 日志滚动时间,单位:小时
    rotation_time: 1
    # 是否展示日志到终端,仅限于调试使用
    log_in_console: false
    # 是否打印颜色日志
    show_color: true
  • 修改chainmaker-spv/build/release/config路径下各链的SDK配置chainmaker_sdk_config_chainx.yml

在chain_client中配置证书信息。
在nodes中配置连接节点信息。注意:修改节点的地址/端口,端口是ChainMaker的RPC端口
注意:SDK配置文件中的路径是基于bin文件夹下spv二进制文件的相对路径,也可以使用绝对路径。私钥和证书配置请按照spvlight模式的不同选择对应的私钥和证书路径。

chain_client:
  # 链ID
  chain_id: "chain1"
  # 组织ID
  org_id: "wx-org1.chainmaker.org"
  # 客户端用户私钥路径(如果配置为chainmaker_spv,此处请配置为client私钥,如果配置为chainmaker_light,此处请配置为light私钥,下面另外三项配置同理)
  user_key_file_path: "../config/crypto-config/wx-org1.chainmaker.org/user/client1/client1.tls.key"
  # 客户端用户证书路径
  user_crt_file_path: "../config/crypto-config/wx-org1.chainmaker.org/user/client1/client1.tls.crt"
  # 客户端用户交易签名私钥路径(若未设置,将使用user_key_file_path)
  user_sign_key_file_path: "../config/crypto-config/wx-org1.chainmaker.org/user/client1/client1.sign.key"
  # 客户端用户交易签名证书路径(若未设置,将使用user_crt_file_path)
  user_sign_crt_file_path: "../config/crypto-config/wx-org1.chainmaker.org/user/client1/client1.sign.crt"

  nodes:
    - # 节点地址,格式为:IP:端口,端口是ChainMaker中的RPC端口
      node_addr: "127.0.0.1:12301"
      # 节点连接数
      conn_cnt: 10
      # RPC连接是否启用双向TLS认证
      enable_tls: true
      # 信任证书池路径
      trust_root_paths:
        - "../config/crypto-config/wx-org1.chainmaker.org/ca"
        - "../config/crypto-config/wx-org2.chainmaker.org/ca"
      # TLS hostname
      tls_host_name: "chainmaker.org"
    - # 节点地址,格式为:IP:端口,端口是ChainMaker中的RPC端口
      node_addr: "127.0.0.1:12302"
      # 节点连接数
      conn_cnt: 10
      # RPC连接是否启用双向TLS认证
      enable_tls: true
      # 信任证书池路径
      trust_root_paths:
        - "../config/crypto-config/wx-org1.chainmaker.org/ca"
        - "../config/crypto-config/wx-org2.chainmaker.org/ca"
      # TLS hostname
      tls_host_name: "chainmaker.org"

11.2.4. 启动SPV轻节点

  • chainmaker-spv/scripts目录,运行 start.sh 脚本,将会调用chainmaker-spv/build/release/bin目录中的start.sh脚本,启动SPV轻节点。

$ ./start.sh
  • 查看进程是否存在

$ ps -ef|grep spv | grep -v grep
501 82533     1   0 12:27AM ttys011    0:00.23 ./spv start -c ../config/spv_config.yml
  • 查看端口是否监听

$ lsof -i:12308
COMMAND   PID      USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
spv     82533 liukemeng   14u  IPv4 0x321a94eae97e5edf      0t0  TCP localhost:12308 (LISTEN)
  • 查看日志

$ tail -f ../build/release/log/spv.log
2021-06-23 00:28:02.313 [INFO]  [Sdk]   chainmaker-sdk/sdk_cert_manage.go:89    [SDK] begin to query cert, [contract:SYSTEM_CONTRACT_CERT_MANAGE]/[method:CERTS_QUERY]
2021-06-23 00:28:02.318 [INFO]  [SPV]   server/spv_server.go:88 Start SPV Server!
2021-06-23 00:28:02.340 [INFO]  [StateManager]  manager/state_manager.go:145    [ChainId:chain1] Start state manager!
2021-06-23 00:28:02.342 [INFO]  [StateManager]  manager/state_manager.go:176    [ChainId:chain1] subscribe block successfully!
2021-06-23 00:28:02.345 [INFO]  [Rpc]   rpcserver/rpc_server.go:65      GRPC Server Listen on 127.0.0.1:12308
2021-06-23 00:28:12.414 [INFO]  [BlockManager]  manager/block_manager.go:167    [ChainId:chain1] spv has synced to the highest block! current local height:1, remote max height:1

11.2.5. 停止SPV轻节点

  • chainmaker-spv/scripts目录,运行 stop.sh 脚本,将会调用chainmaker-spv/build/release/bin目录中的stop.sh脚本,停止SPV轻节点。

$ ./stop.sh

11.2.6. 停止SPV轻节点并清除data和log

  • chainmaker-spv/scripts目录,运行 stop.sh 脚本,并添加clean命令,将会调用chainmaker-spv/build/release/bin目录中的stop.sh脚本,停止SPV轻节点,并清除chainmaker-spv/build/release/data中的所有数据。

$ ./stop.sh clean

11.3. SPV模式独立部署时,Client端验证交易有效性示例

package usecase

import (
	"context"
	"log"

	"chainmaker.org/chainmaker-spv/pb/api"
	"google.golang.org/grpc"
)

func useCase() {
	// 1.构造Client
	conn, err := grpc.Dial("127.0.0.1:12308", grpc.WithInsecure())
	if err != nil {
		log.Println("connection failed!")
		return
	}
	client := api.NewRpcProverClient(conn)

	// 2.构造交易验证信息
	txInfo := &api.TxValidationInfo{
		ChainId: "chainId", // 链Id
		BlockHeight: 1,     // 交易所在区块高度
		//Index: -1,        // 此版本未验证该字段,不需要填写
		TxKey: "TxId",      // 交易Id
		ContractData: &api.ContractData{
			ContractName: "contractName",  // 合约名
			Method: "method",              // 方法名
			//Version: "version",          // 此版本未验证该字段,不需要填写
			Params: []*api.KVPair{
				{Key: "argName1", Value: "argValue1"},  // Key是所调用合约方法的参数名,Value是参数值
				{Key: "argName2", Value: "argValue2"},
				{Key: "argName3", Value: "argValue3"},
			},
			Extra: nil,    // 预留扩展字段
		},
		Extra: nil,        // 预留扩展字段
	}

	// 3.验证交易有效性
	response, err := client.ValidTransaction(context.Background(), txInfo)
	if err != nil {
		log.Println("invalid transaction!")
	}

	if int32(response.Code) != 0 {
		log.Println("invalid transaction!")
	}

	// 4.用户其他逻辑

}

11.4. SPV模式作为组件集成进其他项目时,进行交易有效性验证示例

11.4.1. 创建并启动组件

package usecase

import (
	"log"

	"chainmaker.org/chainmaker-spv/server"
	"go.uber.org/zap"
)

func useCase() {
	var (
		ymlFile = "../config/spv_config.yml"
		logger     = &zap.SugaredLogger{}
	)

	// 1.创建 spv server
	spvServer, err := server.NewSPVServer(ymlFile, logger)
	if err != nil {
		log.Println("new spv server failed!")
		return
	}

	// 2.启动 spv server
	err = spvServer.Start()
	if err != nil {
		log.Println("start spv server failed!")
		return
	}
}

11.4.2. 进行交易有效性验证

func useCase() {
	// 1.构造交易验证信息
	txInfo := &api.TxValidationInfo{
		ChainId: "chainId", // 链Id
		BlockHeight: 1,     // 交易所在区块高度
		//Index: -1,        // 此版本未验证该字段,不需要填写
		TxKey: "TxId",      // 交易Id
		ContractData: &api.ContractData{
			ContractName: "contractName",  // 合约名
			Method: "method",              // 方法名
			//Version: "version",          // 此版本未验证该字段,不需要填写
			Params: []*api.KVPair{
				{Key: "argName1", Value: "argValue1"},  // Key是所调用合约方法的参数名,Value是参数值
				{Key: "argName2", Value: "argValue2"},
				{Key: "argName3", Value: "argValue3"},
			},
			Extra: nil,    // 预留扩展字段
		},
		Extra: nil,        // 预留扩展字段
	}

	// 2.设置验证超时时间(默认设置5000ms)
	timeout := 20000*time.Millisecond

	// 3.验证交易有效性
	err := spvServer.ValidTransaction(txInfo, timeout)
	if err != nil {
		log.Printf("invalid transaction! err:%s", err.Error())
		return
	}

	// 4.用户其他逻辑
}

11.5. Light模式独立部署时,可对外提供如下查询方法

package webserver
import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"

	"chainmaker.org/chainmaker-spv/pb/api"
)

func useCase() {
	// 1.根据chainId和区块height获取区块,其中fromRemote为true表明Light节点从远端链获取,fromRemote为false表明Light节从本地获取(只包含区块头)
	url1 := "http://localhost:8080/GetBlockByHeight?fromRemote=false&chainId=chain1&height=1"
	res1, err := http.Post(url1, "application/json", nil)
	if err != nil {
		log.Error(err)
		return
	}
	content1, err:= ioutil.ReadAll(res1.Body)
	defer res1.Body.Close()
	if err != nil {
		log.Error(err)
		return
	}
	fmt.Print(content1)

	// 2.根据chainId和区块hash获取区块,其中fromRemote为true表明Light节点从远端链获取,fromRemote为false表明Light节从本地获取(只包含区块头)
	url2 := "http://localhost:8080/GetBlockByHash?fromRemote=false&chainId=chain1&blockHash=xxx"
	res2, err := http.Post(url2, "application/json", nil)
	if err != nil {
		log.Error(err)
		return
	}
	content2, err:= ioutil.ReadAll(res2.Body)
	defer res2.Body.Close()
	if err != nil {
		log.Error(err)
		return
	}
	fmt.Print(content2)

	// 3.根据chainId和txId获取获取交易所在区块,其中fromRemote为true表明Light节点从远端链获取,fromRemote为false表明Light节从本地获取(只包含区块头)
	url3 := "http://localhost:8080/GetBlockByTxId?fromRemote=false&chainId=chain1&txId=xxx"
	res3, err := http.Post(url3, "application/json", nil)
	if err != nil {
		log.Error(err)
		return
	}
	content3, err:= ioutil.ReadAll(res3.Body)
	defer res3.Body.Close()
	if err != nil {
		log.Error(err)
		return
	}
	fmt.Print(content3)

	// 4.根据chainId获取最近被提交的区块,其中fromRemote为true表明Light节点从远端链获取,fromRemote为false表明Light节从本地获取(只包含区块头)
	url4 := "http://localhost:8080/GetLastBlock?fromRemote=false&chainId=chain1"
	res4, err := http.Post(url4, "application/json", nil)
	if err != nil {
		log.Error(err)
		return
	}
	content4, err:= ioutil.ReadAll(res4.Body)
	defer res4.Body.Close()
	if err != nil {
		log.Error(err)
		return
	}
	fmt.Print(content4)

	// 5.根据chainId和txId获取同组织内的某一交易,其中fromRemote为true表明Light节点从远端链获取,fromRemote为false表明Light节从本地获取
	url5 := "http://localhost:8080/GetTransactionByTxId?fromRemote=false&chainId=chain1&txId=xxx"
	res5, err := http.Post(url5, "application/json", nil)
	if err != nil {
		log.Error(err)
		return
	}
	content5, err:= ioutil.ReadAll(res5.Body)
	defer res5.Body.Close()
	if err != nil {
		log.Error(err)
		return
	}
	fmt.Print(content5)

	// 6.根据chainId获取light节点同步的同组织交易总数
	url6 := "http://localhost:8080/GetTransactionTotalNum?chainId=chain1"
	res6, err := http.Post(url6, "application/json", nil)
	if err != nil {
		log.Error(err)
		return
	}
	content6, err:= ioutil.ReadAll(res6.Body)
	defer res6.Body.Close()
	if err != nil {
		log.Error(err)
		return
	}
	fmt.Print(content6)

	// 7.根据chainId获取light节点同步的区块总数,最近被提交的区块高度+1
	url7 := "http://localhost:8080/GetBlockTotalNum?chainId=chain1"
	res7, err := http.Post(url7, "application/json", nil)
	if err != nil {
		log.Error(err)
		return
	}
	content7, err:= ioutil.ReadAll(res7.Body)
	defer res7.Body.Close()
	if err != nil {
		log.Error(err)
		return
	}
	fmt.Print(content7)

	// 8.根据chainId获取light节点同步的区块高度
	url8 := "http://localhost:8080/GetCurrentBlockHeight?chainId=chain1"
	res8, err := http.Post(url8, "application/json", nil)
	if err != nil {
		log.Error(err)
		return
	}
	content8, err:= ioutil.ReadAll(res8.Body)
	defer res8.Body.Close()
	if err != nil {
		log.Error(err)
		return
	}
	fmt.Print(content8)

	// 9.根据交易验证信息api.TxValidationInfo验证同组织内交易有效性
	url9 := "http://localhost:8080/ValidTransaction"
	txInfo := &api.TxValidationInfo{
		ChainId: "chainId", // 链Id
		BlockHeight: 1,     // 交易所在区块高度
		//Index: -1,        // 此版本未验证该字段,不需要填写
		TxKey: "TxId",      // 交易Id
		ContractData: &api.ContractData{
			ContractName: "contractName",  // 合约名
			Method: "method",              // 方法名
			//Version: "version",          // 此版本未验证该字段,不需要填写
			Params: []*api.KVPair{
				{Key: "argName1", Value: "argValue1"},  // Key是所调用合约方法的参数名,Value是参数值
				{Key: "argName2", Value: "argValue2"},
				{Key: "argName3", Value: "argValue3"},
			},
			Extra: nil,    // 预留扩展字段
		},
		Extra: nil,        // 预留扩展字段
	}
	js, err := json.Marshal(txInfo)
	if err != nil {
		log.Error(err)
		return
	}
	body := bytes.NewBuffer(js)
	res9, err := http.Post(url9, "application/json", body)
	if err != nil {
		t.Log(err)
		return
	}
	content9, _ := ioutil.ReadAll(res9.Body)
	defer res9.Body.Close()
	fmt.Print(content9)
}

11.6. Light模式作为组件集成进其他项目时,可注册回调函数

package usecase

import (
	"fmt"
	"log"

	"chainmaker.org/chainmaker-spv/common"
	"chainmaker.org/chainmaker-spv/server"
	"go.uber.org/zap"
)

func useCase() {
	var (
		ymlFile = "../config/spv_config.yml"
		logger  = &zap.SugaredLogger{}
	)

	// 1.创建 spv server
	spvServer, err := server.NewSPVServer(ymlFile, logger)
	if err != nil {
		log.Println("new light server failed!")
		return
	}

	// 2.注册回调,当区块在light提交至数据库时被执行
	err = spvServer.RegisterCallBack("chain1", func(block common.Blocker) {
		fmt.Printf("block height: %d", block.GetHeight())
	})

	// 3.启动 spv server
	err = spvServer.Start()
	if err != nil {
		log.Println("start light server failed!")
		return
	}
}