1. Go SDK 使用说明

本篇介绍:

1、环境依赖

2、sdk依赖使用

3、普通合约安装、调用

4、EVM合约安装、调用

5、交易序号(sequence)

6、更多的示例及全部接口

1.1. 长安链SDK概述

  1. 整体介绍

长安链SDK是业务模块与长安链交互的桥梁,支持双向TLS认证,提供安全可靠的加密通信信道。

长安链提供了多种语言的SDK,包括:Go SDKJava SDKPython SDKNodejs SDK方便开发者根据需要进行选用。

提供的SDK接口,覆盖合约管理、链配置管理、证书管理、多签收集、各类查询操作、事件订阅等场景,满足了不同的业务场景需要。

  1. 名词概念说明

  • Node(节点):代表一个链节点的基本信息,包括:节点地址、连接数、是否启用TLS认证等信息

  • ChainClient(链客户端):所有客户端对链节点的操作接口都来自ChainClient

  • 压缩证书:可以为ChainClient开启证书压缩功能,开启后可以减小交易包大小,提升处理性能

1.2. 环境准备

1.2.1. 软件环境依赖

golang : 版本为1.16或以上

下载地址:https://golang.org/dl/

若已安装,请通过命令查看版本:

$ go version
go version go1.16 linux/amd64

1.2.2. 下载安装sdk

进入您的Go项目,执行以下命令添加对sdk的引用:

go get chainmaker.org/chainmaker/sdk-go/v2@v2.3.8

1.2.3. 长安链环境准备

创建一条证书模式的长安链,并确保相关节点网络通畅,相关教程见:《通过命令行体验链》

1.3. 怎么使用SDK

1.3.1. 示例代码

1.3.1.1. 创建节点

设置节点信息,可用作创建与该节点连接的客户端

// 创建节点
func createNode(nodeAddr string, connCnt int) *NodeConfig {
 node := NewNodeConfig(
  // 节点地址,格式:127.0.0.1:12301
  WithNodeAddr(nodeAddr),
  // 节点连接数
  WithNodeConnCnt(connCnt),
  // 节点是否启用TLS认证
  WithNodeUseTLS(true),
  // 根证书路径,支持多个
  WithNodeCAPaths(caPaths),
  // TLS Hostname
  WithNodeTLSHostName(tlsHostName),
 )

 return node
}

1.3.1.2. 以参数形式创建ChainClient

注:示例中证书采用路径方式去设置,也可以使用证书内容去设置,调用WithUserKeyBytes, WithUserCrtBytes等方法,具体请参看:sdk_config.go

// 创建ChainClient
func createClient() (*ChainClient, error) {
 if node1 == nil {
  // 创建节点1
  node1 = createNode(nodeAddr1, connCnt1)
 }

 if node2 == nil {
  // 创建节点2
  node2 = createNode(nodeAddr2, connCnt2)
 }

 chainClient, err := NewChainClient(
  // 设置归属组织
  WithChainClientOrgId(chainOrgId),
  // 设置链ID
  WithChainClientChainId(chainId),
  // 设置logger句柄,若不设置,将采用默认日志文件输出日志
  WithChainClientLogger(getDefaultLogger()),
  // 设置客户端用户私钥路径
  WithUserKeyFilePath(userKeyPath),
  // 设置客户端用户证书
  WithUserCrtFilePath(userCrtPath),
  // 添加节点1
  AddChainClientNodeConfig(node1),
  // 添加节点2
  AddChainClientNodeConfig(node2),
  )

 if err != nil {
  return nil, err
 }

 //启用证书压缩(开启证书压缩可以减小交易包大小,提升处理性能)
 err = chainClient.EnableCertHash()
 if err != nil {
  log.Fatal(err)
 }

 return chainClient, nil
}

1.3.1.3. 以配置文件形式创建ChainClient

注:参数形式和配置文件形式两个可以同时使用,同时配置时,以参数传入为准

