Skip to content

Building Off-Chain Components in Smart Contracts

Off-Chain Components solve issues inherit in Blockchain technology, which isolates contracts from external input. Off-Chain Components are pieces of WASM bytecode embedded in ordinary smart contracts, which provides code to react to both on-chain state changes, and interaction from Web 2.0. Reactions are done through a context object, which provide methods to store data, receive data, or sending transactions to the contract.

These components can be used to build confidential and auditable Key Management Services, Secret-sharing Services, among other types of services. Contracts are implemented in Rust, and can be tested both in Rust and in Java.

Off-Chain Component

Capabilities

The Off-Chain Component is an extension to the Rust smart contracts, which can define two automatic invocations for off-chain interactions. These invocations are run in a different context than the rest of the Rust Smart Contract; they are not like ordinary invocations that respond to on-chain invocations. Rather, they are off-chain invocations that respond to changes to the on-chain state, or to HTTP invocations to the Execution Engine.

Invocations defined are:

  • off_chain_on_state_change: Invoked whenever the contract state is updated on-chain. Intended use case is to perform tasks that can only be performed off-chain, either because it has an effect on confidential data only stored at each off-chain, or because the task needs networking.
  • off_chain_on_http_request: Invoked whenever a HTTP invocation is called on the contract through a specific Execution Engine. Intended use case is to store or load confidential data directly to/from the off-chain component. Read more in the Http Server Section.

Reacting to on-chain state change

The Off-Chain can react to changes to the contract state, through the use of the off_chain_on_state_change annotation. The annotated function is called each time the contract is updated. See below for an example of how to use this annotation:

#[off_chain_on_state_change]
fn on_state_change(
    mut off_chain_context: OffChainContext,
    state: ContractState,
) {
    // React to contract state here...
}

One use case might be to notify the contract when a given time has passed, which can be accomplished by checking the current time and sending a transaction if the threshold has been reached:

#[off_chain_on_state_change]
fn on_state_change(
    mut off_chain_context: OffChainContext,
    state: ContractState,
) {
    if off_chain_context.current_time() >= state.notification_wanted_after {
        off_chain_context.send_transaction(
            Transaction {
                address: off_chain_context.contract_address,
                gas_cost: 1200,
                payload: vec![0x02],
            })
    }
}

Warning

off_chain_on_state_change is called for all invocations sent to the contract, including those sent from off_chain_on_state_change itself. This can result in an infinite loop if the programmer is careless when writing condition checks.

See the Contexts section for more functions that can be called by a Off-Chain Component.

HTTP Server

The Off-Chain can receive HTTP Requests through the Execution Engine, through the use of the off_chain_on_http_request annotation. The annotated function is called with the request that has been made. See the overview for the possible information flows, and below for an example of how to use this annotation:

#[off_chain_on_http_request]
pub fn http_dispatch(
    mut off_chain_context: OffChainContext,
    state: ContractState,
    request: HttpRequestData,
) -> HttpResponseData {
    // React to HttpRequestData
}

The endpoint used to send an HTTP request to the contract has the following format:

https://<EE HOSTNAME>/executioncontainer/<CONTRACT ADDRESS>/<CONTRACT SPECIFIC PATH>

Where <EE HOSTNAME> is the individual Execution Engine's hostname, <CONTRACT ADDRESS> is the contract's on-chain Address, and <CONTRACT SPECIFIC PATH> can be anything that the Off-Chain Component specifies.

See below for an Hello World example:

#[off_chain_on_http_request]
pub fn http_dispatch(
    mut off_chain_context: OffChainContext,
    state: ContractState,
    request: HttpRequestData,
) -> HttpResponseData {
    HttpResponseData::new_with_str(200, "Hello World")
}

This returns "Hello World" no matter which contract path it is called with, and no matter which HTTP method, whether that be GET /hello, PUT /test/path or even DELETE /thingies/123.

