双重实例
一个响应式合约虽然只有一套代码,但它会同时运行在两个不同的环境中:Reactive Network (RN) 和 ReactVM。即每个响应式合约在部署后,实际上存在两个物理上的实例:
- Reactive Network (RN) 实例:表现得像传统的 EVM 区块链,负责与系统合约交互,管理事件的订阅(Subscribe)和取消订阅(Unsubscribe)。
- ReactVM 实例:这是一个受限的隔离环境,专门用于处理事件逻辑。它不直接与外部交互,而是通过 RN 接收事件并发送回调请求。
Reactive Network的本质是普通的 EVM 区块链 + 额外的"系统合约",其监听以太坊、BNB、Polygon、Optimism 等链上的事件,管理订阅关系(订阅/取消订阅),同时接收用户直接发起的交易。
而ReactVM本质是一个"隔离的小虚拟机",每个部署者地址专属一个,它是专门用来处理事件的,同一地址部署的合约可以互相交互,但不能与其他地址的 Reactive Network 合约交互,用户也不能直接调用它。
![[Pasted image 20260310174807.png]]
ReactVM 虽然是隔离的,但可以通过两种方式与外界沟通:
① 源链发生事件 → Reactive Network 转发 → ReactVM 接收并处理
② ReactVM 处理完 → 发请求给 Reactive Network → 目标链执行回调
| 特性 | Reactive Network (前台/大厅) | ReactVM (实验室/引擎) |
|---|---|---|
| 本质 | 标准的 EVM 区块链(类似以太坊)。 | 受限的、隔离的执行环境。 |
| 主要任务 | 管理订阅:负责记录你要听哪些链的事件。 | 处理逻辑:负责计算接收到的事件并决定做什么。 |
| 交互对象 | 任何人(用户可以调用它来修改设置)。 | 只有事件:不直接接触外界,只吃“事件数据”。 |
| 可见性 | 公开,所有节点都能看到状态。 | 隔离,同一部署者的合约才能互通。 |
| 输出结果 | 记录状态、变更订阅列表。 | 产生回调(Callback),让前台去执行。 |
我们可以把整个流程看作一个自动化工厂的运作:
- 订阅(在前台中):你在 Reactive Network 上调用合约,告诉它:“帮我盯着以太坊上的 Uniswap 交易事件。”
- 捕获(从源链到前台):Reactive Network 捕获到了那个交易事件。
- 计算(在实验室里):Reactive Network 把事件数据塞进 ReactVM。在这里,合约的
react()函数开始运行,计算:“现在的价格达到我的止损线了吗?” - 执行(从实验室回到前台):如果满足条件,ReactVM 发出一个指令给 Reactive Network,说:“去帮我给目标链发一个回调,执行卖出操作。”
所以在代码里,我们会看到:
if (!vm) {
// 这部分代码只会在“前台”跑,比如去注册订阅
} else {
// 这部分逻辑只会在“实验室”里跑,处理具体的业务
}
双重实例的意思就是,代码里声明的变量(比如 uint256 public price),在 RN 里有一个值,在 ReactVM 里有另一个完全独立的值。它们就像是在平行宇宙里的同一个人,有着相同的外貌(代码),但经历(状态)完全不同。
使用双重实例是为了解决“高性能”和“安全性”的问题:
- 并行处理(高性能):ReactVM 是按部署者地址隔离的。如果你部署了 100 个合约,它们可以在不同的 ReactVM 里同时跑,互不干扰,也不会堵塞 Reactive Network 主链。
- 状态隔离:
- RN 状态:保存的是“行政数据”(比如:我是否暂停了服务?我订阅了哪个地址?)。
- ReactVM 状态:保存的是“逻辑数据”(比如:上次捕获的价格是多少?我已经触发过回调了吗?)。
识别执行上下文
前面我们知道,同一套代码会在两个环境中运行,但有些函数只能在特定环境中执行,比如:
- 订阅事件 → 只能在 Reactive Network
- 处理事件逻辑 → 只能在 ReactVM
所以合约需要知道其当前处于的环境是哪个。
在 Reactive Network 的设计中,有一个特殊的系统合约地址:0x0000000000000000000000000000000000fffFfF。
- 在 Reactive Network (RN) 中: 这个地址是真实存在的,上面部署了系统逻辑(用来处理订阅)。
- 在 ReactVM 中: 这是一个完全隔离的沙盒环境。为了安全和性能,这个系统合约地址在 ReactVM 里是不存在的,也就是“真空地带”。
合约通过一段简单的汇编代码来做检测:
function detectVm() internal {
uint256 size;
// 使用内联汇编获取目标地址的代码大小
assembly { size := extcodesize(0x0000000000000000000000000000000000fffFfF) }
// 如果大小为 0,说明这个地址没东西 -> 我在 ReactVM 里
vm = size == 0;
}
用汇编的原因是 Solidity 本身没有直接检查"某个地址代码大小"的内置函数,必须用底层的 EVM 汇编指令来实现。这里不直接调用系统合约来判断原因是在 ReactVM 里,合约不存在,所以直接调用是会报错崩溃的。
强制执行上下文
我们通过 detectVm() 识别不同的环境,而下面我们需要给合约的不同功能装上门禁,让其在不同的环境下工作。
我们来设想一个场景,由于同一份代码在两个环境里跑,如果 react() 函数(专门处理繁重逻辑的)不小心在 Reactive Network(主网)上跑了,会发生什么?
- 浪费 Gas:主网上的计算非常昂贵。
- 逻辑冲突:主网的状态和 VM 的状态不一样,混在一起会导致逻辑崩盘。
因此我们导入两个函数:
rnOnly()
modifier rnOnly() {
require(!vm, 'Reactive Network only');
_;
}
- 逻辑:它检查
vm是不是false。 - 直白解释:如果你在 ReactVM(实验室)里尝试调用带这个修饰符的函数,它会直接报错弹出。
- 例子:比如
pause()函数。你只想在主网上通过手动操作来暂停合约,而不希望它在处理某个事件时被自动触发。
vmOnly()
modifier vmOnly() {
require(vm, 'VM only');
_;
}
- 逻辑:它检查
vm是不是true。 - 直白解释:这个函数只能在 ReactVM 那个“黑盒子”里运行。
- 例子:核心函数
react(LogRecord calldata log)。这个函数是专门给 ReactVM 的引擎调用的,它接收外界的事件并决定要不要做跨链操作。
双重变量
我们已经知道同一套代码在两个环境中运行,且各自有独立的状态。但是但这就带来一个问题:“合约里的变量,到底是给哪个环境用的?”
答案就是给两个环境各准备一套专属变量。需要注意的是,变量在两个环境中是独立的,也就是在概念上是两个变量。
RN变量
RN变量继承自 AbstractReactive 合约,当你写 contract MyContract is AbstractReactive 时,不需要自己定义,系统会自动塞给你两个重要的工具:
vm(布尔值):这就是判断环境的函数。合约运行的一瞬间,它就知道自己是在 ReactVM (true) 还是 Reactive Network (false)。service(接口):这是一个“电话机”。只有通过它,你才能告诉系统:“我要监听哪条链上的哪个动作。”
RM变量
RM变量是更加关键的部分,其在你自己的响应式合约里声明,职责就是记录业务逻辑所需的数据和在react() 函数中被读取和修改
实际运行
以Uniswap 止损订单响应式合约的构造函数为例子,如下:
// State specific to ReactVM instance of the contract.
bool private triggered;
bool private done;
address private pair;
address private stop_order;
address private client;
bool private token0;
uint256 private coefficient;
uint256 private threshold;
constructor(
address _pair,
address _stop_order,
address _client,
bool _token0,
uint256 _coefficient,
uint256 _threshold
) payable {
triggered = false;
done = false;
pair = _pair;
stop_order = _stop_order;
client = _client;
token0 = _token0;
coefficient = _coefficient;
threshold = _threshold;
if (!vm) {
service.subscribe(
SEPOLIA_CHAIN_ID,
pair,
UNISWAP_V2_SYNC_TOPIC_0,
REACTIVE_IGNORE,
REACTIVE_IGNORE,
REACTIVE_IGNORE
);
service.subscribe(
SEPOLIA_CHAIN_ID,
stop_order,
STOP_ORDER_STOP_TOPIC_0,
REACTIVE_IGNORE,
REACTIVE_IGNORE,
REACTIVE_IGNORE
);
}
}
在代码中triggered = false 等初始化变量在两个环境中都初始化,但是if (!vm)里的关于监听的代码只在VN中进行。这些变量都是为了react()中的逻辑判断。以下是例子:
// Methods specific to ReactVM instance of the contract.
function react(LogRecord calldata log) external vmOnly {
assert(!done);
if (log._contract == stop_order) {
if (
triggered &&
log.topic_0 == STOP_ORDER_STOP_TOPIC_0 &&
log.topic_1 == uint256(uint160(pair)) &&
log.topic_2 == uint256(uint160(client))
) {
done = true;
emit Done();
}
} else {
Reserves memory sync = abi.decode(log.data, ( Reserves ));
if (below_threshold(sync) && !triggered) {
emit CallbackSent();
bytes memory payload = abi.encodeWithSignature(
"stop(address,address,address,bool,uint256,uint256)",
address(0),
pair,
client,
token0,
coefficient,
threshold
);
triggered = true;
emit Callback(log.chain_id, stop_order, CALLBACK_GAS_LIMIT, payload);
}
}
}
react()函数被标记为vmOnly,它处理两种情报:
来自 stop_order 合约的反馈
if (log._contract == stop_order) {
if (triggered && ... ) {
done = true; // 任务圆满完成,贴上封条
emit Done();
}
}
- 逻辑:它在等一个信号。如果它发现已经触发了卖出(
triggered为真),并且收到了目标链传回来的“确认卖出成功”的日志,它就会把done设为true。代表这笔止损交易彻底结束了。
来自 Uniswap pair 的价格波动
else {
Reserves memory sync = abi.decode(log.data, ( Reserves )); // 解码价格情报
if (below_threshold(sync) && !triggered) { // 跌破线了且还没触发过
triggered = true; // 立刻锁定开关,防止二次触发
emit Callback(...); // 向 Reactive Network 发送“攻击指令”
}
}
- 逻辑:它不断接收 Uniswap 的储备量(Reserves)数据。一旦计算发现价格跌破了
threshold,它会执行两件事:- 把
triggered设为true(从此之后,这个if就再也进不来了,防止重复触发)。 - 发出
Callback事件。注意:在 ReactVM 里发出Callback事件,就相当于向 Reactive Network 发出了一个“跨链代操作”的请求。
- 把
交易执行
使用响应式合约(RC)时,交易主要发生在两个环境中:睿应式网络(Reactive Network)和 ReactVM。每个环境在发起和处理交易方面均遵循不同的规则。
RN交易
在 Reactive Network (RN) 这一侧,交易就像你在以太坊上操作合约一样,是显式的。这里的交易发起者是合约管理员,而操作RN交易的原因一般是执行管理任务,比如“现在市场太波动了,我要暂时关闭这个止损程序”。
比如pause() 和 resume()
- 这两个函数带有
rnOnly。 - 它们会调用
service.unsubscribe。这就像是你打电话给接线员说:“别再把那个交易对的消息转接给我了。” - 结果: 这一动作发生在 RN 的存储状态里,它直接切断了传往 ReactVM 的“情报流”。
同时,RN也可以发起一些事件分发交易,这里就是说订阅事件的时候,当源链(如 Ethereum)产生一个事件时,RN 会启动一个内部交易,把这个事件“快递”给对应的 ReactVM 实例。
RM交易
在 ReactVM 内部,唯一的触发源就是来自 RN 的事件分发,收到RN的分发后去执行react()函数,如果符合就去执行callback。
| 维度 | Reactive Network 交易 | ReactVM 交易 |
|---|---|---|
| 发起者 | 用户(你)或系统节点 | 仅限系统转发的事件 |
| 可达性 | 公开,可以通过钱包/脚本调用 | 私密,外界无法直接触达 |
| 主要修饰符 | rnOnly |
vmOnly |
| 目的 | 修改合约配置、订阅列表 | 运行算法逻辑、产生跨链回调 |
| Gas 消耗 | 消耗 RN 链上的 Gas | 运行逻辑本身不耗 RN Gas(沙盒执行) |