Delegator
By default, Subscriptions are limited to:
On-chain initiation, on-chain fulfillment
- Originating on-chain via the
Coordinator
'screateSubscription()
function or inherited consumers - Fulfillment on-chain via the
Coordinator
'sdeliverCompute()
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
- 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
- Fulfillment on-chain via the
EIP712Coordinator
'sdeliverComputeDelegatee()
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
- The
EIP712Coordinator
does not enforce you to use a monotonically-increasingnonce
. This is to prevent situations where successive delegated transactions depend upon one another. AmaxSubscriberNonce()
function is exposed in theCoordinator
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.