问题
RPC get_transaction
只会查找本节点链上和交易池中的交易,对于进入交易池最终被移除的交易会返回 null
。要通过长连接订阅 rejected_transaction
事件才能知道哪些交易被移除了。
对于大部分场景来说,维持长连接订阅是个代价过大的操作,这个时候可以通过轮询 get_transaction
来判断,如果返回 null
说明交易被拒了,如果返回 status
是 committed
表示已经止链。不过这个方案有一些问题存在:
- 目前 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
增加两个状态rejected
和unknown
。当状态为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
增加两个状态 rejected
和 unknown
。
rejected
: 这个交易被移除了。因为存储限制,节点只能保存最近被移除的交易。unknown
: 节点没有见过这个交易,或者应该是rejected
但是因为存储限制被清理了。
当 tx_status.status
为 rejected
或者 unknown
的时候,返回字段 transaction
必定为 null
。
如果交易状态是 rejected
,新字符串类型字段 tx_status.reason
会返回原因。注意未来的版本中可能会引入新的原因。
同时 get_transaction
增加请求参数 verbosity
,类型是 Uint32,默认为 2。
- 请求参数为 0 (过时): 这个选项用来保持兼容性,会在之后的版本中移除。如果交易状态是
rejected
或者null
,RPC 会像原来那样返回null
。 - 请求参数为 1: 不返回 transaction 内容,
transaction
必定为null
。 - 请求参数为 2: 如果
tx_status.status
为pending
,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
来确认。
相关工作
- 在 CKB Transactions Management Guideline 提供了一些如何管理未上链交易的建议。
- 目前区块浏览器上不会把 dep cell 已经被其它交易使用掉的交易标记为 Rejected,而是会显示成 Pending。
相关反馈
以下内容已经过编辑避免透露隐私。使用 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 直接提交到其它节点。