Schedule Randomness With Gelato, Witnet, API3, and Chainlink Vrf
Learn how to use Gelato in conjunction with Witnet, API3, and Chainlink Vrf to automate randomness.
Join the DZone community and get the full member experience.
Join For FreeFrom roulette wheels to action-adventure games, randomness plays a major role in the gaming world. It is for any scenario where opponents or scenes are created dynamically and for those where users need “on the fly” randomness.
This is a far cry from my memories of the 1980s, when gaming was a lot less enjoyable precisely because everything was predictable- if your character went on a mission once, the next time you played the mission would be exactly the same. As you can imagine, this got very boring very quickly.
However, randomness has many other uses in the web3 ecosystem such as:
DAOs & Public participation processes: One of the greatest web3 characteristics is the existence of communities driving projects. In the long term, I think the projects that will succeed will be those with robust and active communities. In this case, randomness is required to ensure fairness in the reward and participation processes.
NFT generation: Over the past 5 years we have seen the exponential growth of NFTs and digital arts. In some cases, the NFT collections have random characteristics which influence the value.
Lottery: Beyond the traditional lottery games, the DeFi ecosystem allows for innovation such as “no-loss” lottery.
Due to the nature of the blockchain, where miners can create/discard blocks, and “re-roll” the dice until a specific value is found, no manipulable randomness can’t be created on-chain.
In all cases but especially in Lottery and NFT Generation where large value amounts are present, it is imperative to use rrandomness that cannot be manipulated.
Current Solutions
Most of the current solutions are using an Oracle off-chain. We must request numbers, which (once available) will enable us to execute our custom logic. In this article we will explore:
Witnet
API3 QRNG
Chainlink VRF
In this article, I will showcase how to implement the three solutions in conjunction with Gelato.
Automation and Randomness
The need for self-executing methods within smart contracts is growing every day. However the “Big Bang” will happen when blockchain technology will extend into non-blockchain business.
For instance, all processes (no matter the industry) are subject to quality control which relies on random sampling to run the controls to ensure that no bias is present.
Due to the specifics of acquiring random numbers in blockchain through an Oracle, two methods are needed:
One to request the random number to the oracle
The second is a callback for when the number is available. Chainlink Vrf and API3 QRNG execute the callback code in your contract, however, Witnet does not.
Gelato comes in handy to tackle this situation by creating a task, that will check when the random number is available and execute any custom logic. This task will be cancelled after one run
In our showcase project, we will demo this use case for Witnet, Chainlink, and API3.
Show Case Project
Let’s imagine for a moment that we run an automobile factory, and that our main assembly machine has 20 components. Although we do periodic maintenance (one more example of automation), we are required to run random quality control every 10 minutes. Here’s what that would look like:
Choosing 2 components, every component can be controlled once per hour.
Choosing the level of control: express, medium or intensive
Choosing one employee of the 500 to run the quality control
In order to run tamper-proof quality control and avoid bias, our system has to generate random results for all steps.
The repo can be found here.
We will use Gelato as the Master of Ceremony orchestrating all tasks to be run like calling oracles, checking whether results are available, and updating the components arrays. etc.. The deployed dapp on Goerli is live at https://gelato-vrf.web.app
The major contract “ScheduleTheRandomness” can be found here. Etherscan verified logs at:
https://goerli.etherscan.io/address/0x6a0C105A74Ed3359ADDd877049BC129e224b48c0#code
Step 1) Gelato as the Master of Ceremony
We will use gelato to run the plan, every 15 min we will do quality control. In order to do that, we must:
a) Wire the Gelato Infrastructure, first, we have to copy IOPS.sol
and OpsReady.sol
both files can be found here
b) After copying the files, we can then insert the import and inherit the contract:
import {OpsReady} from "./gelato/OpsReady.sol";
import {IOps} from "./gelato/IOps.sol";
contract ScheduleTheRandomness is OpsReady constructor( address payable ops)
OpsReady(ops) { IOps(ops).createTimedTask(,,,);
}
c) Create a timed task:
function createQualityPlanTask() public {
bytes32 taskId = IOps(ops).createTimedTask(
0,
900,
address(this),
this.doQualityControl.selector,
address(this),
abi.encodeWithSelector(this.checkQualityPlanIsActive.selector),
ETH,
false);
qualityPlanTaskId = taskId;
}
This task will check whether the 15 minutes have passed and if an older control is still running checkQualityPlanIsActive()
and then run the doQualityControl()
method where the control flow starts with the different calls to request random numbers.
One very interesting usage of gelato is to execute the custom logic once the random number is available. In the case of Witnet we are obliged to do so as Witnet does not provide a callback, but also we can implement it in Chainlink or API3 to have more control over the callback execution.
We can use Gelato for creating requests to the randomness Oracles as well as to take control over the callback execution
Step 2) API3 QRNG for Random Components
In this step, we will use API3 QRNG by API3 to generate 2 random components to be controlled.
a) We are required to wire the API3 infrastructure into our contract meaning that:- It is necessary to add npm i @api3/airnode-protocol
, and import “@api3/airnode-protocol/contracts/rrp/requesters/RrpRequesterV0.sol";
into your contract.
- We must inherit the contract and pass in the constructor the airnode address for the respective chain, addresses can be found here.
// Goerli 0xa0AD79D995DdeeB18a14eAef56A549A04e3Aa1Bd
contract ScheduleTheRandomness is RrpRequesterV0 {
constructor(address _airnodeRrp)
RrpRequesterV0(_airnodeRrp){
} }
b) To retrieve QRNG we must fund the airnode. To do this, we must create and fund a Sponsor Wallet, either through the API3 Admin CLI or by deriving the sponsor wallet address from the contract address:- npm i @api3/airnode-admin
to derive the sponsor wallet- Call the deriveSponsorWalletAddress().
The xpub and airnode params can be found in the API3 provider section here.
API3 QRNG Quick Recap: we have set and wired the infrastructure, and created(& funded) a sponsor wallet to pay for transactions. Now we will set the parameters and the respective methods
c) Setting the params. We need to pass the following params to our contract
// we will endpointIdUint256 if we only want one random nuumber back // or endpointIdUint256Array if we want more than one random number // as result
function setRequestParameters(
address _airnode, ///
bytes32 _endpointIdUint256Array,
address _sponsorWallet) external {
airnode = _airnode;
endpointIdUint256Array = _endpointIdUint256Array;
sponsorWallet = _sponsorWallet;
}
Both endpointIdUint256
and endpointIdUintArray256
can also be found at the API3 provider section here.
d) In this last step we will need to create the two methods airnodeRrp.makeFullRequest()
and the callback once the random have been delivered, in our case fullfillRandomComponents()
as defined in the request.
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "hardhat/console.sol";
import "@api3/airnode-protocol/contracts/rrp/requesters/RrpRequesterV0.sol";
contract ScheduleTheRandomness is RrpRequesterV0 {
// #region ====== API3 QRNG STATE ================
event RequestedUint256Array(bytes32 indexed requestId, uint256 size);
event ReceivedUint256Array(bytes32 indexed requestId, uint256[] response);
mapping(bytes32 => bool) public expectingRequestWithIdToBeFulfilled;
address public airnode;
bytes32 public endpointIdUint256Array;
address public sponsorWallet;
uint256[] public qrngUint256Array;
// #endregion ====== API3 QRNG STATE ================
constructor(
address _airnodeRrp,
uint64 subscriptionId
) rpRequesterV0(_airnodeRrp) { }
// #region ========= STEP 2 GET RANDOM COMPONENTS WITH API3
/// @notice Sets parameters used in requesting QRNG services
/// @dev No access control is implemented here for convenience. This is not
/// secure because it allows the contract to be pointed to an arbitrary
/// Airnode. Normally, this function should only be callable by the "owner"
/// or not exist in the first place.
/// @param _airnode Airnode address
/// @param _endpointIdUint256Array Endpoint ID used to request a `uint256[]`
/// @param _sponsorWallet Sponsor wallet address
function setRequestParameters(
address _airnode,
bytes32 _endpointIdUint256Array,
address _sponsorWallet
) external {
airnode = _airnode;
endpointIdUint256Array = _endpointIdUint256Array;
sponsorWallet = _sponsorWallet;
}
/// @notice Requests a `uint256[]`
/// @param size Size of the requested array
function makeRequestAPI3RandomComponents(uint256 size) public {
qrngUint256Array = [0, 0];
bytes32 requestId = airnodeRrp.makeFullRequest(
airnode, //// airnode provider
endpointIdUint256Array, /// type of
address(this),
sponsorWallet,
address(this),
this.fulfillRandomComponents.selector,
// Using Airnode ABI to encode the parameters
abi.encode(bytes32("1u"), bytes32("size"), size)
);
// uint256 requestId = 123;
expectingRequestWithIdToBeFulfilled[requestId] = true;
}
/// @notice Called by the Airnode through the AirnodeRrp contract to
/// fulfill the request
/// @param requestId Request ID
/// @param data ABI-encoded response
function fulfillRandomComponents(bytes32 requestId, bytes calldata data)
external
onlyAirnodeRrp
{
require(
expectingRequestWithIdToBeFulfilled[requestId],
"Request ID not known"
);
expectingRequestWithIdToBeFulfilled[requestId] = false;
qrngUint256Array = abi.decode(data, (uint256[]));
}
// #endregion STEP 2 GET RANDOM COMPONENTS WITH API3
}
Step2: Witnet for Random Control Type
In this step, we will request a random number between 1 and 3 to represent the three different control types (express, medium, intensive). To do so we will use the **Randomness Contrac**t by the Witnet oracle.
a) We are required to wire the API3 infrastructure into our contract:npm i witnet-solidity-bridge
, and import “witnet-solidity-bridge/contracts/interfaces/IWitnetRandomess.sol";
.
b) To interact with the Randomness Contract we will need to create an instance of it within our contract. First, we must copy the chain address we are working on (contract addresses). In this particular case, we are on Goerli, so we will pass the Goerli’s Randomness Contract address by deployment. After which, within the constructor, we will create an instance of the Randomness Contract
constructor(IWitnetRandomness _witnetRandomness){ witnet = _witnetRandomness; }
c) We will request a random number from Witnet
function getRandomControlType() public returns (uint8 _controlType) {
latestRandomizingBlock = block.number;
uint256 _usedFunds = witnet.randomize{ value: 0.1 ether }();
if (taskIdByBlock[latestRandomizingBlock] == bytes32(0){
createTaskQualityControl();
}
}
d) Unlike API3 and Chainlink Vrf Witnet does not provide a callback, however, the Randomness Contract provides theisRandomized()
, a method that returns whether the random number can be retrieved or not. Once isRandomized()
is true, we call witnet.random(3,0,latestRandomizingBlok);
We will use Gelato to create an “on the fly” task, that will check when
isRandomized()
returns true, and then will retrieve the random number and continue the control flow
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "hardhat/console.sol";
import {OpsReady} from "./gelato/OpsReady.sol";
import {IOps} from "./gelato/IOps.sol";
import "witnet-solidity-bridge/contracts/interfaces/IWitnetRandomness.sol";
contract ScheduleTheRandomness is
OpsReady,
Ownable
{
// #region ====== WITNET RANDOMNESS CONTRACT STATE ================
uint32 public randomness;
uint256 public latestRandomizingBlock;
mapping(uint256 => bytes32) public taskIdByBlock;
IWitnetRandomness witnet;
// #endregion ====== WITNET RANDOMNESS CONTRACT STATE ================
constructor(
address payable _ops,
IWitnetRandomness _witnetRandomness
)
OpsReady(_ops)
{
witnet = _witnetRandomness;
}
// #region ========= STEP 3 GET CONTROL TYPE WITH WITNET
// Random numbers request
function getRandomControlType() public returns (uint8 _controlType) {
latestRandomizingBlock = block.number;
uint256 _usedFunds = witnet.randomize{value: 0.1 ether}();
if (taskIdByBlock[latestRandomizingBlock] == bytes32(0)) {
createTaskQualityControl();
}
}
// Gelato Task to check whether the random mumber is available under checkerIsRandomized()
// and then execute qualityControlDelivered()
function createTaskQualityControl() public {
bytes32 taskId = IOps(ops).createTaskNoPrepayment(
address(this),
this.qualityControlDelivered.selector,
address(this),
abi.encodeWithSelector(this.checkerIsRandomized.selector),
ETH
);
taskIdByBlock[latestRandomizingBlock] = taskId;
}
// Check if random number is delivered
function checkerIsRandomized()
public
view
returns (bool canExec, bytes memory execPayload)
{
canExec = isRandomnize();
execPayload = abi.encodeWithSelector(this.qualityControlDelivered.selector);
}
function isRandomnize() public view returns (bool ready) {
ready = witnet.isRandomized(latestRandomizingBlock);
}
// Cancel the task as we will require every time only one execution
function cancelQualityTypeByID(bytes32 _taskId) public {
IOps(ops).cancelTask(_taskId);
taskIdByBlock[latestRandomizingBlock] = bytes32(0);
}
// Custome logic to be executed
function qualityControlDelivered() external onlyOps {
assert(latestRandomizingBlock > 0);
randomness = 1 + witnet.random(3, 0, latestRandomizingBlock);
uint256 fee;
address feeToken;
(fee, feeToken) = IOps(ops).getFeeDetails();
_transfer(fee, feeToken);
cancelQualityTypeByID(taskIdByBlock[latestRandomizingBlock]);
}
// #endregion STEP 3 GET CONTROL TYPE WITH WITNET
}
In the next image, we can see both tasks, the first one running every 15 minutes for overall quality control and the second one is active only when we are awaiting the Witnet random number.
STEP 3: Chainlink Vrf to Choose Employee
Our last step is to pick up an employee to run the controls. In order to do so, we will use Chainlink Vrf to get a random number between 1 and 500 (the total number of employees).
a) Chainlink Vrf requires us to create a funded subscription with Link that will later be used by our contract (consumer contract) to pay the fees for requesting random numbers. You can do that here
Once the subscription is created and funded, you can add your contract as a consumer.
Once your contract is added as a consumer to the subscription we can continue with the settings
b) We are required to wire the Chainlink Vrf infrastructure into our contract which means:
-npm i @chainlink/contracts
, and
import “@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol”;
import “@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol”
- Inherit the contract and pass in the constructor the subscription ID and the VRF Coordinator address for the respective chain. Addresses can be found here (in our case is Goerli)
// Goerli
address vrfCoordinator = address(0x2Ca8E0C643bDe4C2E08ab1fA0da3401AdAD7734D);
constructor(
uint64 subscriptionId
)
VRFConsumerBaseV2(vrfCoordinator)
{
// Instance of the coordinator
COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
// Subscription Id
s_subscriptionId = subscriptionId;
}
c) Now that our funding mechanism is ready and contract infrastructure is wired, we must write our request and callback methods COORDINATOR.requestRandomWords()
and fullfillRandomwords()
. Chainlink Vrf offers us the ability to set different parameters when calling requestRandomWords(),
the code and params can be found here:
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "hardhat/console.sol";
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
contract ScheduleTheRandomness is
VRFConsumerBaseV2
{
// #region ====== CHAINLINK VRF STATE ================
VRFCoordinatorV2Interface COORDINATOR;
// Your subscription ID.
uint64 s_subscriptionId;
// Goerli coordinator. For other networks,
// see https://docs.chain.link/docs/vrf-contracts/#configurations
address vrfCoordinator = address(0x2Ca8E0C643bDe4C2E08ab1fA0da3401AdAD7734D);
// The gas lane to use, which specifies the maximum gas price to bump to.
// For a list of available gas lanes on each network,
// see https://docs.chain.link/docs/vrf-contracts/#configurations
bytes32 keyHash =
0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15;
// Depends on the number of requested values that you want sent to the
// fulfillRandomWords() function. Storing each word costs about 20,000 gas,
// so 100,000 is a safe default for this example contract. Test and adjust
// this limit based on the network that you select, the size of the request,
// and the processing of the callback request in the fulfillRandomWords()
// function.
uint32 callbackGasLimit = 100000;
// The default is 3, but you can set this higher.
uint16 requestConfirmations = 3;
// For this example, retrieve 2 random values in one request.
// Cannot exceed VRFCoordinatorV2.MAX_NUM_WORDS.
uint32 numWords = 1;
uint256 public employeeId;
uint256 public s_requestId;
address s_owner;
// #endregion ====== CHAINLINK VRF STATE ================
constructor(
uint64 subscriptionId
)
VRFConsumerBaseV2(vrfCoordinator)
{
COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
s_owner = msg.sender;
s_subscriptionId = subscriptionId;
}
// region ========= STEP 4 GET EMPLOYEEID with CHAINLINK VRF
// Assumes the subscription is funded sufficiently.
function requestEmployeByChainlink() public {
// Will revert if subscription is not set and funded.
s_requestId = COORDINATOR.requestRandomWords(
keyHash,
s_subscriptionId,
requestConfirmations,
callbackGasLimit,
numWords
);
}
function fulfillRandomWords(
uint256, /* requestId */
uint256[] memory randomWords
) internal override {
employeeId = randomWords[0] & (500 + 1);
}
// endregion STEP 4 GET EMPLOYEEID with CHAINLINK VRF
}
Conclusion
In my personal opinion, all the technologies mentioned in this article (Gelato Network, Chainlink Vrf, Witnet, and API3 QRNG) are incredibly user-friendly and easy to learn.
Step by step, the web3 community is solving in a very efficient way the blockchain inherent problems. In the same way, Gelato Network solves the problem of scheduled tasks, and the three Vrf technologies analyzed here offer a very efficient way to create and consume random numbers on-chain.
Gelato Network can be used in different ways when dealing with randomness, from calling a random number at a specific point in time, to once the random number has been delivered, waiting for other conditions to be met in order to execute additional logic.
In our showcase project, for the sake of simplicity, we have done three random consecutively, but we could also parallelize the random calls and create a task that checks whether all three numbers are already available and execute the custom logic.
Moving forward we can foresee automation protocols like Gelato Network as a perfect match for Vrf generation solutions by providing a very elegant way of scheduling random numbers requests and orchestrating the callbacks and execution logic.
Thank you for reading!!
If you have questions or comments, please ping me on twitter @donoso_eth
Resources
API3
Witnet
Chainlink Vrf
Gelato
Published at DZone with permission of Javier Donoso. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments