Skip to main content

VRF

AetherLink VRF delivers provably fair and verifiable random numbers, ensuring the randomness used in smart contracts is tamper-proof and transparent. This is particularly useful for gaming, lotteries, and any application that requires trusted randomness.

1. Preparation

1.1 Import proto

First, you need to import oracle-related proto files into your contract project. You can find the latest proto files through the following links:

1.2 Protobuf file

Then you also need to introduce request_interface.proto in the proto file to inherit the oracle callback function to receive the oracle report.

syntax = "proto3";

package demo;

import "aelf/core.proto";
import "aelf/options.proto";
import "acs12.proto";
import "request_interface.proto";

// The namespace of this class
option csharp_namespace = "AElf.Contracts.VRFDemo";

service VRFDemoContract {
// The name of the state class the smart contract is going to use to access blockchain state
option (aelf.base) = "acs12.proto";
option (aelf.base) = "request_interface.proto";
option (aelf.csharp_state) = "AetherLink.Contracts.VRFDemo.VRFDemoContractState";

rpc Initialize (google.protobuf.Empty) returns (google.protobuf.Empty) { }
rpc Play (google.protobuf.Int64Value) returns (google.protobuf.Empty) { }
}

// An event that will be emitted from contract method call when Play is called.
message PlayOutcomeEvent {
option (aelf.is_event) = true;
int64 won = 1;
}

message RecordInfo {
aelf.Address user_address = 1;
int64 play_amount = 2;
}

2. Getting Started

2.1 Background

Here, we will use the scenario of a guess-the-number game in a Game DApp as the background: After a user initiates a "Play" transaction, the DApp contract needs to generate a true random number and then determine its value. To achieve this, an oracle task will be used to generate a verifiable random number off-chain and submit it on-chain. Based on the result of this random number, the user will either receive rewards or have their bet deducted.

2.2 How to initiate a VRF oracle request

First, you need to define how to initiate a VRF oracle request in the contract.

State.OracleContract.SendRequest.Send(new SendRequestInput
{
SubscriptionId = SubscriptionId,
RequestTypeIndex = 2,
SpecificData = specificData,
TraceId = XXXXX // HASH
});
  • OracleContract: This is the target contract address, the oracle contract.
  • SendRequest: This is the method name for sending the VRF Request to the target contract.
  • SendRequestInput: This is the input parameter of the method for sending transactions to the target contract.
Param NameExplanationType
SubscriptionIdManage the service fee based on this subscription idint32
RequestTypeIndexTask Typeint32, 1=Datafeeds | 2=VRF
SpecificDataDetailed description of VRF tasksByteString
TraceIdThis ID can be used as a unique index to manage your oracle tasks.Aelf.Hash

2.3 Why TraceId?

First, you need to understand that an oracle task is an asynchronous execution process that goes from off-chain to on-chain, back to off-chain, and is finally submitted on-chain by the oracle node. Therefore, you need to store the traceId as an index in the first transaction, and then match it with the information in the second transaction. Here, we add a State called playedRecord in the contract, using traceId as the key for the historical record, and storing the metadata of the historical record as the value.

using AElf.Sdk.CSharp.State;
using AElf.Types;

namespace AElf.Contracts.aetherlink_demo
{
// The state class is access the blockchain state
public partial class aetherlink_demoState : ContractState
{
// A state to check if contract is initialized
public BoolState Initialized { get; set; }
// A state to store the owner address
public SingletonState<Address> Owner { get; set; }

public MappedState<Hash, RecordInfo> PlayedRecords { get; set; }
}
}

2.4 How to generate VRF SpecificData

First, you need to specify an oracle node to perform your random number generation task.

var keyHashs = State.OracleContract.GetProvingKeyHashes.Call(new Empty());
var keyHash = keyHashs[0];

Then bind the oracle node KeyHash in your VRF task and specify the number of random numbers to be generated.

var specificData = new AetherLink.Contracts.VRF.Coordinator.SpecificData
{
KeyHash = keyHash,
NumWords = 1,
RequestConfirmations = 1
}.ToByteString();
ParamsExplanationType
KeyHashOracle Node Public Key HashHash
NumWordsNumber of Random Hashs Generatedint32
RequestConfirmationsNumber of Blocks to Wait for Transmittedint32

2.5 How to handle oracle vrf callbacks

The length of the HashList depends on the NumWords you specified when creating the oracle task, which is the number of random hashes generated. Next, you can use this random hash for your random number game.

public override Empty HandleOracleFulfillment(HandleOracleFulfillmentInput input)
{
var randomHashList = HashList.Parser.ParseFrom(input.Response);
...
}

2.6 Complete code

You can get the complete contract code from GitHub: https://github.com/AetherLinkProject/aetherLink-contracts/tree/feature/aetherlink-vrf-demo/contract/AetherLink.Contracts.VRFDemo

