跳转到主要内容

Documentation Index

Fetch the complete documentation index at: https://docs.stable.xyz/llms.txt

Use this file to discover all available pages before exploring further.

摘要

系统交易为 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 预编译合约。

架构概述

system-transaction-architecture

StableSystem 预编译合约

StableSystem 预编译合约位于 0x0000000000000000000000000000000000009999,处理需要发出 EVM 事件的协议级操作。目前它支持解绑完成通知。
interface IStableSystem {
    /// @notice 处理排队的解绑完成并发出 EVM 事件
    /// @param blockHeight 处理完成的区块高度
    /// @dev 只能由系统交易调用(from = 0x1)
    /// @dev 每次调用最多处理 100 个完成
    /// @dev 自动从队列中删除已处理的完成
    function notifyUnbondingCompletions(int64 blockHeight) external;

    /// @notice 当解绑操作完成时发出
    /// @param delegator 委托代币的地址
    /// @param validator 代币委托给的验证者地址
    /// @param amount 完成解绑的代币数量(以 uusdc 为单位)
    event UnbondingCompleted(
        address indexed delegator,
        address indexed validator,
        uint256 amount
    );

    /// @notice 调用者未授权(不是系统交易发送者)
    error Unauthorized();
}

系统交易发送者

系统交易使用 0x0000000000000000000000000000000000000001 作为发送者地址。该地址:
  • 不需要签名验证
  • 只能由 PrepareProposal 中创建的交易使用
  • 用户或合约无法伪造
  • 通过 SystemTxDecorator ante 处理程序跳过费用扣除
EVM 通过检查 msg.sender == 0x1 来识别系统交易。预编译合约可以使用此功能来限制仅协议操作。

事件驱动流程

当用户的解绑期完成时,会发生以下情况:
  1. Stable SDK 层: 质押模块的 EndBlocker 完成解绑并发出 EventTypeCompleteUnbonding,包含委托者地址、验证者地址和金额。
  2. 检测: x/stable 模块的 EndBlocker 在质押之后运行,并扫描区块事件日志中的解绑事件。每当有代币完成解绑,该模块都将在队列中添加一条记录,包含委托者地址、验证者地址、金额和区块高度。
  3. 系统交易生成:在下一个区块的 PrepareProposal 中,应用程序查询所有排队的完成。如果存在任何完成,它会创建一个调用 StableSystem.notifyUnbondingCompletions(blockHeight) 的系统交易,使用当前区块高度。此交易放在区块前面,在任何用户交易之前。
  4. 执行: 在区块执行期间,系统交易首先运行。预编译合约查询该区块高度排队的完成状态,为每个完成发出一个 UnbondingCompleted 事件(最多 100 个),并从队列中删除它们。
  5. EVM 可见性: 事件出现在交易收据和日志中,对 eth_getLogs 查询、区块浏览器和任何监控 StableSystem 预编译合约的应用程序可见。

批处理

为防止区块变得过大,系统每个区块最多处理 100 个解绑完成。如果队列中存在 150 条记录:
  • 区块 N:创建处理完成 0-99 的系统交易
  • 区块 N+1:创建处理完成 100-149 的系统交易
预编译合约直接查询状态,而不是在 calldata 中接收完成数据。这使交易大小可预测,并将数据从昂贵的 calldata 移动到更便宜的状态读取。

使用示例

最常见的用例是需要在解绑期完成时通知用户的质押仪表板。以下是如何设置解绑完成监听器。
import { ethers } from 'ethers';

// StableSystem 预编译合约地址
const STABLE_SYSTEM_ADDRESS = '0x0000000000000000000000000000000000009999';

// UnbondingCompleted 事件的 ABI
const STABLE_SYSTEM_ABI = [
  'event UnbondingCompleted(address indexed delegator, address indexed validator, uint256 amount)'
];

