Fabric v1.x 应用开发指南

文章目录

  • 1 技术栈
    • 1.1 经典软件工程管理
    • 1.2 PKI密码体系
    • 1.3 chaincode开发语言
    • 1.4 devOps运维开发
  • 2 chaincode API解析
    • 2.1 chaincode代码示例
    • 2.2 chaincode接口(shim.ChaincodeStubInterface)
      • 获取参数(Params)
      • 访问世界状态(Access world states)
      • 历史记录查询(Witness the immutable)
      • 键值范围查询(List for NoSQL: key range)
      • 复合key(Composite key)
      • 富查询(CouchDB or default levelDB)
    • 2.3 复合API
      • 跨智能合约调用(Cross chaincode invoke)
      • 客户端身份解析(Client Identity Chaincode Library)
      • 获得Transient对象(GetTransient)
      • 私有数据(PrivateData)
      • Key级别的背书(Key level endorsement)
  • 3 Init函数
  • 4 Invoke VS Query
  • 5 开发模式
  • 6 开发周期
    • 6.1 peer部分
      • (1) 生成加密物料(Generate crypto material)
      • (2) 生成创世区块(Generate genesis block file)
      • (3) 启动orderer和peer(Start orderer and peer)
    • 6.2 channel部分
      • (1) 生成channel引导文件(Generate channel bootstrap file)
      • (2) 创建channel(Create channel)
      • (3) Peer节点加入到channel(Peer join channel)
    • 6.3 chaincode生命周期
      • (1) 打包与安装chaincode(Package, install chaincode)
      • (2) 实例化chaincode(Instantiate chaincode)
      • (3) 升级chaincode(Upgrade chaincode)
  • 7、 黑盒开发

1 技术栈

1.1 经典软件工程管理

Fabric应用开发者需要了解经典软件工程管理:

  • 依赖管理(调用第三方库):govendor/dep, npm/yarn, gradle/maven, pip
  • 异常处理:defer, saync/await
  • 测试流水线:Smoke, Unit/Mock, SI tests

1.2 PKI密码体系

Fabric应用开发者需要精通PKI密码体系,包括:

  • ECDSA
  • X.509
  • HSM和pkcs11

1.3 chaincode开发语言

chaincode开发支持的语言包括:

  • Golang
  • Node.js
  • Java

1.4 devOps运维开发

devOps运维开发技术栈包括:

  • 受认证的Fabric管理员(Certified Hyperledger Fabric Administrator):CLI in Unix/Linux
  • SDK语言:Java/Golang/Node.js/Python
  • Docker
  • gRPC

2 chaincode API解析

2.1 chaincode代码示例

chaincode的package包名必须是main,因为容器里会去找main函数。
必须实现Init和Invoke两个函数,这样才会构成一个chaincode。在初始化和升级chaincode时,会调用到Init函数;query以及invoke的第一阶段,会调用到Invoke函数。

package main
type StressChaincode struct{}
//called when initialize, upgrade
func (t *StressChaincode) Init(stub shim.ChaincodeStubInterface) peer.Response {
    return shim.Success(nil)
}
//called when query, phase-1 of invoke
func (t *StressChaincode) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
    return shim.Success(nil)
}
func main() {
    err:=shim.Start(new(StressChaincode))
}

2.2 chaincode接口(shim.ChaincodeStubInterface)

获取参数(Params)

从外部传入chaincode的参数列表可以通过下面的两个接口获取到。GetFunctionAndParameters可以将第一个元素转换成功能名称。

GetArgs() [][]byte
GetFunctionAndParameters() (string, []string) //returns 1st argument as function,"fcn", rest as params

访问世界状态(Access world states)

Fabric的数据存储采用的是KV形式,根据key值就可以从数据库中获取、更新、删除数据。

GetState(key string)
PutState(key string, value []byte)
DelState(key string)

历史记录查询(Witness the immutable)

GetHistoryForKey可以返回关于某个key的所有历史操作。要求在peer节点配置“core.ledger.history.enableHistoryDatabase=true”,才能正常调用。

GetHistoryForKey(key string) iterator<KeyModification>

列表的每个元素都是结构体KeyModification。

type KeyModification struct {
    TxId string			// transaction交易号
    Value []byte		// 历史值
    Timestamp TimeLong	// timestamp provided by the client in the proposal header.
    IsDelete bool		// 是否是删除操作
}

