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:
import "./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), gasleft(), 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.
It is recommended to use the payable
modifier for the function that calls asyncCall()
and the function that is called by an async call. If there is a discrepancy between the message value and how much gas needs to be purchased for executing the message and the function being called is not payable
, the message will fail. If the function is payable
, the contract will simply purchase gas and if there is any remainder, credit it to its balance.
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.
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:
import "./Nil.sol";
contract Caller {
using Nil for address;
function call(address dst) public payable {
dst.asyncCall(address(0), address(0), gasleft(), false, msg.value, abi.encodeWithSignature("increment()"));
}
}
Contract 2 has the following structure:
import "./Nil.sol";
contract Receiver {
uint256 private value;
function increment() payable public {
value += 1;
}
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 calls a function inside Contract 2 that makes an async call back to Contract 1.
Contract 1 acts as a simple escrow mechanism:
import "./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 dst, address participantOne, address participantTwo) public payable {
dst.asyncCall(address(0), address(0), 100000, false, 1000000, abi.encodeWithSignature("verify(address, address)", participantOne, participantTwo));
}
function resolve(address participantOne, address participantTwo) public payable {
deposits[participantOne] -= msg.value;
deposits[participantTwo] += msg.value;
}
function verifyExternal(uint256 messageHash, bytes calldata authData) external view returns (bool) {
return true;
}
}
Contract 2 acts as a validator for escrow resolution:
import "./Nil.sol";
contract Validator {
using Nil for address;
function verify(address participantOne, address participantTwo) public payable {
if (participantOne != participantTwo) {
Nil.asyncCall(msg.sender, address(0), address(0), 100000, false, 1000000, abi.encodeWithSignature("resolve(address, address)", participantOne, participantTwo));
}
}
}
Contract 2 triggers the resolve()
function within Contract 1 as a callback but only if the escrow participants are two distinct addresses.