# WINkLink Verifiable Random Number Service

# Overview

Verifiable Random Function (VRF) is the public-key version of a keyed cryptographic hash, which can be used as a random number. Only the holder of the private key can compute the hash, but anyone with the public key can verify the correctness of the hash. VRF can be used to generate secure and reliable random numbers.

Random number is determined by seed (provided by users), nonce (private state of VRFCoordinator contract) , block hash (the block of the request event) and private key of oracle node.

The generation process of VRF is:

  • A Dapp contract sends out an on-chain request for a random number;
  • Once the off-chain oracle node listens for the request, it generates a random number attaching the cryptographic proof to make the generated random number verifiable, and then submits them back to an oracle contract (VRFCoordinator);
  • Once the random number proof is verified by the oracle contract, the random number is published to the Dapp contract through a callback function.

The process above ensures that the random number cannot be tampered with nor manipulated by anyone, including oracle operators, miners, users and even smart contract developers.

WINkLink VRF is a provably-fair and verifiable source of randomness designed for Dapp contracts. Dapp contract developers can use WINkLink VRF as a tamper-proof RNG (Random Number Generator) to build reliable smart contracts for any applications which rely on unpredictable random number:

  • Blockchain games and NFTs
  • Random assignment of duties and resources (e.g. randomly assigning judges to cases)
  • Choosing a representative sample for consensus mechanisms

The WINkLink VRF solution contains both off-chain and on-chain components:

  • VRF Coordinator (on-chain component): Crafted to interact with the VRF service. It emits an event when a request for randomness is initiated, and then validates the resultant random number and proof of how it was generated by the VRF service.
  • VRF Wrapper (on-chain component): A wrapper for the VRF Coordinator that offers an interface for consuming contracts.
  • VRF service (off-chain node): Listens for requests by subscribing to the VRF Coordinator event logs and calculates a random number based on the block hash and nonce. The VRF service then sends a transaction to the VRFCoordinator including the random number and a proof of how it was generated.

# Direct Funding flow

vrf-direct-funding-flow.png

# Subscription flow

vrf-subscription-flow.png

# Tron Nile VRF Contracts

For convenience, Nile testnet has deployed WinkMid contract and encapsulated the WIN token on it. Developers can use this contract address directly without additional deployment. Users can also claim test TRX and WIN tokens from the Faucet address provided by Nile testnet.

Item Value
WIN Token TNDSHKGBmgRx9mDYA9CnxPx55nu672yQw2
WinkMid TLLEKGqhH4MiN541BDaGpXD7MRkwG2mTro
BlockHashStore TBpTbK9KQzagrN7eMKFr5QM2pgZf6FN7KA
VRFCoordinatorV2 TDidecxMyGMgqvYS7nmpMQCZ16HqqV5Fke
VRFV2Wrapper TMNRLGXhe3gzbUyWccuQAKhfVKFyqmLE1W
Fee 10 WIN

Testnet Faucet: https://nileex.io/join/getJoinPage (opens new window)

# Tron Mainnet VRF Contracts

Item Value
WIN Token TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7
WinkMid TVMhaFMynYqTRLB1xShYL7wBwdmQHH6bKV
BlockHashStore TRGmef4qUdNJ4xTEL96hToGuMTNst57aS1
VRFCoordinatorV2 TZCz1BcnYviUNDiLvG6ZeuC477YupDgDA7
VRFV2Wrapper TGDVwQRKwtNHrwy4RFG49b2HTHkvWckP5N
Fee 10 WIN

# VRF Request Process

  1. The Dapp contract invokes the calculateRequestPrice function of VRFV2Wrapper to estimate the total transaction cost required for random number generation.

  2. The Dapp contract calls the transferAndCall function of WinkMid to pay the calculated request price to the Wrapper. This method sends Wink tokens and executes the onTokenTransfer logic of VRFV2Wrapper.

  3. The onTokenTransfer logic of VRFV2Wrapper triggers the requestRandomWords function of VRFCoordinatorV2 to request a random number.

  4. The VRFCoordinatorV2 contract emits the RandomWordsRequested event.

  5. The VRF node captures this event and awaits the specified number of block confirmations. It then returns the random values and proof through the fulfillRandomWords function to the VRFCoordinatorV2 contract.

  6. The VRFCoordinatorV2 contract verifies the proof on the blockchain and subsequently calls back the fulfillRandomWords function of VRFV2Wrapper.

  7. Lastly, the VRFV2Wrapper calls back the Dapp contract to complete the request.

# Before you start

Maintainers for WINkLink need to understand how the TRON platform works, and know about smart contract deployment and the process of calling them. You're suggested to read related TRON official documents, particularly those on contract deployment on TronIDE.

Prepare the node account. You should read related Node account preparation doc.

# VRFCoordinatorV2 Contract

VRFCoordinatorV2 contract is deployed on the TRON public chain with the following features:

  • Receive random number requests from Dapp contract and emit VRFRequest event
  • WIN transfer as fees, will be sent along with the request
  • Accept random number and the proof submitted from WINkLink node
  • VRFCoordinator contract will verify the proof before sending the random number to Dapp contract
  • Calculate the WINkLink node rewards for the request fulfilment

Some parameters are needed in the constructor function when deploying a VRFCoordinator contract:

constructor(
address wink,
address blockhashStore,
address winkMid
)

_blockHashStore BlockHashStore address, _win WIN token address, _winkMid WinkMid contract address.

TIP

Nile Testnet

# VRFV2Wrapper Contract

VRFV2Wrapper streamlining the interaction and allowing direct calls from the Dapp to the VRFCoordinatorV2 contract.

Configuration parameters
keyHash : Node keyhash
maxNumWords : maximum number of random words per vrf request,currently set as 10

# Authorize a Node Account

Node account needs authorization to submit data to VRFCoordinatorV2 contract, otherwise error will be reported.

The owner of the VRFCoordinatorV2 contract is required to call the contract below and add the node account to the whitelist:

function registerProvingKey(address oracle, uint256[2] calldata publicProvingKey) external onlyOwner

_oracle is the address of the registered node, which is used to receive the WIN token paid by DAPP , _publicProvingKey is the public key used by the registration node to generate random numbers,

Call example:

registerProvingKey(TYmwSFuFuiDZCtYsRFKCNr25byeqHH7Esb,['6273228386041830135141271310112248407537170435188969735053134748570771583756',67273502359025519559461602732298865784327759914690240925031700564257821594585'])

# Dapp Contract

Main steps to set up your consuming contract:

  • a) Import and inherit VRFV2WrapperConsumerBase
// SPDX-License-Identifier: MIT
// An example of a consumer contract that directly pays for each request.
pragma solidity ^0.8.7;

import "./VRFV2WrapperConsumerBase.sol";

contract VRFv2DirectFundingConsumer is VRFV2WrapperConsumerBase{}
  • b) Contract must implement the fulfillRandomWords function, which is the callback VRF function. Here, you add logic to handle the random values after they are returned to your contract.
function fulfillRandomWords(
uint256 _requestId,
uint256[] memory _randomWords
)
  • c) Contract invoke requestRandomness function to trigger a VRF request.
function requestRandomWords()
external
onlyOwner
returns (uint256 requestId)
{
  requestId = requestRandomness(
  msg.sender,
  callbackGasLimit,
  requestConfirmations,
  numWords
);
s_requests[requestId] = RequestStatus({
  paid: VRF_V2_WRAPPER.calculateRequestPrice(callbackGasLimit, numWords),
  randomWords: new uint256[](0),
  fulfilled: false
});
requestIds.push(requestId);
lastRequestId = requestId;
emit RequestSent(requestId, numWords);
return requestId;
}

# Dapp Contract Sample

Deploy sample consumer contract VRFv2DirectFundingConsumer.sol。

Constructor parameters:
_winkAddress:wink token contract address
_winkMid: winkMid contract address
_wrapper:VRFV2Wrapper contract address
_numWords: number of random words per vrf request

// SPDX-License-Identifier: MIT
// An example of a consumer contract that directly pays for each request.
pragma solidity ^0.8.7;

import "./ConfirmedOwner.sol";
import "./VRFV2WrapperConsumerBase.sol";

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */

contract VRFv2DirectFundingConsumer is
VRFV2WrapperConsumerBase,
ConfirmedOwner
{
    address winkAddress;

    event RequestSent(uint256 requestId, uint32 numWords);
    event RequestFulfilled(
        uint256 requestId,
        uint256[] randomWords,
        uint256 payment
    );

    struct RequestStatus {
        uint256 paid; // amount paid in wink
        bool fulfilled; // whether the request has been successfully fulfilled
        uint256[] randomWords;
    }
    mapping(uint256 => RequestStatus)
    public s_requests; /* requestId --> requestStatus */

    // past requests Id.
    uint256[] public requestIds;
    uint256 public lastRequestId;

    // Depends on the number of requested values that you want sent to the
    // fulfillRandomWords() function. 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 = 0;

    // 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 VRFV2Wrapper.getConfig().maxNumWords.
    uint32 numWords;

    constructor(
        address _winkAddress,
        address _winkMid,
        address _wrapper,
        uint32 _numWords
    )
    ConfirmedOwner(msg.sender)
    VRFV2WrapperConsumerBase(_winkAddress, _winkMid, _wrapper) {
        winkAddress = _winkAddress;
        numWords = _numWords;
    }

    function requestRandomWords()
    external
    onlyOwner
    returns (uint256 requestId)
    {
        requestId = requestRandomness(
            callbackGasLimit,
            requestConfirmations,
            numWords
        );
        s_requests[requestId] = RequestStatus({
            paid: VRF_V2_WRAPPER.calculateRequestPrice(callbackGasLimit, numWords),
            randomWords: new uint256[](0),
            fulfilled: false
        });
        requestIds.push(requestId);
        lastRequestId = requestId;
        emit RequestSent(requestId, numWords);
        return requestId;
    }

    function fulfillRandomWords(
        uint256 _requestId,
        uint256[] memory _randomWords
    ) internal override {
        require(s_requests[_requestId].paid > 0, "request not found");
        s_requests[_requestId].fulfilled = true;
        s_requests[_requestId].randomWords = _randomWords;
        emit RequestFulfilled(
            _requestId,
            _randomWords,
            s_requests[_requestId].paid
        );
    }

    function getRequestStatus(
        uint256 _requestId
    )
    external
    view
    returns (uint256 paid, bool fulfilled, uint256[] memory randomWords)
    {
        require(s_requests[_requestId].paid > 0, "request not found");
        RequestStatus memory request = s_requests[_requestId];
        return (request.paid, request.fulfilled, request.randomWords);
    }
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./ConfirmedOwnerWithProposal.sol";

/**
 * @title The ConfirmedOwner contract
 * @notice A contract with helpers for basic contract ownership.
 */
contract ConfirmedOwner is ConfirmedOwnerWithProposal {
    constructor(address newOwner) ConfirmedOwnerWithProposal(newOwner, address(0)) {}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./OwnableInterface.sol";

/**
 * @title The ConfirmedOwner contract
 * @notice A contract with helpers for basic contract ownership.
 */
contract ConfirmedOwnerWithProposal is OwnableInterface {
    address private s_owner;
    address private s_pendingOwner;

    event OwnershipTransferRequested(address indexed from, address indexed to);
    event OwnershipTransferred(address indexed from, address indexed to);

    constructor(address newOwner, address pendingOwner) {
        require(newOwner != address(0), "Cannot set owner to zero");

        s_owner = newOwner;
        if (pendingOwner != address(0)) {
            _transferOwnership(pendingOwner);
        }
    }

    /**
     * @notice Allows an owner to begin transferring ownership to a new address,
   * pending.
   */
    function transferOwnership(address to) public override onlyOwner {
        _transferOwnership(to);
    }

    /**
     * @notice Allows an ownership transfer to be completed by the recipient.
   */
    function acceptOwnership() external override {
        require(msg.sender == s_pendingOwner, "Must be proposed owner");

        address oldOwner = s_owner;
        s_owner = msg.sender;
        s_pendingOwner = address(0);

        emit OwnershipTransferred(oldOwner, msg.sender);
    }

    /**
     * @notice Get the current owner
   */
    function owner() public view override returns (address) {
        return s_owner;
    }

    /**
     * @notice validate, transfer ownership, and emit relevant events
   */
    function _transferOwnership(address to) private {
        require(to != msg.sender, "Cannot transfer to self");

        s_pendingOwner = to;

        emit OwnershipTransferRequested(s_owner, to);
    }

    /**
     * @notice validate access
   */
    function _validateOwnership() internal view {
        require(msg.sender == s_owner, "Only callable by owner");
    }

    /**
     * @notice Reverts if called by anyone other than the contract owner.
   */
    modifier onlyOwner() {
        _validateOwnership();
        _;
    }
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface OwnableInterface {
    function owner() external returns (address);

    function transferOwnership(address recipient) external;

    function acceptOwnership() external;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./TRC20Interface.sol";
import "./VRFV2WrapperInterface.sol";

/** *******************************************************************************
 * @notice Interface for contracts using VRF randomness through the VRF V2 wrapper
 * ********************************************************************************
 * @dev PURPOSE
 *
 * @dev Create VRF V2 requests without the need for subscription management. Rather than creating
 * @dev and funding a VRF V2 subscription, a user can use this wrapper to create one off requests,
 * @dev paying up front rather than at fulfillment.
 *
 * @dev Since the price is determined using the gas price of the request transaction rather than
 * @dev the fulfillment transaction, the wrapper charges an additional premium on callback gas
 * @dev usage, in addition to some extra overhead costs associated with the VRFV2Wrapper contract.
 * *****************************************************************************
 * @dev USAGE
 *
 * @dev Calling contracts must inherit from VRFV2WrapperConsumerBase. The consumer must be funded
 * @dev with enough WINK to make the request, otherwise requests will revert. To request randomness,
 * @dev call the 'requestRandomness' function with the desired VRF parameters. This function handles
 * @dev paying for the request based on the current pricing.
 *
 * @dev Consumers must implement the fullfillRandomWords function, which will be called during
 * @dev fulfillment with the randomness result.
 */
abstract contract VRFV2WrapperConsumerBase {
    TRC20Interface internal immutable WINK_TOKEN;
    WinkMid internal immutable WINK_MID;
    VRFV2WrapperInterface internal immutable VRF_V2_WRAPPER;

    /**
     * @param _winkMid is the address of WinkMid
   * @param _vrfV2Wrapper is the address of the VRFV2Wrapper contract
   */
    constructor(address _wink, address _winkMid, address _vrfV2Wrapper) {
        WINK_TOKEN = TRC20Interface(_wink);
        WINK_MID = WinkMid(_winkMid);
        VRF_V2_WRAPPER = VRFV2WrapperInterface(_vrfV2Wrapper);
    }

    /**
     * @dev Requests randomness from the VRF V2 wrapper.
   *
   * @param _callbackGasLimit is the gas limit that should be used when calling the consumer's
   *        fulfillRandomWords function.
   * @param _requestConfirmations is the number of confirmations to wait before fulfilling the
   *        request. A higher number of confirmations increases security by reducing the likelihood
   *        that a chain re-org changes a published randomness outcome.
   * @param _numWords is the number of random words to request.
   *
   * @return requestId is the VRF V2 request ID of the newly created randomness request.
   */
    function requestRandomness(
        uint32 _callbackGasLimit,
        uint16 _requestConfirmations,
        uint32 _numWords
    ) internal returns (uint256 requestId) {
        uint64 amount = VRF_V2_WRAPPER.calculateRequestPrice(_callbackGasLimit, _numWords);
        WINK_TOKEN.approve(address(WINK_MID), amount);
        WINK_MID.transferAndCall(
            address(VRF_V2_WRAPPER),
            amount,
            abi.encode(_callbackGasLimit, _requestConfirmations, _numWords)
        );
        return VRF_V2_WRAPPER.lastRequestId();
    }

    /**
     * @notice fulfillRandomWords handles the VRF V2 wrapper response. The consuming contract must
   * @notice implement it.
   *
   * @param _requestId is the VRF V2 request ID.
   * @param _randomWords is the randomness result.
   */
    function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal virtual;

    function rawFulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) external {
        require(msg.sender == address(VRF_V2_WRAPPER), "only VRF V2 wrapper can fulfill");
        fulfillRandomWords(_requestId, _randomWords);
    }
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface VRFV2WrapperInterface {
    /**
     * @return the request ID of the most recent VRF V2 request made by this wrapper. This should only
   * be relied option within the same transaction that the request was made.
   */
    function lastRequestId() external view returns (uint256);

    /**
     * @notice Calculates the price of a VRF request with the given callbackGasLimit at the current
   * @notice block.
   *
   * @dev This function relies on the transaction gas price which is not automatically set during
   * @dev simulation. To estimate the price at a specific gas price, use the estimatePrice function.
   *
   * @param _callbackGasLimit is the gas limit used to estimate the price.
   */
    function calculateRequestPrice(uint32 _callbackGasLimit, uint32 _numWords) external view returns (uint64);

    //   /**
    //   * @notice Estimates the price of a VRF request with a specific gas limit and gas price.
    //   *
    //   * @dev This is a convenience function that can be called in simulation to better understand
    //   * @dev pricing.
    //   *
    //   * @param _callbackGasLimit is the gas limit used to estimate the price.
    //   * @param _requestGasPriceWei is the gas price in wei used for the estimation.
    //   */
    //   function estimateRequestPrice(uint32 _callbackGasLimit, uint256 _requestGasPriceWei) external view returns (uint256);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

abstract contract TRC20Interface {

    function totalSupply() public view virtual returns (uint);

    function balanceOf(address guy) public view virtual returns (uint);

    function allowance(address src, address guy) public view virtual returns (uint);

    function approve(address guy, uint wad) public  virtual returns (bool);

    function transfer(address dst, uint wad) public virtual returns (bool);

    function transferFrom(address src, address dst, uint wad) public virtual returns (bool);

    event Transfer(address indexed from, address indexed to, uint tokens);
    event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}

abstract contract WinkMid {

    function setToken(address tokenAddress) public virtual;

    function transferAndCall(address to, uint64 tokens, bytes calldata _data) public virtual returns (bool success);

    function balanceOf(address guy) public view virtual returns (uint);

    function allowance(address src, address guy) public view virtual returns (uint);

}

# How to setup Verifiable Random Function Contracts

# WinkMid Contract

WINkLink uses WIN (TRC20) as the base token for the whole platform.

WINkLink adopts the transferAndCall feature, i.e. calling one of the callback functions while transferring TRC20 tokens to contracts, a feature similar to ERC677 yet adopting different interface parameters.

Given that we cannot modify contracts or add interfaces for most of the tokens issued, WINkLink provides WinkMid wrapper contract, which helps wrapping any TRC20 token and provides transferAndCall interface.

The contract code is available at WinkMid.sol.

For convenience, Nile TestNet has deployed WinkMid contract and encapsulated the WIN token on it. Developers may use this contract address directly without additional deployment. Users may also claim test TRX and WIN tokens from the Faucet address provided by Nile TestNet.

TIP

Nile Testnet

WIN TRC20 Contract Address: TNDSHKGBmgRx9mDYA9CnxPx55nu672yQw2

WinkMid Contract Address: TJpkay8rJXUWhvS2uL5AmMwFspQdHCX1rw

Testnet Faucet: https://nileex.io/join/getJoinPage (opens new window)

When deploying WinkMid contract, developers need to provide the encapsulated TRC20 token address (i.e. WIN token address) for the constructor.

Developers do not need to call WinkMid contract directly, as it's wink a helper for caller contracts.

WIN token address and WinkMid contract address are needed in the constructor function when deploying an Coordinator contract.

# VRFCoordinatorV2

The coordinator is the main contract that handles all VRF requests and fulfillments. Deploy the contract with respective arguments.

Oracle must register its node address in base58 with the proving keys to the coordinator before initiating requests, otherwise requests will fail.

# VRFV2Wrapper

The wrapper contract acts as an access layer for direct funding consumers, topup sufficient wink to subscription for internal circulation using WinkMid’s transferAndCall.

Data passed in to be abi encoded subsctiption ID value i.e. 0x0000000000000000000000000000000000000000000000000000000000000007 for sub id = 7)

TIP

keyhash refers to the oracle node’s keyhash, it can be obtained through Operator UI or CLI

# Consumers

  • VRFv2DirectFundingConsumer

    Direct funding consumer directly debits Wink tokens from user’s account on request. This consumer interfaces with the wrapper contract for requests.

  • VRFv2SubscriptionConsumer

    Subscription consumer requires a subscription manager to maintain an active subscription. This consumer interfaces directly with the coordinator contract using a valid subscription id for requests.

WARNING

The consumer contracts provided in the code is a working sample, users are expected to write their own consumer contracts based on their use cases.

# How to launch a Verifiable Random Number Service Node

# Node Deployment

WINkLink node can be deployed after the contract is deployed.

WINkLink node (project directory node) code is available at: https://github.com/tron-oracle/winklink-2.0/tree/main (opens new window).

WARNING

Current node implementation includes the adapter for accessing token price via exchange APIs. Please run the node in a stable network environment outside Mainland China.

# Prepare Node Account

Each WINkLink node must be linked to a TRON account for calling Aggregator contract to transmit data.

After generating the account address and the private key, the developer can test TRX token on the Testnet Faucet page.The token is used for paying the handling fees on calling the smart contracts.

Account will be generated on the initial run of the node and the private key will be stored in the keychain. Node will use this account for price feed transmissions.

WARNING

account generated is not activated, please transfer any amount of TRX into the account for activation

# Required Environment

WINkLink node relies on a running PostgreSQL database. Developers can find more information in the official documentation https://www.postgresql.org .

TIP

Here we assume that the username and the password for the PostgreSQL instance deployed locally are root:root respectively. Please use a strong password or other verification methods in the production environment.

WINkLink node is written in Go programming language and requires Golang environment.

# Node Configuration

WINkLink node is configured using TOML files. Main config is tools/config/config.toml. With secrets.toml you can specify a db instance to be used. Below is a sample template for reference.

# secrets.toml
[Database]
URL = 'postgresql://root:root@localhost:5432/winklink?sslmode=disable' # Require
AllowSimplePasswords = true

[Password]
Keystore = 'keystorePassword' # Required

[Tron]
TronApiKey = 'apiKey'

After the node configuration file is confirmed, it is required to create vrfpassword and apicredentials files and write the userid and password to access the node’s api:

# apicredentials
example.user@fake.email
totallyNotFakePassword (16 characters long)
# vrfpassword
totallyNotFakePassword (16 characters long)

TIP

It is important that you keep private information safe.

# Building a docker image for the node

Use the following command to build a standard linux docker image:

# build a docker image
docker buildx build --platform linux/amd64 -t winklink-2.0 -f core/winklink.Dockerfile .

After building, we can tag and push it to the desired repository for deployment.

# Start a Node from source code

Install go1.20 (opens new window)

Go into the base directory of the source code winklink-2.0

Build the command line interface with

make install

Start your WINkLink node using the following command with the respective configuration items:

winklink -c /tools/config/config.toml -s /tools/config/secrets.toml node start -p /tools/secrets/vrfpassword -a /tools/secrets/apicredentials

WARNING

Your node account must have enough TRX tokens for contract calls. You can apply testnet tokens at Testnet Faucet.

# Add a VRF Job to Your Node

Below is an example template of the bare minimal parameters required to create a VRF job spec.

type = "vrf"
schemaVersion = 1
name = "vrf-delete-test"
forwardingAllowed = false
coordinatorAddress = "THE-SMART-CONTRACT-EIP55-COORDINATOR-ADDRESS"
fromAddresses = [ "THE-CURRENT-NODE-EIP55-ADDRESS" ]
minIncomingConfirmations = 1
publicKey = "THE-CURRENT-NODE-PUBLIC-KEY"
observationSource = """
decode   [type="tvmabidecodelog"]
vrf      [type=vrfbuilder]
tvmcall  [type=tvmcall contract="THE-SMART-CONTRACT-TRON-ADDRESS" extractRevertReason=true]

decode->vrf->tvmcall
"""

With this, the node will be ready to handle incoming VRF requests.