键值范围查询(List for NoSQL: key range)

根据键值返回对应区间的所有状态数据,限制最多100个,如果超过100个建议使用pageAPI,或者超过100个的时候不要继续迭代了,这时可以修改startKey再做一次GetStateByRange查询。

GetStateByRange(startKey, endKey string) iterator<KV>

列表的每个元素都是结构体KV。

type KV struct {
    Namespace string // same as chaincode ID 当前运行的智能合约ID,其他API都不能获取该数据。
    Key string
    Value []byte
}

复合key(Composite key)

用不可打印(unprintable)的字符拼接成key,称为复合key。由于包含了不可打印字符,通过GetStateByRange将查询不到想要的数据。
复合key可以扩展数据命名空间,防止键重叠。比如两个系统可能有相同的合同号,通常用“公司名+下划线+合同号”作为key,但是下划线也可能是合同号一部分,会引发一些问题,通过composite key用不可打印字符连接,因为很难从键盘输入一个不可打印的字符,解决合同号冲突的问题。

CreateCompositeKey(objectType string, attributes []string) (string, error)
SplitCompositeKey(compositeKey string) (string, []string, error)

富查询(CouchDB or default levelDB)

启用CouchDB的先决条件: 需要在peer中配置以下内容

'CORE_LEDGER_STATE_STATEDATABASE=CouchDB',
'CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=${container_name}:5984',
'CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=${user}',
'CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=${password}'

启用CouchDB可以支持富查询(Rich query),利用couchdb的语法按查询规则做值检索,查询值里面的某些字段是否符合要求,只返回符合要求的值。而levelDB需要查询出所有数据一一判断,效率非常慢。富查询API如下:

GetQueryResult(query string)

但是CouchDB也有一些弊端:

  • CouchDB的网络损耗对性能有影响,而levelDB则保存在本地,不存在该问题;
  • Couchdb 只存储 world states,历史记录仍然保存在levelDB
  • 需要预置索引集合couchdb metadata: index.json
  • 安全漏洞和操作复杂性:password是明文,另外couchDB是单独服务,增加了运维复杂性

2.3 复合API

复合API关注的不仅仅是chaincode自身的一些内容,还需要关注一些其他问题,比如需要运维、架构的配合。

跨智能合约调用(Cross chaincode invoke)

通过InvokeChaincode可以进行跨智能合约的调用,InvokeChaincode不会创建另一个新交易,具有上下文不变性(Context invariant),包括CID、transient Map、TxID、timestamp等。

InvokeChaincode(calledChaincode, args, channel) peer.Response
  • 如果被调用的chaincode在同一个channel上(参数channel为空),会将被调用的chaincode的读写集加入到当前交易中,进行合并;
  • 如果被调用的chaincode在不同的channel上,只会将结果返回到当前交易中,不会影响读写集。类似于“query”一样,PutState也不会有任何效果。

客户端身份解析(Client Identity Chaincode Library)

通过GetCreator接口获取client的信息,识别当前交易是由谁发起的(不是背书人)。

GetCreator() ([]byte, error)

Client Identity Chaincode Library是一个工具类,将GetCreator获取的信息解析为结构体clientIdentityImpl,得到具体的交易发起人信息。

// ClientIdentityImpl implements the ClientIdentity interface
type clientIdentityImpl struct {
    mspID string
    cert *x509.Certificate
    attrs *attrmgr.Attributes
}

获得Transient对象(GetTransient)

GetTransient返回“ChaincodeProposalPayload.Transient”字段,它是一个包含一些数据(比如加密数据)的map,可用于实现某种形式的application-level的机密性。'ChaincodeProposalPayload’字段内容不会出在交易中,也不会出现在账本中。比如有时可能希望传入密钥给chaincode签名,就可以使用Transient Map。

我们完全可以将所有argument参数都放到Transient Map中,用作隐私保护,而argument中的数据是可以被其他peer看到的,因为argument是会包含在上下文中,黑客可以轻易获取到。Transient Map只有当前执行智能合约的peer可以看到。

私有数据(PrivateData)

使用PrivateData需要一些先决条件:

  • 必须手动设置anchor peers
  • 初始化chaincode的时候,需要预定义集合,包括{permissioned orgs,requiredPeerCount,maxPeerCount}
    collection策略里只能使用’OR’,也可以使用’1-of’,禁止使用’2-of’和’and’。(会尽可能散布到策略里所有organization)

