L1.2 - Events and Callbacks

EVM中的事件

事件允许智能合约在满足特定条件时,通过记录特定信息来与外部世界进行通信。有了事件,去中心化应用(dApps)就可以直接响应发生的动作,而不需要不停地主动查询(轮询)区块链状态。

EVM 会对事件进行索引(Index),这使得监控区块链活动(如转账、价格变化)变得非常高效和容易。当合约触发事件时,数据被存储在交易的日志(Logs)中。这些日志虽然附着在区块链的区块上,但它们不会直接影响区块链的状态(例如,它们不会改变账户余额或合约变量的值)。

开发者通过两个核心关键字来使用事件:

触发(Emit):使用 emit 关键字来正式记录数据。

例如:emit PriceUpdated("ETH", newEthPrice);

定义(Define):使用 event 关键字,后接事件名称和要记录的数据类型。

例如:event PriceUpdated(string symbol, uint256 newPrice);

设想一个需要实时价格信息来执行其逻辑的智能合约,例如一个根据最新市场价格动态调整抵押品要求的去中心化金融(DeFi)借贷平台。该合约可能定义如下事件:

event PriceUpdated(string symbol, uint256 newPrice);

当智能合约从 Chainlink 的预言机接收到新的价格更新时,会触发 PriceUpdated 事件:

emit PriceUpdated("ETH", newEthPrice);

响应式合约中的事件处理

接口

为了实现自动化反应,响应式合约必须遵循一套标准的交互协议,即实现 IReactive 接口。

pragma solidity >=0.8.0;

import './IPayer.sol';

interface IReactive is IPayer {
    struct LogRecord {
        uint256 chain_id;
        address _contract;
        uint256 topic_0;
        uint256 topic_1;
        uint256 topic_2;
        uint256 topic_3;
        bytes data;
        uint256 block_number;
        uint256 op_code;
        uint256 block_hash;
        uint256 tx_hash;
        uint256 log_index;
    }

    event Callback(
        uint256 indexed chain_id,
        address indexed _contract,
        uint64 indexed gas_limit,
        bytes payload
    );
    
    function react(LogRecord calldata log) external;
}

整个接口的核心数据包是 LogRecord 结构

当源链(如以太坊)发生事件时,睿应网络会将该事件打包成一个 LogRecord 结构体发送给合约。你可以把它想象成一封包含所有关键信息的“挂号信”:

具体来说:

  • chain_id :事件来源的区块链 ID。
  • _contract :发出该事件的合约地址。
  • topic_0 至 topic_3 :日志的已索引主题(indexed topics)。
  • data :事件日志中的未索引数据。
  • block_number :事件发生的区块编号。
  • op_code :可能表示操作码。
  • block_hash 、 tx_hash 和 log_index :用于追踪事件来源与上下文的其他标识符。

react()是睿应合约的核心入口。

  • 触发机制:响应式网络会持续监控日志,一旦发现符合你设置的订阅条件的事件,就会自动调用这个 react() 方法,并将上述 LogRecord 丢进去。
  • 动态处理:通过解析输入的数据,合约可以动态决定接下来要做什么(比如:如果价格低于 X,就准备卖出)。
  • 权限限制:它被标记为 external,意味着它只接受来自网络层的主动触发,而不能被合约内部随意调用。

Callback回调

响应式合约本身运行在睿应网络中,它不能直接在以太坊上写数据。因此,它通过发出一个特定格式的日志(即 Callback 事件),告诉睿应网络:“请帮我在那条链上执行这个操作”。

因此当 react() 函数决定要行动时,它不会直接去操作目标链,而是通过抛出一个 Callback 事件来发出指令。

  • 这个事件包含了目标链的 ID、目标合约地址、Gas 限制以及最重要的 payload(执行载荷)
  • 睿应网络捕捉到这个 Callback 后,会负责在目标链上把这笔交易“跑”出来。

具体来说:

  • chain_id :该事件的区块链 ID。
  • _contract :触发该事件的合约地址。
  • gas_limit :为回调函数分配的最大 Gas 量。
  • payload :随回调函数一同传递的编码数据。

整个过程是完全自动化且去中心化的:

  1. 触发(Emit):当响应式合约的逻辑判断需要采取行动时,它会 emit Callback(...)
  2. 检测(Detect):睿应网络(Reactive Network)作为一个底层基础设施,会实时监控这些回调事件。
  3. 处理与提交(Process & Submit):一旦检测到事件,网络会解析 payload 中的交易详情,并使用你提供的 chain_idgas_limit,在目标链上创建并提交一笔真实的交易。

需要注意的是,代码并不是直接跑在主网上,而是在一个名为 ReactVM 的私有沙箱中运行,运行后的callback才有睿应网执行跨链操作。

  • 隔离性:响应式合约只能与同一个部署者部署的合约进行交互。
  • 安全性:这种隔离机制确保了即使在处理复杂的跨链事件时,合约环境也是受控且安全的,防止了恶意的跨合约攻击。

身份替换机制

在传统的跨链操作中,证明“谁发起了请求”通常非常复杂。睿应层通过协议层的强制操作简化了这一过程:

  • 强制覆盖:当你构建 payload(执行载荷)时,无论你为第一个参数填入了什么值,响应式网络在将交易提交到目标链之前,都会自动且强制性地将前 160 位(即第一个参数的位置)替换为调用方合约的 RVM ID
  • RVM ID 的本质:这个 ID 等同于该响应式合约在 ReactVM 中的地址,且与该合约部署者的地址完全一致。
  • 不可伪造性:由于替换发生在网络底层,开发者无法通过代码篡改这个身份标识。这为目标链合约提供了一个不可信环境下的“信任原点”。

这一机制直接影响了你编写 Solidity 代码的方式:

  • 参数类型限制:你目标链上的被调用函数,其第一个参数必须是 address 类型
  • 变量名无关性:无论你在目标链合约中给第一个参数起什么名字(比如 callerorigin),它接收到的永远是发起请求的 RC 合约地址。

一个Uniswap 止损单例子:

bytes memory payload = abi.encodeWithSignature(
   "stop(address,address,address,bool,uint256,uint256)",
   address(0),  // The ReactVM address
   pair,        // The Uniswap pair address involved in the transaction
   client,      // The address of the client initiating the stop order
   token0,      // The address of the first token in the pair
   coefficient, // A coefficient determining the sale price
   threshold    // The price threshold at which the sale should occur
);
emit Callback(chain_id, stop_order, CALLBACK_GAS_LIMIT, payload);

这里需要注意的是第一个参数填 address(0),起到占位符作用。