func createClientWithConfig() (*ChainClient, error) {

 chainClient, err := NewChainClient(
  WithConfPath("./testdata/sdk_config.yml"),
 )

 if err != nil {
  return nil, err
 }

 //启用证书压缩(开启证书压缩可以减小交易包大小,提升处理性能)
 err = chainClient.EnableCertHash()
 if err != nil {
  return nil, err
 }

 return chainClient, nil
}

1.3.2. 使用加密私钥

SDK 支持使用密码加密的私钥文件,避免私钥明文存储在磁盘上。适用于对私钥安全性有较高要求的生产环境。

1.3.2.1. 生成加密私钥

使用 OpenSSL 对已有明文私钥进行 AES-256-CBC 加密:

# ECC / EC 私钥加密
openssl ec -in client1.sign.key -out client1.sign.key.enc -aes256 -passout pass:your_password

# SM2 国密私钥加密
openssl ec -in client1.tls.key -out client1.tls.key.enc -aes256 -passout pass:your_password

# RSA 私钥加密
openssl rsa -in client1.sign.key -out client1.sign.key.enc -aes256 -passout pass:your_password

生成后的文件内容为标准 PEM 加密格式(例如 EC 或 SM2):

-----BEGIN EC PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,<hex-iv>

<base64-encrypted-data>
-----END EC PRIVATE KEY-----

或国密 SM2 格式:

-----BEGIN SM2 PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,<hex-iv>

<base64-encrypted-data>
-----END SM2 PRIVATE KEY-----

1.3.2.2. 配置使用

sdk_config.yml 中将私钥路径指向加密文件,并设置对应的密码字段。

涉及的密码字段:

YAML 配置项 Option 函数 适用场景
user_sign_key_pwd WithUserSignKeyPwd 交易签名私钥密码(PWK/Cert 模式通用)
user_key_pwd WithUserKeyPwd TLS 通信私钥密码
user_enc_key_pwd WithUserEncKeyPwd 国密双证书加密私钥密码(GMTLS 场景)

1.3.2.3. 编码方式

chainClient, err := NewChainClient(
    WithUserSignKeyFilePath("./crypto-config/.../client1.sign.key.enc"),
    WithUserSignKeyPwd("your_password"),
    // ...其他配置
)

说明:

  • 如果私钥未加密(明文 PEM),无需设置密码字段

  • 设置了多余的密码(对非加密私钥设置密码)不会报错,SDK 有容错处理

  • 密码错误时 SDK 会返回明确的错误提示

1.3.2.4. 部署wasm合约

下文,将演示通过sdk部署wasm合约,

sdk_user_contract_claim_test.go

func testUserContractClaimCreate(t *testing.T, client *ChainClient,
 admin1, admin2, admin3, admin4 *ChainClient, withSyncResult bool, isIgnoreSameContract bool) {

 resp, err := createUserContract(client, admin1, admin2, admin3, admin4,
  claimContractName, claimVersion, claimByteCodePath, common.RuntimeType_WASMER, []*common.KeyValuePair{}, withSyncResult)
 if !isIgnoreSameContract {
  require.Nil(t, err)
 }

 fmt.Printf("CREATE claim contract resp: %+v\n", resp)
}

