Hyperledger Fabric 使用 CouchDB 和复杂智能合约开发
前言
在上个实验中,我们已经实现了简单智能合约实现及客户端开发,但该实验中智能合约只有基础的增删改查功能,且其中的数据管理功能与传统 MySQL 比相差甚远。本文将在前面实验的基础上,将 Hyperledger Fabric 的默认数据库支持 LevelDB 改为 CouchDB 模式,以实现更复杂的数据检索功能。此外,对上个实验的简单智能合约进一步进行功能上和设计上的扩展,最终实现了智能合约的分包、分页查询、多字段富查询、查询交易历史记录等功能。
网络架构
本文网络结构直接将 Hyperledger Fabric无排序组织以Raft协议启动多个Orderer服务、TLS组织运行维护Orderer服务 中创建的 4-2_RunOrdererByCouncil 复制为 7_CouchDBAndComplexContract 并修改(建议直接将本案例仓库 FabricLearn 下的 7_CouchDBAndComplexContract 目录拷贝到本地运行),文中大部分命令在 Hyperledger Fabric定制联盟链网络工程实践 中已有介绍因此不会详细说明,默认情况下,所有操作皆在 7_CouchDBAndComplexContract 根目录下执行。修改成功后网络共包含四个组织—— council 、 soft 、 web 、 hard , 其中 council 组织为网络提供 TLS-CA 服务,并且运行维护着三个 orderer 服务;其余每个组织都运行维护着一个 peer 节点、一个 couchDB 服务、一个 admin 用户和一个 user 用户,实验最终网络结构如下:
| 项 | 运行端口 | 说明 |
|---|---|---|
council.ifantasy.net | 7050 | council 组织的 CA 服务, 为联盟链网络提供 TLS-CA 服务 |
orderer1.council.ifantasy.net | 7051 | council 组织的 orderer1 服务 |
orderer1.council.ifantasy.net | 7052 | council 组织的 orderer1 服务的 admin 服务 |
orderer2.council.ifantasy.net | 7054 | council 组织的 orderer2 服务 |
orderer2.council.ifantasy.net | 7055 | council 组织的 orderer2 服务的 admin 服务 |
orderer3.council.ifantasy.net | 7057 | council 组织的 orderer3 服务 |
orderer3.council.ifantasy.net | 7058 | council 组织的 orderer3 服务的 admin 服务 |
soft.ifantasy.net | 7250 | soft 组织的 CA 服务, 包含成员: peer1 、 admin1 、user1 |
peer1.soft.ifantasy.net | 7251 | soft 组织的 peer1 成员节点 |
couchdb.soft.ifantasy.net | 7255 | soft 组织的 couchdb 成员节点 |
web.ifantasy.net | 7350 | web 组织的 CA 服务, 包含成员: peer1 、 admin1 、user1 |
peer1.web.ifantasy.net | 7351 | web 组织的 peer1 成员节点 |
couchdb.web.ifantasy.net | 7355 | web 组织的 couchdb 成员节点 |
hard.ifantasy.net | 7450 | hard 组织的 CA 服务, 包含成员: peer1 、 admin1 、user1 |
peer1.hard.ifantasy.net | 7451 | hard 组织的 peer1 成员节点 |
couchdb.hard.ifantasy.net | 7455 | hard 组织的 couchdb 成员节点 |
添加CouchDB支持并启动网络
添加CouchDB支持
首先,在 envpeer1soft 、 envpeer1soft 、 envpeer1soft 中添加 CouchDB 版本变量:
export COUCHDB_VERSION=3.2
然后,向 compose/docker-base.yaml 文件添加基础 CouchDB 镜像:
couchdb-base:image: couchdb:${COUCHDB_VERSION}environment:- COUCHDB_USER=admin- COUCHDB_PASSWORD=adminpwnetworks:- ${DOCKER_NETWORKS}
之后,向 compose/docker-compose.yaml 中的每个组织添加 CouchDB 容器:
couchdb.soft.ifantasy.net:container_name: couchdb.soft.ifantasy.netextends:file: docker-base.yamlservice: couchdb-baseports:- 7255:5984couchdb.web.ifantasy.net:container_name: couchdb.web.ifantasy.netextends:file: docker-base.yamlservice: couchdb-baseports:- 7355:5984couchdb.hard.ifantasy.net:container_name: couchdb.hard.ifantasy.netextends:file: docker-base.yamlservice: couchdb-baseports:- 7455:5984
最后,修改 compose/docker-compose.yaml 中每个 peer 容器的储存方式(以 peer1.soft.ifantasy.net 为例):
peer1.soft.ifantasy.net:container_name: peer1.soft.ifantasy.netextends:file: docker-base.yamlservice: peer-baseenvironment:- CORE_PEER_ID=peer1.soft.ifantasy.net- CORE_PEER_LISTENADDRESS=0.0.0.0:7251- CORE_PEER_ADDRESS=peer1.soft.ifantasy.net:7251- CORE_PEER_LOCALMSPID=softMSP- CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer1.soft.ifantasy.net:7251- CORE_LEDGER_STATE_STATEDATABASE=CouchDB- CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb.soft.ifantasy.net:5984 # 必须为容器内端口- CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=admin- CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=adminpwvolumes:- ${LOCAL_CA_PATH}/soft.ifantasy.net/registers/peer1:${DOCKER_CA_PATH}/peerports:- 7251:7251depends_on:- couchdb.soft.ifantasy.net
注意,参数 CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS 后的服务端口必须为 couchdb 容器的内部端口,原因不得而知, 完整代码见 FabricLearn 下的 7_CouchDBAndComplexContract/compose 目录。
启动实验网络
在上述修改完成后,在 7_CouchDBAndComplexContract 目录下按顺序执行以下命令启动基础实验网络:
- 设置DNS(如果未设置):
./setDNS.sh - 设置环境变量:
source envpeer1soft - 启动CA网络:
./0_Restart.sh - 注册用户:
./1_RegisterUser.sh - 获取用户证书:
./2_EnrollUser.sh - 配置通道:
./3_Configtxgen.sh
网络启动成功后可见包含 couchdb 容器:

