区块链教程 | 使用WeBASE进行“两阶段交易”

作者:黎宁

作为一个友好的、功能丰富的区块链中间件平台, WeBASE致力于提高区块链开发者的运维与管理效率。在新近发布的 WeBASE v1.5.2 中,一大优化是提供了获取交易编码的接口,更方便用户使用”两阶段交易”。

“两阶段交易”是什么?“两阶段交易”是指分成两个步骤发送交易,即对交易编码并签名、将交易提交到链上这两个阶段:

  • 第一阶段:构造并获取交易编码值,并通过私钥对交易编码值签名;
  • 第二阶段:发送交易,也就是将已签名的编码值发送到链上。

在WeBASE v1.5.2中,我们在WeBASE-Front节点前置服务中增加了获取交易编码值的功能。该接口可以返回未签名的交易编码值,也可以返回通过WeBASE-Front本地私钥或WeBASE-Sign私钥签名后的交易编码值。获得已签名的编码值后,用户直接调用前置服务的提交交易接口即可完成“两阶段交易”。

以下演示,我们通过WeBASE-Front节点前置服务接口获取交易编码值,并通过FISCO-BCOS Java-SDK对编码值进行签名,最后通过接口提交交易来加深对“两阶段交易”的了解。

|前期准备

部署HelloWorld合约

在发起交易之前,首先要确保在链上部署一个可调用的合约。这里以WeBASE-Front “合约仓库-工具合约”中的 “HelloWorld” 合约为例,部署一份 HelloWorld 合约。 我们在 WeBASE-Front 的合约IDE中编译一份 HelloWorld 合约并完成部署操作,如下图所示:

../../../_images/ide.png

获得合约地址、合约ABI等信息后,我们根据 WeBASE-Front 的接口文档指引,调用获取交易编码接口。

查看接口文档

两阶段交易中,第一步交易编码并签名可以通过 WeBASE-Front 的 /trans/convertRawTxStr/withSign 接口构造一个已签名的交易体,接口文档简介如下:

../../../_images/with-sign-api.png

值得一提的是,调用 /trans/convertRawTxStr/withSign 接口时:

  • 如果传入了 signUserId 非空,则返回的交易体编码值是通过signUserId对应私钥签名后的交易体编码值。
  • 如果传入的 signUserId 为空,则返回的是未签名的交易体编码值,开发者也可以通过JAVA-SDK用私钥对该值签名。

获取已签名的交易编码值后,就可以进行第二步的提交交易操作了。

../../../_images/signed-tx-api.png

在 WeBASE-Front 中,我们可以通过 /trans/signed-transaction 接口,将已签名的交易体编码值,完成交易上链并获得交易回执。

