摘要
系统交易为 Stable 协议提供了一种为 Stable SDK 操作发出 EVM 事件的方式。当质押事件(如解绑完成)在 SDK 层发生时,协议会自动生成发出相应事件的 EVM 交易,使这些操作对 EVM 工具和应用程序完全可见。动机
Stable 上的 EVM 用户和应用程序期望通过标准 EVM 接口(如eth_getLogs)监控区块链事件。但关键操作发生在 Stable SDK 模块中,这些模块不会自然地发出 EVM 事件。这造成了可见性差距:EVM dapps 无法轻松跟踪用户的代币何时完成解绑。
系统交易弥合了这一差距。当质押模块完成解绑操作时,Stable 的 x/stable 模块会检测到该事件并生成一个调用 StableSystem 预编译合约(0x0000000000000000000000000000000000009999)的系统交易。然后,预编译合约会发出任何 dapp 都可以订阅的适当 EVM 事件。系统交易使用特殊的发送者地址(0x0000000000000000000000000000000000000001)运行,只有协议才能使用该地址。这可以防止任何人伪造协议事件,同时保持事件发出在链上的无需信任和可验证性。
规范
系统交易通过三个主要组件工作:x/stable 模块的 EndBlocker、PrepareProposal 处理程序和 StableSystem 预编译合约。架构概述

