Infernet
SDK
Patterns
Delegator

Delegator

Technical Reference

By default, Subscriptions are limited to:

On-chain initiation, on-chain fulfillment

  1. Originating on-chain via the Coordinator's createSubscription() function or inherited consumers
  2. Fulfillment on-chain via the Coordinator's deliverCompute() function

But, this severely limits valid use-cases where a developer may want to initiate a subscription entirely off-chain, submitting the request directly to an Infernet node over the Internet, following a pattern that goes something like:

Off-chain initiation, on-chain fulfillment

  1. Originating off-chain via an EIP-712 signed (opens in a new tab) subscription created by the developer on behalf of their contract, posted directly to an Infernet node
  2. Fulfillment on-chain via the EIP712Coordinator's deliverComputeDelegatee() function, which atomically creates a subscription and delivers a response

Introducing the Delegator

A simple way to enable this use-case is to allow developer smart contracts to sign-off on creating a new subscription, entirely off-chain. But, because smart contracts themselves don't have a private key, we can elect an Externally-owned account (EOA) (opens in a new tab) via Delegator.sol that manages this process on behalf of the contract.

Imagine we're trying to replicate the callback subscription we created in the CallbackConsumer, but with the request for compute output initiated completely off-chain:

Inherit Delegator.sol

Starting backwards from the CallbackConsumer example's final-state, we can retrofit off-chain initiation.

In your smart contract, you must inherit the Delegator.sol abstract contract found in infernet/core/patterns/Delegator.sol:

import {Delegator} from "infernet/core/pattern/Delegator.sol";
 
contract MyContract is CallbackConsumer, Delegator {
    // ...
}

Initialize the Delegator

Once inherited, you must provide the initial delegatee (EOA responsible for signing on behalf of the contract) address to the Delegator constructor:

import {Delegator} from "infernet/core/pattern/Delegator.sol";
 
contract MyContract is CallbackConsumer, Delegator {
    // ...
    constructor(
        address infernetCoordinator,
        address delegatee
    ) CallbackConsumer(infernetCoordinator) Delegator(delegatee) {}
    // ...
}

Allow updating delegatee

By default, the Delegator exposes an _updateSigner function (technical reference) that allows the inheriting smart contract to update the assigned delegatee. You may choose to optionally expose this function.

️⚠️

Note that you should only expose _updateSigner to authorized callers in your smart contract, since it allows unilaterally issuing subscriptions on its' behalf.

import {Delegator} from "infernet/core/pattern/Delegator.sol";
 
contract MyContract is CallbackConsumer, Delegator {
    // ...
    constructor(
        address infernetCoordinator,
        address delegatee
    ) CallbackConsumer(infernetCoordinator) Delegator(delegatee) {}
 
    function updateSigner(address newSigner) external onlyAuthorized {
        _updateSigner(newSigner);
    }
    // ...
}

Submit off-chain subscriptions

Now, your delegatee address can sign-off on off-chain Subscriptions, fulfilled by the EIP712Coordinator's createSubscriptionDelegatee() and deliverComputeDelegatee() functions.

Creating off-chain subscriptions

Once we've setup our smart contract to accept off-chain subscriptions, we can now work on generating and signing these subscriptions outside of Solidity. The following examples implement an off-chain subscription in a few common languages:

  import asyncio
  from time import time
  from eth_account import Account
  from web3 import AsyncWeb3, AsyncHTTPProvider
  from constants import COORDINATOR_ABI, INFERNET_COORDINATOR_ADDR
  from eth_account.messages import encode_structured_data, SignableMessage
 
  async def create_signed_subscription() -> None:
      # Setup delegatee private key
      DELEGATEE_PK: str = "0x..."
 
      # Setup my smart contract address
      MY_CONTRACT_ADDR: str = "0x..."
 
      # Setup Web3
      RPC_URL: str = "http://localhost:8545"
      rpc = AsyncWeb3(AsyncHTTPProvider(RPC_URL))
      await rpc.is_connected()
 
      # Setup coordinator contract
      coordinator = rpc.eth.contract(
          address=INFERNET_COORDINATOR_ADDR,
          abi=COORDINATOR_ABI
      )
 
      # Get nonce of my contract
      nonce: int = int(
          await coordinator.functions.maxSubscriberNonce(MY_CONTRACT_ADDR).call()
      )
 
      # Setup expiry 1h into future
      expiry: int = int(time()) + 3600
 
      # Generated typed data of my subscription
      # Here, you would populate details about your subscription
      typed_msg: SignableMessage = encode_structured_data(
          {
              "types": {
                  "EIP712Domain": [
                      {"name": "name", "type": "string"},
                      {"name": "version", "type": "string"},
                      {"name": "chainId", "type": "uint256"},
                      {"name": "verifyingContract", "type": "address"},
                  ],
                  "DelegateSubscription": [
                      {"name": "nonce", "type": "uint32"},
                      {"name": "expiry", "type": "uint32"},
                      {"name": "sub", "type": "Subscription"},
                  ],
                  "Subscription": [
                      {"name": "owner", "type": "address"},
                      {"name": "activeAt", "type": "uint32"},
                      {"name": "period", "type": "uint32"},
                      {"name": "frequency", "type": "uint32"},
                      {"name": "redundancy", "type": "uint16"},
                      {"name": "maxGasPrice", "type": "uint48"},
                      {"name": "maxGasLimit", "type": "uint32"},
                      {"name": "containerId", "type": "bytes"},
                      {"name": "features", "type": "bytes"},
                  ],
              },
              "primaryType": "DelegateSubscription",
              "domain": {
                  "name": "InfernetCoordinator",
                  "version": "1",
                  "chainId": chain_id,
                  "verifyingContract": INFERNET_COORDINATOR_ADDR,
              },
              "message": {
                  "nonce": nonce,
                  "expiry": expiry,
                  "sub": {
                      "owner": owner,
                      "activeAt": active_at,
                      "period": period,
                      "frequency": frequency,
                      "redundancy": redundancy,
                      "maxGasPrice": max_gas_price,
                      "maxGasLimit": max_gas_limit,
                      "containerId": container_id,
                      "features": features,
                  },
              },
          }
      )
 
      # Sign message
      signed_message = Account.sign_message(typed_msg, DELEGATEE_PK)
 
      # Create subscription with off-chain signature
      await coordinator.functions.createSubscriptionDelegatee(
          nonce,
          expiry,
          # Subscription parameters
          (owner, active_at, period, frequency, redundancy, max_gas_price,
           max_gas_limit, model_id, features),
          # Delegatee signature
          signed_message.v,
          signed_message.r.to_bytes(32, "big"),
          signed_message.s.to_bytes(32, "big")
      )
 

Best practices

  1. The EIP712Coordinator does not enforce you to use a monotonically-increasing nonce. This is to prevent situations where successive delegated transactions depend upon one another. A maxSubscriberNonce() function is exposed in the Coordinator that keeps track of the highest nonce your contract has used. We recommend incrementing this value serially in your off-chain signing processes to prevent collisions.