引言
这周恰逢期中,时间仓促,代码上有许多地方可以优化,但也只能留到之后的几次作业上了。
阅读建议:参考和链码样例为写链码前用样例试手内容,与作业相关的内容是“资产管理”之后的代码。
代码参考及学习资料在“参考”一栏中。
参考
https://blog.csdn.net/zekdot/article/details/120397660?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-1-120397660-blog-125920006.pc_relevant_3mothn_strategy_and_data_recovery&spm=1001.2101.3001.4242.2&utm_relevant_index=4
https://blog.csdn.net/weixin_49422491/article/details/125380911
https://blog.csdn.net/weixin_44676392/article/details/87938176
https://blog.csdn.net/weixin_44676392/article/details/87938451
https://blog.csdn.net/qq_41988893/article/details/119706443
fabric提供了fabric-contract-api-go和fabric-chaincode-go两个包来编写链码, 这里以fabric-contract-api-go为例进行链码编写
链码样例
1.声明合约
package main
import (
"errors"
"fmt"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
)
type SimpleContract struct {
contractapi.Contract
}
2.编写合约函数
规则:
1、第一个参数必须是*contractapi.TransactionContext类型
2、函数最多返回两个值,第二个值必须是error类型
// 添加数据
func (sc *SimpleContract) Create(ctx contractapi.TransactionContextInterface, key string, value string) error {
existing, err := ctx.GetStub().GetState(key)
if err != nil {
return errors.New("Unable to interact with world state")
}
if existing != nil {
return fmt.Errorf("Cannot create world state pair with key %s. Already exists", key)
}
err = ctx.GetStub().PutState(key, []byte(value))
if err != nil {
return errors.New("Unable to interact with world state")
}
return nil
}
// 读取数据
func (sc *SimpleContract) Read(ctx contractapi.TransactionContextInterface, key string) (string, error) {
existing, err := ctx.GetStub().GetState(key)
if err != nil {
return "", errors.New("Unable to interact with world state")
}
if existing == nil {
return "", fmt.Errorf("Cannot read world state pair with key %s. Does not exist", key)
}
return string(existing), nil
}
3.创建并启动链码
package main
import (
"github.com/hyperledger/fabric-contract-api-go/contractapi"
)
func main() {
simpleContract := new(SimpleContract)
cc, err := contractapi.NewChaincode(simpleContract)
if err != nil {
panic(err.Error())
}
if err := cc.Start(); err != nil {
panic(err.Error())
}
}
完整合约代码:
package main
import (
"errors"
"fmt"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
)
type User struct {
}
type SimpleContract struct {
contractapi.Contract
}
//用户定义Struct
// 添加数据
func (sc *SimpleContract) Create(ctx contractapi.TransactionContextInterface, key string, value string) error {
existing, err := ctx.GetStub().GetState(key)
if err != nil {
return errors.New("Unable to interact with world state")
}
if existing != nil {
return fmt.Errorf("Cannot create world state pair with key %s. Already exists", key)
}
err = ctx.GetStub().PutState(key, []byte(value))
if err != nil {
return errors.New("Unable to interact with world state")
}
return nil
}
// 读取数据
func (sc *SimpleContract) Read(ctx contractapi.TransactionContextInterface, key string) (string, error) {
existing, err := ctx.GetStub().GetState(key)
if err != nil {
return "", errors.New("Unable to interact with world state")
}
if existing == nil {
return "", fmt.Errorf("Cannot read world state pair with key %s. Does not exist", key)
}
return string(existing), nil
}
func main() {
simpleContract := new(SimpleContract)
cc, err := contractapi.NewChaincode(simpleContract)
if err != nil {
panic(err.Error())
}
if err := cc.Start(); err != nil {
panic(err.Error())
}
}
一个简单的智能合约就编写完了。
特别注意,这不是一个完整的链码程序,中间缺少了用户定义的struct.
test-network网络测试
启动fabric网络
-
进入fabric-sample的test-network目录
$ cd fabric-samples/test-network
-
运行
./network.sh up
启动网络
magpie@Goserver:~/fabric-samples01/test-network$ ./network.sh up
Starting nodes with CLI timeout of '5' tries and CLI delay of '3' seconds and using database 'leveldb' with crypto from 'cryptogen'
LOCAL_VERSION=2.4.6
DOCKER_IMAGE_VERSION=2.4.6
/home/magpie/fabric-samples01/test-network/../bin/cryptogen
Generating certificates using cryptogen tool
Creating Org1 Identities
+ cryptogen generate --config=./organizations/cryptogen/crypto-config-org1.yaml --output=organizations
org1.example.com
+ res=0
Creating Org2 Identities
+ cryptogen generate --config=./organizations/cryptogen/crypto-config-org2.yaml --output=organizations
org2.example.com
+ res=0
Creating Orderer Org Identities
+ cryptogen generate --config=./organizations/cryptogen/crypto-config-orderer.yaml --output=organizations
+ res=0
Generating CCP files for Org1 and Org2
/home/magpie/fabric-samples01/test-network/../bin/configtxgen
Generating Orderer Genesis block
+ configtxgen -profile TwoOrgsOrdererGenesis -channelID system-channel -outputBlock ./system-genesis-block/genesis.block
2022-10-16 06:15:53.999 UTC [common.tools.configtxgen] main -> INFO 001 Loading configuration
2022-10-16 06:15:54.026 UTC [common.tools.configtxgen.localconfig] completeInitialization -> INFO 002 orderer type: etcdraft
2022-10-16 06:15:54.026 UTC [common.tools.configtxgen.localconfig] completeInitialization -> INFO 003 Orderer.EtcdRaft.Options unset, setting to tick_interval:"500ms" election_tick:10 heartbeat_tick:1 max_inflight_blocks:5 snapshot_interval_size:16777216
2022-10-16 06:15:54.026 UTC [common.tools.configtxgen.localconfig] Load -> INFO 004 Loaded configuration: /home/magpie/fabric-samples01/test-network/configtx/configtx.yaml
2022-10-16 06:15:54.028 UTC [common.tools.configtxgen] doOutputBlock -> INFO 005 Generating genesis block
2022-10-16 06:15:54.028 UTC [common.tools.configtxgen] doOutputBlock -> INFO 006 Writing genesis block
+ res=0
[+] Running 7/7
⠿ Volume "docker_orderer.example.com" Created 0.0s
⠿ Volume "docker_peer0.org1.example.com" Created 0.0s
⠿ Volume "docker_peer0.org2.example.com" Created 0.0s
⠿ Container peer0.org1.example.com Started 3.4s
⠿ Container peer0.org2.example.com Started 1.3s
⠿ Container orderer.example.com Started 2.3s
⠿ Container cli Started 4.1s
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
70d6427003ae hyperledger/fabric-tools:latest "/bin/bash" 4 seconds ago Up Less than a second cli
0f2f91968493 hyperledger/fabric-peer:latest "peer node start" 4 seconds ago Up 3 seconds 0.0.0.0:9051->9051/tcp, :::9051->9051/tcp, 7051/tcp, 0.0.0.0:9445->9445/tcp, :::9445->9445/tcp peer0.org2.example.com
615cea63009c hyperledger/fabric-orderer:latest "orderer" 4 seconds ago Up 2 seconds 0.0.0.0:7050->7050/tcp, :::7050->7050/tcp, 0.0.0.0:9443->9443/tcp, :::9443->9443/tcp orderer.example.com
1db85f663965 hyperledger/fabric-peer:latest "peer node start" 4 seconds ago Up 1 second 0.0.0.0:7051->7051/tcp, :::7051->7051/tcp, 0.0.0.0:9444->9444/tcp, :::9444->9444/tcp peer0.org1.example.com
d10bd7ff864d hyperledger/explorer:latest "docker-entrypoint.s…" 34 minutes ago Exited (1) 33 minutes ago explorer.mynetwork.com
2ad7a1e8464e hyperledger/explorer-db:latest "docker-entrypoint.s…" 34 minutes ago Up 34 minutes (healthy) 5432/tcp explorerdb.mynetwork.com
efd328836573 portainer/portainer-ce "/portainer" 3 days ago Up About an hour 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp, 0.0.0.0:9000->9000/tcp, :::9000->9000/tcp, 9443/tcp portainer
最终出现以上输出日志则表示网络启动成功,每个加入Fabric网络的Node和User都需要隶属于某个组织,以上网络中包含了两个平行组织—peer0.org1.example.com和
peer0.org2.example.com,它还包括一个作为ordering service维护网络的
orderer.example.com`。
创建channel
上节已经在机器上运行了peer节点和orderer节点,现在可以使用network.sh为Org1和Org2之间创建channel。channel是特定网络成员之间的私有通道,只能被属于该通道的组织使用,并且对网络的其他成员是不可见的。每个channel都有一个单独的区块链账本,属于该通道的组织可以让其下peer加入该通道,以让peer能够存储channel上的帐本并验证账本上的交易。
使用以下命令创建自定义通道testchannel:
$ ./network.sh createChannel -c testchannel
部署chaincode
部署链码前,建议到链码子目录下执行go mod tidy,检查链码调用的包存在。同时,可能需要sudo apt install jq。创建通道后,您可以开始使用智能合约与通道账本交互。智能合约包含管理区块链账本上资产的业务逻辑,由成员运行的应用程序网络可以在账本上调用智能合约创建,更改和转让这些资产。可以通过./network.sh deployCC
命令部署智能合约,但本过程可能会出现很多问题。使用以下命令部署chaincode:
$ ./network.sh deployCC -c testchannel -ccn basic -ccp ../asset-transfer-basic/chaincode-go -ccl go
此命令执行后可能会出现错误:scripts/deployCC.sh: line 114: log.txt: Permission denied
,很明显这是权限不足所致,加上sudo试试:
$sudo ./network.sh deployCC -c testchannel -ccn basic -ccp ../asset-transfer-basic/chaincode-go -ccl go
chaincode
code1
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
)
// SmartContract provides functions for managing an Asset
type SmartContract struct {
contractapi.Contract
}
// Asset describes basic details of what makes up a simple asset
type Asset struct {
ID string `json:"ID"`
Color string `json:"color"`
Size int `json:"size"`
Owner string `json:"owner"`
AppraisedValue int `json:"appraisedValue"`
}
// InitLedger adds a base set of assets to the ledger
func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
assets := []Asset{
{ID: "asset1", Color: "blue", Size: 5, Owner: "cuteAlgernon", AppraisedValue: 300},
{ID: "asset2", Color: "red", Size: 5, Owner: "Biosheep", AppraisedValue: 1000},
{ID: "asset3", Color: "green", Size: 10, Owner: "Jin Soo", AppraisedValue: 500},
{ID: "asset4", Color: "yellow", Size: 10, Owner: "Max", AppraisedValue: 600},
{ID: "asset5", Color: "black", Size: 15, Owner: "Adriana", AppraisedValue: 700},
{ID: "asset6", Color: "white", Size: 15, Owner: "Michel", AppraisedValue: 800},
}
for _, asset := range assets {
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
err = ctx.GetStub().PutState(asset.ID, assetJSON)
if err != nil {
return fmt.Errorf("failed to put to world state. %v", err)
}
}
return nil
}
// CreateAsset issues a new asset to the world state with given details.
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error {
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if exists {
return fmt.Errorf("the asset %s already exists", id)
}
asset := Asset{
ID: id,
Color: color,
Size: size,
Owner: owner,
AppraisedValue: appraisedValue,
}
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
return ctx.GetStub().PutState(id, assetJSON)
}
// ReadAsset returns the asset stored in the world state with given id.
func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) {
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return nil, fmt.Errorf("failed to read from world state: %v", err)
}
if assetJSON == nil {
return nil, fmt.Errorf("the asset %s does not exist", id)
}
var asset Asset
err = json.Unmarshal(assetJSON, &asset)
if err != nil {
return nil, err
}
return &asset, nil
}
// UpdateAsset updates an existing asset in the world state with provided parameters.
func (s *SmartContract) UpdateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error {
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("the asset %s does not exist", id)
}
// overwriting original asset with new asset
asset := Asset{
ID: id,
Color: color,
Size: size,
Owner: owner,
AppraisedValue: appraisedValue,
}
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
return ctx.GetStub().PutState(id, assetJSON)
}
// DeleteAsset deletes an given asset from the world state.
func (s *SmartContract) DeleteAsset(ctx contractapi.TransactionContextInterface, id string) error {
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("the asset %s does not exist", id)
}
return ctx.GetStub().DelState(id)
}
// AssetExists returns true when asset with given ID exists in world state
func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return false, fmt.Errorf("failed to read from world state: %v", err)
}
return assetJSON != nil, nil
}
// TransferAsset updates the owner field of asset with given id in world state.
func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, id string, newOwner string) error {
asset, err := s.ReadAsset(ctx, id)
if err != nil {
return err
}
asset.Owner = newOwner
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
return ctx.GetStub().PutState(id, assetJSON)
}
// GetAllAssets returns all assets found in world state
func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]*Asset, error) {
// range query with empty string for startKey and endKey does an
// open-ended query of all assets in the chaincode namespace.
resultsIterator, err := ctx.GetStub().GetStateByRange("", "")
if err != nil {
return nil, err
}
defer resultsIterator.Close()
var assets []*Asset
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return nil, err
}
var asset Asset
err = json.Unmarshal(queryResponse.Value, &asset)
if err != nil {
return nil, err
}
assets = append(assets, &asset)
}
return assets, nil
}
func main() {
assetChaincode, err := contractapi.NewChaincode(&SmartContract{})
if err != nil {
log.Panicf("Error creating asset-transfer-basic chaincode: %v", err)
}
if err := assetChaincode.Start(); err != nil {
log.Panicf("Error starting asset-transfer-basic chaincode: %v", err)
}
}
code2-1资产管理
package atcc
// 导入必要的依赖
import (
"fmt"
"encoding/json"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
)
type SmartContract struct {
contractapi.Contract
}
// 定义资产的数据结构,并使用注解的方式来辅助序列化,marshal函数会使用字母序对key进行排序,这样可以
// 保证其序列化之后具有唯一性,即不会出现导出的json字符串中ID字段在Color字段前面这种情况,
// 这样做的主要原因是为了保证输入输出的唯一性,防止背书验证的时候失败。
type Asset struct {
AppraisedValue int `json:"AppraisedValue"`
Color string `json:"Color"`
ID string `json:"ID"`
Owner string `json:"Owner"`
Size int `json:"Size"`
}
// 使用数据对链码进行初始化。
func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
assets := []Asset{
{ID: "asset1", Color: "blue", Size: 5, Owner: "Tomoko", AppraisedValue: 300},
{ID: "asset2", Color: "red", Size: 5, Owner: "Brad", AppraisedValue: 400},
{ID: "asset3", Color: "green", Size: 10, Owner: "Jin Soo", AppraisedValue: 500},
{ID: "asset4", Color: "yellow", Size: 10, Owner: "Max", AppraisedValue: 600},
{ID: "asset5", Color: "black", Size: 15, Owner: "Adriana", AppraisedValue: 700},
{ID: "asset6", Color: "white", Size: 15, Owner: "Michel", AppraisedValue: 800},
}
for _, asset := range assets {
// 序列化资产
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
// 按照id存储序列化后的资产
err = ctx.GetStub().PutState(asset.ID, assetJSON)
if err != nil {
return fmt.Errorf("failed to put to world state. %v", err)
}
}
return nil
}
// 通过传入参数来创建一个账本上不存在的资产,这里有在后面实现的方法AssetExists来检查是否存在某个key为id的资产。
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error {
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if exists {
return fmt.Errorf("the asset %s already exists", id)
}
asset := Asset{
ID: id,
Color: color,
Size: size,
Owner: owner,
AppraisedValue: appraisedValue,
}
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
return ctx.GetStub().PutState(id, assetJSON)
}
// 从账本中读取资产,调用GetState来实现
func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) {
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return nil, fmt.Errorf("failed to read from world state: %v", err)
}
if assetJSON == nil {
return nil, fmt.Errorf("the asset %s does not exist", id)
}
var asset Asset
err = json.Unmarshal(assetJSON, &asset)
if err != nil {
return nil, err
}
return &asset, nil
}
// 更新资产,这里实现逻辑是根据传入参数创建一个新的资产并序列化,然后覆盖原来的资产。
func (s *SmartContract) UpdateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error {
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("the asset %s does not exist", id)
}
// overwriting original asset with new asset
asset := Asset{
ID: id,
Color: color,
Size: size,
Owner: owner,
AppraisedValue: appraisedValue,
}
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
return ctx.GetStub().PutState(id, assetJSON)
}
// 删除资产,直接调用DelState函数来实现删除。
func (s *SmartContract) DeleteAsset(ctx contractapi.TransactionContextInterface, id string) error {
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("the asset %s does not exist", id)
}
return ctx.GetStub().DelState(id)
}
// 检查id对应的资产是否存在,判断能不能读取出value即可。
func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return false, fmt.Errorf("failed to read from world state: %v", err)
}
return assetJSON != nil, nil
}
// 资产转移,实质是修改资产结构体的owner字段。
func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, id string, newOwner string) error {
asset, err := s.ReadAsset(ctx, id)
if err != nil {
return err
}
asset.Owner = newOwner
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
return ctx.GetStub().PutState(id, assetJSON)
}
// 读取全部资产,调用GetStateByRange函数来获取账本上的全部记录。
func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]*Asset, error) {
// range query with empty string for startKey and endKey does an
// open-ended query of all assets in the chaincode namespace.
resultsIterator, err := ctx.GetStub().GetStateByRange("", "")
if err != nil {
return nil, err
}
defer resultsIterator.Close()
var assets []*Asset
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return nil, err
}
var asset Asset
err = json.Unmarshal(queryResponse.Value, &asset)
if err != nil {
return nil, err
}
assets = append(assets, &asset)
}
return assets, nil
}
code2-2
assetsManager.go
package main
import (
"log"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
"main/atcc"
)
func main() {
assetChaincode, err := contractapi.NewChaincode(&atcc.SmartContract{})
if err != nil {
log.Panicf("Error creating atcc chaincode: %v", err)
}
if err := assetChaincode.Start(); err != nil {
log.Panicf("Error starting atcc chaincode: %v", err)
}
}
构建链码
go mod tidy
go mod vendor
使用chaincode
初始化账本
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile "$ORDERER_CA" -C $CHANNEL_NAME -n ${CC_NAME} --peerAddresses localhost:7051 --tlsRootCertFiles organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt --isInit -c '{"function":"InitLedger","Args":[]}'
获取当前资产
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile "$ORDERER_CA" -C $CHANNEL_NAME -n ${CC_NAME} --peerAddresses localhost:7051 --tlsRootCertFiles organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"GetAllAssets","Args":[]}'
总结
随着实验次数的增多,肉眼可见的是文件夹越来越混乱——比如这次的网络,需要在前几次实验中找到内容,而每次实验都涉及到文件创建、下载、删除,有时哪怕一次实验,都会鼓捣出很多奇奇怪怪的文件夹,有时会出现这样一种状况:我知道网络是通的,但不知道是那个文件能够跑通,毕竟,如果按照教程顺利过关,是对具体内容没有深刻印象的,直到需要再次使用的时候。
链码编写(改写)难度不算特别大,但如果 涉及到网络上跑通,再加之账本的增删改查,如果找不到之前实验的基础内容,无异于再做一次。