Key级别的背书(Key level endorsement)

原来背书策略只能细化到chaincode这一级,意味着如果需要更新背书策略,只能通过升级智能合约的方式。有了key级别的背书策略,不需要升级chaincoe,只需要对某一些特定的key做背书策略配置,意味着有可能某些智能合约版本确在不同组织间不一样,但是在涉及这些key的操作,就会要求它们变成一样,不然就会背书失败。这样就使得key的意义更加广泛,变成资产的代表,对这个资产所有合约,都需要Key level endorsement来保证合约的一致性,不像以前key只是一个数据项,背书策略只能保证合约本身的一致性,合约放宽,key收紧。
获取和设置背书策略的API如下:

GetStateValidationParameter(key string) ([]byte, error)
SetStateValidationParameter(key string, ep []byte) error // ep是通过KeyLevelEndorsement的build接口构造出来的,因为只能add或delet,所有只支持'AND'的关系

如果一个key的数据被修改,而且账本中存在key级别的背书策略,这个key级别的背书策略将会覆盖chaincode级别的背书策略。这意味着chaincode级别的背书策略不用一直修改,不用再管理了,只要SetStateValidationParameter就可以了。

3 Init函数

Init函数需要注意以下几点:

  1. Init函数具有不可查询性,外部获取不到peer.response的结果(set payload in peer.response in vain)
  2. Chaincode升级时该函数会被执行,需要小心值的修改
  3. privateData未就绪,调用privateData相关API会报错
    stub.putPrivateData==>Error: collection config not define for namespace [collectionName]

4 Invoke VS Query

Invoke是两阶段提交:

  • 阶段1:sendTransactionProposal
    在chaincode中执行Invoke功能。如果容器不存在的话,构建chaincode容器“dev-*”,这时会意外的长时间等待首次调用
  • 阶段2:sendTransaction
  • 可选阶段:等待eventHub响应

Query则只有第一个阶段。

由于Invoke的两阶段提交,chaincode中在putstate操作后,无法query到最新的结果,因为还没写到账本中,还需等待第二阶段提交。

5 开发模式

Fabric节点运行模式分为一般模式和开发模式:

  • 一般模式中,chaincode运行在docker容器中,正常开发构造镜像需要45秒,每次开发调试都需要创建docker镜像,非常繁杂;
  • 开发模式(通过–peer-chaincodedev指定)中,Chaincode运行在本地,开发调试相对容易(可以省去45秒),不需要创建docker镜像,但是需要外部配合,也比较复杂。

但是开发模式需要一定的外部配合,比如需要做一个开发模式的orderer,另外还不允许使用TLS。如果不想用开发模式,可以用MockStub代替stub,模拟chaincode的运行环境,进行测试;还可以用chaincode upgrade代替部署,这时不需要清除整个容器。

6 开发周期

6.1 peer部分

(1) 生成加密物料(Generate crypto material)

可以通过Binary可执行文件Cryptogen生成加密物料,另外也可以通过Fabric CA代替该功能。

  • Restful http server “fabric-ca-server”(Binary, docker image ‘fabric-ca’)
    • CA Service:Register, revoke, Enroll, reenroll, generate CRL
    • Identity Service
    • Certificate Service
    • Affiliation Service
  • “fabric-ca-client” (Binary, helper in sdks)
    $ npm fabric-ca-client

(2) 生成创世区块(Generate genesis block file)

创世区块文件是Orderer启动的必需文件。一般是通过Binary可执行文件configtxgen读取configtx.yaml配置文件,生成genesis.block。
Fabric v1.x 应用开发指南_第1张图片

channelName=testchainid
configtxgen -outputBlock $outputFile -profile $profile -channelID $channelName

Genesis block是系统channel的起始配置。

(3) 启动orderer和peer(Start orderer and peer)

  • Orderer最小配置
'ORDERER_GENERAL_LISTENADDRESS=0.0.0.0', // used to self identify
'ORDERER_GENERAL_TLS_ENABLED=${!!tls}',
'ORDERER_GENERAL_GENESISMETHOD=file',
'ORDERER_GENERAL_GENESISFILE=${exports.container.CONFIGTX}/${BLOCK_FILE}', -----
'ORDERER_GENERAL_LOCALMSPID=${id}', ----- // 与genesis block中的orderer的组织相对应
'ORDERER_GENERAL_LOCALMSPDIR=${configPath}', ----- // 与Cryptogen生成的文件路径相对应
  • Peer最小配置