It is the contract's own responsibility to implement routing. Because each Off-Chain Component runs independently and handles requests manually. The matchit is a good starting point for matching routes (e.g., /api/data). See the Secret Sharing Example Contract for one way to do it.

Notifying the Smart Contract

For cases where Off-Chain Components are doing important work, is receiving information from external sources, or otherwise need to inform the Smart Contract of what is going on, it is possible to send a Transaction using the OffChainContext::send_transaction function.

This function sends a Transaction to the smart contract, signed by the Execution Engine with a specified amount of gas to be paid by the engine. The contract address is specified in the OffChainContext::contract_address field.

This shows how to use the send_transaction function:

ctx.send_transaction(Transaction {
    address: ctx.contract_address,
    gas_cost: 1200,
    payload: vec![0x02],
});

The function is guarenteed to block until the contract is guarenteed to receive the transaction. It doesn't guarentee that the transaction will succeed, nor does it guarentee any ordering of the transactions, nor how long it will take for the transaction to be sent.

The transaction will be sent by the Execution Engine, and the specific Address used to send the Transaction is available as the OffChainContext::execution_engine_address.

Warning

Transactions are not instantly received by the smart contract, and the off-chain invocations can be invoked with outdated states. The smart contract should account for the possible discrepancies between the on-chain state, and the off-chain state.

This is mostly relevant when sending transactions from off_chain_on_state_change where the Off-Chain Component should remember which transactions it has sent (for example, for a task queue, to store the ids of completed tasks), to avoid sending repeats.

Engine-local Storage

Off-Chain Components have access to individualized storage for each engine the component is running on; this allows the component to store whatever information that is deemed necessary for performing its function, whether that is confidential information or just bookkeeping for when interacting with the blockchain or other systems.

The storage is fully type parameterizable for key and value, and can be used like so:

let mut storage: OffChainStorage<u32, MyStruct> = ctx.storage(b"BUCKET_ID");

storage.insert(123, MyStruct { a: 456, b: 789 });

assert_eq!(storage.get(123).unwrap().a, 456);

The off-chain storage is isolated such that data is only available to the contract (as defined by a specific contract Address) at the specific engine the data was stored. Data cannot be leaked from contract to contract, even if they run precisely the same binary code.

Unit testing in Rust

Off-Chain Components can be tested by both writing Rust unit tests alongside the contract code, and by writing integration tests in Java using the Junit Contract Test Framework.

Integration testing in Java

This section focuses on integration tests; read this Rust Book article about unit tests to learn how to write unit tests.

Test classes that extend JunitContractTest can use the inherited blockchain object to interact with the underlying blockchain, and importantly for testing Off-Chain Components, it is possible to call addExecutionEngine to create engines that automatically respond to contract state updates, as shown below.

@ContractTest
void setupContractAndEngines() {
    final TestExecutionEngine ENGINE_1 = blockchain.addExecutionEngine(p -> true, ENGINE_KEY_1);
    final TestExecutionEngine ENGINE_2 = blockchain.addExecutionEngine(p -> true, ENGINE_KEY_2);
    contractAddress = blockchain.deployContract(sender, CONTRACT_BYTES, MyContract.initialize());

    // ENGINE_1 and ENGINE_2 is automatically called and can
    // effect the contract state.
}

Any interactions made to the smart contract will automatically result in a call to the Execution Engine, as shown below.

@ContractTest(previous = "setupContractAndEngines")
void testInteract() {
    // ENGINE_1 and ENGINE_2 are automatically called and can
    // effect the contract state.
    blockchain.sendAction(SENDER_ADDRESS, contractAddress, OffChainSecretSharing.interact());
}

The TestExecutionEngine object returned by addExecutionEngine exposes methods to inspect the off-chain state, and to emulate various off-chain events. For example, HTTP requests can be sent to individual engines as show below.

