- 区块链
- DeFi
- 金融科技
【免费下载链接】bisq
A decentralized bitcoin exchange network
导读
本文深入解析 Bisq 去中心化比特币交易网络中的"所有者断连回溯"(Owner Disconnect Backdating)机制——一个用于处理仅当数据所有者在线时才有效的存储条目(即报价 Offer)的生命周期治理方案。当节点与某条报价的所有者意外断连时,节点会将存储条目的 TTL 缩短一半,使离线所有者的报价更快过期并从网络中清除,从而避免"死报价"(dead offers)长期污染订单簿。读完本文,你将掌握该规范的全部规则、安全不变量,以及它在 Bisq 源码(P2PDataStorage、ProtectedStorageEntry、CloseConnectionReason等)中的完整实现路径与测试验证。
适用范围与核心问题
在 Bisq 的 P2P 分布式存储中,大部分条目(如 Mailbox 消息、投票、争议数据等)在写入后即长期有效,但有一类条目仅在所有者在线期间有效——目前仅有报价(Offer)属于这一类。
- 报价负载(
OfferPayloadBase)实现了 RequiresOwnerIsOnlinePayload,该接口要求负载携带所有者节点地址getOwnerNodeAddress(); - 报价同样实现
ExpirablePayload,带有一个 TTL(生存时间); - 所有者在 TTL 到期前周期性刷新自己的条目,节点在 TTL 到期后移除对应条目。
问题的核心场景是:当报价所有者的应用进入待机模式、崩溃、断网或异常退出时,它往往来不及广播移除消息。此时网络中其他节点依然存着这条报价,若完全等待 TTL 自然耗尽,过期报价会残留在订单簿上,让其他用户看到并尝试对谈一个已经下线的 maker,造成糟糕的体验。
为此,Bisq 设计了一个"断连回溯"机制:当节点发现与某条报价所有者的连接丢失时,将该条目的 TTL 剩余时间主动缩短,让离线所有者的报价更快消失。
关键定义:什么算"断连"
本规范的规则建立在区分"意图性关闭"(intended close)与"非意图性关闭"(unintended close)之上:
| 关闭类型 | 典型场景 | 是否回溯 |
|---|---|---|
| 意图性关闭 | 节点自身关闭、节点因连接数限制主动断开、对端地址未知而关闭、对端主动请求关闭 | 否 |
| 非意图性关闭 | 连接发生错误、对端违反规则(rule violation) | 是(仅限出站连接) |
在源码层面,这一分类由 CloseConnectionReason.java 枚举承载,每个枚举值都带有一个boolean isIntended标志:
APP_SHUT_DOWN(true, true)、CLOSE_REQUESTED_BY_PEER(false, true)、TOO_MANY_CONNECTIONS_OPEN(true, true)、TOO_MANY_SEED_NODES_CONNECTED(true, true)、UNKNOWN_PEER_ADDRESS(true, true)属于意图性关闭(isIntended = true);SOCKET_CLOSED、RESET、SOCKET_TIMEOUT、TERMINATED、CORRUPTED_DATA、NO_PROTO_BUFFER_DATA、NO_PROTO_BUFFER_ENV、UNKNOWN_EXCEPTION、SEND_MSG_FAILURE、SEND_MSG_TIMEOUT、RULE_VIOLATION、PEER_BANNED、INVALID_CLASS_RECEIVED、MANDATORY_CAPABILITIES_NOT_SUPPORTED均为非意图性关闭(isIntended = false)。
安全不变量:只有"真正知道所有者离线"才能回溯
规范确立了一条核心安全不变量:
一条丢失的连接只有在该节点确认连接对端就是该条目的所有者时,才被允许缩短条目的 TTL。
为什么这条不变量如此重要?关键在于节点对对端地址的可信度取决于连接方向:
出站连接(Outbound Connection):地址可信
节点主动拨号建立出站连接,对端的地址就是节点自己拨打的地址(NodeAddress),且任何对端声称的"发送者地址"都不能替换它。在主网上,节点仅通过 Tor 连接,Tor 协议本身会对洋葱地址进行认证,进一步保证了地址真实性。
入站连接(Inbound Connection):地址不可信
节点被动接受对端发起的入站连接,对端只在消息中声称自己的地址,节点无法验证其真实性。
攻击模型:入站回溯的代价
如果允许入站连接触发回溯,任何恶意节点都可以冒用另一节点的地址然后主动关闭连接。只需要一两次这样的操作,就可以将被冒用者的报价条目推至 TTL 末尾,令其被本地移除。
尤其值得注意的是:所有者的刷新操作无法恢复已被移除的条目。一条报价一旦被移除,在所有者重新发布它、或节点重新向对等节点请求数据之前,该报价都是缺失的。因此入站连接如果也能触发回溯,攻击者可以低成本地在全网节点上批量"下架"竞争对手的报价,形成一种廉价而高效的 DoS 手段。
规则:三条明确的回溯准则
规范最终将回溯行为收敛为三条清晰规则:
- 非意图性关闭的出站连接:对该连接地址对应的每条所有者条目,将其 TTL 缩短一半(
TTL / 2)。条目在 TTL 耗尽后被移除,除非所有者抢先刷新它。 - 意图性关闭:不回溯任何条目。
- 入站连接的关闭:无论对端声称什么地址,都不回溯任何条目。
源码实现:从断连事件到条目过期
事件入口:P2PDataStorage.onDisconnect
回溯逻辑的核心实现在 P2PDataStorage.java 的onDisconnect(CloseConnectionReason, Connection)中,该方法严格按规范逐条执行:
@Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { if (closeConnectionReason.isIntended) return; // Only on an outbound connection is the peer's address the one we dialed. On an inbound // connection the peer only claims an address, so its disconnect does not show that the // owner of that address went offline. if (!(connection instanceof OutboundConnection)) return; if (!connection.getPeersNodeAddressOptional().isPresent()) return; NodeAddress peersNodeAddress = connection.getPeersNodeAddressOptional().get(); // Backdate all the eligible payloads based on the node that disconnected map.values().stream() .filter(protectedStorageEntry -> protectedStorageEntry.getProtectedStoragePayload() instanceof RequiresOwnerIsOnlinePayload) .filter(protectedStorageEntry -> ((RequiresOwnerIsOnlinePayload) protectedStorageEntry.getProtectedStoragePayload()).getOwnerNodeAddress().equals(peersNodeAddress)) .forEach(protectedStorageEntry -> { protectedStorageEntry.backDate(); }); }执行流程与规范一一对应:
- 意图性关闭直接返回:
closeConnectionReason.isIntended为 true 时不进行任何回溯; - 仅处理出站连接:非
OutboundConnection实例(即入站连接)直接返回,即使对端声称的地址与某条目的所有者地址相同; - 对端地址必须已知:
getPeersNodeAddressOptional()为空则跳过(对应"对端地址未知"的意图性关闭场景也被双重保护); - 精确匹配所有者地址:在本地存储
map中筛选出所有RequiresOwnerIsOnlinePayload且ownerNodeAddress等于断连对端地址的条目; - 执行回溯:调用
protectedStorageEntry.backDate()。
源码注释还透露了设计权衡:之所以只将 TTL 缩短一半而非直接移除,是因为节点会遇到一些并非由所有者真正离线引起的连接掉落(源码中记载在压力测试中观察到大量节点挤在单一路由器下时会出现SOCKET_TIMEOUT、TERMINATED(EOFException)之类的非预期断连)。缩短一半 TTL 相当于给出宽容度,给所有者通过刷新消息恢复的机会。
回溯操作本身:ProtectedStorageEntry.backDate
ProtectedStorageEntry.java 中的backDate()是实现回溯的直接操作:
public void backDate() { if (protectedStoragePayload instanceof ExpirablePayload) creationTimeStamp -= ((ExpirablePayload) protectedStoragePayload).getTTL() / 2; }它把条目的creationTimeStamp向前拨TTL / 2毫秒。由于过期判断基于creationTimeStamp(而非剩余时间),回溯实际上相当于把条目的"出生时间"提前,从而让它在更早的时钟点被判定为过期。
配套的过期判断方法isExpired(Clock)实现了 TTL 语义:
public boolean isExpired(Clock clock) { return protectedStoragePayload instanceof ExpirablePayload && (clock.millis() - creationTimeStamp) > ((ExpirablePayload) protectedStoragePayload).getTTL(); }注意creationTimeStamp字段不是final的——这是它能够被回溯修改的前提,而ProtectedStorageEntry本身被持久化存储,说明回溯结果会进入本地数据存储。
周期清理:removeExpiredEntries
回溯后的条目并不会立即被删除,而是等待周期性的过期清理。在 P2PDataStorage.java 中,节点在完成网络引导(onBootstrapped)后,通过UserThread.runPeriodically以CHECK_TTL_INTERVAL_SEC = 60秒的间隔周期运行removeExpiredEntries(),将isExpired(clock)为真的条目从存储 map 和数据存储中移除。
源码中还有一个值得注意的细节:移除条目并不会同时移除其序列号记录。即使条目已过期,节点仍保留它的序列号,这样当新对等节点再次发送该数据的 ADD 消息时,会因序列号未增大而被拒绝,从而避免已过期对象被反复重新注入网络。
刷新机制:所有者的"续命"通道
与回溯相对的另一面是刷新。报价所有者在 TTL 耗尽前通过RefreshOfferMessage周期续命:
- 客户端侧入口是 OfferBookService.refreshTTL,它会先检查交易版本过滤策略,再调用
p2PService.refreshTTL; - 周期性调度由 OpenOfferManager 负责,其
REFRESH_INTERVAL_MS = 6分钟(TimeUnit.MINUTES.toMillis(6)),即每 6 分钟对每个在售报价调用一次offerBookService.refreshTTL,而报价 TTL 为 9 分钟,留有充足的续命余量; - 网络侧接收处理在
P2PDataStorage.refreshTTL(P2PDataStorage.java):校验序列号严格递增、签名有效后,用新的creationTimeStamp重建ProtectedStorageEntry并覆盖存储,同时将该刷新消息继续广播给其他对等节点。
RefreshOfferMessage的生成逻辑见P2PDataStorage.getRefreshTTLMessage:对负载哈希取当前序列号 +1,用所有者私钥签名,广播给网络。刷新是"推翻回溯"的唯一手段——回溯把条目推向 TTL 终点,刷新则把条目重置回完整的 TTL。
数值推演:报价 TTL 的"生死时钟"
报价的 TTL 定义在 OfferPayloadBase.java:
public static final long TTL = TimeUnit.MINUTES.toMillis(9);即报价 TTL 为9 分钟。结合以上机制,可以推导出报价从发布到消失的完整时间线:
| 事件 | 时刻(相对报价发布时间) |
|---|---|
报价发布,creationTimeStamp = T0,TTL = 9 分钟 | T0 |
所有者每 6 分钟刷新一次(REFRESH_INTERVAL_MS),重置 TTL | T0 + 6 分钟起 |
所有者意外离线,节点经历一次非意图出站断连,回溯TTL / 2 | T1 |
| 条目在 T0 - (T1 - T0) + 9 分钟处判定过期(等价于剩余 TTL 缩短一半) | T1 + 剩余 TTL / 2 |
| 周期性清理任务(每 60 秒)在最近的检查点将其移除 | 过期后 60 秒内 |
若断连发生时条目刚被刷新(剩余 TTL 接近 9 分钟),一次回溯会让其剩余 TTL 变为约 4.5 分钟;若断连发生在两次刷新的末期,一次回溯即可迅速将其推向过期。规范中明确给出的量化结论是:报价的 TTL 为 9 分钟,因此离线所有者的报价最长可能比原先多停留 4.5 分钟(这是针对未经历回溯节点的情形),而对于经历了回溯的节点,报价会以一半 TTL 的速度更快消失。
测试验证:规则的可执行证明
规范中的三条规则在 P2PDataStoreDisconnectTest.java 中得到了完整的单元测试覆盖,每个测试用例与规范条款一一对应:
| 测试方法 | 场景 | 预期 |
|---|---|---|
peerConnectionUnknown | 出站连接但对端地址未知 | 不回溯 |
connectionClosedIntended | CLOSE_REQUESTED_BY_PEER(意图性关闭) | 不回溯 |
connectionClosedSkipsItemsPeerInfoBadState | 对端地址信息缺失 | 不回溯 |
connectionClosedReduceTTL | 非意图性出站断连,条目属于该对端 | 回溯(TTL 减半) |
connectionClosedSkipsItemsNotFromPeer | 断连对端地址 ≠ 条目所有者地址 | 不回溯 |
connectionClosedInboundPeerClaimsOwnerAddress | 入站连接声称所有者地址 | 不回溯(安全不变量) |
connectionClosedTwiceExpiresEntry | 出站连接两次非意图断连 | 两次回溯后条目过期并被移除 |
connectionClosedTwiceInboundPeerClaimsOwnerAddress | 入站连接两次断连 | 条目保留 |
其中connectionClosedTwiceExpiresEntry直观地演示了回溯的数学语义:每次回溯把条目向前推TTL / 2,两次回溯正好把条目推到 TTL 终点,此后只需时钟再前进 1 毫秒,removeExpiredEntries()就会将其移除。测试还通过注入假的时钟(clockFake.increment(1))精确验证了过期判定的边界行为。
兼容性与行为影响
规范明确声明该机制不改变任何网络消息格式,也不改变任何持久化数据:
- 回溯不是广播事件:它只修改本地存储条目的
creationTimeStamp。节点在响应其他对等节点的数据请求时,会以被回溯后的创建时间对外提供该条目; - 节点间行为不对称:某节点与所有者的连接是入站方向时,它不会回溯,而是继续等待 TTL 自然结束;请求它的对等节点获得的仍是未回溯的创建时间;
- 影响面:这一不对称主要影响接受大量入站连接的节点——尤其是种子节点(seed nodes)。这类节点更难通过断连事件判断所有者离线,因此在它们那里,离线报价可能多停留一段时间。
总结
所有者断连回溯是 Bisq P2P 存储协议中一个精巧的生命周期治理机制,它用"确认知道所有者离线才缩短 TTL"的安全不变量,在快速清除死报价与防止恶意下架攻击之间取得了平衡。从规范到实现,它完整落地于P2PDataStorage.onDisconnect→ProtectedStorageEntry.backDate→removeExpiredEntries的调用链,并通过CloseConnectionReason.isIntended、OutboundConnection类型检查与所有者地址精确匹配三重防线保证了安全性,最终由覆盖全部规则的单元测试锁定行为。对于希望深入理解 Bisq P2P 数据层设计,或在类似去中心化存储场景中设计"在线所有者条目"清理策略的开发者,这是一份值得对照源码研读的参考规范。
- 区块链
- DeFi
- 金融科技
【免费下载链接】bisq
A decentralized bitcoin exchange network
相关推荐
NetBox Owner Groups(所有者组)模型详解:组织与管理资源所有者的分组机制
NetBox Owner Groups(所有者组)模型详解:组织与管理资源所有者的分组机制 导读 Owner Groups(所有者组)是 NetBox 中用于
后端网络数据建模Kun 子代理结果工件的关联所有权与回收语义:linked-owner 保留机制全解析
Kun 子代理结果工件的关联所有权与回收语义:linked owner 保留机制全解析 本篇围绕 Kun(Local first AI agent worksp
人工智能AI Agent自主智能体桌面应用MCP Clientsgo-redis重连机制:网络中断自动恢复策略
go redis重连机制:网络中断自动恢复策略 在网络应用开发中,Redis连接稳定性是保证系统可靠性的关键因素。go redis作为Go语言中最流行的Redi
后端数据库客户端缓存
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考