# 有序交易 阅读该文档您可以了解到长安链 `v2.3.9` 引入的**有序交易**能力:它为每个账户维护一个交易序列号(`sequence`,简称 `seq`),使同一账户发出的交易可以按照指定顺序在链上执行,从而实现交易的严格排序与防重放。本文将介绍该功能的基本概念、启用条件、开启/关闭方式,以及如何通过 CMC 与 SDK 发送有序交易。 ## 功能简介 在默认情况下,长安链对同一账户的多笔交易并不保证执行顺序:交易被打包进区块的先后顺序,可能与业务发送顺序不一致。对于需要严格串行执行的业务(例如账户扣款、状态机流转等),这种乱序可能带来问题。 有序交易通过为每个账户引入**交易序列号**来解决该问题: - 链上为每个账户维护一个**已确认序列号**(`confirmedSeq`)。它随该账户成功上链的有序交易递增,初始值为 `0`。 - 每笔交易的负载中都有一个 `sequence` 字段: - `sequence = 0`:**无序交易**(默认值)。不参与序列号校验,与旧版本、旧业务完全兼容。 - `sequence != 0`:**有序交易**。该值必须恰好等于账户当前 `confirmedSeq + 1`,否则交易会被拒绝。 - 有序交易上链后,账户的 `confirmedSeq` 递增为该交易的 `sequence`,下一笔有序交易需使用 `confirmedSeq + 1`。 > **说明**:有序交易是**可选**能力,采用「显式选择加入(opt-in)」的方式。不设置 `sequence`(即保持为 `0`)的交易行为与旧版本一致,因此升级到 `v2.3.9` 后原有业务无需任何改动。 **适用场景** - 需要保证同一账户交易严格按序执行的业务。 - 需要防止交易被重复提交(重放)的场景:同一 `sequence` 只能成功上链一次。 ## 启用条件 有序交易的校验需要同时满足以下**两个条件**,缺一不可: 1. **区块版本 ≥ `v2.3.9`(`2030900`)**:这是协议层的门槛。低于该版本的历史区块保持原有兼容行为,不做序列号校验。 2. **链配置开关 `enable_tx_seq` 已开启**:在区块配置(`bc.yml` 的 `block` 节点)中开启。 只有两个条件都满足时,节点才会对区块内 `sequence != 0` 的交易执行序列号校验。若开关关闭,则所有交易均按无序交易处理。 ## 开启 / 关闭功能 `enable_tx_seq` 属于区块配置项,可在**创建链时**通过配置文件设置,也可在**运行时**通过链配置更新交易开启/关闭(需管理员签名)。 ### 配置文件参考 在各节点的 `bc.yml` 中,`block` 节点下新增 `enable_tx_seq`: ```yaml # Block config settings block: # 其他区块配置项 ... # 是否开启有序交易(交易序列号)校验,默认 false enable_tx_seq: true ``` ### 通过 CMC 开启 / 关闭 > **权限说明**:切换该开关走链配置更新(`BLOCK_UPDATE`)方法,默认权限为 `MAJORITY`(多数管理员),即需要**超过半数的组织管理员**联合签名背书才能生效。因此下方命令通过 `--admin-key-file-paths` / `--admin-crt-file-paths` 传入多个管理员的私钥与证书(PK 模式则传入 `--admin-key-file-paths` 与 `--admin-org-ids`)。 在链运行过程中,可通过 CMC 发送链配置更新交易来切换开关(需多数管理员联合签名): ```sh ./cmc client chainconfig block txseqenable \ --tx-seq-enable=true \ --sdk-conf-path=./testdata/sdk_config.yml \ --admin-key-file-paths=./testdata/crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.sign.key,./testdata/crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.sign.key,./testdata/crypto-config/wx-org3.chainmaker.org/user/admin1/admin1.sign.key \ --admin-crt-file-paths=./testdata/crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.sign.crt,./testdata/crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.sign.crt,./testdata/crypto-config/wx-org3.chainmaker.org/user/admin1/admin1.sign.crt ``` - `--tx-seq-enable=true`:开启有序交易;设置为 `false` 则关闭。该参数必填。 参考链接:[cmc 开启/关闭有序交易](../dev/命令行工具.html#chainConfig.enableTxSeq) ### 通过 SDK 开启 / 关闭 **Go SDK 方式** 调用链配置更新负载构造方法 `CreateChainConfigBlockUpdateWithEnableTxSeqPayload`,最后一个参数 `enableTxSeq` 即为开关值: ```go payload, err := cc.CreateChainConfigBlockUpdateWithEnableTxSeqPayload( txTimestampVerify, blockTimestampVerify, txTimeout, blockTimeout, blockTxCapacity, blockSize, blockInterval, txParameterSize, true, // enableTxSeq:true 开启,false 关闭 ) // 随后由多管理员对 payload 进行背书签名,并发送链配置更新交易 ``` **Java SDK 方式** 调用 `createPayloadOfChainConfigBlockUpdateWithEnableTxSeq`,倒数第二个参数 `enableTxSeq` 即为开关值: ```java Request.Payload payload = chainClient.createPayloadOfChainConfigBlockUpdateWithEnableTxSeq( txTimestampVerify, txTimeout, blockTxCapacity, blockSize, blockInterval, txParameterSize, true, // enableTxSeq:true 开启,false 关闭 rpcCallTimeout); // 随后由多管理员对 payload 进行背书签名,并发送链配置更新交易 ``` ## 发送有序交易 发送有序交易分两步:先**查询账户当前的 `confirmedSeq`**,再将新交易的 `sequence` 设置为 `confirmedSeq + 1` 后发送。 ### 查询账户序列号 **CMC 方式** ```sh # 查询指定账户的已确认序列号 ./cmc client contract user query-sequence \ --address=0x1234... \ --sdk-conf-path=./testdata/sdk_config.yml # 不指定 --address 时,查询当前 SDK 配置账户自身的序列号 ./cmc client contract user query-sequence \ --sdk-conf-path=./testdata/sdk_config.yml ``` 返回结果形如 `{"sequence": 5}`,其中 `5` 即该账户当前的 `confirmedSeq`。 参考链接:[cmc 查询账户序列号](../dev/命令行工具.html#userContract.querySequence) **Go SDK 方式** ```go // 方式一:查询指定地址账户的 confirmedSeq confirmedSeq, err := cc.GetAccountSequence(address) // 方式二:查询当前 SDK 客户端账户自身的 confirmedSeq(内部自动推导账户地址) confirmedSeq, err := cc.GetCurrentAccountSequence() ``` **Java SDK 方式** ```java // 方式一:查询指定地址账户的 confirmedSeq long confirmedSeq = chainClient.getAccountSequence(address, rpcCallTimeout); // 方式二:查询当前 SDK 客户端账户自身的 confirmedSeq(内部自动推导账户地址) long confirmedSeq = chainClient.getCurrentAccountSequence(rpcCallTimeout); ``` ### 发送有序交易 **CMC 方式** 在 `contract user invoke` 等发送交易的命令中,追加 `--sequence` 参数即可发送有序交易。`--sequence` 取值需为账户当前 `confirmedSeq + 1`(默认 `0` 表示不设置,即无序交易): ```sh ./cmc client contract user invoke \ --contract-name=fact \ --method=save \ --params="{\"file_hash\":\"ab3456df5799b87c77e7f88\",\"file_name\":\"name007\"}" \ --sequence=6 \ --sdk-conf-path=./testdata/sdk_config.yml \ --sync-result=true ``` **Go SDK 方式** 通过 `CreatePayload` 构造负载时,将 `seq` 参数设置为 `confirmedSeq + 1`: ```go // 1. 查询当前 confirmedSeq confirmedSeq, err := cc.GetCurrentAccountSequence() if err != nil { log.Fatalf("get account sequence failed: %v", err) } // 2. 计算下一笔有序交易的序列号 nextSeq := confirmedSeq + 1 // 3. 构造带 sequence 的负载 payload := cc.CreatePayload( "", // txId:空表示自动生成 common.TxType_INVOKE_CONTRACT, // 交易类型 contractName, // 合约名 method, // 方法名 params, // 参数([]*common.KeyValuePair,无参数可传 nil) nextSeq, // sequence:confirmedSeq + 1 nil, // gas limit:nil 表示使用默认值 ) // 4. 生成签名交易并发送 txReq, err := cc.GenerateTxRequest(payload, nil) if err != nil { log.Fatalf("generate tx request failed: %v", err) } resp, err := cc.SendTxRequest(txReq, -1, true) ``` **Java SDK 方式** 有序交易通过为每个账户维护交易序列号(`sequence`)实现同一账户交易的按序执行。发送前先查询账户当前已确认序列号(`confirmedSeq`),再将新交易的 `sequence` 设置为 `confirmedSeq + 1`: ```java try { // 1. 查询当前账户的 confirmedSeq long confirmedSeq = chainClient.getCurrentAccountSequence(rpcCallTimeout); // 2. 构造payload(invokeContractPayload等方法返回的payload.sequence默认为0,需手动设置为 confirmedSeq + 1) Request.Payload payload = chainClient.invokeContractPayload(contractName, method, "", params); payload = payload.toBuilder().setSequence(confirmedSeq + 1).build(); // 3. 发送交易 ResultOuterClass.TxResponse responseInfo = chainClient.sendContractRequest(payload, null, rpcCallTimeout, syncResultTimeout); } catch (Exception e) { e.printStackTrace(); } ``` > **提示**:完整可运行示例——Go SDK 见 `sdk-go/examples/account_sequence/main.go`;Java SDK 见 [JavaSDK 使用说明](../sdk/JavaSDK使用说明.html) 的「发送有序交易」小节。 ## 校验规则与常见错误 当有序交易功能启用后,节点会对区块内 `sequence != 0` 的交易执行两项校验: - **唯一性**:同一个区块内,同一账户最多只能有一笔非 `0` 序列号的有序交易。 - **连续性**:有序交易的 `sequence` 必须严格等于该账户当前 `confirmedSeq + 1`,不能跳号,也不能重复使用已确认的序列号。 `sequence = 0` 的无序交易不参与上述校验,可与有序交易在同一区块内共存。 常见错误与对应现象: | 现象 | 原因 | | --- | --- | | 交易被拒绝,提示序列号不是下一位(`tx seq is not next`) | `sequence` 不等于 `confirmedSeq + 1`(跳号、重复、或用了已确认的旧值) | | 交易被拒绝,提示区块内重复有序交易(`duplicate ordered tx in block`) | 同一账户在同一区块内提交了多笔非 `0` 序列号交易 | | 交易未被校验、按无序处理 | `enable_tx_seq` 开关未开启,或区块版本低于 `v2.3.9` | ## 与普通交易的关键差异(重点) 有序交易在**打包**与**校验**两个环节都与普通(无序)交易有明显不同,使用前请重点关注以下几点。 ### 同一账户每个区块至多一笔有序交易 这是有序交易**最重要**的特性:**同一账户在同一个区块内最多只能有一笔非 `0` 序列号的交易**。该约束在两个环节被强制: - **出块打包阶段**:交易池为每个账户只挑选一笔「紧邻下一笔」的有序交易进块,同账户的其余有序交易会被放回,等待后续区块; - **区块校验阶段**:若一个区块中出现同账户多笔有序交易,整个区块判定为非法(`duplicate ordered tx in block`)。 > **吞吐影响(务必评估)**:因此同一账户的有序交易**无法在一个区块内并行打包**,其吞吐上限为「**每区块 1 笔**」。若某账户需要连续提交 N 笔有序交易,这 N 笔至少会被分散到 N 个连续区块中依次上链。对于高频账户,应评估是否确需有序;不需要严格排序的交易应保持为无序(`sequence = 0`),以便同账户在一个区块内并行打包,不受此限制。 ### 只有「紧邻下一笔」才会进块,跳号会被暂缓 打包时只有 `sequence == confirmedSeq + 1` 的交易才会被选入区块: - `sequence > confirmedSeq + 1`(跳号,前序尚未上链):交易被**放回交易池等待**,直到前序上链、`confirmedSeq` 推进后才会被再次选中; - `sequence <= confirmedSeq`(已被确认的旧序列号):交易被**永久驱逐**。 > **跳号卡顿(gap stall)**:如果先提交了 `confirmedSeq + 2` 而 `confirmedSeq + 1` 尚未上链,则 `+2` 会一直等待,直到 `+1` 被确认后才能进块。因此务必**按序提交**,避免中间缺口阻塞该账户后续所有有序交易。 ### 本地提交(RPC)与节点间转发(P2P)的准入不同 - 通过**本地 RPC** 提交的有序交易,交易池要求**严格连续**,会在入口尽早拒绝跳号交易,避免无效交易占用交易池; - 通过 **P2P** 从其他节点转发来的有序交易,交易池仅做**窗口内去重**、容忍暂时跳号(用于桥接尚缺前序的落后节点,待前序补齐后即可连续打包)。 业务方一般只需关注 RPC 侧的「严格连续」要求:即每次都基于最新 `confirmedSeq` 计算 `confirmedSeq + 1` 后再提交。 ## 兼容性与注意事项 - **默认无序、向后兼容**:`sequence` 默认为 `0`,升级到 `v2.3.9` 后原有业务无需改动即可继续运行。有序交易需业务方显式设置 `sequence` 才会生效。 - **有序与无序可混用**:同一账户既可以发送无序交易(`sequence = 0`),也可以发送有序交易(`sequence != 0`),两者互不影响,可在同一区块内共存。 - **关闭开关后的行为**:关闭 `enable_tx_seq` 后,节点不再校验序列号,所有交易均按无序交易处理;账户已累积的 `confirmedSeq` 会保留,重新开启后从原值继续递增。 - **序列号获取时机**:应在发送每一笔有序交易前查询最新的 `confirmedSeq`,避免因本地缓存导致跳号;并发发送同账户多笔有序交易时需自行保证序列号的分配与顺序。