Infernet
SDK
Architecture

Architecture

The Infernet SDK is composed of two core sets of contracts.

At it's heart, lies the Coordinator. This contract:

  • Is responsible for managing Subscriptions, the core unit of the Infernet SDK
  • Manages the node lifecycle (via Manager.sol), allowing nodes to register and activate in the network
  • Is what Infernet nodes listen to for details about new or cancelled requests for compute
  • Is the authorized middle-man proxy between Infernet nodes and subscribing user contracts

Developers inherit from a set of Consumers in their smart contracts, namely CallbackConsumer and SubscriptionConsumer. These contracts:

  • Expose simple functions to create different types of Subscriptions at the Coordinator
  • Allow receiving subscription responses via an authorized callback function only from the Coordinator
🛡️

Notice that your contracts inherit from Consumers that explicitly set the address to a Coordinator. Coordinator's are responsible for enforcing subscription correctness, ensuring things like: unique nodes only respond once per interval, or that nodes adhere to your subscription settings, before any responses reach your smart contracts.

Subscriptions

Subscriptions are the core units of the Infernet SDK.

What is a subscription?

A subscription is a request (one-time or recurring) made by a user to an Infernet node to process some compute. Users initiate subscriptions; nodes fulfill these subscriptions.

A subscription is referenced by its ID, a monotonically-increasing, unique identifier.

Subscriptions can be created on-chain via the Coordinators createSubscription() function or off-chain from an authorized signer and EIP-712 message (opens in a new tab) via the createSubscriptionDelegatee() function.

For most developers, it is unnecessary to manipulate raw subscriptions, since we expose simple consumer interfaces (CallbackConsumer, SubscriptionConsumer) that developers can inherit to handle the bulk of background logic. Still, it is useful to understand how a subscription works.

Subscriptions in definition

In definition, a subscription is a struct containing nine parameters:

ParameterDefinition
ownerThe on-chain address that owns a subscription. This is usually your applications' smart contract. By default, it is initialized to the address that creates the subscription. This is (1) the address that receives any subscription outputs, and (2) the only address that can cancel a subscription.
containerIdThis is the unique identifier of the compute container(s) you want an Infernet node to run. Your subscriptions' inputs are passed to these containerId container(s) and any output is directly returned. You can specify more than one container to run by delimiting with a comma (,)—for example: container1-id,container2-id,container3-id.
inputsThere are two ways to expose input data or features for an Infernet node to consume. You can either encode the data in this parameter or leave this empty and expose a getContainerInputs() function in your consumer contract.
frequencyHow many times is a subscription processed? If set to 1, a subscription is processed once. If set to 2, a subscription is processed twice. If set to UINT256_MAX, a subscription is processed indefinitely (so long as other conditions hold true).
periodIn seconds, how often is a subscription processed? If set to 60, I want a response every 60 seconds, frequency times. Can be set to 0 if a subscription is processed once.
redundancyHow many unique nodes do I want responses from in each period time? If set to 1, owner receives up to 1 successful response each period. If set to 5, owner receives up to 5 successful responses, each period.
activeAtWhen does the subscription allow receiving its first response? When period > 0, this is default set to currentTimestamp + period. As in, the first response is received period from creation. When period = 0, this is set to currentTimestamp, allowing immediate response.
maxGasPriceWhat is the max gas price of a response transaction that a node should send for your subscription?
maxGasLimitWhat is the max gas limit of a response transaction that a node should send for your subscription? Generally, this should equal the amount of gas it takes for your contracts callback function processing + DELIVERY_OVERHEAD_WEI (a fixed surcharge for the delivery operation).

Example subscriptions

It is useful to illustrate some example subscriptions as they would appear as raw structs:

One-time subscription example

My contract is at address(MY_DEFI_CONTRACT). I want MY_DEFI_CONTAINER to be called with MY_DEFI_INPUTS. I want up to 3 nodes to process this computation, only once, and return immediately. I don't want any future computation subscription beyond this one request. The computation I do in my contract when an output is received costs 200_000 gas and I don't want nodes to respond if the gas price is greater than 100 gwei.