合约开发
本节所用智能合约由前一篇文章 Hyperledger Fabric 智能合约开发及 fabric-sdk-go/fabric-gateway 使用示例 改进(拆分)而来,在上篇文章的基础上对合约进行分包分文件处理,使项目具有更好的目录结构。在实验根目录 7_CouchDBAndComplexContract 下创建目录 project_contract 作为智能合约根目录,在 project_contract 下执行以下命令初始化 GO 模块:
go mod init github.com/wefantasy/FabricLearn/7_CouchDBAndComplexContract/project_contract
tools 层
tools 层主要用于编写智能合约通用工具,创建 tools/contract.go 工具类,主要包含以下函数:
ConstructResultByIterator: 根据 fabric 查询结果shim.StateQueryIteratorInterface生成对应切片。// 根据查询结果生成切片 func ConstructResultByIterator[T interface{}](resultsIterator shim.StateQueryIteratorInterface) ([]*T, error) {var txs []*Tfor resultsIterator.HasNext() {queryResult, err := resultsIterator.Next()if err != nil {return nil, err}var tx Terr = json.Unmarshal(queryResult.Value, &tx)if err != nil {return nil, err}txs = append(txs, &tx)}fmt.Println("select result length: ", len(txs))return txs, nil }SelectByQueryString: 根据 couchdb 查询字符串完成查询操作,并返回对应切片。// 根据查询字符串查询 func SelectByQueryString[T interface{}](ctx contractapi.TransactionContextInterface, queryString string) ([]*T, error) {resultsIterator, err := ctx.GetStub().GetQueryResult(queryString)if err != nil {return nil, err}defer resultsIterator.Close()return ConstructResultByIterator[T](resultsIterator) }SelectByQueryStringWithPagination: 根据 couchdb 查询字符串分页查询,并返回对应切片。// 根据擦查询字符串分页查询 func SelectByQueryStringWithPagination[T interface{}](ctx contractapi.TransactionContextInterface, queryString string, pageSize int32, bookmark string) (*model.PaginatedQueryResult[T], error) {resultsIterator, responseMetadata, err := ctx.GetStub().GetQueryResultWithPagination(queryString, pageSize, bookmark)if err != nil {return nil, err}defer resultsIterator.Close()var txs []Tfor resultsIterator.HasNext() {queryResult, err := resultsIterator.Next()if err != nil {return nil, err}var tx Terr = json.Unmarshal(queryResult.Value, &tx)if err != nil {return nil, err}txs = append(txs, tx)}return &model.PaginatedQueryResult[T]{Records: txs,FetchedRecordsCount: responseMetadata.FetchedRecordsCount,Bookmark: responseMetadata.Bookmark,}, nil }SelectHistoryByIndex: 获得交易创建之后的所有变化(区块链账本)。// 获得交易创建之后的所有变化. func SelectHistoryByIndex[T interface{}](ctx contractapi.TransactionContextInterface, index string) ([]model.HistoryQueryResult[T], error) {resultsIterator, err := ctx.GetStub().GetHistoryForKey(index)if err != nil {return nil, err}defer resultsIterator.Close()var records []model.HistoryQueryResult[T]for resultsIterator.HasNext() {response, err := resultsIterator.Next()if err != nil {return nil, err}var tx Tif len(response.Value) > 0 {err = json.Unmarshal(response.Value, &tx)if err != nil {return nil, err}}record := model.HistoryQueryResult[T]{TxId: response.TxId,Record: tx,IsDelete: response.IsDelete,}records = append(records, record)}return records, nil }
model 层
model层主要用于申明合约所用数据结构,其中 model/project.go 内容如下:
package modeltype Project struct {
Table string `json:"table" form:"table"` // 数据库标记
ID string `json:"ID"` // 项目唯一ID
Name string `json:"Name"` // 项目名称
Username string `json:"username"` // 项目主要负责人
Organization string `json:"Organization"` // 项目所属组织
Category string `json:"Category"` // 项目所属类别
Url string `json:"Url"` // 项目介绍地址
Describes string `json:"Describes"` // 项目描述
}func (o *Project) Index() string {
o.Table = "project"
return o.ID
}func (o *Project) IndexKey() string {
return "table~ID~name"
}func (o *Project) IndexAttr() []string {
return []string{o.Table, o.ID, o.Name}
}
其中 Index 函数用于标识模型的唯一主键; IndexKey 函数用于标识自建索引的字段,其中命名方式必须与字段申明的结构体标记 json 一致(大小写);IndexAttr 用于构造具体的索引。model/user.go 申明了用户的字段信息:
package model// User 用户表
type User struct {
Table string `json:"table" form:"table"` // 数据库标记
Username string `json:"username" form:"username"` //用户账户
Name string `json:"name" form:"name"` //真实姓名
Email string `json:"email" form:"email"` // 邮箱
Phone string `json:"phone" form:"phone"` // 手机
}func (o *User) Index() string {
o.Table = "user"
return o.Username
}func (o *User) IndexKey() string {
return "table~username~name"
}func (o *User) IndexAttr() []string {
return []string{o.Table, o.Username, o.Name}
}
model/base.go 申明了基于 CouchDB 的富查询结果模型:
package modelimport "time"// 历史查询结果
type HistoryQueryResult[T interface{}] struct {
Record T `json:"record"`
TxId string `json:"txId"`
Timestamp time.Time `json:"timestamp"`
IsDelete bool `json:"isDelete"`
}// 分页查询结果
type PaginatedQueryResult[T interface{}] struct {
Records []T `json:"records"`
FetchedRecordsCount int32 `json:"fetchedRecordsCount"`
Bookmark string `json:"bookmark"`
}
contract 层
contract 层用于实现智能合约的核心逻辑(本示例为 model 的增删改查),由于结合了 CouchDB ,所以相比上个实验需要更复杂的实现。以 contract/project.go 为例进行介绍,由于代码太长在此就不再粘贴(完整代码参考 project.go),其中主要功能及实现方式如下:
- 插入数据( Insert ):先使用
ctx.GetStub().PutState(tx.Index(), txb)方法插入数据,然后调用ctx.GetStub().CreateCompositeKey(tx.IndexKey(), tx.IndexAttr())方法为该数据创建 CouchDB 索引,最后调用ctx.GetStub().PutState(indexKey, value)将索引存入链上。 - 更新数据( Update ):先使用
indexKey, err := ctx.GetStub().CreateCompositeKey(otx.IndexKey(), otx.IndexAttr())得到旧数据的索引,再调用ctx.GetStub().DelState(indexKey)删除旧数据的索引,然后调用ctx.GetStub().PutState(tx.Index(), txb)更新数据,最后分别调用ctx.GetStub().CreateCompositeKey(tx.IndexKey(), tx.IndexAttr())和ctx.GetStub().PutState(indexKey, value)创建新数据索引并存入链上。 - 删除数据( Delete ):先使用
ctx.GetStub().DelState(anstx.Index())删除旧数据,再调用indexKey, err := ctx.GetStub().CreateCompositeKey(tx.IndexKey(), tx.IndexAttr())得到旧数据索引,最后通过ctx.GetStub().DelState(indexKey)删除旧数据索引。 - 读取指定index的记录( SelectByIndex ):使用形如
{"selector":{"ID":"%s", "table":"project"}}的 CouchDB 查询语法根据索引查询数据。 - 读取所有数据( SelectAll ):使用形如
{"selector":{"table":"project"}}的 CouchDB 查询语法查询所有相关数据。 - 按某索引查询所有数据( SelectBySome ):使用形如
{"selector":{"%s":"%s", "table":"project"}}的 CouchDB 查询语法根据索引查询数据。 - 富分页查询所有数据( SelectAllWithPagination ):使用形如
{"selector":{"table":"project"}}的 CouchDB 查询语法调用上述分页查询数据工具 tools.SelectByQueryStringWithPagination 来查询数据。 - 按关键字富分页查询所有数据 SelectBySomeWithPagination ):使用形如
{"selector":{"%s":"%s","table":"project"}}的 CouchDB 查询语法调用上述分页查询数据工具tools.SelectByQueryStringWithPagination来查询数据。 - 按某索引查询数据历史( SelectHistoryByIndex ):调用上述历史数据查询工具
tools.SelectHistoryByIndex来查询数据。
contract/user.go 为 model/user.go 的核心操作逻辑,此示例只包含简单的功能,完整源码参考 user.go。
main 主函数
主函数完整代码如下所示:
package mainimport ("github.com/hyperledger/fabric-contract-api-go/contractapi""github.com/wefantasy/FabricLearn/7_CouchDBAndComplexContract/project_contract/contract"
)func main() {chaincode, err := contractapi.NewChaincode(&contract.UserContract{}, &contract.ProjectContract{})if err != nil {panic(err)}if err := chaincode.Start(); err != nil {panic(err)}
}
多智能合约只需在 main 的 contractapi.NewChaincode 函数中按顺序申明即可。在智能合约编写完毕后使用 go mod vendor 来打包依赖,上述工作完成后 project_contract 目录结构及解释如下所示:
project_contract
├── contract // 智能合约核心逻辑
│ ├── project.go
│ └── user.go
├── go.mod
├── go.sum
├── main.go // 智能合约入口函数
├── model // 申明数据模型
│ ├── base.go // 申明分页等数据结构
│ ├── project.go
│ └── user.go
├── tools // 工具目录
│ └── contract.go // 智能合约通用工具,查询历史/分页查询等
└── vendor // 依赖目录
合约部署和测试
如无特殊说明,以下命令默认运行于实验根目录 7_CouchDBAndComplexContract 下:
- 合约打包
source envpeer1soft peer lifecycle chaincode package basic.tar.gz --path project_contract --label basic_1 - 三组织安装
source envpeer1softpeer lifecycle chaincode install basic.tar.gzpeer lifecycle chaincode queryinstalledsource envpeer1webpeer lifecycle chaincode install basic.tar.gzpeer lifecycle chaincode queryinstalledsource envpeer1hardpeer lifecycle chaincode install basic.tar.gzpeer lifecycle chaincode queryinstalled - 三组织批准
注意:由于我们有两个智能合约,且每个智能合约都包含export CHAINCODE_ID=basic_1:22e38a78d2ddfe9c3cbeff91140ee209c901adcc24cd2b11f863a53abcdc825a source envpeer1soft peer lifecycle chaincode approveformyorg -o orderer1.council.ifantasy.net:7051 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --version 1.0 --sequence 1 --waitForEvent --package-id $CHAINCODE_ID peer lifecycle chaincode queryapproved -C testchannel -n basic --sequence 1 source envpeer1web peer lifecycle chaincode approveformyorg -o orderer3.council.ifantasy.net:7057 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --version 1.0 --sequence 1 --waitForEvent --package-id $CHAINCODE_ID peer lifecycle chaincode queryapproved -C testchannel -n basic --sequence 1 source envpeer1hard peer lifecycle chaincode approveformyorg -o orderer2.council.ifantasy.net:7054 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --version 1.0 --sequence 1 --waitForEvent --package-id $CHAINCODE_ID peer lifecycle chaincode queryapproved -C testchannel -n basic --sequence 1InitLedger函数来初始化数据,所以在这里以及后续链码操作中需要删除--init-required参数(因为合约不需要初始化)。 - 提交链码
source envpeer1soft peer lifecycle chaincode commit -o orderer2.council.ifantasy.net:7054 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --version 1.0 --sequence 1 --peerAddresses peer1.soft.ifantasy.net:7251 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE --peerAddresses peer1.web.ifantasy.net:7351 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE - 初始化链码数据并测试
source envpeer1soft peer chaincode invoke -o orderer1.council.ifantasy.net:7051 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --peerAddresses peer1.soft.ifantasy.net:7251 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE --peerAddresses peer1.web.ifantasy.net:7351 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE -c '{"Args":["UserContract:InitLedger"]}' peer chaincode invoke -o orderer1.council.ifantasy.net:7051 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --peerAddresses peer1.soft.ifantasy.net:7251 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE --peerAddresses peer1.web.ifantasy.net:7351 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE -c '{"Args":["ProjectContract:InitLedger"]}'peer chaincode invoke -o orderer1.council.ifantasy.net:7051 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --peerAddresses peer1.soft.ifantasy.net:7251 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE --peerAddresses peer1.web.ifantasy.net:7351 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE -c '{"Args":["UserContract:GetAllUsers"]}' peer chaincode invoke -o orderer1.council.ifantasy.net:7051 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --peerAddresses peer1.soft.ifantasy.net:7251 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE --peerAddresses peer1.web.ifantasy.net:7351 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE -c '{"Args":["ProjectContract:SelectAll"]}' peer chaincode invoke -o orderer1.council.ifantasy.net:7051 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --peerAddresses peer1.soft.ifantasy.net:7251 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE --peerAddresses peer1.web.ifantasy.net:7351 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE -c '{"Args":["ProjectContract:SelectBySome", "name", "工作室联盟链管理系统"]}'
注意,在多合约的情况下调用链码,需要在所调用的合约函数前指定所属合约,如 ProjectContract:SelectBySome ,其它合约示例调用方式大致一样,在此不再赘述。此外由于 CouchDB 自带了数据库管理界面,则可以通过本例中任意一个 CouchDB 的服务地址来访问链上数据,如 http://192.168.27.72:7355/_utils/#login (虚拟机IP为 192.168.27.72 ,soft 组织的 CouchDB 端口 7355),输入docker中配置的账户admin密码adminpw即可进入系统:


至此,本实验基本完成。
可能存在的问题
peer lifecycle chaincode install时遇到错误:
Error creating tx-manage chaincode: Error compiling schema for DataContract [SelectBySomeWithPagination]. Return schema invalid. Object has no key 'PaginatedQueryResult[github.com'
panic: Error creating tx-manage chaincode: Error compiling schema for DataContract [SelectBySomeWithPagination]. Return schema invalid. Object has no key 'PaginatedQueryResult[github.com'goroutine 1 [running]:
log.Panicf({0xa24b02?, 0x1?}, {0xc00014ff50?, 0x407679?, 0x404c71?})/usr/local/go/src/log/log.go:392 +0x67
main.main()/chaincode/input/src/main.go:201 +0x8e
原因及解决方法: 所用 docker fabric 2.4 镜像的 Golang 版本太低不支持泛型,需要删除并重新安装 docker fabric 2.4 (尽管 tag 一样,但镜像内容会更新)。
- 智能合约调用时遇到错误:
[notice] 2022-11-13T12:13:49.502557Z nonode@nohost <0.286.0> -------- rexi_server : started servers
[notice] 2022-11-13T12:13:49.504490Z nonode@nohost <0.290.0> -------- rexi_buffer : started servers
[warning] 2022-11-13T12:13:49.530610Z nonode@nohost <0.298.0> -------- creating missing database: _nodes
[info] 2022-11-13T12:13:49.530670Z nonode@nohost <0.299.0> -------- open_result error {not_found,no_db_file} for _nodes
[error] 2022-11-13T12:13:49.537681Z nonode@nohost <0.304.0> -------- CRASH REPORT Process (<0.304.0>) with 2 neighbors crashed with reason: no match of right hand value {error,enospc} at couch_bt_engine:init/2(line:154) <=
……
原因及解决方法: 可能是 docker volume 把硬盘占满了,使用 docker volume rm $(docker volume ls -qf dangling=true) 清除所有再重试
使用
- 遇到错误:
# github.com/hyperledger/fabric-sdk-go/internal/github.com/hyperledger/fabric/discovery/client
/root/go/pkg/mod/github.com/hyperledger/fabric-sdk-go@v1.0.0/internal/github.com/hyperledger/fabric/discovery/client/api.go:47:38: undefined: discovery.ChaincodeCall
/root/go/pkg/mod/github.com/hyperledger/fabric-sdk-go@v1.0.0/internal/github.com/hyperledger/fabric/discovery/client/client.go:83:63: undefined: discovery.ChaincodeInterest
/root/go/pkg/mod/github.com/hyperledger/fabric-sdk-go@v1.0.0/internal/github.com/hyperledger/fabric/discovery/client/client.go:120:65: undefined: discovery.ChaincodeCall
/root/go/pkg/mod/github.com/hyperledger/fabric-sdk-go@v1.0.0/internal/github.com/hyperledger/fabric/discovery/client/client.go:124:23: undefined: discovery.ChaincodeInterest
/root/go/pkg/mod/github.com/hyperledger/fabric-sdk-go@v1.0.0/internal/github.com/hyperledger/fabric/discovery/client/client.go:229:105: undefined: discovery.ChaincodeCall
/root/go/pkg/mod/github.com/hyperledger/fabric-sdk-go@v1.0.0/internal/github.com/hyperledger/fabric/discovery/client/client.go:247:64: undefined: discovery.ChaincodeCall
/root/go/pkg/mod/github.com/hyperledger/fabric-sdk-go@v1.0.0/internal/github.com/hyperledger/fabric/discovery/client/client.go:604:48: undefined: discovery.ChaincodeInterest
/root/go/pkg/mod/github.com/hyperledger/fabric-sdk-go@v1.0.0/internal/github.com/hyperledger/fabric/discovery/client/client.go:620:35: undefined: discovery.ChaincodeCall
原因及解决方法: github.com/hyperledger/fabric-sdk-go 需要指定 20220117 版本,将 go.mod 文件对应依赖替换如下:
github.com/hyperledger/fabric-sdk-go v1.0.1-0.20220117114400-c848d119936b。
- 遇到错误:
Error compiling schema for ****[**]. Return schema invalid. Object has no key 'Wrapper[[]<part of module name>'
原因及解决方法:智能合约返回值不支持泛型,将智能合约返回值换成 interface{} 即可。
- 查询历史记录出现遇到错误:
Error: could not assemble transaction: ProposalResponsePayloads do not match (base64):
原因及解决方法:链码输出(返回)数据中不要使用地址传递(推荐值传递),因为地址都是动态分配,每次取到的值都不一样,造成共识失败。
- 遇到错误:
Failed to evaluate: Multiple errors occurred: - Transaction processing for endorser [localhost:7451]: Chaincode status Code: (500) UNKNOWN. Description: Error handling success response. Value did not match schema:\n1. return: Invalid type. Expected: array, given: string - Transaction processing for endorser [localhost:7251]: Chaincode status Code: (500) UNKNOWN. Description: Error handling success response. Value did not match schema:\n1. return: Invalid type. Expected: array, given: string
原因及解决方法:链码返回值不能为 []byte ,这是一个 fabric 的 bug,对于复杂返回类型建议直接返回字符串 string
相关文章:
Hyperledger Fabric 使用 CouchDB 和复杂智能合约开发
前言 在上个实验中,我们已经实现了简单智能合约实现及客户端开发,但该实验中智能合约只有基础的增删改查功能,且其中的数据管理功能与传统 MySQL 比相差甚远。本文将在前面实验的基础上,将 Hyperledger Fabric 的默认数据库支持 …...
这应该是最全的,Fiddler手机App抓包详解,看完还不会来找我...
目录:导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结(尾部小惊喜) 前言 什么是抓包&#…...
使用ubuntu-base制作根文件系统
1:ubuntu官网下载最小根文件系统: 放置到电脑的ubuntu中, Mkdir Ubuntu_rootfs Cd Ubuntu_rootfs Sudo tar –zxvf Ubuntu-bash-xxxxxx.tar.gz 2:电脑的ubuntu安装qemu搭建arm模拟系统 将/usr/bin/qemu-arm-static/(64位拷贝…...
springboot-mybatis的分页查询的接口
一、安装依赖 <dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>1.4.6</version> </dependency> 二、代码定义 分页对象类 Data AllArgsConstruct…...
网络音频终端音频编码解码终端
网络对讲终端SV-7011V 网络对讲终端SV-7011V,采用了ARM音频DSP架构,集网络对讲、网络广播、监听等功能于一身,内置麦克风、配置line out、line in、Mic in功能输出接口,适用于学校,机场,广场等场所。 产品…...
cordova 12 编译失败Could not find method compile() for arguments [com....]
问题: Could not find method compile() for arguments [com.tencent.mm.opensdk:wechat-sdk-android-with-mta:] on object of type org.gradle.api.internal.artifacts.dsl.dependencies.DefaultDependencyHandler. 问题原因: 在Cordova项目中&…...
快速开发平台 WebBuilder 9 发布
WebBuilder 是一款强大,全面和高效的应用开发和运行平台。基于浏览器的集成开发环境,智能化的设计,能轻松完成常规桌面应用和面向手机等的移动应用开发。高效、稳定和可扩展的特点,适合复杂企业级应用的运行。跨平台、数据库和浏览…...
GPT-5出世,需5万张H100!英伟达GPU陷短缺风暴
随着人工智能技术的不断突破,自然语言处理领域也掀起了一波又一波的革命。从GPT-3.5的惊艳登场,到紧随其后的GPT-4的惊世震人,人们仿佛置身于科幻电影中,亲历了一场场技术的奇迹。然而,这场奇迹背后却逐渐显现出一道不…...
Jmeter学习和一个关于jmeter获取X-XSRF-TOKEN时的坑
Jmeter学习和一个关于jmeter获取X-XSRF-TOKEN时的坑 现在想对一个接口做性能测试,需要测试它多个线程并发下的调用 1.新建测试计划和线程组 略 2.新建http接口 一个完整的http接口包含请求头和请求,这里就需要两个组件:HTTP request、HT…...
SQL注入总结
0x00 前言 简单的总结一下SQL注入的内容 0x01 常规注入 通过特殊字符测试闭合,通常使用的闭合方式有 " ) ")等 1.判断字段 a union select 1,2,3 #2.执行基础内容 a union select 1,database(),3 #3.查表 a union select 1,group_concat(table_na…...
Java 实现 后端分页
背景说明 在实际开发中,基于数据库表结构进行SQL查询,如果要对结果进行分页,可以借助一些工具类,如:基于Mybatis的 工具类 PageHelper。 但是,有时分页的对象是经过一些业务逻辑处理的列表,如…...
数据结构 | 搜索和排序——排序
目录 一、冒泡排序 二、选择排序 三、插入排序 四、希尔排序 五、归并排序 六、快速排序 排序是指将集合中的元素按照某种顺序排序的过程。 一、冒泡排序 冒泡排序多次遍历列表。它比较相邻的元素,将不合顺序的交换。每一轮遍历都将下一个最大值放到正确的位…...
【嵌入式环境下linux内核及驱动学习笔记-(18)LCD驱动框架1-LCD控制原理】
目录 1、LCD显示系统介绍1.1 LCD显示基本原理1.1.1 颜色的显示原理:1.1.2 图像的构成 1.2 LCD接口介绍1.2.1 驱动接口 - MCU接口1.2.2 驱动接口 - RGB接口1.2.3 驱动接口 - LVDS接口1.2.4 驱动接口 - MIPI接口1.2.5 RGB / MIPI / LVDS三种接口方式的区别:…...
【unity】ShaderGraph实现等高线和高程渐变设色
【unity】ShaderGraph实现等高线和高程渐变设色 等高线的实现思路 方法一: 通过Position节点得到顶点的高度(y)值,将高度值除去等高距离取余,设定余数的输出边界(step) 方法二: 将…...
快速修复应用程序中的问题的利器—— Android热修复
热修复技术在Android开发中扮演着重要的角色,它可以帮助开发者在不需要重新发布应用程序的情况下修复已经上线的应用程序中的bug或者添加新的功能。 一、热修复是什么? 热修复(HotFix)是一种在运行时修复应用程序中的问题的技术…...
什么是全局代理,手机怎么设置全局代理
目录 什么是全局代理 全局代理的优缺点 优点 缺点 手机怎么设置全局代理 注意事项 总结 在计算机网络和信息安全中,全局代理是一种常用的技术手段,用于将网络流量通过代理服务器进行转发和处理。本文将介绍什么是全局代理,探讨全局代理…...
技术领先产品ASSAR300一一基于SAR成像的角雷达产品,助力自动泊车
作为自动驾驶应用场景中最先被推广和商业化落地的自动泊车功能,目前是在一些限定环境下实现了功能跑通。面对多种多样的复杂停车场场景,系统需要不断增强感知算法能力或寻求新的传感器技术,来提升对周围环境感知和对障碍物探测的精准度。 传…...
单元测试之 - Spring框架提供的单元/集成测试注解
Spring框架提供了很多注解来辅助完成单元测试和集成测试(备注:这里的集成测试指容器内部的集成测试,非系统间的集成测试),先看看Spring框架提供了哪些注解以及对应的作用。RunWith(SpringRunner.class) / ExtendWith(SpringExtension.class)&…...
深入学习 Redis - 事务、实现原理、指令使用及场景
目录 一、Redis 事务 vs MySQL事务 二、Redis 事务的执行原理 2.1、执行原理 2.2、Redis 事务设计这么简单,为什么不涉及成 MySQL 那样强大呢? 三、Redis 事务的使用 3.1、使用场景 3.2、具体演示 开启/执行/放弃事务 watch 监控 watch 实现原理…...
异步javaScript
在本文中,我们将解释什么是异步编程,为什么我们需要它,并简要讨论 JavaScript 历史上异步函数是怎样被实现的。 预备知识:基本的计算机素养,以及对 JavaScript 基础知识的一定了解,包括函数和事件处理程序…...
【Oracle APEX开发小技巧12】
有如下需求: 有一个问题反馈页面,要实现在apex页面展示能直观看到反馈时间超过7天未处理的数据,方便管理员及时处理反馈。 我的方法:直接将逻辑写在SQL中,这样可以直接在页面展示 完整代码: SELECTSF.FE…...
Qt Widget类解析与代码注释
#include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this); }Widget::~Widget() {delete ui; }//解释这串代码,写上注释 当然可以!这段代码是 Qt …...
Unit 1 深度强化学习简介
Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库,例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体,比如 SnowballFight、Huggy the Do…...
云原生玩法三问:构建自定义开发环境
云原生玩法三问:构建自定义开发环境 引言 临时运维一个古董项目,无文档,无环境,无交接人,俗称三无。 运行设备的环境老,本地环境版本高,ssh不过去。正好最近对 腾讯出品的云原生 cnb 感兴趣&…...
【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的“no matching...“系列算法协商失败问题
【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的"no matching..."系列算法协商失败问题 摘要: 近期,在使用较新版本的OpenSSH客户端连接老旧SSH服务器时,会遇到 "no matching key exchange method found", "n…...
Mysql8 忘记密码重置,以及问题解决
1.使用免密登录 找到配置MySQL文件,我的文件路径是/etc/mysql/my.cnf,有的人的是/etc/mysql/mysql.cnf 在里最后加入 skip-grant-tables重启MySQL服务 service mysql restartShutting down MySQL… SUCCESS! Starting MySQL… SUCCESS! 重启成功 2.登…...
Vue ③-生命周期 || 脚手架
生命周期 思考:什么时候可以发送初始化渲染请求?(越早越好) 什么时候可以开始操作dom?(至少dom得渲染出来) Vue生命周期: 一个Vue实例从 创建 到 销毁 的整个过程。 生命周期四个…...
Vue 模板语句的数据来源
🧩 Vue 模板语句的数据来源:全方位解析 Vue 模板(<template> 部分)中的表达式、指令绑定(如 v-bind, v-on)和插值({{ }})都在一个特定的作用域内求值。这个作用域由当前 组件…...
二维FDTD算法仿真
二维FDTD算法仿真,并带完全匹配层,输入波形为高斯波、平面波 FDTD_二维/FDTD.zip , 6075 FDTD_二维/FDTD_31.m , 1029 FDTD_二维/FDTD_32.m , 2806 FDTD_二维/FDTD_33.m , 3782 FDTD_二维/FDTD_34.m , 4182 FDTD_二维/FDTD_35.m , 4793...
Spring AOP代理对象生成原理
代理对象生成的关键类是【AnnotationAwareAspectJAutoProxyCreator】,这个类继承了【BeanPostProcessor】是一个后置处理器 在bean对象生命周期中初始化时执行【org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization】方法时…...