6.2 channel部分

(1) 生成channel引导文件(Generate channel bootstrap file)

与生成创世区块一样,同样是通过Binary可执行文件configtxgen来生成用户channel的引导文件。
Fabric v1.x 应用开发指南_第2张图片

configtxgen -outputCreateChannelTx $outputFile -profile $profile -channelID $channelName

mychannel.tx为用户channel的起始配置。

(2) 创建channel(Create channel)

SDK读取channel引导文件mychannel.tx,传入到request域中,request中包含orderer参数,将channel的配置发送到orderer中,这样就完成了channel的创建。
Fabric v1.x 应用开发指南_第3张图片
创建于更新channel的方法是一样的。

Client.js#createChannel(request)
{
	return this._createOrUpdateChannel
	(request, request && request.envelope);
}

(3) Peer节点加入到channel(Peer join channel)

  • 可以通过Binary可执行文件peer进行加入channel的操作,其中必须要能在同一个文件系统寻址到mychannel.tx文件,读取后会在某个位置生成mychannel文件,不允许join两次(报错的时候可以看到mychannel文件位置)。
    Fabric v1.x 应用开发指南_第4张图片
$ peer channel join
  • 另一个方法是通过SDK进行加入channel的操作。SDK先去找orderer获取mychannel.tx的Js Data版本,获取完之后发送给peer,peer节点用该数据生成mychannel文件。
    Fabric v1.x 应用开发指南_第5张图片
Channel.js#joinChannel

6.3 chaincode生命周期

chaincode的生命周期包括Package、Install、Instantiate、Running、Upgrade等阶段,如下图:
Fabric v1.x 应用开发指南_第6张图片

(1) 打包与安装chaincode(Package, install chaincode)

安装chaincode实际上就是把chaincode打包一下,放到Peer上。

  • Commands: peer chaincode
$ peer chaincode package -s -S -i "AND('OrgA.admin')" ... ccpack.out
$ peer chaincode signpackage ccpack.out signedccpack.out

打包时“ -s -S”选项可以创建可以由多个所有者签名的package,会被认为是还没被完全签名的输出。否则,该过程将创建一个仅包含实例化策略SignedCDS(仅有一个签名)。

  • Client.js#installChaincode [SignedCDS] default instantiate policy 不支持多签名版本
    Params: chaincode name/id, peer, path, version, [instantiate policy] [metadataPath]
    metadataPath是couchDB在做索引集合的操作的时候,提供index.json的路径

(2) 实例化chaincode(Instantiate chaincode)

实例化chaincode通过sendInstantiateProposal发出一个交易,然后进行交易共识。本质上还是调用channel._sendChaincodeProposal。

(3) 升级chaincode(Upgrade chaincode)

更新背书策略(endorsement policy)、更新私有数据收集策略( collection policy)、更新智能合约内容或希望重新执行Init函数时,都会需要执行chaincode升级操作。
升级chaincode通过sendUpgradeProposal发出一个交易,然后进行交易共识。 与实例化一样,本质上也是调用channel._sendChaincodeProposal。

[package ->] install -> upgrade(upgrade之前必须先install,以另一个chaincode version)

7、 黑盒开发

黑盒开发可以让开发者不用关心peer、orderer、channel这些模块,进行纯业务开发。

需要参与的点如下:

  • chaincode: {ID, args接口定义, transientMap接口定义}
  • Fabric User: {key-pair, mspid(组织id)}
    以哪个身份来调用这个合约,如果是离线签署交易,私钥可能在移动设备里,由移动设备签署之后将交易打包给你;
  • 背书节点: {domain, TLS证书}
    合约安装的地方,需要知道背书节点的位置;
  • Orderer: {domain, TLS证书}
    需要将背书后的交易发送给Orderer,需要知道Orderer的位置;
  • [可选]CA: {domain, TLS证书}
    在线的情况下,如果需要通过CA获取新的用户,需要知道CA服务器的位置;有一个Connection profile可以参考;
  • [可选]discover service和发现节点
    不用单独指定哪些是背书节点,只需要知道哪些是发现服务节点,通过这个节点可以知道哪些服务节点是可用的,直接发送就可以,就会显得更加黑盒了。

你可能感兴趣的:(Hyperledger,Fabric,#,Fabric,v1.x)