// 连接到 Stable 网络
const provider = new ethers.JsonRpcProvider('https://rpc.testnet.stable.xyz');
const stableSystem = new ethers.Contract(
  STABLE_SYSTEM_ADDRESS,
  STABLE_SYSTEM_ABI,
  provider
);

// 订阅所有解绑完成
stableSystem.on('UnbondingCompleted', (delegator, validator, amount, event) => {
  console.log('解绑完成!');
  console.log('委托者:', delegator);
  console.log('验证者:', validator);
  console.log('金额:', ethers.formatEther(amount), '代币');
  console.log('区块:', event.log.blockNumber);
  console.log('交易哈希:', event.log.transactionHash);
});

此监听器将在任何用户的解绑完成时触发。在生产环境中部署 dApp 时需要过滤特定用户的事件。

过滤特定用户的事件

要仅接收特定委托者地址的事件,请使用索引事件参数创建过滤器:
// 仅监视特定用户的解绑
const userAddress = '0xabcd...';

const filter = stableSystem.filters.UnbondingCompleted(userAddress);

stableSystem.on(filter, (delegator, validator, amount, event) => {
  // 这仅针对指定用户的解绑触发
  showNotification(`您的 ${ethers.formatEther(amount)} 代币解绑完成!`);
  refreshUserBalance(userAddress);
});
如果您正在构建特定于验证者的仪表板,您还可以按验证者过滤:
// 监视来自特定验证者的所有解绑
const validatorAddress = '0x1234...';

const validatorFilter = stableSystem.filters.UnbondingCompleted(null, validatorAddress);

stableSystem.on(validatorFilter, (delegator, validator, amount) => {
  updateValidatorStats(validator, amount);
});

查询历史事件

如果您的 dApp 需要显示过去解绑完成的历史记录,您可以使用带有区块范围的事件过滤器查询历史事件:
// 获取用户在最近 1000 个区块中的所有解绑
const currentBlock = await provider.getBlockNumber();
const filter = stableSystem.filters.UnbondingCompleted(userAddress);

const events = await stableSystem.queryFilter(
  filter,
  currentBlock - 1000,
  currentBlock
);

const unbondingHistory = events.map(event => ({
  delegator: event.args.delegator,
  validator: event.args.validator,
  amount: ethers.formatEther(event.args.amount),
  blockNumber: event.blockNumber,
  txHash: event.transactionHash
}));

console.log('最近的解绑:', unbondingHistory);

集成指南

步骤 1:添加 Stable System 合约接口

首先,将 StableSystem 预编译合约接口添加到您的项目中。如果您使用 Foundry 或 Hardhat,请创建一个新的接口文件:
interface IStableSystem {
    event UnbondingCompleted(
        address indexed delegator,
        address indexed validator,
        uint256 amount
    );
}
如果您正在构建一个没有 Solidity 合约的纯前端 dApp,您只需要事件的 ABI 片段:
const STABLE_SYSTEM_ABI = [
  'event UnbondingCompleted(address indexed delegator, address indexed validator, uint256 amount)'
];

步骤 2:设置事件监听器

初始化您的 ethers.js provider 并创建指向 StableSystem 预编译合约地址的合约实例。预编译合约始终部署在 Stable 测试网和主网的 0x00000000000....0000009999 注意:预编译合约尚未部署在 Stable 主网上,将在 v1.2.0 升级后提供。
const provider = new ethers.JsonRpcProvider(RPC_URL);
const stableSystem = new ethers.Contract(
  '0x0000000000000000000000000000000000009999',
  STABLE_SYSTEM_ABI,
  provider
);

步骤 3:在应用程序逻辑中处理事件

订阅事件并相应地更新应用程序状态。常见模式包括:
  • 余额更新:当解绑完成时,刷新用户的代币余额
  • 通知系统:在用户的解绑完成时显示 toast 通知
  • 仪表板统计:实时更新质押指标和图表
  • 交易历史:将已完成的解绑添加到用户的活动源

