Skip to main content

Handling async execution

To support cross-shard communication, =nil; supports smart contracts making async calls to one another directly in Solidity code.

This tutorial provides a brief theoretical and practical primer on async execution, and gives suggestions on how this feature can be integrated into an application.

Performing an async call

Consider a smart contract with the following code:

pragma solidity ^0.8.9;

import "@nilfoundation/smart-contracts/contracts/Nil.sol";

contract CallerAsync {
using Nil for address;

event CallCompleted(address indexed dst);

function call(address dst) public payable {
dst.asyncCall(
address(0),
address(0),
500000,
Nil.FORWARD_NONE,
false,
msg.value,
abi.encodeWithSignature("funcName")
);
emit CallCompleted(dst);
}
}

The asyncCall() function makes it possible for the contract to call a function in another smart contract regardless of the shard where this contract is located. There are no additional actions required for making async calls: simply adding asyncCall() and specifying its arguments is sufficient.

info

The asyncCall() function is a shortcut for executing a precompiled contract responsible for managing async calls across the cluster.

tip

To initiate an async call, the 'caller' contract must have sufficient funds to pay for initiating a new message.

Async calls on the same shard

asyncCall() works even if the contract with the specified address is located on the same shard. The mechanism remains exactly the same as with cross-shard communications: the function will produce a new message, and the requested function in another contract will be executed whenever said message is processed by the shard. This usually occurs within the space of two-three blocks.

Retreiving values

The Nil.sol extension library also exposes the awaitCall() function.

The function allows for calling another smart contract asynchronously and retrieving the result of the call. This is useful if the contract logic needs to mimick the typical async/await flow prevalent in programming languages other than Solidity. Note that the feature is currently experimental and may change in future releases.

State

State is not persistent before and after asyncCall, and can be changed.

Consider the following example of an Awaiter contract:

pragma solidity ^0.8.11;

import "@nilfoundation/smart-contracts/contracts/Nil.sol";

contract Awaiter {
using Nil for address;

uint256 public result;

function call(address dst) public{
bytes memory temp;
bool ok;
(temp, ok) = Nil.awaitCall(
dst,
Nil.ASYNC_REQUEST_MIN_GAS,
abi.encodeWithSignature("getValue()")
);

require(ok == true, "Result not true");

result = abi.decode(temp, (uint256));
}

function getResult() public view returns (uint256) {
return result;
}
}

The contract is designed to call theCounter contract asynchronously and store the returned value inside the result variable.

info

When using awaitCall every call produces a new message that must be processed for the destination shard to retreive the necessary value. If many such calls are sent, execution will be halted for each call and only resumed after receiving a response. In contrast, many asyncCalls can be sent within the same block without halting execution.

Payment

The processing of sending a request message using awaitCall and receiving a response can only be paid by the message value.

Examples

Consider two contracts deployed on two different shards in =nil;:

  • Contract 1 (caller) is deployed on Shard 1
  • Contract 2 (receiver) is deployed on Shard 2

There are two possible patterns for cross-shard communication between Contract 1 and Contract 2.

Calling a contract on another shard

In this pattern, Contract 1 simply calls a function in Contract 2 without receiving a result.

Contract 1 has the following structure:

pragma solidity ^0.8.9;

import "@nilfoundation/smart-contracts/contracts/Nil.sol";

contract Caller {
using Nil for address;

event CallCompleted(address indexed dst);

function call(address dst) public payable {
dst.asyncCall(
address(0),
address(0),
500000,
Nil.FORWARD_NONE,
false,
0,
abi.encodeWithSignature("funcName")
);
emit CallCompleted(dst);
}
}

Contract 2 has the following structure:

pragma solidity ^0.8.0;

contract Counter {
uint256 private value;

event ValueChanged(uint256 newValue);

function increment() public {
value += 1;
emit ValueChanged(value);
}

function getValue() public view returns (uint256) {
return value;
}
}

Whenever the call() function is called inside Contract 1, a new outgoing message is spawned. When Shard 2 picks up this message and processes it, Contract 2 calls its increment() function. After getValue() is called, the result should display the total number of times the call() function was executed by Contract 1.

Callback pattern

In this pattern, Contract 1 defines a special callback function and then uses the sendRequest() method to perform an async request to another shard. Whenever the request executes, a response message is sent back to the caller. The callback function is then executed and the response value can be retrieved via the responseData argument.

tip

When using sendRequest(), the contract can continue execution without having to wait for the response. There are also no manipulations with the contract state in contrast to awaitCall(). This makes sendRequest() much cheaper compared to awaitCall().

tip

The sendRequest() also has an additional signature that allows for sending custom currencies to contracts while performing requests:

function sendRequestWithTokens(
address dst,
uint256 value,
Token[] memory tokens,
bytes memory context,
bytes memory callData
) internal {}

Contract 1 acts as a simple escrow mechanism. The submitForVerification() function accepts the address of the validator and the addresses of the escrow participants. The function then sends a request to the validator while assigning resolve() as the callback. Whenever the validator processes the request, Contract 1 can retrieve the returned data.

pragma solidity ^0.8.9;

import "@nilfoundation/smart-contracts/contracts/Nil.sol";

contract Escrow {
using Nil for address;
mapping(address => uint256) private deposits;

function deposit() public payable {
deposits[msg.sender] += msg.value;
}

function submitForVerification(
address validator,
address participantOne,
address participantTwo
) public payable {
bytes memory context = abi.encodeWithSelector(this.resolve.selector, participantOne, participantTwo, msg.value);
bytes memory callData = abi.encodeWithSignature("validate(address, address)", participantOne, participantTwo);
Nil.sendRequest(validator, 0, Nil.ASYNC_REQUEST_MIN_GAS, context, callData);
}

function resolve(
bool success,
bytes memory returnData,
bytes memory context
) public payable {
require(success, "Request failed!");
(address participantOne, address participantTwo, uint256 value) = abi.decode(context, (address, address, uint256));
bool isValidated = abi.decode(returnData, (bool));
if (isValidated) {
deposits[participantOne] -= value;
deposits[participantTwo] += value;
}
}

function verifyExternal(
uint256 messageHash,
bytes calldata authData
) external view returns (bool) {
return true;
}
}

Contract 2 acts as the validator for escrow resolution:

pragma solidity ^0.8.9;

import "@nilfoundation/smart-contracts/contracts/Nil.sol";
contract Validator {
using Nil for address;

function validate(
address participantOne,
address participantTwo
) public returns (bool) {
bool isValidated = (participantOne != participantTwo);
return isValidated;
}
}