using AElf;
using AElf.Contracts.MultiToken;
using AElf.Contracts.VRFDemo;
using AElf.Sdk.CSharp;
using AElf.Types;
using AetherLink.Contracts.Consumer;
using AetherLink.Contracts.Oracle;
using AetherLink.Contracts.VRF.Coordinator;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;

namespace AetherLink.Contracts.VRFDemo;

public partial class VRFDemoContract : VRFDemoContractContainer.VRFDemoContractBase
{
private const string OracleContractAddress = "21Fh7yog1B741yioZhNAFbs3byJ97jvBmbGAPPZKZpHHog5aEg"; // tDVW oracle contract address
private const string TokenSymbol = "ELF";
private const long MinimumPlayAmount = 1_000_000; // 0.01 ELF
private const long MaximumPlayAmount = 1_000_000_000; // 10 ELF
private const long SubscriptionId = 1; // input your subscriptionId

// Initializes the contract
public override Empty Initialize(Empty input)
{
Assert(State.Initialized.Value == false, "Already initialized.");
State.Initialized.Value = true;
State.TokenContract.Value = Context.GetContractAddressByName(SmartContractConstants.TokenContractSystemName);
State.OracleContract.Value = Address.FromBase58(OracleContractAddress);
return new Empty();
}

public override Empty HandleOracleFulfillment(HandleOracleFulfillmentInput input)
{
var userRecord = State.PlayedRecords[input.TraceId];
if (userRecord != null) return new Empty();
var randomHashList = HashList.Parser.ParseFrom(input.Response);
var userAddress = userRecord.UserAddress;
var playAmount = userRecord.PlayAmount;
if (IsWinner(randomHashList.Data[0]))
{
State.TokenContract.Transfer.Send(new TransferInput
{
To = userAddress,
Symbol = TokenSymbol,
Amount = playAmount
});

Context.Fire(new PlayOutcomeEvent
{
Won = playAmount
});
}
else
{
State.TokenContract.TransferFrom.Send(new TransferFromInput
{
From = userAddress,
To = Context.Self,
Symbol = TokenSymbol,
Amount = playAmount
});

Context.Fire(new PlayOutcomeEvent
{
Won = -playAmount
});
}

return new Empty();
}

public override Empty Play(Int64Value input)
{
var playAmount = input.Value;
Assert(playAmount is >= MinimumPlayAmount and <= MaximumPlayAmount, "Invalid play amount.");
var balance = State.TokenContract.GetBalance.Call(new GetBalanceInput
{
Owner = Context.Sender,
Symbol = TokenSymbol
}).Balance;
Assert(balance >= playAmount, "Insufficient balance.");

var contractBalance = State.TokenContract.GetBalance.Call(new GetBalanceInput
{
Owner = Context.Self,
Symbol = TokenSymbol
}).Balance;
Assert(contractBalance >= playAmount, "Insufficient contract balance.");

var keyHashs = State.OracleContract.GetProvingKeyHashes.Call(new Empty());
var keyHash = keyHashs.Data[0];
var specificData = new SpecificData
{
KeyHash = keyHash,
NumWords = 1,
RequestConfirmations = 1
}.ToByteString();

var request = new SendRequestInput
{
SubscriptionId = SubscriptionId,
RequestTypeIndex = 2,
SpecificData = specificData,
};

var traceId = HashHelper.ConcatAndCompute(
HashHelper.ConcatAndCompute(HashHelper.ComputeFrom(Context.CurrentBlockTime),
HashHelper.ComputeFrom(Context.Origin)), HashHelper.ComputeFrom(request));
request.TraceId = traceId;
State.OracleContract.SendRequest.Send(request);

State.PlayedRecords[traceId] = new()
{
UserAddress = Context.Sender,
PlayAmount = input.Value
};

return new Empty();
}

private bool IsWinner(Hash randomHash)
=> int.Parse(randomHash.ToHex().Substring(0, 8), System.Globalization.NumberStyles.HexNumber) % 2 == 0;
}

2.7 Interact with Your Deployed Smart Contract

2.7.1 Playing the Lottery Game

$ aelf-command send ${CONTRACT_ADDRESS} -a ${WALLET_ADDRESS} -p ${WALLET_PASSWORD} -e https://tdvw-test-node.aelf.io Play
  • Wait for the off-chain oracle node to execute (approximately 3-9 seconds), then check your balance.
$ aelf-command call ASh2Wt7nSEmYqnGxPPzp4pnVDU4uhj1XW9Se5VeZcX2UDdyjx -a ${WALLET_ADDRESS} -p ${WALLET_PASSWORD} -e https://tdvw-test-node.aelf.io GetBalance
  • You will be prompted for the following:
    - Enter the required param `<symbol>`: ELF
- Enter the required param `<owner>`: $WALLET_ADDRESS