# Go SDK 使用说明 本篇介绍: 1、环境依赖 2、sdk依赖使用 3、普通合约安装、调用 4、EVM合约安装、调用 5、交易序号(sequence) 6、更多的示例及全部接口 ## 长安链SDK概述 1. 整体介绍 长安链`SDK`是业务模块与长安链交互的桥梁,支持双向`TLS`认证,提供安全可靠的加密通信信道。 长安链提供了多种语言的`SDK`,包括:`Go SDK`、`Java SDK`、`Python SDK`、`Nodejs SDK`方便开发者根据需要进行选用。 提供的`SDK`接口,覆盖合约管理、链配置管理、证书管理、多签收集、各类查询操作、事件订阅等场景,满足了不同的业务场景需要。 2. 名词概念说明 - **`Node`(节点)**:代表一个链节点的基本信息,包括:节点地址、连接数、是否启用`TLS`认证等信息 - **`ChainClient`(链客户端)**:所有客户端对链节点的操作接口都来自`ChainClient` - **压缩证书**:可以为`ChainClient`开启证书压缩功能,开启后可以减小交易包大小,提升处理性能 ## 环境准备 ### 软件环境依赖 **golang** : 版本为1.16或以上 下载地址: 若已安装,请通过命令查看版本: ```bash $ go version go version go1.16 linux/amd64 ``` ### 下载安装sdk 进入您的Go项目,执行以下命令添加对sdk的引用: ```bash go get chainmaker.org/chainmaker/sdk-go/v2@v2.3.8 ``` ### 长安链环境准备 创建一条证书模式的长安链,并确保相关节点网络通畅,相关教程见:[《通过命令行体验链》](../quickstart/通过命令行体验链.md) ## 怎么使用SDK ### 示例代码 #### 创建节点 设置节点信息,可用作创建与该节点连接的客户端 ```go // 创建节点 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 } ``` #### 以参数形式创建ChainClient > 注:示例中证书采用路径方式去设置,也可以使用证书内容去设置,调用`WithUserKeyBytes, WithUserCrtBytes`等方法,具体请参看:`sdk_config.go` ```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 } ``` #### 以配置文件形式创建ChainClient > 注:参数形式和配置文件形式两个可以同时使用,同时配置时,以参数传入为准 ```go 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 } ``` ### 使用加密私钥 SDK 支持使用密码加密的私钥文件,避免私钥明文存储在磁盘上。适用于对私钥安全性有较高要求的生产环境。 #### 生成加密私钥 使用 OpenSSL 对已有明文私钥进行 AES-256-CBC 加密: ```bash # 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, -----END EC PRIVATE KEY----- ``` 或国密 SM2 格式: ``` -----BEGIN SM2 PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: AES-256-CBC, -----END SM2 PRIVATE KEY----- ``` #### 配置使用 在 `sdk_config.yml` 中将私钥路径指向加密文件,并设置对应的密码字段。 涉及的密码字段: | YAML 配置项 | Option 函数 | 适用场景 | |---|---|---| | `user_sign_key_pwd` | `WithUserSignKeyPwd` | 交易签名私钥密码(PWK/Cert 模式通用) | | `user_key_pwd` | `WithUserKeyPwd` | TLS 通信私钥密码 | | `user_enc_key_pwd` | `WithUserEncKeyPwd` | 国密双证书加密私钥密码(GMTLS 场景) | #### 编码方式 ```go chainClient, err := NewChainClient( WithUserSignKeyFilePath("./crypto-config/.../client1.sign.key.enc"), WithUserSignKeyPwd("your_password"), // ...其他配置 ) ``` > **说明:** > - 如果私钥未加密(明文 PEM),无需设置密码字段 > - 设置了多余的密码(对非加密私钥设置密码)不会报错,SDK 有容错处理 > - 密码错误时 SDK 会返回明确的错误提示 #### 部署wasm合约 下文,将演示通过sdk部署wasm合约, > `sdk_user_contract_claim_test.go` ```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 ``` #### 调用wasm合约 下文,将演示通过sdk调用wasm合约, > `sdk_user_contract_claim_test.go` ```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 } ``` #### 创建及调用evm合约 > `sdk-go/examples/user_contract_evm_balance/main.go`() ### 交易序号(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` 不会用于有序交易控制。 #### 查询账户已确认交易序号 推荐使用 `GetCurrentAccountSequence` 查询当前 SDK 客户端账户的 `confirmedSeq`,SDK 会根据认证模式和链配置中的地址类型自动推导账户地址: ```go confirmedSeq, err := client.GetCurrentAccountSequence() if err != nil { return err } nextSeq := confirmedSeq + 1 ``` 如果需要查询指定账户,可以使用 `GetAccountSequence`。参数 `address` 为链上账户地址,可以是公钥地址或证书别名: ```go confirmedSeq, err := client.GetAccountSequence(address) if err != nil { return err } nextSeq := confirmedSeq + 1 ``` #### 构造并发送带交易序号的交易 SDK 中大多数便捷的交易构造方法默认将 `Payload.Sequence` 设置为 `0`。需要使用交易序号时,可以通过 `CreatePayload` 直接传入 `nextSeq`,然后完成签名和发送: ```go 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`,再在签名之前设置序号: ```go payload.Sequence = confirmedSeq + 1 ``` > `payload` 签名后不能再修改交易序号,否则签名将失效。交易发送失败时,应根据链上实际确认结果重新查询 `confirmedSeq`,不要直接重复使用旧序号。 #### 开启或关闭交易序号校验 链管理员可以使用 `CreateChainConfigBlockUpdateWithEnableTxSeqPayload` 生成包含交易序号开关的链配置更新 `payload`: ```go 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. 该功能从 Go SDK `v2.3.9` 开始支持,使用时请确认 SDK 版本和链配置均满足要求。 2. `confirmedSeq` 只表示链上已经确认的序号,不包含交易池中尚未确认的交易。 3. 同一账户并发提交多笔交易时,业务侧需要负责序号分配,避免多个交易使用同一个 `sequence`。 4. 交易发送结果不确定时,应先查询账户的最新 `confirmedSeq`,再决定是否重试,避免重复使用旧序号。 5. `GetCurrentAccountSequence` 会先读取链配置以确定地址类型,查询指定账户时应确保传入地址与链的地址类型一致。 完整示例请参见 [`sdk-go/examples/account_sequence/main.go`](https://git.chainmaker.org.cn/chainmaker/sdk-go/-/blob/master/examples/account_sequence/main.go)。 ### 更多示例和用法 > 更多示例和用法,请参看单元测试用例
示例: () | 功能 | 单测代码 | | -------- | ----------------------------- | | 用户合约 | `sdk_user_contract_test.go` | | 系统合约 | `sdk_system_contract_test.go` | | 链配置 | `sdk_chain_config_test.go` | | 证书管理 | `sdk_cert_manage_test.go` | | 消息订阅 | `sdk_subscribe_test.go` | ### demo sdk-go demo参考: [文件 · v2.3.8 · chainmaker / sdk-go-demo · ChainMaker](https://git.chainmaker.org.cn/chainmaker/sdk-go-demo/-/tree/v2.3.8) ## 接口说明 请参看:[《chainmaker-go-sdk》](https://git.chainmaker.org.cn/chainmaker/sdk-go/-/blob/v2.3.8/sdk_interface.md) 所有go-sdk接口参考: https://git.chainmaker.org.cn/chainmaker/sdk-go/-/blob/v2.3.8/sdk_interface.go