Games with oracles on blockchains (Part II)

Thomas
Dune Network
Published in
7 min readMay 28, 2020
Photo by Brett Jordan on Unsplash

Last time, we examined various ways for an oracle to provide randomness for games on the blockchain. Today we take a closer look at our implementation of what we call OnChainAlea, our on-chain mediator between games and oracles. It is written in the Love smart contract language.

If you haven’t , we recommend you read the last article before getting back here.

As we explained then, this contract acts as an intermediate between an oracle (or possibly several, in the future) and various users, most of which we expect to be DApps. A DApp may request a random integer (from, say, 0 to 9) from the mediator. Let’s call such a random number a prophecy in the rest of this post. The oracle can then supply a prophecy to the mediator, which is passed on to the DApp. Using this mechanism has at least two benefits. First, we provide a low transaction fee average cost (since a hash commitment to a random number takes non-negligible space on the chain, it is better to group them). Second, we provide high assurance that the prophecy is tamper-proof, meaning that the random value has not changed between its commitment by the oracle and its request by the DApp. This is at the cost of the slightly increased latency due to the indirection through our mediator.

Let’s examine our ~600-LOC smart-contract (which can be found here):

Storage

The storage of the contract is composed of owner-defined origination parameters and permanent data structures.

Owner-defined parameters

The following are set at deployment time.

  • owners: the addresses controlling the oracle.
  • nb_prophecies_per_commit: the number of prophecies to which the oracle commits every time it submits a hash.
  • nonces_safety_limit: maximal number of batch commitments an oracle may insert without revealing them. We will see later that the oracle has to pay for every commitment it does not honor, hence it is imperative that it remain "solvable" with respect to this "debt".
  • gc_protection_window: Minimal number of blocks during which prophecies may not be garbage collected: DApps may need answers to remain for say, a few days, before a game is closed. Note that we are considering ways to make the history of prophecies permanently accessible using Merkle trees and an incentive system, without clogging the storage of the contract. We may address this point in a later post.
  • min_safety_deposit_per_reservation: A deposit per prophecy that the oracle may lose if it does not honor its obligations (such as actually revealing the prophecy).
  • reservation_cost: cost for a DApp to reserve a slot and obtain a prophecy. This serves in part the purpose of paying the oracle for its service.
  • prophecy_reveal_delay: Maximum delay to reveal a prophecy after a reservation is made.
  • nonce_reveal_delay: Maximum delay to reveal a nonce after all the prophecies for a corresponding commit are revealed.

Other storage

The storage contains:

  • various maps storing the address of the oracle, non garbage-collected commitments, reserved prophecies, revealed prophecies, and a lookup table to match query ids to slot indices.
  • Indices for the last_inserted_commit, the last_revealed_commit, the last_GCed_commit, the last_reserved_prophecy and the last_revealed_prophecy.
  • A terminated boolean which, if true, means the contract is dead and makes it unusable.
  • A min_safety_deposit (which is automatically computed from user-defined parameters). It is deposited by the oracle. It allows compensation of all flouted parties in the worst case of oracle malfunction, whether accidental or malicious.
  • The above user-defined parameters.

Entrypoints and views

Entrypoints and views for dapps

On the DApp-facing side, the main entrypoints are:

  1. val%entry reserve storage _d (qid : query_id)
    The reserve entrypoint expects a query id of type nat (non-negative integers). This id should be unique from the point of view of the Dapp, the easiest way being to increment a counter every time a new prophecy is needed. The effect of this function is to book a "slot" in one of the upcoming batches of prophecies. The function may fail if no commitment has been inserted and all current slots have been booked.
  2. val%view get_answer storage ((qid,addr) : query_id * address option) : nat option
    The get_answer view allows anyone to observe the prophecy corresponding to a given query_id and (optionally) address. Note that this function may also be called off-chain with an RPC, which can be useful when building the client-facing UI of a DApp.
  3. val%entry denounce_prophecy storage d (i : slot_index)
    The denounce_prophecy entrypoint can be called by anyone, and its existence is meant to hopefully ensure that it will never be called. It takes as input the index i of a slot for which the caller thinks the oracle has not provided a prophecy, even though prophecy_reveal_delay blocks have been published since the oracle's initial commitment to its batch. This triggers a reimbursement of the DApp of min_safety_deposit_per_reservation DUN from the oracle's initial security bond.
    This reimbursement may be used in turn for the DApp to provide guarantees to its user: if a game stalls because of foul play on the oracle side, the DApp can implement a mechanism to pay the user back, no questions asked. Of course, we envision several mediators for various levels of risk: min_safety_deposit_per_reservation may be 1 cent, 1 euro or a 100 depending on the stakes.
  4. val%entry denounce_comm storage d (i : comm_index)
    Similarly to denounce_prophecy, the denounce_comm entrypoint is a deterrent which can be called by anyone to punish the oracle for not revealing a nonce for a batch of prophecies (the nonce is crucial to check that prophecies have not been tampered with by the oracle). If the denunciation is well-founded, a payment is made to each Dapp of min_safety_deposit_per_reservation for each slot in a batch (there are nb_prophecies_per_commit per batch).