上述各个接口的调用方法都可以在 WeBASE-Front 的接口文档中找到(https://webasedoc.readthedocs.io/zh\_CN/latest/docs/WeBASE-Front/interface.html)。

|结合WeBASE-Front接口进行“两阶段交易”

获取交易编码值

下面以 WeBASE-Sign 签名的获取交易编码接口( /trans/convertRawTxStr/withSign )为例,获取未签名的交易编码值。

我们可以访问 WeBASE-Front 的 Swagger 进行接口调用(如,http://localhost:5002/WeBASE-Front/swagger-ui.html),找到Swagger接口列表中的”transaction interface”交易接口一栏,点开 /trans/convertRawTxStr/withSign 即可。

../../../_images/swagger2.png

在文章开头我们提到,“两阶段交易”的第一阶段是交易编码并通过私钥对编码值签名。

因此,我们调用接口时传入的 “signUserId” 为空字符串,接口将返回未签名的交易编码值,稍后我们再通过 Java-SDK 手动对编码值签名。在调用 /trans/convertRawTxStr/local 接口时同理,user地址字段为空字符串时也会返回未签名的交易编码值。

我们以调用HelloWorld合约的 “set” 方法为例,按接口文档填入对应参数。

首先,点开Swagger中的 /trans/convertRawTxStr/withSign 接口,再填入参数包括合约ABI、合约地址、函数名及函数入参、群组ID和WeBASE-Sign的私钥用户ID signUserId,点击”Try it out”输入参数,删除不必要的字段。注意,其中signUserId为空字符串。

../../../_images/swagger-empty-id.png

点击”Execute”即可发起调用,获得未签名的交易编码值。接口返回值为:

../../../_images/unsigned-code.png

拿到未签名的交易编码值之后,我们接下来通过 Java-SDK 对编码值进行签名。

对交易编码值签名

下面我们使用 FISCO-BCOS Java-SDK 加载私钥,对上文获取的未签名交易编码值进行签名操作,并根据 RawTransaction 交易体再次编码,得到最终签名后的交易编码值。

  1. public void testSign(TransactionEncoderService encoderService, RawTransaction rawTransaction) {
  2. // 未签名的交易编码值
  3. String encodedTransaction = "0xf8a9a001b41b2cc71fe0bf0450f1fa4d820209b6686a8f226d217be0bc51cd9fc4a020018405f5e100820204941f2dfecfd75b883b51762aef6326d3ae9ad5230180b8644ed3885e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000033132330000000000000000000000000000000000000000000000000000000000010180";
  4. // 私钥
  5. String privateKey = "0x123";
  6. // ECDSA 加密套件
  7. CryptoSuite cryptoSuite = new CryptoSuite(CryptoType.ECDSA_TYPE);
  8. // 对待签名的编码值作哈运算
  9. String hashMessageStr = cryptoSuite.hash(encodedTransaction);
  10. System.out.println("hashMessageStr: " + hashMessageStr);
  11. // 创建私钥对
  12. CryptoKeyPair myKeyPair = cryptoSuite.createKeyPair(privateKey);
  13. // 对交易编码值签名
  14. SignatureResult signedTx = cryptoSuite.sign(hashMessageStr, myKeyPair);
  15. // 获得最终签名后的交易编码值
  16. byte[] signedTransaction = encoderService.encode(rawTransaction, signedTx);
  17. // 转十六进制字符串
  18. String signedTransactionStr = Numeric.toHexString(signedTransaction);
  19. System.out.println("signedTransactionStr: " + signedTransactionStr);
  20. }

提交交易

有了已签名的交易编码值后,我们可以调用 /trans/signed-transaction 接口,将该交易发到链上,获得交易回执。这里我们继续使用 Swagger 调用该接口。

../../../_images/submit-signed.png

提交请求后,接口返回了交易的回执。可以根据交易回执判断交易是否执行成功。

../../../_images/receipt.png

当看到返回的交易回执中显示 status 为 0x0,也就意味着交易执行成功了。

|交易编码接口源码解析

WeBASE-Front 源码中,位于 transaction 包里的 TransService 包含了对交易编码并签名的具体代码。

获取交易编码值

我们找到 createRawTxEncoded() 方法,该方法通过合约函数的ABI,合约函数的函数名 funcName 和合约函数入参 funcParam 等参数构造了 Function 实例,并通过FunctionEncoder 将 Function 实例进行编码得到字符串 encodedFunction(代码中的 cryptoSuite 是国密或非国密的加密套件,可用于计算哈希、创建私钥对、签名等)。

  1. // 构造Function实例
  2. Function function = new Function(funcName, contractFunction.getFinalInputs(),
  3. contractFunction.getFinalOutputs());
  4. // 编码Function
  5. FunctionEncoder functionEncoder = new FunctionEncoder(cryptoSuite);
  6. String encodedFunction = functionEncoder.encode(function);

下面使用 convertRawTx2Str() 方法,该方法主要负责构造 RawTransaction 交易体。

构造 RawTransaction 需要传入一个随机数和从节点获取当前的 BlockLimit 值(避免重复提交交易)、合约地址和上文获得的 encodedFunction 等参数。

  1. // 构造交易体
  2. BigInteger randomId = new BigInteger(250, new SecureRandom());
  3. BigInteger blockLimit = web3j.getBlockLimit();
  4. RawTransaction rawTransaction =
  5. RawTransaction.createTransaction(randomId, Constants.GAS_PRICE,
  6. Constants.GAS_LIMIT, blockLimit, contractAddress, BigInteger.ZERO, encodedFunction,
  7. new BigInteger(Constants.chainId), BigInteger.valueOf(groupId), "");
  8. // 编码交易体RawTransaction
  9. TransactionEncoderService encoderService = new TransactionEncoderService(cryptoSuite);
  10. byte[] encodedTransaction = encoderService.encode(rawTransaction, null);

对交易编码值签名

对交易编码值签名前,WeBASE-Front 中会根据传入的 user 字段和 isLocal 字段判断:

  • 如果 user 字段为空,则将 encodedTransaction 转为十六进制后返回。该值就是第一阶段未签名的交易编码值。
  • 如果 user 字段非空, isLocal 字段为 true,则 user 为 WeBASE-Front 本地的用户私钥,通过本地私钥对交易编码值 encodedTransaction 签名。注意,签名前还需对 encodedTransaction 进行一次哈希运算后再签名。
  • 如果 user 字段非空, isLocal 字段为 false,则 user 为 WeBASE-Sign 托管私钥的signUserId,通过签名服务对交易体编码值 encodedTransaction 签名。注意,此处签名前没有对 encodedTransaction 进行哈希,而是直接转为十六进制发到签名服务,签名服务拿到该值后再做哈希运算并签名返回结果。

下面展示的代码为 isLocal 字段为 false,user 字段非空,其值为 signUserId 的交易编码值签名逻辑。

我们将交易编码值 encodedTransaction 转十六进制后,传到签名服务进行签名,得到了 String 格式的签名结果 signDataStr ,将签名结果反序列化,得到了签名结果 SignatureResult。同时,通过 TransactionEncoderService 将签名结果和上文构造的 RawTransaction 实例进行编码,最终可得到十六进制的已签名的交易编码值 signResultStr 。

  1. // encodedTransaction转十六进制
  2. String hashMessageStr = Numeric.toHexString(encodedTransaction);
  3. // 通过WeBASE-Sign签名
  4. EncodeInfo encodeInfo = new EncodeInfo(user, hashMessageStr);
  5. String signDataStr = keyStoreService.getSignData(encodeInfo);
  6. // 反序列化签名结果
  7. SignatureResult signData = CommonUtils.stringToSignatureData(signDataStr, cryptoSuite.cryptoTypeConfig);
  8. // 加入签名结果,再次编码
  9. byte[] signedMessage = encoderService.encode(rawTransaction, userSignResult);
  10. // 转为十六进制
  11. String signResultStr = Numeric.toHexString(signedMessage);

至此,获取交易编码,对交易编码签名交易体,并对编码值签名的过程就完成了。

值得一提的是,提交交易后获得的交易哈希 TransHash 值是通过对签名交易体编码值进行哈希计算得到的,有了交易哈希,也可以在提交交易后,直接根据交易哈希到链上查询交易回执。

  1. // 通过CryptoSuite实例计算signResultStr的交易哈希值
  2. String transHash = cryptoSuite.hash(signResultStr);