RFC(ckb): 方便通过 RPC 查询交易状态

, ,

English Version

问题

RPC get_transaction 只会查找本节点链上和交易池中的交易,对于进入交易池最终被移除的交易会返回 null。要通过长连接订阅 rejected_transaction 事件才能知道哪些交易被移除了。

对于大部分场景来说,维持长连接订阅是个代价过大的操作,这个时候可以通过轮询 get_transaction 来判断,如果返回 null 说明交易被拒了,如果返回 statuscommitted 表示已经止链。不过这个方案有一些问题存在:

  • 目前 ckb-rs 有个 Bug (ckb#2907),在 send_transaction 成功后马上调用 get_transaction 有可能返回 null 然后过一会才能查询到。
  • 无法区分提供的 hash 是节点没有见过的交易还是处理过后被移除的交易。
  • 使用 get_transaction 每次都会把 transaction 返回回来,对于轮询状态来说是个不必要的性能开销。

本 RFC 针对这个问题提出针对 get_transaction 的修改建议。

方案

  • 查找 ckb#2907 的原因并修复。
  • get_transaction 返回结果不再返回 null,改成返回字段 transaction 有可能为 null。字段 tx_status.status 增加两个状态 rejectedunknown。当状态为 rejected 的时候,新加一个字段 tx_status.reason 返回交易被移除的原因。
  • get_transaction 增加请求参数,可以选择只返回 tx_status,这时候返回结果中的 transaction 必定为 null
  • 增加一个 RPC get_tx_pool_entry,可以查询更详细的交易池中的交易状态。

修改 RPC 方法 get_transaction

RPC get_transaction 返回结果不再返回 null,改成返回字段 transaction 有可能为 null

字段 tx_status.status 增加两个状态 rejectedunknown

  • rejected: 这个交易被移除了。因为存储限制,节点只能保存最近被移除的交易。
  • unknown: 节点没有见过这个交易,或者应该是 rejected 但是因为存储限制被清理了。

tx_status.statusrejected 或者 unknown 的时候,返回字段 transaction 必定为 null

如果交易状态是 rejected,新字符串类型字段 tx_status.reason 会返回原因。注意未来的版本中可能会引入新的原因。

同时 get_transaction 增加请求参数 verbosity,类型是 Uint32,默认为 2。

  • 请求参数为 0 (过时): 这个选项用来保持兼容性,会在之后的版本中移除。如果交易状态是 rejected 或者 null,RPC 会像原来那样返回 null
  • 请求参数为 1: 不返回 transaction 内容,transaction 必定为 null
  • 请求参数为 2: 如果 tx_status.statuspending, proposed, 或者 committed 时,transaction 会返回交易内容,否则 transaction 字段为 null

为了支持 rejected 状态,节点必须保存最近交易池中被移除的交易,包括通过 RPC 提交,通过 P2P 网络收到交易,和因为主链重组而被回滚但是因为冲突没法加入到交易池的交易。

默认配置只保存最近 7 天的不超过 10,000,000 条的被移除交易。可以根据节点存储大小来调整配置。因为只用保存交易 hash,每条记录占用 32 个字节,不考虑额外开销的情况下 10,000,000 条大概会占用 300M 左右的磁盘存储空间。

[tx_pool]
...
keep_rejected_tx_hashes_days = 7
keep_rejected_tx_hashes_count = 10_000_000

使用场景

首先推荐应用在交易确认上链之前,或者决定要丢弃之前,都在本地保存交易副本。不要依赖 ckb 节点保存未上链交易。目前 ckb 实现中重启不会恢复交易池,ckb#2656 中增加了恢复的功能也有可能会出现恢复失败的情况,比如磁盘错误。或者来不及保存的情况,比如机器突然断电。

单节点

对于只使用一个节点来发送交易和确认交易状态的应用,在 send_transaction 成功后可以通过 get_transaction(verbosity = 1) 来轮询。

  • 如果返回 unknown 或者 rejected 都当作 rejected 处理。因为 send_tranasction 成功必然是节点见过的交易。当然存在节点重启后丢失交易池中交易的情况。这时推荐使用保存的交易副本使用 send_transaction 重发一次交易确认,如果 send_transaction 拒绝了交易,可以根据返回的错误信息处理。如果 send_transaction 成功了,说明是节点丢失了交易,这个时候可以继续轮询。
  • 如果确认交易 rejected 了,可以根据应用场景放弃或者使用新的 Cells 重新组装交易。
  • 如果确认交易 committed 了,最好等待足够的确认块数才认为交易上链成功了。
  • 如果轮询了很长时间,都不能确认交易 rejected 或者 committed 了,可以通过 get_tx_pool_entry 来确认是不是没有广播到足够多的节点,使用 send_transaction 重发交易可以强制节点再次通知 P2P 网络该交易的 hash。也可以在论询时简单的加上定时重新调用 send_transaction 的办法来处理。

多节点

对于使用多个 CKB 节点组成负载均衡的场景,建议使用 Master-Standby 的方式,Master 失败了再切换到备用机器上。或者加上会话管理 (Sticky Sessions Management),Master 不是固定的,而是每个会话会分配到一台作为 Master,比如根据客户端 IP 哈希来选择。

简单的 Round Robin 的负载均衡,或者在 Master-Standby 中有节点出现故障的情况,都会出现 send_transaction 成功后没有及时广播到其它节点的情况,这个时候应该根据 get_transaction 的结果来确定是否要重新通过 send_transaction 发送交易。

在附录中提供了一些让多个节点交易广播更及时和可靠的方案供参考。

临时方案

使用没有实现本 RFC 的节点版本时,可以使用以下临时方案。

send_transaction 成功后通过 get_tranasction 轮询状态。如果在 send_transaction 成功后 20s 内返回 null 当作 pending 处理。如果 20s 之后返回 null 当成 rejected 处理,也可以通过重放 send_transaction 来确认。

相关工作

相关反馈

以下内容已经过编辑避免透露隐私。使用 A:B: 来区分发言人,A 是提交反馈者,B 是技术支持。

2021-07-23

A: 发交易时,有出现两种失败情况,第一种是因为cell抢占,交易Rejected;另外一种是发送交易没报错,但浏览器查不到,节点也查不到,这种一般是什么原因导致的呢?

2021-07-29

A: 交易卡了半小时了, 节点版本 0.42

B: 前两个 cell dep 都被花掉了

A: 那两个 cell 会一直更新,过了这么长时间了,肯定被花费掉了,所以这个不是这笔交易一直 pending 的原因。其实刚上链的时候如果就被花费掉了,节点是会即时报错的,但是没有报错还在 pending 中,可以推断当时是没有被花掉的

A: 这是第一个问题,另一个问题是,如果时间过长,走到了其中一个 cell 被花掉的场景了,调用节点 rpc 接口是没有任何返回的,跟正常交易刚刚推到节点的返回没有区别,所以我们希望这个时候能不能返回一点东西呢,比如 rejected 也好,方便后续的错误逻辑处理。

未解决的问题

由于P2P和PoW的存在,即使实施了这个RFC,交易状态的转换也会非常复杂。 例如,交易仍然可能突然变成 unknown 然后再变成 pending。因为交易首先被从池中移除,后来又从其他节点被转发回来。

另外目前没有任何 RPC 可以告诉你一个交易是否已经被广播到 P2P 网络。这是一个有用的提示用来确定是否重新广播交易。

附录

多节点交易同步建议

互加白名单

首先推荐节点间互加白名单。

ckb.toml 文件的 [network] 模块中可以通过 whitelist_peers 增加白名单。 节点会优先连白名单中的节点,并尽可能保证连接不被断开。如果断开也会一直尝试重新连接。这个配置是一个数组,每个成员类似于以下格式

"/ip4/10.0.0.1/tcp/8115/p2p/QmWxucJPjKpfZuG7kTzYQLzRfv1h8nyMjnLBFxHDWFENjA"

基中 ip4/ 后面的是节点的 IP,如果是同一个机房,使用内网 IP 可以充分利用内网带宽。而 8115 是 p2p 网络监听的节点,对应的是配置文件 ckb.toml 中的 [network].listen_addresses 里配置的端口号。

p2p/ 后面的部分是节点的 peer-id,这个可以通过以下命令获得:

ckb peer-id from-secret --secret-path data/network/secret_key

注意如果 CKB 节点重新初始化或者 data 目录下的 network/secret_key 文件丢失了,都会导致 peer-id 变化,需要更新 whitelist_peers 配置。

以三个节点为例,IP 分别是 10.0.0.1, 10.0.0.2, 和 10.0.0.3。在这三个节点初始化好并且至少运行过一次 ckb run 之后,通过 ckb peer-id 获取他们各自的 peer-id 如下:

  • 10.0.0.1: QmWxucJPjKpfZuG7kTzYQLzRfv1h8nyMjnLBFxHDWFENjA
  • 10.0.0.2: QmTPYTsio5MGQkPTdVwYgM5xKcKGftx9qoBALhJi7oUKNt
  • 10.0.0.3: QmQ7k9RYAgvWt5mWvbGG85SiXf23hjGSVjmtnsHMqzs7Hx

这样在 10.0.0.2 上应该添加另外两台节点到白名单中:

whitelist_peers = [
  "/ip4/10.0.0.1/tcp/8115/p2p/QmWxucJPjKpfZuG7kTzYQLzRfv1h8nyMjnLBFxHDWFENjA",
  "/ip4/10.0.0.3/tcp/8115/p2p/QmQ7k9RYAgvWt5mWvbGG85SiXf23hjGSVjmtnsHMqzs7Hx"
]

其它两台节点也相应的添加另外两台到白名单中。

群发交易

最直接的办法是在调用 send_transaction 的时候同时往多个节点发。

在支持的的负载均衡中也可以配置成如果发现是 send_transaction 方法,就往所有节点发,将最先返回的结果作为最终结果返回。如果不支持也可以自己实现一个 send_transaction 代理来群发。

另一个方案是在每个节点上部署一个交易转发器。交易转发器通过长连接监听 new_transaction 事件。收到新交易后通过 RPC 直接提交到其它节点。

2 Likes

使用 null 还是 status: unknown?

命名:用 conflict 还是 reject

要不要增加字段 reason 来说明原因?还是未来提供更多的 status value 来表达其它状态?

RPC 是否需要支持?目前缺少标准来判断少于多少个表示没有充分广播。


RFC 中没有提到的:目前 conflict 的状态更新不够及时,对于没有 propose 的交易要得到 propose 之后才会检查 conflict 并移除。

更新日志

  • 本 RFC 不会增加新 RPC get_pool_entry
  • 增加新字段 tx_status.reason

命名:用 conflict 还是 reject

“conflict” 只能表达双花的场景,“reject” 的含义更广,能覆盖如不成熟、交易结构不合法等场景。所以我倾向于 “reject”。

响应消息中附带一个 structured reason 的话,我觉得或许能方便用户端排查问题和监控。比如双花的话,该交易是跟另外哪个交易冲突了。

string 比较灵活,不同的原因需要的信息差异太大了。

1 Like