StableSystem 预编译合约
StableSystem 预编译合约位于0x0000000000000000000000000000000000009999,处理需要发出 EVM 事件的协议级操作。目前它支持解绑完成通知。
系统交易发送者
系统交易使用0x0000000000000000000000000000000000000001 作为发送者地址。该地址:
- 不需要签名验证
- 只能由 PrepareProposal 中创建的交易使用
- 用户或合约无法伪造
- 通过 SystemTxDecorator ante 处理程序跳过费用扣除
msg.sender == 0x1 来识别系统交易。预编译合约可以使用此功能来限制仅协议操作。
事件驱动流程
当用户的解绑期完成时,会发生以下情况:- Stable SDK 层: 质押模块的 EndBlocker 完成解绑并发出 EventTypeCompleteUnbonding,包含委托者地址、验证者地址和金额。
- 检测: x/stable 模块的 EndBlocker 在质押之后运行,并扫描区块事件日志中的解绑事件。每当有代币完成解绑,该模块都将在队列中添加一条记录,包含委托者地址、验证者地址、金额和区块高度。
- 系统交易生成:在下一个区块的 PrepareProposal 中,应用程序查询所有排队的完成。如果存在任何完成,它会创建一个调用 StableSystem.notifyUnbondingCompletions(blockHeight) 的系统交易,使用当前区块高度。此交易放在区块前面,在任何用户交易之前。
- 执行: 在区块执行期间,系统交易首先运行。预编译合约查询该区块高度排队的完成状态,为每个完成发出一个 UnbondingCompleted 事件(最多 100 个),并从队列中删除它们。
- EVM 可见性: 事件出现在交易收据和日志中,对 eth_getLogs 查询、区块浏览器和任何监控 StableSystem 预编译合约的应用程序可见。
批处理
为防止区块变得过大,系统每个区块最多处理 100 个解绑完成。如果队列中存在 150 条记录:- 区块 N:创建处理完成 0-99 的系统交易
- 区块 N+1:创建处理完成 100-149 的系统交易
使用示例
最常见的用例是需要在解绑期完成时通知用户的质押仪表板。以下是如何设置解绑完成监听器。过滤特定用户的事件
要仅接收特定委托者地址的事件,请使用索引事件参数创建过滤器:查询历史事件
如果您的 dApp 需要显示过去解绑完成的历史记录,您可以使用带有区块范围的事件过滤器查询历史事件:集成指南
步骤 1:添加 Stable System 合约接口
首先,将 StableSystem 预编译合约接口添加到您的项目中。如果您使用 Foundry 或 Hardhat,请创建一个新的接口文件:步骤 2:设置事件监听器
初始化您的 ethers.js provider 并创建指向 StableSystem 预编译合约地址的合约实例。预编译合约始终部署在 Stable 测试网和主网的0x00000000000....0000009999。
注意:预编译合约尚未部署在 Stable 主网上,将在 v1.2.0 升级后提供。
步骤 3:在应用程序逻辑中处理事件
订阅事件并相应地更新应用程序状态。常见模式包括:- 余额更新:当解绑完成时,刷新用户的代币余额
- 通知系统:在用户的解绑完成时显示 toast 通知
- 仪表板统计:实时更新质押指标和图表
- 交易历史:将已完成的解绑添加到用户的活动源
步骤 4:处理连接问题
由于事件订阅依赖于持久的 websocket 连接,因此为生产 dApp 实现重新连接逻辑:为什么采用这种方法?
与自定义索引器相比
以前,Stable SDK 要求 dApp 开发人员运行自定义索引器,监视 SDK 事件并将它们存储在数据库中。这增加了操作开销并引入了潜在的故障点。 使用系统交易,无需单独的索引器基础设施。EVM 的日志系统原生支持这类事件,每个 RPC 节点都已经索引和提供。任何标准 web3 库都可以订阅这些事件,无需额外工具。与轮询 SDK 端点相比
没有系统交易,EVM dApps 需要定期调用 Stable SDK REST 端点来检查解绑期是否已完成。这会产生几个问题:- 延迟增加:5-10 秒的轮询间隔意味着用户可能需要等待那么长时间才能看到更新
- 更高的负载:每个 dApp 实例轮询端点都会增加 RPC 基础设施的负载
- 复杂性:dApps 需要同时处理 web3 提供程序(用于 EVM 交互)和 Stable SDK REST 客户端(用于 SDK 查询)
- 无实时更新:轮询本质上无法提供即时通知
安全保证
无需信任的事件发出
系统交易在PrepareProposal ABCI 阶段创建,只有验证者才能执行。用户提交的交易无法伪造系统发送者地址(0x1),因为 EVM 的状态转换逻辑强制只有到 StableSystem 预编译合约地址的交易才能跳过签名验证。
这意味着:
- 用户无法伪造解绑完成事件
- 用户无法从自己的交易中调用
notifyUnbondingCompletions - 发出
UnbondingCompleted事件的唯一方法是在 Stable SDK 质押模块中实际完成解绑
无额外信任假设
系统交易不会引入超出区块链共识已经需要的新安全假设。如果您相信验证者正确执行区块,您就可以相信系统交易事件准确反映了 Stable SDK 状态变化。 事件发出过程是确定性的:给定EndBlock 中相同的 SDK 事件,所有诚实的验证者将在 PrepareProposal 期间产生相同的系统交易。共识机制确保验证者就包含哪些系统交易达成一致。
区块最终性
Stable 区块链通过 StableBFT 的共识机制使用快速最终性。一旦提交了一个区块,它就会立即最终化,无法重组。这意味着一旦您收到UnbondingCompleted 事件,您就可以相信它是永久的。
不需要像在概率最终性链上那样等待多个确认。dApps 可以在收到事件后立即更新用户余额并显示通知。
性能和限制
批处理大小约束
每个区块通过系统交易最多处理 100 个解绑完成。此限制存在是为了防止在解绑活动高峰期间区块大小无限制。 在实践中,假设平均区块时间为 0.7 秒,每个区块 100 个完成提供了约 9000 个完成/分钟的吞吐量。正常的质押活动很少达到此限制。在特殊情况下,完成可能会在完全处理之前排队几个区块。Gas 消耗
系统交易在执行期间消耗 gas,这在区块的 gas 限制中计算。gas 成本与正在处理的完成数量成线性比例:- 基本函数调用:约 21,000 gas
- 每个事件发出:约 3,000 gas
- 读取状态:每个完成约 2,000 gas
通知延迟
当解绑期在区块 N 期间完成时:- Stable 模块的
EndBlock在区块 N 的状态中排队完成 - 区块 N+1 的
PrepareProposal创建系统交易 - 系统交易在区块 N+1 期间执行,发出事件
高负载场景
如果解绑完成的到达速度快于每个区块 100 个,它们会在队列中累积。队列按 FIFO 顺序处理,因此最旧的完成始终首先通知。 在持续的高负载期间,队列可能会暂时增长。但是,一旦高峰消退,完成较少的后续区块将逐渐排空队列。该系统旨在处理突发而不丢失事件。未来扩展
系统交易机制为将任何 Stable SDK 操作桥接到 EVM 事件空间提供了通用模式。虽然目前仅用于解绑完成,但该架构可以扩展以涵盖其他用例:质押操作
除了解绑之外,其他质押事件可以发出 EVM 通知:- 验证者的佣金率变化
- 验证者入狱和出狱

