How does Automata VRF work

The VRF system consists of:

  • DRAND as an entropy source.

  • Off-chain oracle.

  • AutomataVRFCoordinator contract.

The flow for each component is the following:

  • An off chain oracle is responsible for performing the following tasks in a trusted manner (e.g. execution in TEE)

    • Computes the SHA256 hash of the signature, to match with the randomness value given by DRAND.

    • Computes hash_to_field operation on the message.

    • Generates SNARK proof for the BLS12-381 signature verification.

    • Generates a final uint256 random number.

    • Produces secp256k1 signature over the random number.

    • Submits the final random number, the secp256k1 signature, computed hash_to_field value and SNARK proofs to VRFCoordinator

  • VRFCoordinator is responsible for the following:

    • After the Off Chain Oracle has published randomness on chain:

      • Performs ecrecover to ensure the randomness is signed by a trusted off chain oracle.

      • Re-construct hash_to_field to match with the input. This step serves as a point of revert for mismatch of hash, to prevent spending unnecessary gas on the SNARK verification.

      • Performs SNARK verification of the BLS12-381 Signature.

      • After passing all of the above verification steps, it updates the state of the program, allowing consumers to fetch the latest round of randomness.

    • Consumers can invoke:

      • VRFCoordinator.getLatestRandomness()

      • VRFCoordinator.getLatestRandomWords(uint32 numWords)

      • VRFCoordinator.requestRandomWords()

      • Or query event logs, to get randomness from past rounds.

Detailed Description of the Trusted Off Chain Oracle Workflow

  1. Upon receiving new randomness from DRAND, it performs SHA256 of the current BLS signature to verify the correctness of randomness. Denote DRAND randomness as A.

  2. Sends the DRAND parameters to an API service that generates the following:

    1. 55x7 integer representation of the BLS signature for the current round.

    2. hash_to_field value.

    3. SNARK proof for the BLS signature verification.

  3. Generates a randomness of its own, denoted as B.

  4. Performs A XOR B, to generate C.

  5. Generates a final randomness R, using C as the seed.

  6. Signs over the hash of the concatenation of R and SNARK proofs with secp256k1 ECDSA.

  7. Submits the randomness to AutomataVRFCoordinator, with the following parameters:

    1. DRAND return values.

    2. SNARK proof, consists of uint256[2] proof_a, uint256[2][2] proof_b and uint256[2] proof_c. See here for reference of converting proof.json to the proper format.

    3. hash_to_field

    4. The final R value. (Randomness)

    5. The ECDSA signature

struct SnarkProof {
  uint256[2] a;
  uint256[2][2] b;
  uint256[2] c;
}

struct SignedRandomness {
  uint256 randomness;
  uint8 v;
  bytes32 r;
  bytes32 s;
}

struct DrandParam {
  uint64 currentRound;
  uint256[7][2][2] currentSig; // offload conversion from hex to 55x7 representation
  bytes prevSig;
}

/// @notice caller must be granted with SUBMITTER_ROLE
function submitRandomness(
  DrandParam calldata drand,
  SnarkProof calldata proof,
  uint256[7][2][2] calldata hash_to_field, // offload conversion from hex to 55x7 representation
  SignedRandomness calldata randomness
) external;

Detailed Description of AutomataVRFCoordinator

The consumer should be aware of the difference between a randomness value and random words:

  • Randomness: A single uint256 verifiable random value generated by the oracle. It can be used as a seed to perform more complex random generation.

  • Random words: This is a list of uint256 words generated by taking the hash of the randomness value concatenated by its index. It can generate at most 2**32 random words in a single call. Consumers are free to specify the number of random words to produce from the randomness generated at the latest round. The code below describes how they are computed:

uint256 randomness;
uint256[] memory randomWords = new uint256[](numOfWords);

for (uint256 i = 0; i < numOfWords; i++) {
  randomWords[i] = uint256(keccak256(abi.encodePacked(randomness, i)));
}

There are three ways which a consumer can fetch the latest randomness or list of random words:

  1. Invoke VRFCoordinator.getLatestRandomness(). This method returns the final randomness R generated by the off chain oracle. This is most likely called by an EOA, or a smart contract that does not conform with the Chainlink VRF workflow.

  2. Invoke VRFCoordinator.getLatestRandomWords(uint32 numWords). This method returns a list of random words, with the number of elements specified by numWords. It is most likely called by an EOA, or a smart contract that does not conform with the Chainlink VRF workflow.

  3. To provide compatibility with existing contracts that consumes Chainlink VRF (e.g. the calling contract extends the VRFConsumerBaseV2 interface), consumers can invoke the requestRandomWords() entrypoint.

Additional note on #3: If the consumer contract were to indirectly invoke the entrypoint via the LINK token transfer callback, the Receiver handler function would forward the call to the entrypoint and refund LINK tokens back to the consumer contract. However, this also would cost more gas than directly calling the entrypoint.

Last but not least, an event is defined in this program to allow consumers to query for randomness produced for all past rounds.

event RandomnessPublished(uint256 indexed roundId, uint256 randomness);

Consumer-facing Interface for AutomataVRFCoordinator

// this interface is loosely based on the Chainlink VRFCoordinatorV2 interface

/// @dev since we do not charge consumers for their LINK tokens
/// we need to overwrite the onTokenTransfer() method to forward calls to requestRandomWords()
/// and refund LINK tokens to consumers
import {ERC677ReceiverInterface} from "@chainlink/contracts/src/v0.8/interfaces/ERC677ReceiverInterface.sol";

interface IAutomataVRFCoordinator is ERC677ReceiverInterface {
  
  /**
   * @notice this method is callable only by contracts that
   * extends the VRFConsumerBaseV2 contract
   *
   * @param numWords consumers specify the number of random words to be gemerated
   * 
   * Other parameters are simply placeholders to keep the interface consistent
   * with Chainlink VRFCoordinatorV2.
   *
   * @dev rawFulfillRandomWords() callback is done at the end of this function.
   * This is because the off chain oracle actively publishes new randomness
   * within a defined period.
   * 
   * @return the currentRoundId
   **/
  function requestRandomWords(
    bytes32,
    uint64,
    uint16,
    uint32,
    uint32 numWords
  ) external returns (uint256 roundId);

    /**
     * @notice fetch the latest round of randomness generated by the oracle
     **/
    function getLatestRandomness() external view returns (uint256 randomness);

    /**
     * @notice this method can be called by any addresses to get a list of random words
     * generated from the latest round of randomness
     **/
    function getLatestRandomWords(uint32 numWords) external view returns (uint256[] memory randomWords);

    function getCurrentRound() external view returns (uint256 roundId);
}

Last updated