// Pseudocode
Subscription({
    owner: address(MY_DEFI_CONTRACT), // Recipient + subscription owner
    frequency: 1, // Processing only once
    period: 0, // Not recurring
    redundancy: 3, // 3 responses for my once frequency
    activeAt: now(), // Immediately active
    containerId: "MY_DEFI_CONTAINER",
    inputs: MY_DEFI_INPUTS,
    maxGasPrice: 100e9, // 100 gwei
    maxGasLimit: 200_000 + DELIVERY_OVERHEAD_WEI, // my gas overhead + response overhead
})

Recurring subscription example

My contract is at address(MY_DEFI_CONTRACT). I want MY_DEFI_CONTAINER to be called with dynamic inputs. I have exposed a getContainerFeatures() function in my contract. I want a response 31 times, once every day, with up to 2 nodes responding each time. The computation I do in my contract when an output is received costs 100_000 gas and I don't want nodes to respond if the gas price is greater than 20 gwei.

// Pseudocode
Subscription({
    owner: address(MY_DEFI_CONTRACT), // Recipient + subscription owner
    frequency: 31, // Processing 31 times
    period: 1 days, // Recurring every day
    redundancy: 2, // 2 responses for each daily frequency
    activeAt: now() + 1 days, // Active at the next period
    containerId: "MY_DEFI_CONTAINER",
    inputs: "", // Using dynamic getContainerFeatures() function exposed in MY_DEFI_CONTRACT
    maxGasPrice: 20e9, // 20 gwei
    maxGasLimit: 100_000 + DELIVERY_OVERHEAD_WEI, // my gas overhead + response overhead
})

Delivery intervals

All fulfilled computation requests are uniquely identified by a combination of three parameters:

  • subscriptionId
  • interval
  • respondingNodeAddress

While the subscriptionId and respondingNodeAddress are self-explanatory, interval is not.

An interval is the current cycle of a subscription. As in, segmenting elapsed time since the start of a subscription (remember, activeAt) by the period of a subscription. For example, where t is current time:

  • A subscription that started at time 0, repeating indefinitely every 10 seconds is at interval 1 when 0 <= t < 10 and 2 when 10 <= t < 20.
  • A subscription that started at time 1500, occuring only once, is at interval 1 when t >= 1500.

To illustrate, here is a subscription with the parameters:

  • frequency = 3, so a maximum of 3 intervals
  • period = 10, so an interval every 10s
  • redundancy = 2, so up to 2 responses per interval

Intervals visualized

⚠️

Make note of the fact that the second interval only has 1 node response, even when redundancy = 2. Redundancy is simply an upper bound, and when conditions don't hold (say, blockchain gas fees surpass the maxGasPrice a consumer is willing to pay), intervals remain empty. This also limits stale responses.

Managing subscriptions

Subscriptions are managed at the Coordinator. Developers rarely have to access these raw functions and should instead opt to use one of the provided Consumer contracts. Still, for reference:

Coordinator

The Coordinator is the coordination layer between developer smart contracts and off-chain Infernet nodes.

Coordinator and my contract

Developer smart contracts interface with the Coordinator in two ways:

  1. Smart contracts create Subscriptions with the coordinator
  2. Smart contracts accept inbound subscription fulfillments from nodes, via the coordinator

Behind the scenes, the Coordinator performs an extensive set of checks and safety measures before delivering responses to developer smart contracts, including:

  • Ensuring subscription responses are sent to the right owner
  • Ensuring only active subscriptions are fulfilled (remember, activeAt)
  • Ensuring current, not stale subscriptions are fulfilled (via period, interval)
  • Ensuring only up to frequency responses are sent each interval
  • Ensuring unique nodes respond each interval
  • Ensuring responses consume under or equal to maxGasLimit gas
  • Ensuring responses are submitted at under or equal to maxGasPrice price
  • Ensuring responses execute successfully
