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
- 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 Coordinator
s createSubscription()
function or off-chain from an authorized signer and EIP-712 message (opens in a new tab) via the createSubscriptionDelegatee()
function.
Optionally, Subscriptions can pay for compute responses from nodes and proof verification from verifiers in their choice of paymentAmount
of paymentToken
.
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 11 parameters:
Parameter | Definition |
---|---|
owner | The 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. |
containerId | This 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 . |
frequency | How 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). |
period | In 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. |
redundancy | How 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 . |
activeAt | When 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. |
lazy | Whether to receive a subscription response eagerly or lazily. When set to true (lazily), compute responses are stored in the Inbox , with results delivered by index to the consuming smart contract. |
verifier | Optional verifier contract (implementing the IVerifier interface) to restrict subscription payments on the basis of proof verification. By default, can be set to address(0) for no proof verification. If a verifier is specified, and proof verification fails, no payment is deducted from a consuming smart contract. |
paymentAmount | Optional amount to pay in paymentToken each time a subscription is processed. Can be set to 0 for no payment. |
paymentToken | Optional payment token. Can be set to address(0) for payment in ETH or any ERC20 token address. If paymentAmount is set to 0 , this parameter is ignored as there is no subscription payment. |
wallet | Optional Wallet to use to pay for compute subscription responses. owner must be an approved spender of Wallet . Wallet (s) can be created via the WalletFactory . |
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 wantMY_DEFI_CONTAINER
to be called with inputs exposed via my contractsgetContainerInputs()
function. I want up to3
nodes to process this computation, onlyonce
, and return eagerly. I don't want any future computation subscription beyond this one request. I don't want to associate any payment or proof verification for this subscription.
// 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,
lazy: false, // Eager return
verifier: address(0), // No proof verification
// No associated payment
paymentAmount: 0,
paymentToken: address(0),
wallet: address(0)
})
Recurring subscription example
My contract is at
address(MY_DEFI_CONTRACT)
. I wantMY_DEFI_CONTAINER
to be called with dynamic inputs. I have exposed agetContainerInputs()
function in my contract. I want a response31
times, once everyday
, with up to2
nodes responding eagerly each time. I don't want to associate any payment or proof verification for this subscription.
// 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,
lazy: false, // Eager return
verifier: address(0), // No proof verification
// No associated payment
paymentAmount: 0,
paymentToken: address(0),
wallet: address(0)
})
One-time lazy subscription w/ 1 ETH payment example
My contract is at
address(MY_DEFI_CONTRACT)
. I wantMY_DEFI_CONTAINER
to be called with inputs exposed via my contractsgetContainerInputs()
function. I want up to3
nodes to process this computation, onlyonce
, and return lazily. For each of these responses, I want to incentivize the nodes with a payout of1 ETH
per compute response fromaddress(MY_WALLET)
which I have created via the Wallet Factory and funded with3 ETH
. I don't want to restrict this payout on the basis of successful proof verification.
// 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,
lazy: true, // Lazy return
verifier: address(0), // No proof verification
// 1 ETH payment
paymentAmount: 1 ether,
paymentToken: address(0),
wallet: address(MY_WALLET)
})
One-time subscription w/ 1 ETH payment but only on successful proof verification example
My contract is at
address(MY_DEFI_CONTRACT)
. I wantMY_DEFI_CONTAINER
to be called with inputs exposed via my contractsgetContainerInputs()
function. I want up to3
nodes to process this computation, onlyonce
, and return lazily. For each of these responses, I want to incentivize the nodes with a payout of1 ETH
per compute response fromaddress(MY_WALLET)
, which I have created via the Wallet Factory and funded with3 ETH
, but only if their response contains a proof that passes verification from theaddress(VERIFIER)
verifier contract.
// 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,
lazy: true, // Lazy return
verifier: address(VERIFIER), // Proof verification via contract implementing IVerifier
// 1 ETH payment
paymentAmount: 1 ether,
paymentToken: address(0),
wallet: address(MY_WALLET)
})
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 interval1
when0 <= t < 10
and2
when10 <= t < 20
. - A subscription that started at time
1500
, occuring only once, is at interval1
whent >= 1500
.
To illustrate, here is a subscription with the parameters:
frequency = 3
, so a maximum of3
intervalsperiod = 10
, so an interval every10s
redundancy = 2
, so up to2
responses per interval
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.sol
:- Exposes
createSubscription()
that allows creating new subscriptions - Exposes
cancelSubscription()
that allows cancelling a created subscription - Exposes
getSubscriptionInterval()
to easily calculate the current interval for a subscription - Exposes the current highest subscription ID via
id()
- Exposes all subscriptions via an ID to subscription mapping:
subscriptions()
- Exposes
deliverCompute()
to let Infernet nodes fulfill subscriptions
- Exposes
EIP712Coordinator.sol
:- Exposes an off-chain delegated subscription creation function:
createSubscriptionDelegatee()
- Exposes
deliverComputeDelegatee()
to let Infernet nodes atomically create and fulfill a signed, off-chain delegate subscription
- Exposes an off-chain delegated subscription creation function:
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:
- Smart contracts create Subscriptions with the coordinator
- 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 eachinterval
- Ensuring unique nodes respond each
interval
- Ensuring payments are escrowed and processed
- Ensuring proof verification occurs successfully
- 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. We provide an out-of-the-box pattern for this in the
Allowlist.
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:
SubscriptionCreated
— a subscription is createdSubscriptionCancelled
— a subscription is cancelledSubscriptionFulfilled
— a subscription receives a response
An off-chain observer like an Infernet node is able to track the state of all subscriptions in the system via the Coordinator.
Tracking subscription inputs
All developer smart contracts 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.
By default, this function is abstracted away from a consumer when inheriting the CallbackConsumer
for simplicity.
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 fulfilleddeliveryInterval
— the subscription interval a response is being delivered for (must be current)input
— optional container input bytesoutput
— optional container output bytesproof
— optional execution proof bytesnodeWallet
— optional nodeWallet
for payments
At the 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 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:
- If neither parameter exists, the on-chain
output
field is empty - If one parameter exists, the on-chain
output
field is the value of that parameter - If both parameters exist, the on-chain
output
is the encoded concatenation of bothrawOutput
andprocessedOutput
(akin toabi.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.
Inbox
The Inbox
is an optional contract that developers can choose to use to lazily store and retrieve timestamp-ordered compute responses from containers in the Infernet system. Two of its applications include:
Lazy response consumption
By default, when creating a subscription with the lazy
parameter set to false
, a response callback to a consuming contract's rawReceiveCompute
function contains all input
, output
, and proof
bytes. Jog your memory by referencing the function signature below:
function rawReceiveCompute(
uint32 subscriptionId,
uint32 interval,
uint16 redundancy,
address node,
// In an eager system (with `lazy == false`), these parameters contain data
bytes calldata input,
bytes calldata output,
bytes calldata proof,
// But, these parameters are empty
bytes32 containerId,
uint256 index
) external {}
In eager systems, this approach works well but leaves responsibility for storage of these compute outputs to a downstream consumer.
With the v1.0.0
release of the Infernet SDK, we introduced a new way to store and consume these outputs lazily by specifying lazy
as true
during subscription creation. With this parameter toggled, rather than responses returning input
, output
, and proof
bytes, they instead store this data to an append-only Inbox
contract, returning a reference containerId
and index
that consumers can use to read from the Inbox
instead.
This approach of lazily storing and consuming data is especially useful for applications that don't need to immediately consume responses. For example, an on-chain smart contract that aggregates over compute outputs, may choose to store responses lazily and retrieve them in the future in bulk. Alternatively, another contract may only choose to retrieve outputs in the case of dispute from a user.
Optimistic writes
The second application of the Inbox
is for optimistic writes.
Today, consumers in the Infernet system are only able to consume compute responses for Subscriptions that they have created and requested. Yet, a common on-chain mechanism (especially used amongst oracle systems) is to optimistically publish responses on-chain that any consumer can read, without having previously requested them.
The Inbox
exposes a function write()
which enables exactly this use-case, allowing any node operator to optimistically submit responses for a container without a consumer requesting a response.
Because anyone can publish to the Inbox
via the write()
function, it is especially important when consuming outputs from the Inbox
to validate the responding node
and a (subscriptionId, interval)
-pair if retrieving a response for a lazy subscription.
Nodes that provide compute responses without associated subscriptions will find their InboxItem
-entries have nullified subscriptionId
and interval
(s). This makes it easy for downstream consumers to safely verify the outputs they consume.
Payments
With the release of Infernet v1.0.0
, the ability to pay for Subscription
(s) was introduced. At its core, payments functionality is made up of a few components:
Wallet
smart contracts used by nodes, consumers, and verifiers (created viaWalletFactory
)IVerifier
-implementing optional proof verification contracts (new participant)- A simple protocol
Fee
registry (with fees currently set to0%
) - Slight modifications to a
Subscription
object to allow specifying payments
Through these components, consumers can now choose to pay for each Subscription
compute response they receive, optionally restricting payment only to nodes that respond with valid proofs of execution.
Wallet
Every
- Consumer that wants to pay for a subscription
- Node that wants to be paid for their compute
- Verifier that wants to be paid for proof verification
must have a Wallet
. Wallet
(s) are simple smart contracts that hold some ETH
or ERC20
balance, and have a single owner
. They can be created by calling createWallet()
on the WalletFactory
.
At any point of time:
- A wallet can have some available balance (that an
owner
can withdraw) - Some escrowed balance (that is temporarily locked in payment escrow, for up to 1-week max)
- Delegate one (or multiple)
consumer
contract(s) to spend its balance (akin toERC20
allowances)
By keeping Wallet
(s) seperate from consumer contracts, the Infernet system ensures safety through isolation. Only an owner
and the Infernet coordinator
itself can manage wallet balances.
For consumers
For consumers that wish to use their Wallet
balance to pay for a subscription, the process is straightforward:
- First, a wallet
owner
mustapprove()
the consumer address to spend sometoken
amount
from its balance - Next, during subscription creation (via
CallbackConsumer
orSubscriptionConsumer
), a consumer needs to simply populate thepaymentAmount
,paymentToken
, andwallet
parameters
For node operators
For node operators that want to be paid for their compute responses, you must simply supply a nodeWallet
(see payment_address
) when submitting responses via deliverCompute()
or deliverComputeDelegatee()
.
By default, the Infernet node handles payments for you.
For proof verifiers
For proof verification smart contracts implementing the IVerifier
interface, a valid Wallet
address must be returned from the getWallet()
function.
End-to-end flows
Payments functionality enables three general flows for consumers, nodes, and verifiers alike.
Payment without proof verification
- A
Wallet
owner
approvesaddress(consumer)
to spend up topaymentAmount
paymentToken
- The consumer contract creates a new
Subscription
that offers to paypaymentAmount
paymentToken
from the aforementionedWallet
for each subscription response - The
Subscription
does not specify anyverifier
- Every time a
node
responds to theSubscription
, it is instantly paidpaymentAmount
paymentToken
from the consumersWallet
, minus some protocolfee
Payment with atomic proof verification
- A
Wallet
owner
approvesaddress(consumer)
to spend up topaymentAmount
paymentToken
- The consumer contract creates a new
Subscription
that offers to paypaymentAmount
paymentToken
from the aforementionedWallet
for each subscription response - The
Subscription
explicitly specifies averifier
contract that must validate the returnedproof
before payment is released. Thisverifier
contract can optionally charge somefee
for its verification service - Every time a
node
responds to theSubscription
, it puts uppaymentAmount
paymentToken
in slashable funds should itsproof
verification fail - Upon submission of a subscription response, the
verifier
contract is atomically called to verify theproof
. If valid (true
), thenode
is instantly paidpaymentAmount
paymentToken
from the consumersWallet
, minus some protocolfee
and verifierfee
. If invalid (false
), the node'sWallet
is slashedpaymentAmount
paymentToken
, and the consumer is instantly paidpaymentAmount
paymentToken
, minus some protocolfee
and verifierfee
Payment with optimistic proof verification
- A
Wallet
owner
approvesaddress(consumer)
to spend up topaymentAmount
paymentToken
- The consumer contract creates a new
Subscription
that offers to paypaymentAmount
paymentToken
from the aforementionedWallet
for each subscription response - The
Subscription
explicitly specifies averifier
contract that must validate the returnedproof
before payment is released. Thisverifier
contract can optionally charge somefee
for its verification service - This
verifier
may take up to 7 days (maximum enforced by Infernet) to verify a proof (imagine this being a multisig wallet or an UMA dispute oracle) - Every time a
node
responds to theSubscription
, it puts uppaymentAmount
paymentToken
in slashable funds should itsproof
verification fail. These funds are escrowed (simply locked in the respectiveWallet
via theCoordinator
) for the duration of the proof verification. - Upon submission of a subscription response, the
verifier
contract is called to verify theproof
- The
verifier
then has 7 days to attest to the validity of the proof. If valid (true
), thenode
is instantly paidpaymentAmount
paymentToken
from the consumersWallet
, minus some protocolfee
and verifierfee
. If invalid (false
), the node'sWallet
is slashedpaymentAmount
paymentToken
, and the consumer is instantly paidpaymentAmount
paymentToken
, minus some protocolfee
and verifierfee
- Should the
verifier
fail to attest within 7 days, any party can finalize the system in favor of thenode
Keen observers will notice that, behind-the-scenes, atomic and optimistic proof verification are implemented in the same way. The key difference being that in the atomic case, funds are escrowed and settled within the same transaction, whereas in the optimistic case, settlement can take up to 7 days.
Capital-constrained nodes who are confident in their proof
validity may choose to exploit Flash Loans (opens in a new tab) to fund their atomic escrow capital requirements.