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 Caller {
using Nil for address;
event CallCompleted(address indexed dst);
function call(address dst) public payable {
dst.asyncCall(address(0), 0, 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.
The asyncCall()
function is a shortcut for executing a precompiled contract responsible for managing async calls across the cluster.
To initiate an async call, the 'caller' contract must have sufficient funds to pay for initiating a new message.
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 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.
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.
The processing of sending a request message using awaitCall
and receiving a response can only be paid by the message value
.
As its second argument, awaitCall()
accepts the amount of gas that will be reserved instantly and will later be used to process the received response.
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;
receive() external payable {}
function call(address dst) public {
Nil.asyncCall(
dst,
msg.sender,
0,
abi.encodeWithSignature("increment()")
);
}
function verifyExternal(
uint256,
bytes calldata
) external pure returns (bool) {
return true;
}
}
Contract 2 is theCounter
contract.
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.
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()
.
As its third argument, sendRequest()
accepts the amount of gas that will be reserved instantly and will later be used to process the execution of the callback function.
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 is NilBase {
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 onlyResponse {
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;
}
}