Entrypoints and views for the oracle

Most of the rest of the contract deals with entrypoints for the oracle.

  1. val%entry insertCommitments storage d (cl : bytes list)
    insertCommitments
    enables the oracle to send one or more commitments, each for a nonce and a list of prophecies. Recall from last time that a commitment is computed as
    hₖ = sha256( nonce, u₀ … uₙ)
  2. val%entry revealProphecies storage d (pl : nat list)
    revealProphecies
    enables the oracle to reveal one or more prophecies for already inserted commitments. Only prophecies whose slot have already been reserved by some DApp or user may be revealed. The oracle should be especially careful not to attempt to reveal values which have not been committed (for example by attempting to simulate the execution of the revealProphecies function off-chain, on a local node) because, even if the transaction fails, the value it contains should now be considered public.
  3. val%entry revealNonces storage d (nl : nat list)
    In the same way, revealNonces enables the oracle to reveal one or more nonces for already made commitments whose prophecies have all been revealed. In the same way, the oracle should be careful to check that these properties are satisfied, let it should inadvertently reveal information which may be used to cheat its users.
  4. val%entry gc storage d (() : unit)
    gc
    garbage collects old commitments and revealed and reserved prophecies, so as to maintain a low cost of storage for the contract. In future versions, we might keep a root hash of an off-chain Merkle tree containing all past history, and incentives for actors to store it and provide parts of it on-demand.
  5. val%entry withdraw storage d ((amount, dest) : dun * address)
    If the contract is terminated and the oracle has fulfilled all its obligations, it may withdraw its security bond using this entrypoint. Nothing can be done by the contract unless the security bond is at least min_safety_deposit.
  6. val%entry deposit storage d (_ : unit)
    The oracle (or anyone on its behalf) may deposit money into the contract in order to reach min_safety_deposit and be able to function normally.
  7. val%entry terminate storage d (_ : unit)
    The terminate entry will only function if all obligations (nonces and prophecies revealed) are met. It will make the contract unusable from then on. All withdrawals must be made before terminate is called.
  8. val%entry delegate storage d (pkh_opt : keyhash option)
    The owner may choose to delegate the contract’s balance to a baker using the delegate entrypoint.

Observations on trust

We want to address one attack channel: the oracle might offer its prophecies to the highest bidder, off-chain, before they are revealed on-chain. Thus, the oracle could theoretically cheat its users (the DApps and/or the users of the DApps) while still scrupulously meeting its (smart-)contractual obligations to them. This does not make such an oracle unusable, however:

  • In the long term, a bias would show in the results of the DApp and the reputation of the oracle (which is all it has to show for itself) would take a significant blow;
  • If the incentives are correctly balanced on the DApp side, the maximum feasible cost of knowing a prophecy in advance should be very low. Thus the oracle would need to sell this information to many users to make it worth its while, which in turn would increase the risk of being outed and losing all its customer base.

In our upcoming Raffle smart contract, we enable any user to add randomness to the oracle, to prevent collusion. This is too expensive however to do in the case of games with smaller stakes.

Conclusion

The full source code of the contract is available here but we hope that this article has clearly articulated the architecture of our OnChainAlea mediator contract. It plays an intermediate role, for both safety and efficiency reasons, between an oracle and DApps in need of random numbers (or prophecies) to properly function. Next time, we will showcase Dapps using slightly different oracles.

Footnotes:

  1. By batching prophecies instead of sending one commitment for every prophecy.
  2. that is to say, that it has not been modified upon learning what it would be used for.

--

--