️🦹

A Coordinator is the last checkpoint and intermediary between the outside world and your contracts' rawReceiveCompute callback function. While the default coordinator performs an extensive set of checks and safety measures, at some point, there may exist alternative coordinator implementations that offer their own unique set of features. It is important to be cautious and audit your coordinator implementation.

🛡️

Notice that while a Coordinator enforces that unique nodes respond to a subscription each interval, it makes no guarantees about which nodes respond. If you are performing compute using a private containerId or accepting optimistic responses (outputs without an on-chain, succinctly-verifiable proof), you may choose to restrict the set of Infernet nodes that can respond to your subscriptions in your own smart contract, permissioning the _receiveCompute() function.

Coordinator and Infernet nodes

Infernet nodes track state of current subscriptions and deliver subscription output via the Coordinator.

Tracking subscription state

By default, the coordinator exposes view functions like subscriptions() to access the current state of any subscription by its subscription ID. In addition, the Coordinator emits events when:

Through syncing to a snapshot block and continuously replaying these events, an off-chain observer like an Infernet node is able to track the state of all subscriptions in the system.

Tracking subscription inputs

Subscription inputs can be exposed to off-chain Infernet nodes in one of two ways:

  1. At subscription creation, users can specify arbitrary bytes in the subscription's inputs parameter. These can only be set once (thus, commonly used by the CallbackConsumer) and are read by nodes at the same time they collect subscription state.
  2. Developer smart contracts can expose a getContainerInputs() view function that dynamically exposes features based on the subscriptionID, interval, timestamp and caller (fulfilling node). Infernet nodes call this function off-chain when processing a subscription interval for the most up-to-date inputs to consume.

Fulfilling subscriptions

When ready, Infernet nodes perform off-chain computation and fulfill subscription responses at the Coordinator via functions like deliverCompute() or deliverComputeDelegatee(), specifying:

  • subscriptionId — the ID of the subscription being fulfilled
  • deliveryInterval — the subscription interval a response is being delivered for (must be current)
  • input — optional container input bytes
  • output — optional container output bytes
  • proof — optional execution proof bytes

At the Ritual ML Workflow container layer, developers can return encoded bytes corresponding to rawInput, processedInput, rawOutput, procesedOutput, and proof fields. The Infernet Node simply consumes and packs these parameters into the on-chain input, output, and proof fields.

💾

By default, the Infernet SDK enforces no set structure or encoding for input, output, or proof, instead resorting to a dynamic and arbitrary bytes calldata type. This affords developers maximum flexibility to use the Ritual ML Workflows to construct the appropriate on-chain response for their contract. For example, for optimistic computation where a succinct proof cannot be generated, the data field can instead be used to transport arbitrary encoded request metadata.

Generally, container outputs are published on-chain in one of three ways. Taking the rawOutput and processedOutput parameters as an example:

  1. If neither parameter exists, the on-chain output field is empty
  2. If one parameter exists, the on-chain output field is the value of that parameter
  3. If both parameters exist, the on-chain output is the encoded concatenation of both rawOutput and processedOutput (akin to abi.encode(rawOutput, processedOutput))
🪛

On Layer-2 networks that derive their security from Ethereum mainnet, the bulk of a transactions cost is its L1 data fee (opens in a new tab), the cost to publish its transaction data to Ethereum. On L2 networks, when using callback-based systems like Infernet that transport new data to the chain via calldata, you should keep in mind the structure of your data and ways to optimize (opens in a new tab) what is posted on-chain. Choose to perform complex, yet cheap, computation on-chain rather than publish simple but sparse data via calldata.

⚠️

As a part of nodes delivering outputs to the Coordinator, developer smart contracts' rawReceiveCompute functions are called. These functions perform arbitrary execution (for example, in cases where a succinct proof is applicable, proof validation would occur within this function). For this reason, it is crucial for Infernet nodes to simulate transactions before they are broadcasted on-chain, to prevent transaction failure due to developers' callback functions failing.