10. 有序交易
阅读该文档您可以了解到长安链 v2.3.9 引入的有序交易能力:它为每个账户维护一个交易序列号(sequence,简称 seq),使同一账户发出的交易可以按照指定顺序在链上执行,从而实现交易的严格排序与防重放。本文将介绍该功能的基本概念、启用条件、开启/关闭方式,以及如何通过 CMC 与 SDK 发送有序交易。
10.1. 功能简介
在默认情况下,长安链对同一账户的多笔交易并不保证执行顺序:交易被打包进区块的先后顺序,可能与业务发送顺序不一致。对于需要严格串行执行的业务(例如账户扣款、状态机流转等),这种乱序可能带来问题。
有序交易通过为每个账户引入交易序列号来解决该问题:
链上为每个账户维护一个已确认序列号(
confirmedSeq)。它随该账户成功上链的有序交易递增,初始值为0。每笔交易的负载中都有一个
sequence字段:sequence = 0:无序交易(默认值)。不参与序列号校验,与旧版本、旧业务完全兼容。sequence != 0:有序交易。该值必须恰好等于账户当前confirmedSeq + 1,否则交易会被拒绝。
有序交易上链后,账户的
confirmedSeq递增为该交易的sequence,下一笔有序交易需使用confirmedSeq + 1。
说明:有序交易是可选能力,采用「显式选择加入(opt-in)」的方式。不设置
sequence(即保持为0)的交易行为与旧版本一致,因此升级到v2.3.9后原有业务无需任何改动。
适用场景
需要保证同一账户交易严格按序执行的业务。
需要防止交易被重复提交(重放)的场景:同一
sequence只能成功上链一次。
10.2. 启用条件
有序交易的校验需要同时满足以下两个条件,缺一不可:
区块版本 ≥
v2.3.9(2030900):这是协议层的门槛。低于该版本的历史区块保持原有兼容行为,不做序列号校验。链配置开关
enable_tx_seq已开启:在区块配置(bc.yml的block节点)中开启。
只有两个条件都满足时,节点才会对区块内 sequence != 0 的交易执行序列号校验。若开关关闭,则所有交易均按无序交易处理。
10.3. 开启 / 关闭功能
enable_tx_seq 属于区块配置项,可在创建链时通过配置文件设置,也可在运行时通过链配置更新交易开启/关闭(需管理员签名)。
10.3.1. 配置文件参考
在各节点的 bc.yml 中,block 节点下新增 enable_tx_seq:
# Block config settings
block:
# 其他区块配置项 ...
# 是否开启有序交易(交易序列号)校验,默认 false
enable_tx_seq: true
10.3.2. 通过 CMC 开启 / 关闭
权限说明:切换该开关走链配置更新(
BLOCK_UPDATE)方法,默认权限为MAJORITY(多数管理员),即需要超过半数的组织管理员联合签名背书才能生效。因此下方命令通过--admin-key-file-paths/--admin-crt-file-paths传入多个管理员的私钥与证书(PK 模式则传入--admin-key-file-paths与--admin-org-ids)。
在链运行过程中,可通过 CMC 发送链配置更新交易来切换开关(需多数管理员联合签名):
./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 开启/关闭有序交易
10.3.3. 通过 SDK 开启 / 关闭
Go SDK 方式
调用链配置更新负载构造方法 CreateChainConfigBlockUpdateWithEnableTxSeqPayload,最后一个参数 enableTxSeq 即为开关值:
payload, err := cc.CreateChainConfigBlockUpdateWithEnableTxSeqPayload(
txTimestampVerify, blockTimestampVerify,
txTimeout, blockTimeout, blockTxCapacity, blockSize, blockInterval,
txParameterSize,
true, // enableTxSeq:true 开启,false 关闭
)
// 随后由多管理员对 payload 进行背书签名,并发送链配置更新交易
Java SDK 方式
调用 createPayloadOfChainConfigBlockUpdateWithEnableTxSeq,倒数第二个参数 enableTxSeq 即为开关值:
Request.Payload payload = chainClient.createPayloadOfChainConfigBlockUpdateWithEnableTxSeq(
txTimestampVerify, txTimeout, blockTxCapacity,
blockSize, blockInterval, txParameterSize,
true, // enableTxSeq:true 开启,false 关闭
rpcCallTimeout);
// 随后由多管理员对 payload 进行背书签名,并发送链配置更新交易
10.4. 发送有序交易
发送有序交易分两步:先查询账户当前的 confirmedSeq,再将新交易的 sequence 设置为 confirmedSeq + 1 后发送。
10.4.1. 查询账户序列号
CMC 方式
# 查询指定账户的已确认序列号
./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 查询账户序列号
Go SDK 方式
// 方式一:查询指定地址账户的 confirmedSeq
confirmedSeq, err := cc.GetAccountSequence(address)
// 方式二:查询当前 SDK 客户端账户自身的 confirmedSeq(内部自动推导账户地址)
confirmedSeq, err := cc.GetCurrentAccountSequence()
Java SDK 方式
// 方式一:查询指定地址账户的 confirmedSeq
long confirmedSeq = chainClient.getAccountSequence(address, rpcCallTimeout);
// 方式二:查询当前 SDK 客户端账户自身的 confirmedSeq(内部自动推导账户地址)
long confirmedSeq = chainClient.getCurrentAccountSequence(rpcCallTimeout);
10.4.2. 发送有序交易
CMC 方式
在 contract user invoke 等发送交易的命令中,追加 --sequence 参数即可发送有序交易。--sequence 取值需为账户当前 confirmedSeq + 1(默认 0 表示不设置,即无序交易):
./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:
// 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:
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 使用说明 的「发送有序交易」小节。
10.5. 校验规则与常见错误
当有序交易功能启用后,节点会对区块内 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 |
10.6. 与普通交易的关键差异(重点)
有序交易在打包与校验两个环节都与普通(无序)交易有明显不同,使用前请重点关注以下几点。
10.6.1. 同一账户每个区块至多一笔有序交易
这是有序交易最重要的特性:同一账户在同一个区块内最多只能有一笔非 0 序列号的交易。该约束在两个环节被强制:
出块打包阶段:交易池为每个账户只挑选一笔「紧邻下一笔」的有序交易进块,同账户的其余有序交易会被放回,等待后续区块;
区块校验阶段:若一个区块中出现同账户多笔有序交易,整个区块判定为非法(
duplicate ordered tx in block)。
吞吐影响(务必评估):因此同一账户的有序交易无法在一个区块内并行打包,其吞吐上限为「每区块 1 笔」。若某账户需要连续提交 N 笔有序交易,这 N 笔至少会被分散到 N 个连续区块中依次上链。对于高频账户,应评估是否确需有序;不需要严格排序的交易应保持为无序(
sequence = 0),以便同账户在一个区块内并行打包,不受此限制。
10.6.2. 只有「紧邻下一笔」才会进块,跳号会被暂缓
打包时只有 sequence == confirmedSeq + 1 的交易才会被选入区块:
sequence > confirmedSeq + 1(跳号,前序尚未上链):交易被放回交易池等待,直到前序上链、confirmedSeq推进后才会被再次选中;sequence <= confirmedSeq(已被确认的旧序列号):交易被永久驱逐。
跳号卡顿(gap stall):如果先提交了
confirmedSeq + 2而confirmedSeq + 1尚未上链,则+2会一直等待,直到+1被确认后才能进块。因此务必按序提交,避免中间缺口阻塞该账户后续所有有序交易。
10.6.3. 本地提交(RPC)与节点间转发(P2P)的准入不同
通过本地 RPC 提交的有序交易,交易池要求严格连续,会在入口尽早拒绝跳号交易,避免无效交易占用交易池;
通过 P2P 从其他节点转发来的有序交易,交易池仅做窗口内去重、容忍暂时跳号(用于桥接尚缺前序的落后节点,待前序补齐后即可连续打包)。
业务方一般只需关注 RPC 侧的「严格连续」要求:即每次都基于最新 confirmedSeq 计算 confirmedSeq + 1 后再提交。
10.7. 兼容性与注意事项
默认无序、向后兼容:
sequence默认为0,升级到v2.3.9后原有业务无需改动即可继续运行。有序交易需业务方显式设置sequence才会生效。有序与无序可混用:同一账户既可以发送无序交易(
sequence = 0),也可以发送有序交易(sequence != 0),两者互不影响,可在同一区块内共存。关闭开关后的行为:关闭
enable_tx_seq后,节点不再校验序列号,所有交易均按无序交易处理;账户已累积的confirmedSeq会保留,重新开启后从原值继续递增。序列号获取时机:应在发送每一笔有序交易前查询最新的
confirmedSeq,避免因本地缓存导致跳号;并发发送同账户多笔有序交易时需自行保证序列号的分配与顺序。