func createUserContract(client *ChainClient, admin1, admin2, admin3, admin4 *ChainClient,
 contractName, version, byteCodePath string, runtime common.RuntimeType, kvs []*common.KeyValuePair, withSyncResult bool) (*common.TxResponse, error) {

 payloadBytes, err := client.CreateContractCreatePayload(contractName, version, byteCodePath, runtime, kvs)
 if err != nil {
  return nil, err
 }

 // 各组织Admin权限用户签名
 signedPayloadBytes1, err := admin1.SignContractManagePayload(payloadBytes)
 if err != nil {
  return nil, err
 }

 signedPayloadBytes2, err := admin2.SignContractManagePayload(payloadBytes)
 if err != nil {
  return nil, err
 }

 signedPayloadBytes3, err := admin3.SignContractManagePayload(payloadBytes)
 if err != nil {
  return nil, err
 }

 signedPayloadBytes4, err := admin4.SignContractManagePayload(payloadBytes)
 if err != nil {
  return nil, err
 }

 // 收集并合并签名
 mergeSignedPayloadBytes, err := client.MergeContractManageSignedPayload([][]byte{signedPayloadBytes1,
  signedPayloadBytes2, signedPayloadBytes3, signedPayloadBytes4})
 if err != nil {
  return nil, err
 }

 // 发送创建合约请求
 resp, err := client.SendContractManageRequest(mergeSignedPayloadBytes, createContractTimeout, withSyncResult)
 if err != nil {
  return nil, err
 }

 err = checkProposalRequestResp(resp, true)
 if err != nil {
  return nil, err
 }

 return resp, nil

1.3.2.5. 调用wasm合约

下文,将演示通过sdk调用wasm合约,

sdk_user_contract_claim_test.go

func testUserContractClaimInvoke(client *ChainClient,
 method string, withSyncResult bool) (string, error) {

 curTime := fmt.Sprintf("%d", CurrentTimeMillisSeconds())
 fileHash := uuid.GetUUID()
 params := map[string]string{
  "time":      curTime,
  "file_hash": fileHash,
  "file_name": fmt.Sprintf("file_%s", curTime),
 }

 err := invokeUserContract(client, claimContractName, method, "", params, withSyncResult)
 if err != nil {
  return "", err
 }

 return fileHash, nil
}

func invokeUserContract(client *ChainClient, contractName, method, txId string, params map[string]string, withSyncResult bool) error {

 resp, err := client.InvokeContract(contractName, method, txId, params, -1, withSyncResult)
 if err != nil {
  return err
 }

 if resp.Code != common.TxStatusCode_SUCCESS {
  return fmt.Errorf("invoke contract failed, [code:%d]/[msg:%s]\n", resp.Code, resp.Message)
 }

 if !withSyncResult {
  fmt.Printf("invoke contract success, resp: [code:%d]/[msg:%s]/[txId:%s]\n", resp.Code, resp.Message, resp.ContractResult.Result)
 } else {
  fmt.Printf("invoke contract success, resp: [code:%d]/[msg:%s]/[contractResult:%s]\n", resp.Code, resp.Message, resp.ContractResult)
 }

 return nil
}

1.3.2.6. 创建及调用evm合约

1.3.3. 交易序号(sequence)

交易序号功能要求长安链和 Go SDK 均使用 v2.3.9 或更高版本。使用该功能前,请确认链版本和 SDK 版本均不低于 v2.3.9,并确保链配置已开启 enable_tx_seq

sdk_config.yml 只用于配置客户端身份、证书/私钥、节点、TLS、RPC 等连接参数,不需要增加 enable_tx_seq 字段。交易序号校验属于链上配置,应由链管理员通过 CreateChainConfigBlockUpdateWithEnableTxSeqPayload 生成链配置更新 payload,并完成管理员背书和发送。

交易序号用于保证同一账户提交的交易按照序号顺序执行。启用该功能后,客户端需要为交易设置正确的 sequence

  • confirmedSeq:账户在链上已经确认的交易序号。

  • sequence:待提交交易的序号,通常设置为 confirmedSeq + 1

使用交易序号前,需要在链配置中开启交易序号校验(enable_tx_seq)。如果链未开启该校验,交易中的 sequence 不会用于有序交易控制。

1.3.3.1. 查询账户已确认交易序号

推荐使用 GetCurrentAccountSequence 查询当前 SDK 客户端账户的 confirmedSeq,SDK 会根据认证模式和链配置中的地址类型自动推导账户地址:

confirmedSeq, err := client.GetCurrentAccountSequence()
if err != nil {
    return err
}

nextSeq := confirmedSeq + 1

如果需要查询指定账户,可以使用 GetAccountSequence。参数 address 为链上账户地址,可以是公钥地址或证书别名:

confirmedSeq, err := client.GetAccountSequence(address)
if err != nil {
    return err
}

nextSeq := confirmedSeq + 1

1.3.3.2. 构造并发送带交易序号的交易

SDK 中大多数便捷的交易构造方法默认将 Payload.Sequence 设置为 0。需要使用交易序号时,可以通过 CreatePayload 直接传入 nextSeq,然后完成签名和发送:

confirmedSeq, err := client.GetCurrentAccountSequence()
if err != nil {
    return err
}

payload := client.CreatePayload(
    "",                            // txId为空时由SDK自动生成
    common.TxType_INVOKE_CONTRACT, // 普通合约调用
    contractName,
    method,
    kvs,
    confirmedSeq+1, // sequence必须使用下一个待提交序号
    nil,            // 使用默认交易限制
)

txReq, err := client.GenerateTxRequest(payload, nil)
if err != nil {
    return err
}

resp, err := client.SendTxRequest(txReq, -1, true)
if err != nil {
    return err
}

也可以先使用其他 SDK 方法生成 payload,再在签名之前设置序号:

payload.Sequence = confirmedSeq + 1

payload 签名后不能再修改交易序号,否则签名将失效。交易发送失败时,应根据链上实际确认结果重新查询 confirmedSeq,不要直接重复使用旧序号。

1.3.3.3. 开启或关闭交易序号校验

链管理员可以使用 CreateChainConfigBlockUpdateWithEnableTxSeqPayload 生成包含交易序号开关的链配置更新 payload

payload, err := client.CreateChainConfigBlockUpdateWithEnableTxSeqPayload(
    txTimestampVerify,
    blockTimestampVerify,
    txTimeout,
    blockTimeout,
    blockTxCapacity,
    blockSize,
    blockInterval,
    txParameterSize,
    true, // true:开启交易序号校验;false:关闭交易序号校验
)
if err != nil {
    return err
}

// 按链配置更新流程收集所需的管理员背书签名。
endorser, err := client.SignChainConfigPayload(payload)
if err != nil {
    return err
}

_, err = client.SendChainConfigUpdateRequest(
    payload,
    []*common.EndorsementEntry{endorser},
    -1,
    true,
)
return err

链配置更新通常需要多个组织的管理员共同签名。上例仅展示当前客户端生成一个管理员签名的方式,实际使用时应按照链的权限策略收集完整的管理员背书后,再调用 SendChainConfigUpdateRequest。关闭交易序号校验时,将最后一个参数设置为 false

1.3.3.4. 使用注意事项

  1. 该功能从 Go SDK v2.3.9 开始支持,使用时请确认 SDK 版本和链配置均满足要求。

  2. confirmedSeq 只表示链上已经确认的序号,不包含交易池中尚未确认的交易。

  3. 同一账户并发提交多笔交易时,业务侧需要负责序号分配,避免多个交易使用同一个 sequence

  4. 交易发送结果不确定时,应先查询账户的最新 confirmedSeq,再决定是否重试,避免重复使用旧序号。

  5. GetCurrentAccountSequence 会先读取链配置以确定地址类型,查询指定账户时应确保传入地址与链的地址类型一致。

完整示例请参见 sdk-go/examples/account_sequence/main.go

1.3.4. 更多示例和用法

更多示例和用法,请参看单元测试用例
示例: (https://git.chainmaker.org.cn/chainmaker/sdk-go/-/blob/master/examples)

功能 单测代码
用户合约 sdk_user_contract_test.go
系统合约 sdk_system_contract_test.go
链配置 sdk_chain_config_test.go
证书管理 sdk_cert_manage_test.go
消息订阅 sdk_subscribe_test.go

1.3.5. demo

sdk-go demo参考:

文件 · v2.3.8 · chainmaker / sdk-go-demo · ChainMaker

1.4. 接口说明

请参看:《chainmaker-go-sdk》 所有go-sdk接口参考: https://git.chainmaker.org.cn/chainmaker/sdk-go/-/blob/v2.3.8/sdk_interface.go