Skip to main content

Lesson 2: ReactVM and Reactive Network As Dual-State Environment

Overview

In the previous lesson, we discussed one of the basic concepts of Reactive Smart Contracts (RSCs) — Inversion of Control. In this one, we will focus on another crucial property of RSCs: the fact they exist in two instances with separate states in the Reactive Network and ReactVM. Understanding this idea is necessary for successful Reactive Smart Contract development.

By the end of this lesson, you will learn to:

  • Distinguish both environments where a Reactive Smart Contract is executed.
  • Identify the current environment.
  • Manage data with two separate states.
  • Understand the types of transactions RSCs operate with.

Differences Between the Reactive Network and ReactVM

Each Reactive Smart Contract has two instances — one on the Reactive Network and the other in its separate ReactVM. It is important to note that both instances are physically stored and executed on each network node. Parallelizing RSCs is an architectural decision made to ensure high performance even with big numbers of events. We will talk more about that in one of our next articles. Read more on the ReactVM architecture.

Reactive Network | React Vm

Reactive Network operates as a typical EVM blockchain with the addition of system contracts that allow subscribing to and unsubscribing from the events of the L1 that we are monitoring, for example, Ethereum, BNB, etc. Polygon or Optimism can also be called L1s in the given context. Each deployer address has a dedicated ReactVM.

ReactVM is a restricted virtual machine designed to process events in isolation. Contracts deployed from one address are executed in one ReactVM. They can interact with each other but not with other contracts on the Reactive Network.

Contracts within a single ReactVM can interact with the external world in two ways, both through the Reactive Network:

  • They react to specified events to which they are subscribed and are executed when these events occur.

  • Based on the execution of the code with the inputs from events, the ReactVM sends requests to the Reactive Network for callbacks to L1s to perform the resulting on-chain actions.

Therefore, for each Reactive Smart Contract deployed, we have two instances of it with separate states, but the same code. Additionally, each method is expected to be executed in one or both environments and to interact with one or both states. This leads us to the question of how we identify, within the code, with which state we are currently working.

Identifying the Execution Context

The execution context within these dual environments is determined using a boolean variable, typically named vm. This variable helps the contract identify its current operating environment:

  • vm = false indicates execution within the Reactive Network.

  • vm = true indicates execution within the ReactVM.

vm variable is initialized in the constructor after the attempt to call the system contract. System contracts are only deployed in the Reactive Network whereas the ReactVM does not have a separate instance of them. So, if the call is unsuccessful, indicating that we are operating within a separate ReactVM instance rather than within the Reactive Network, the vm variable is set to true; otherwise, it remains false.

(bool subscription_result,) = address(service).call(payload);
if (!subscription_result) {
vm = true;
}

We need to make sure that each method/function is executed only in the environment in which it is supposed to. We have to do it through modifiers that check the vm variable that is initiated in the constructor.

modifier rnOnly() {
require(!vm, 'Reactive Network only');
_;
}

modifier vmOnly() {
require(vm, 'VM only');
_;
}

Managing Dual Variable Sets for Each State

To adapt to these two operational states, RSCs should manage two sets of variables:

  • Variables for the Reactive Network: used for interactions with system smart contracts, along with subscribing to and receiving events.

  • Variables for the ReactVM: used within the ReactVM to execute the reaction logic based on the received events.

We recommend the aforementioned separation of variables to ensure that operations relevant to each environment are executed properly without any interference. You can follow our notation for your development. However, you could also try implementing more advanced techniques such as using one variable in two instances of an RSC differently. vm is a prime example of a variable that holds distinct values across various RSC instances. Similarly, you could optimize gas usage by repurposing variables for different functions across different instances.

In the examples presented in this course, we consistently specify in the comments which state corresponds to each variable. Let’s briefly examine our Uniswap stop order example. Here’s how we separate the variables within it:

// State specific to reactive network contract instance

address private owner;
bool private paused;
ISubscriptionService private service;


// State specific to ReactVM contract instance

address private l1;
mapping(address => Tick[]) private reserves;

The variables used for managing events subscriptions pertain to the Reactive Network contract instance, where system contracts exist there. Conversely, the variables employed in the logic executed after events are specific to the ReactVM contract instance.

Transaction Execution

Let us consider the types of transactions that are executed in the Reactive Network and ReactVM for a specific Reactive Smart Contract.

Reactive Network Transactions

There are two ways of initiating a transaction on the Reactive Network: directly by a user or triggered by an event on an L1 chain.

Users can directly initiate transactions on the Reactive Network by invoking methods specific to the Reactive Smart Contract (RSC) instance dedicated to the Reactive Network. These methods interact with the state of the Reactive Network and enable various functionalities. For example, a user might initiate a transaction by calling the pause() function to suspend event reception and response temporarily.

// Methods specific to reactive network contract instance

function pause() external rnOnly onlyOwner {
require(!paused, 'Already paused');
service.unsubscribe(
SEPOLIA_CHAIN_ID,
l1,
PAYMENT_REQUEST_TOPIC_0,
REACTIVE_IGNORE,
REACTIVE_IGNORE,
REACTIVE_IGNORE
);
paused = true;
}

In the Reactive Network, transactions also occur when an event of interest is emitted on an L1. In that case, the system contract dispatches the event to all active subscribers, which are separate ReactVMs.

ReactVM Transactions

In the ReactVM, transactions can't be initiated directly by users. Instead, they are triggered when an event of interest occurs on an L1, at which point the system contract dispatches the event to the corresponding ReactVM for execution.

When an RSC in the ReactVM receives an event, it triggers the react() function. This part of the code outlines the actual logic of how we react to events: how we update the state and which callbacks to L1 chains we emit. It's important to note that a callback to an L1 chain initiates the resulting transaction on the relevant L1 network.

We will consider other examples of react() functions for different use cases closely in our next lessons.

Conclusion

In this lesson, we’ve explored how Reactive Smart Contracts (RSCs) operate within two distinct environments: the Reactive Network and a separate ReactVM. We’ve examined smart contract code to understand how to manage this dual state through separate sets of variables and method modifiers. We’ve also discussed the types of transactions our RSCs are designed to handle. This understanding marks a crucial step forward in our journey toward writing our first Reactive Smart Contract.