From 78c4a2372de570a48753ca30d8f6ea5eeca33aa7 Mon Sep 17 00:00:00 2001 From: Kyon <32325790+kyonRay@users.noreply.github.com> Date: Thu, 29 Feb 2024 18:10:44 +0800 Subject: [PATCH] (sdk): add assemble transaction service doc. (#1783) --- .../docs/sdk/java_sdk/assemble_service.md | 182 ++++++++++++++++ .../docs/sdk/java_sdk/assemble_transaction.md | 7 +- 3.x/zh_CN/docs/sdk/java_sdk/index.md | 2 + .../sdk/java_sdk/transaction_data_struct.md | 205 ++++++++++++++++++ 4 files changed, 395 insertions(+), 1 deletion(-) create mode 100644 3.x/zh_CN/docs/sdk/java_sdk/assemble_service.md create mode 100644 3.x/zh_CN/docs/sdk/java_sdk/transaction_data_struct.md diff --git a/3.x/zh_CN/docs/sdk/java_sdk/assemble_service.md b/3.x/zh_CN/docs/sdk/java_sdk/assemble_service.md new file mode 100644 index 000000000..8d3b9290c --- /dev/null +++ b/3.x/zh_CN/docs/sdk/java_sdk/assemble_service.md @@ -0,0 +1,182 @@ +# (New)构造新版本交易 + +标签:``java-sdk`` ``发送交易`` ``使用接口签名发送交易`` ``组装交易`` ``合约调用`` ``v1`` + +---- + +```eval_rst +.. important:: + FISCO BCOS 在3.6.0版本以后开始支持V1版本的交易,在3.7.0版本以后开始支持V2交易,在使用之前请确认发送的节点版本。3.6.0版本特性请参考:`v3.6.0 <../introduction/change_log/3_6_0.html>`_ +``` + +```eval_rst +.. note:: + 交易的数据结构以及拼装方式可以参考 `这里 <./transaction_data_struct.html>`_ +``` + +FISCO BCOS 在3.6.0版本以后开始支持V1版本的交易,在3.7.0版本以后开始支持V2交易,新增以下五个字段: + +```c++ +string value; // v1交易新增字段,原生转账金额 +string gasPrice; // v1交易新增字段,执行时gas的单价(gas/wei) +long gasLimit; // v1交易新增字段,交易执行时gas使用的上限 +string maxFeePerGas; // v1交易新增字段,EIP1559预留字段 +string maxPriorityFeePerGas; // v1交易新增字段,EIP1559预留字段 +vector extension; // v2交易新增字段,用于额外存储 +``` + +为了解决未来可能的增加交易字段的需求,Java SDK支持全新的能够支持灵活拼装的交易服务,方便用户开发者灵活使用。 + +## 1. TransactionManager + +灵感来自Web3J,抽象了发送交易/调用请求的接口,提供GasProvider以及NonceAndBlockLimitProvider的注入接口,供用户自定义交易。在TransactionManager中传入的data均为ABI编码过后的字节数组。 + +TransactionManager是一个抽象类,有以下实现: + +- `DefaultTransactionManager` :默认的TransactionManager,在签名交易时使用Client初始化时生成的密钥。 +- `ProxySignTransactionManager` :将签名外置的TransactionManager,用户可以自行实现`AsyncTransactionSignercInterface`的接口,设置进ProxySignTransactionManager对象,在签名时均使用所实现的AsyncTransactionSignercInterface对象进行签名。 + +### 1.1 接口列表 + +TableManager的抽象接口如下所示: + +```java +// 注入Gas Provider +public abstract void setGasProvider(ContractGasProvider gasProvider); +// 注入Nonce 和 BlockLimit Provider +public abstract void setNonceProvider(NonceAndBlockLimitProvidernonceProvider nonceProvider); +// 发送已经ABI编码调用参数过后的交易 +public abstract TransactionReceipt sendTransaction(AbiEncodedRequestrequest request) throws JniException; +// 异步发送已经ABI编码调用参数过后的交易 +public abstract String asyncSendTransaction(AbiEncodedRequest request, TransactionCallback callback) throws JniException; +// 构造已经ABI编码调用参数过后的交易,并进行签名,返回raw transaction +public abstract TxPair createSignedTransaction(AbiEncodedRequestrequest request) throws JniException; +// 发起合约调用 +public abstract Call sendCall(String to, byte[] data, Stringsignature sign); +// 异步发起合约调用 +public abstract void asyncSendCall(String to, byte[] data, RespCallback callback); +``` + +### 1.2 DefaultTransactionManager + +- DefaultTransactionManager是默认的TransactionManager,在签名交易时使用Client初始化时生成的密钥。 +- 使用默认的ContractGasProvider,默认返回的gaslimit为9000000,gas price为4100000000 +- 使用默认的NonceAndBlockLimitProvider,默认返回的block limit为Client接口getBlockLimit返回的值,默认返回的nonce为UUID。 + +### 1.3 ProxySignTransactionManager + +- 将签名外置的TransactionManager,用户可以自行实现`AsyncTransactionSignercInterface`的接口,设置进ProxySignTransactionManager对象,在签名时均使用所实现的AsyncTransactionSignercInterface对象进行签名。 +- 使用默认的ContractGasProvider,默认返回的gaslimit为9000000,gas price为4100000000 +- 使用默认的NonceAndBlockLimitProvider,默认返回的block limit为Client接口getBlockLimit返回的值,默认返回的nonce为UUID。 +- 使用默认的AsyncTransactionSignercInterface:TransactionJniSignerService,默认仍然还是使用Client初始化时生成的密钥。 + +用户可以调用`setAsyncTransactionSigner`接口替换自己实现AsyncTransactionSignercInterface接口的对象。 + +### 1.4 使用示例 + +```java +// 初始化SDK和Client +BcosSDK sdk = BcosSDK.build(CONFIG_FILE); +Client client = sdk.getClient("group0"); + +// 初始化ProxySignTransactionManager +ProxySignTransactionManager proxySignTransactionManager = new ProxySignTransactionManager(client); +// ProxySignTransactionManager可以接受AsyncTransactionSignercInterface的实现作为构造函数参数 +proxySignTransactionManager = new ProxySignTransactionManager(client, (hash, transactionSignCallback) -> { + SignatureResult sign = client.getCryptoSuite().sign(hash, client.getCryptoSuite().getCryptoKeyPair()); + transactionSignCallback.handleSignedTransaction(sign); +}); +// ProxySignTransactionManager.setAsyncTransactionSigner接口可以修改AsyncTransactionSignercInterface的实现 +proxySignTransactionManager.setAsyncTransactionSigner((hash, transactionSignCallback) -> { + SignatureResult sign = client.getCryptoSuite().sign(hash, client.getCryptoSuite().getCryptoKeyPair()); + transactionSignCallback.handleSignedTransaction(sign); +}); + +// 对合约参数进行编解码 +byte[] abiEncoded = contractCodec.encodeMethod(abi, method, params); + +// 采用链式构造AbiEncodedRequest,传入contractAddress、nonce、blockLimit等重要参数,最后使用buildAbiEncodedRequest结束构造。 +AbiEncodedRequest request = + new TransactionRequestBuilder() + .setTo(contractAddress) + .setAbi(abi) + .setNonce(nonce) + .setBlockLimit(blockLimit) + .setExtension("Hello World".getBytes()) + .setGasPrice(BigInteger.TEN) + .buildAbiEncodedRequest(abiEncoded); + +// 同步发送上链,获得回执 +TransactionReceipt receipt = proxySignTransactionManager.sendTransaction(request); +``` + +## 2. AssembleTransactionService + +AssembleTransactionService集成了TransactionManager、ContractCodec以及TransactionDecoderService,用户只需要传入调用合约的参数,返回的结果就包含已经解析完毕的合约返回值。 + +AssembleTransactionService可以切换依赖的TransactionManager,默认为DefaultTransactionManager,可切换为ProxySignTransactionManager。 + +### 2.1 接口列表 + +```java +// 设置TransactionManager +public void setTransactionManager(TransactionManager transactionManager); +// 发送调用合约交易 +public TransactionResponse sendTransaction(BasicRequest request); +// 发送部署合约交易 +public TransactionResponse deployContract(BasicDeployRequest request); +// 异步发送调用合约交易 +public String asyncSendTransaction(BasicRequest request, TransactionCallback callback); +// 异步发送部署合约交易 +public String asyncDeployContract(BasicDeployRequest request, TransactionCallback callback); +// 发送查询合约请求 +public CallResponse sendCall(BasicRequest request); +// 异步发送查询合约请求 +public void asyncSendCall(BasicRequest request, RespCallback callback); +``` + +### 2.2 使用示例 + +```java +// 初始化SDK和Client +BcosSDK sdk = BcosSDK.build(CONFIG_FILE); +Client client = sdk.getClient("group0"); + +// 初始化AssembleTransactionService +AssembleTransactionService transactionService = new AssembleTransactionService(client); +// 初始化ProxySignTransactionManager +ProxySignTransactionManager proxySignTransactionManager = new ProxySignTransactionManager(client, (hash, transactionSignCallback) -> { + SignatureResult sign = client.getCryptoSuite().sign(hash, client.getCryptoSuite().getCryptoKeyPair()); + transactionSignCallback.handleSignedTransaction(sign); +}); +// 手动切换TransactionManager +transactionService.setTransactionManager(proxySignTransactionManager); +// 构造HelloWorld set参数 +List params = new ArrayList<>(); +params.add("Hello AssembleTransactionService"); +// 构造调用HelloWorld set的请求,传入contractAddress、nonce、blockLimit等重要参数,最后使用buildRequest结束构造。 +TransactionRequest request = + new TransactionRequestBuilder(abi, "set", contractAddress) + .setNonce(nonce) + .setBlockLimit(blockLimit) + .setExtension("HelloWorld".getBytes()) + .setGasPrice(BigInteger.TEN) + .buildRequest(params); + +// 同步发送上链,获得返回 +TransactionResponse transactionResponse = transactionService.sendTransaction(request); + +// 也可以构造使用String类型的参数 +List params = new ArrayList<>(); +params.add("[[\"0xabcd\"],[\"0x1234\"]]"); +// 构造调用合约setBytesArrayArray接口的请求,参数为bytes二维数组,传入contractAddress、nonce、blockLimit等重要参数,最后使用buildStringParamsRequest结束构造。 +TransactionRequestWithStringParams requestWithStringParams = + new TransactionRequestBuilder(abi, "setBytesArrayArray", contractAddress) + .setNonce(nonce) + .setBlockLimit(blockLimit) + .setExtension("HelloWorld".getBytes()) + .setGasPrice(BigInteger.TEN) + .buildStringParamsRequest(params); +// 同步发送上链,获得返回 +TransactionResponse transactionResponse = transactionService.sendTransaction(requestWithStringParams); +``` diff --git a/3.x/zh_CN/docs/sdk/java_sdk/assemble_transaction.md b/3.x/zh_CN/docs/sdk/java_sdk/assemble_transaction.md index 44524a394..8273bf8a1 100644 --- a/3.x/zh_CN/docs/sdk/java_sdk/assemble_transaction.md +++ b/3.x/zh_CN/docs/sdk/java_sdk/assemble_transaction.md @@ -9,6 +9,11 @@ Java SDK同时支持将 `solidity` 转换为 `java` 文件后,调用相应的 `java` 方法部署和调用合约,也支持构造交易的方式部署和调用合约,这里主要展示交易构造与发送,前者的使用方法请参考 `这里 <./contracts_to_java.html>`_ ``` +```eval_rst +.. note:: + 交易的数据结构可以参考 `这里 <./transaction_data_struct.html>`_ +``` + ## 1. 概念解析:合约的部署与调用 合约的操作可分为合约部署和合约调用两大类。其中合约调用又可以被区分为『交易』和『查询』。 @@ -290,7 +295,7 @@ future.exceptionally( }); ``` -## 4. 详细API功能介绍 +## 11. 详细API功能介绍 `AssembleTransactionProcessor`支持自定义参数发送交易,支持异步的方式来发送交易,也支持返回多种封装方式的结果。 diff --git a/3.x/zh_CN/docs/sdk/java_sdk/index.md b/3.x/zh_CN/docs/sdk/java_sdk/index.md index efcc2d4bc..d326abde2 100644 --- a/3.x/zh_CN/docs/sdk/java_sdk/index.md +++ b/3.x/zh_CN/docs/sdk/java_sdk/index.md @@ -20,6 +20,8 @@ Java SDK 提供了访问 FISCO BCOS 节点的Java API,支持节点状态查询 contracts_to_java.md assemble_transaction.md remote_sign_assemble_transaction.md + assemble_service.md + transaction_data_struct.md rpc_api.md precompiled_service_api.md transaction_decode.md diff --git a/3.x/zh_CN/docs/sdk/java_sdk/transaction_data_struct.md b/3.x/zh_CN/docs/sdk/java_sdk/transaction_data_struct.md new file mode 100644 index 000000000..e344f9326 --- /dev/null +++ b/3.x/zh_CN/docs/sdk/java_sdk/transaction_data_struct.md @@ -0,0 +1,205 @@ +# 交易与回执数据结构与组装过程 + +标签:``java-sdk`` ``组装交易`` ``数据结构`` ``交易`` ``交易回执`` + +--- + +## 1. 交易数据结构解释 + +3.0的交易定义于FISCO-BCOS仓库中 `bcos-tars-protocol/bcos-tars-protocol/tars/Transaction.tars` 中定义,可见链接:[Transaction.tars](https://github.com/FISCO-BCOS/FISCO-BCOS/blob/master/bcos-tars-protocol/bcos-tars-protocol/tars/Transaction.tars)。数据结构如下所示: + +```c++ +module bcostars { + struct TransactionData { + 1 optional int version; // 交易版本号,目前已有v0,v1,v2三种交易 + 2 optional string chainID; // 链名 + 3 optional string groupID; // 群组名 + 4 optional long blockLimit; // 交易限定执行的区块高度 + 5 optional string nonce; // 交易唯一性标识 + 6 optional string to; // 交易调用的合约地址 + 7 optional vector input; // 交易调用合约的参数,经过ABI/Scale编码 + 8 optional string abi; // ABI的JSON字符串,建议在部署合约时带上ABI + 9 optional string value; // v1交易新增字段,原生转账金额 + 10 optional string gasPrice; // v1交易新增字段,执行时gas的单价(gas/wei) + 11 optional long gasLimit; // v1交易新增字段,交易执行时gas使用的上限 + 12 optional string maxFeePerGas; // v1交易新增字段,EIP1559预留字段 + 13 optional string maxPriorityFeePerGas; // v1交易新增字段,EIP1559预留字段 + 14 optional vector extension; // v2交易新增字段,用于额外存储 + }; + + struct Transaction { + 1 optional TransactionData data; // 交易基础字段 + 2 optional vector dataHash; // 交易基础字段data的哈希值 + 3 optional vector signature; // 对交易基础字段data的哈希值字节的签名 + 4 optional long importTime; // 交易到达交易池的时间 + 5 optional int attribute; // 交易属性,EVM交易为1,默认可填写0;WASM交易为2;WASM部署交易为2 || 8; + // 6 optional string source; + 7 optional vector sender; // 交易发起的EOA地址 + 8 optional string extraData; // 交易额外字段,该字段不计算哈希 + }; +}; +``` + +## 2. 交易回执数据结构解释 + +3.0的交易回执定义于FISCO-BCOS仓库中 `bcos-tars-protocol/bcos-tars-protocol/tars/TransactionReceipt.tars` 中定义,可见链接:[TransactionReceipt.tars](https://github.com/FISCO-BCOS/FISCO-BCOS/blob/master/bcos-tars-protocol/bcos-tars-protocol/tars/TransactionReceipt.tars)。数据结构如下所示: + +```c++ +module bcostars { + struct LogEntry { // 事件结构体 + 1 optional string address; // 事件合约地址 + 2 optional vector> topic; // 事件的indexed字段的topic,最多为4 + 3 optional vector data; // 事件的indexed字段以外的数值,ABI编码 + }; + + struct TransactionReceiptData { // 交易回执基础类型 + 1 require int version; // 交易回执的版本,目前已有v0,v1,v2 + 2 optional string gasUsed; // 交易使用的gas + 3 optional string contractAddress; // 交易调用的合约地址,若为部署合约交易则为新的合约地址 + 4 optional int status; // 交易执行状态,为0则成功;非0为不成功,将在output写入错误信息(Error(string) ABI编码后的值) + 5 optional vector output; // 交易执行返回值 + 6 optional vector logEntries; // 事件列表 + 7 optional long blockNumber;// 交易执行所在的块高 + 8 optional string effectiveGasPrice; // v1版本新增字段,交易执行时生效的gas单价(gas/wei) + }; + + struct TransactionReceipt { // 交易回执类型 + 1 optional TransactionReceiptData data; // 交易回执基础类型 + 2 optional vector dataHash; // 交易回执基础类型data的哈希值 + 3 optional string message; // 交易回执返回信息 + }; +}; +``` + +## 3. 交易的组装过程 + +由上所示,SDK需要先组装好 `TransactionData`,再组装交易数据结构为 `Transaction`,最后发到区块链节点。具体步骤如下: + +- 交易调用合约的实际参数,使用ABI/Scale编码后作为 `input` 字段; +- 传入`blockLimit`字段,一般为当前块高+600; +- 传入`nonce`字段,一般为随机16进制字符串; +- 传入其他参数,构造出 `TransactionData` 结构体对象; +- 对`TransactionData`的对象进行哈希计算,哈希计算算法可见第4小节; +- 使用密钥对上一步骤计算出的哈希值(字节数组)进行签名计算,得出签名; +- 传入其他参数,构造出 `Transaction` 结构体对象; +- 使用`Tars`编码对 `Transaction` 结构体对象进行编码; +- 得到最终的交易Raw data,发送到链上。 + +## 4. TransactionData哈希计算算法与示例 + +TransactionData在进行哈希计算时,是对该对象内的所有字段的字节进行拼装,最后对字节数组进行哈希计算。C++实现的示例如下: + +```c++ +int32_t version = boost::endian::native_to_big((int32_t)hashFields.version); +hasher.update(version); +hasher.update(hashFields.chainID); +hasher.update(hashFields.groupID); +int64_t blockLimit = boost::endian::native_to_big((int64_t)hashFields.blockLimit); +hasher.update(blockLimit); +hasher.update(hashFields.nonce); +hasher.update(hashFields.to); +hasher.update(hashFields.input); +hasher.update(hashFields.abi); +// if version == 1, update value, gasPrice, gasLimit, maxFeePerGas, maxPriorityFeePerGas to +// hashBuffer calculate hash +if ((uint32_t)hashFields.version >= (uint32_t)bcos::protocol::TransactionVersion::V1_VERSION) +{ + hasher.update(hashFields.value); + hasher.update(hashFields.gasPrice); + int64_t bigEndGasLimit = boost::endian::native_to_big((int64_t)hashFields.gasLimit); + hasher.update(bigEndGasLimit); + hasher.update(hashFields.maxFeePerGas); + hasher.update(hashFields.maxPriorityFeePerGas); +} +if ((uint32_t)hashFields.version >= (uint32_t)bcos::protocol::TransactionVersion::V2_VERSION) +{ + hasher.update(hashFields.extension); +} +hasher.final(out); +``` + +Java实现的示例如下: + +```java +ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); +// version +byteArrayOutputStream.write(toBytesPadded(BigInteger.valueOf(getVersion()), 4)); +// chainId +byteArrayOutputStream.write(getChainID().getBytes()); +// groupId +byteArrayOutputStream.write(getGroupID().getBytes()); +// blockLimit +byteArrayOutputStream.write(toBytesPadded(BigInteger.valueOf(getBlockLimit()), 8)); +// nonce +byteArrayOutputStream.write(Hex.decode(getNonce())); +// to +byteArrayOutputStream.write(getTo().getBytes()); +// input +byteArrayOutputStream.write(Hex.decode(getInput())); +// abi +byteArrayOutputStream.write(getAbi().getBytes()); +if (getVersion() == TransactionVersion.V1.getValue()) { + byteArrayOutputStream.write(getValue().getBytes()); + byteArrayOutputStream.write(getGasPrice().getBytes()); + byteArrayOutputStream.write(toBytesPadded(BigInteger.valueOf(getGasLimit()), 8)); + byteArrayOutputStream.write(getMaxFeePerGas().getBytes()); + byteArrayOutputStream.write(getMaxPriorityFeePerGas().getBytes()); +} +if (getVersion() == TransactionVersion.V2.getValue()) { + byteArrayOutputStream.write(getExtension()); +} +return byteArrayOutputStream.toByteArray(); +``` + +## 5. TransactionReceiptData哈希计算算法与示例 + +如同第4节所述,TransactionReceiptData的哈希计算方法也是对该对象内的所有字段的字节进行拼装,最后对字节数组进行哈希计算。C++实现的示例如下: + +```c++ +int32_t version = boost::endian::native_to_big((int32_t)hashFields.version); +hasher.update(version); +hasher.update(hashFields.gasUsed); +hasher.update(hashFields.contractAddress); +int32_t status = boost::endian::native_to_big((int32_t)hashFields.status); +hasher.update(status); +hasher.update(hashFields.output); +if(hashFields.version >= int32_t(bcos::protocol::TransactionVersion::V1_VERSION)) +{ + hasher.update(hashFields.effectiveGasPrice); +} +for (auto const& log : hashFields.logEntries) +{ + hasher.update(log.address); + for (auto const& topicItem : log.topic) + { + hasher.update(topicItem); + } + hasher.update(log.data); +} +int64_t blockNumber = boost::endian::native_to_big((int64_t)hashFields.blockNumber); +hasher.update(blockNumber); +hasher.final(out); +``` + +Java实现的示例如下: + +```java +ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); +byteArrayOutputStream.write(toBytesPadded(BigInteger.valueOf(getVersion()), 4)); +byteArrayOutputStream.write(getGasUsed().getBytes()); +byteArrayOutputStream.write(getContractAddress().getBytes()); +byteArrayOutputStream.write(toBytesPadded(BigInteger.valueOf(getStatus()), 4)); +byteArrayOutputStream.write(getOutput().getBytes()); +if (getVersion() == TransactionVersion.V1.getValue()) { + byteArrayOutputStream.write(getEffectiveGasPrice().getBytes()); +} +for (Logs logEntry : getLogEntries()) { + byteArrayOutputStream.write(logEntry.getAddress().getBytes()); + for (String topic : logEntry.getTopics()) { + byteArrayOutputStream.write(topic.getBytes()); + } + byteArrayOutputStream.write(logEntry.getData().getBytes()); +} +byteArrayOutputStream.write(toBytesPadded(getBlockNumber(), 8)); +return byteArrayOutputStream.toByteArray(); +```