@ContractTest(previous = "testInteract")
void testHttpRequest() {
    HttpRequestData request = new HttpRequestData("GET", "/my/path/", Map.of(), "");

    // ENGINE_1 is automatically called to respond to the HTTP reque.
    HttpResponseData response =
        ENGINE_1.makeHttpRequest(contractAddress, request).response();

    assertThat(response.statusCode()).isEqualTo(200);
}

Examples

Here are examples of how to do common operations in the Off-Chain Component.

How to send a transaction

See the time notification example above

How to control the nodes associated?

It is recommended to have a list of Execution Engines in the contract state to help clients find the endpoints they have to invoke, and to validate whether any given invocation is coming from an authorized engine.

struct Engine {
    /// The blockchain address/identity of the engine.
    address: Address,
    /// Endpoint whereby clients can access the HTTP endpoint.
    endpoint: String,
}

struct ContractState {
    engines: Vec<Engine>,
    // Other fields...
}

impl ContractState {
    fn assert_valid_engine(&self, address: Address) {
        assert!(state.engines.iter().any(|e| e.address == address), "Engine not assigned to this contract");
    }
}

#[action(shortname = 0x01)]
pub fn on_chain_invocation_called_by_engine(
    contract_context: ContractContext,
    mut state: ContractState,
) -> ContractState {
    state.assert_valid_engine(contract_context.sender);

    // Do things that only engines should be able to do...
}

#[off_chain_on_http_request]
pub fn http_dispatch(
    mut off_chain_context: OffChainContext,
    state: ContractState,
    request: HttpRequestData,
) -> HttpResponseData {
    state.assert_valid_engine(off_chain_context.execution_engine_address);

    // Create response...
}

How to add a REST-endpoint where users can fetch data

Here is an example of using Off-Chain Components as a web server for assets stored on the blockchain:

struct ContractState {
    asset: String,
}

#[off_chain_on_http_request]
pub fn http_dispatch(
    mut off_chain_context: OffChainContext,
    state: ContractState,
    request: HttpRequestData,
) -> HttpResponseData {
    HttpResponseData::new_with_str(200, state.asset)
}

Work Queues: Orchestration of off-chain tasks

The recommended work flow for state synchronization is work queues, where the on-chain orchestrates the work that is being done by the off-chains.

One possible flow for work queues is:

  1. Some event on the chain triggers a new work item, which is stored in the state of the contract.
  2. The off-chains is notified about a state update. They notice the work item, and start performing the required action.
  3. Once the work item is completed, the off-chain sends a transaction to the on-chain, and stores a reminder to itself that it has completed the work, possibly with some result data.
  4. If the off-chain is notified about state changes after it has sent the transaction, but before the transaction is reflected in the state, it will remember that it had already done the work (as saved at step 3), and doesn't need to do anything.
  5. The on-chain is eventually given the transaction, and saves the result.
  6. Once all off-chains has reported completion, the contract marks the work item as done and continues to the next.

Let us elaborate on the above with a Web3-to-Web 2.0 notification service example with a single engine:

  1. A contract requests the notification service contract about sending an HTTP request to a specific URL.
  2. The contract queues the notification, and waits on completion.
  3. The off-chain notices the notification, and sends it. Immediately after it saves that it has done the work (to avoid sending more than one HTTP request), and informs the on-chain that it has completed the task.
  4. The on-chain then removes the notification work item once it receives the completion transaction from the off-chain.

Sequence Diagram for an Notification Service

Best practice when coding off-chain components

While Off-chain components are part of smart contracts, they are also extremenly flexible. Therefore they provide fewer convenience functions. Please keep in mind these potential issues when coding your off-chain component:

  • The smart contract must implement authentication for actions that are to be called by the Off-Chain Component, as these actions can also be called by other contracts or users.
  • The Off-Chain Component can be run by one or more Execution Engines, depending upon the use case.
  • Off-Chain Components must implement their own authentication for the HTTP endpoints.
  • Anybody can setup an Execution Engine that runs the off-chain code of your contract. This is not an issue if the code has proper authentication.