步骤 4:处理连接问题

由于事件订阅依赖于持久的 websocket 连接,因此为生产 dApp 实现重新连接逻辑:
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;

function setupEventListener() {
  const provider = new ethers.WebSocketProvider('wss://rpc.testnet.stable.xyz');

  provider.on('error', (error) => {
    console.error('Provider 错误:', error);
    if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
      reconnectAttempts++;
      setTimeout(() => setupEventListener(), 5000);
    }
  });

  const stableSystem = new ethers.Contract(
    '0x0000000000000000000000000000000000009999',
    STABLE_SYSTEM_ABI,
    provider
  );

  stableSystem.on('UnbondingCompleted', handleUnbonding);
}

为什么采用这种方法?

与自定义索引器相比

以前,Stable SDK 要求 dApp 开发人员运行自定义索引器,监视 SDK 事件并将它们存储在数据库中。这增加了操作开销并引入了潜在的故障点。 使用系统交易,无需单独的索引器基础设施。EVM 的日志系统原生支持这类事件,每个 RPC 节点都已经索引和提供。任何标准 web3 库都可以订阅这些事件,无需额外工具。

与轮询 SDK 端点相比

没有系统交易,EVM dApps 需要定期调用 Stable SDK REST 端点来检查解绑期是否已完成。这会产生几个问题:
  • 延迟增加:5-10 秒的轮询间隔意味着用户可能需要等待那么长时间才能看到更新
  • 更高的负载:每个 dApp 实例轮询端点都会增加 RPC 基础设施的负载
  • 复杂性:dApps 需要同时处理 web3 提供程序(用于 EVM 交互)和 Stable SDK REST 客户端(用于 SDK 查询)
  • 无实时更新:轮询本质上无法提供即时通知
系统交易通过 dApps 已经用于 EVM 交互的相同 websocket 连接提供实时事件通知。这简化了开发人员体验并降低了基础设施成本。

安全保证

无需信任的事件发出

系统交易在 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
100 个完成的完整批次消耗大约 521,000 gas。由于 Stable 的区块 gas 限制为 100,000,000,这代表不到 0.6% 的可用区块空间。

通知延迟

当解绑期在区块 N 期间完成时:
  1. Stable 模块的 EndBlock 在区块 N 的状态中排队完成
  2. 区块 N+1 的 PrepareProposal 创建系统交易
  3. 系统交易在区块 N+1 期间执行,发出事件
这意味着解绑完成和发出 EVM 事件之间存在一个区块的延迟(大约 0.7 秒)。对于大多数用例,此延迟是可以接受的,因为解绑期本身为 7 天。

高负载场景

如果解绑完成的到达速度快于每个区块 100 个,它们会在队列中累积。队列按 FIFO 顺序处理,因此最旧的完成始终首先通知。 在持续的高负载期间,队列可能会暂时增长。但是,一旦高峰消退,完成较少的后续区块将逐渐排空队列。该系统旨在处理突发而不丢失事件。

未来扩展

系统交易机制为将任何 Stable SDK 操作桥接到 EVM 事件空间提供了通用模式。虽然目前仅用于解绑完成,但该架构可以扩展以涵盖其他用例:

质押操作

除了解绑之外,其他质押事件可以发出 EVM 通知:
  • 验证者的佣金率变化
  • 验证者入狱和出狱

治理执行

当治理提案通过并执行时,系统交易可以发出带有提案 ID 和执行结果的事件。这将允许 dApps 对参数变化或升级做出反应,而无需轮询治理模块。

通用事件桥

该模式可以推广为可配置的事件桥,其中每个模块注册哪些 SDK 事件应该镜像到 EVM。这将提供对所有 Stable SDK 操作的全面可见性,而无需每个模块的自定义逻辑。关键架构原则是系统交易仍然是协议级功能,仅由验证者在区块提案期间创建。
Last modified on April 2, 2026