Skip to content

Latest commit

 

History

History
138 lines (98 loc) · 12.5 KB

20230728-commit-reveal.md

File metadata and controls

138 lines (98 loc) · 12.5 KB
status flip authors sponsor updated
Released
123
Tarak Ben Youssef (tarak.benyoussef@dapperlabs.com)
2023-08-31

FLIP 123: On-chain Random beacon history for commit-reveal schemes

Objective

Although Cadence exposes a safe randomness generated by the Flow protocol, transactions submitted by a non-trusted party are able to reject their results after the random is revealed. The purpose of the FLIP is to provide a safe pattern to use randomness in transactions so that it's not possible to revert non-favorable randomized transaction results.

Motivation

The Flow Virtual Machine (FVM) underwent changes that made the Cadence randomness function unsafeRandom (a new name is being discussed in another FLIP) backed by an unbiasable, unpredictable and verifiable distributed randomness. The distributed randomness is generated within the Flow protocol for every block and we refer to it by source of randomness SoR, as in the original Flow paper.

The Cadence function can be used safely in some applications where the transaction results are NOT deliberately reverted after the random number is revealed (a contract distributing random NFTs to registered users or on-chian lucky draw).

However, many applications require a non-trusted party (for instance app users) to submit a transaction calling a randomized (non-deterministic) contract. A user can write the transaction in a way that it inspects the app contract results and cancel the changes if they are not favorable. As an example, consider a simple coin toss randomized contract where users can bet an amount of tokens against a random binary output. If the coin toss contract outputs 1, the user doubles their bet. If the coin toss contract outputs 0, the user loses their bet in favour of the coin toss. Although the user (or the honest coin toss contract) cannot predict or bias the outcome, the user transaction can check the randomized result and cancel the transaction if they are losing their bets. This can be done by calling an exception causing the transaction to error. All temporary state changes are cancelled and the user can repeat the process till they double their bet.

Note that this is an inherent behavior of atomic contract platforms where the programming language is able to error and all temporary results of the transaction are not applied to to the state. While this is not a limitation of the programming language or the safe distributed randomness, a contract platform should provide developers with tools to implement a non-revertible usage of randomness.

User Benefit

Adding a safe pattern to reveal randomness without the transaction results being reverted unlocks application relying on randomness (for instance the coin toss contract described above). Not providing such pattern pushes developers to rely on risky solutions with possible security flaws.

Design Proposal

The proposed design is a commit-reveal pattern.

The solution requires infrastructure changes to provide new data to the transaction execution environment:

  1. a new FVM function that exposes the current block's SoR (more precisely a derived value from the protocol SoR_A) to Cadence runtime. Note that unsafeRandom only exposes randoms derived from SoR through a pseudo-random generator (PRG) but not the SoR itself.
  2. a new system core-contract RandomBeaconHistory that stores all history of SoRs indexed by block height (starting from the block height where the feature is deployed). The new FVM function in (1) is only available to the history contract, and is not available to other non-system transactions. Note that system-transactions in Flow are executed at the end of each block, after all non-system transactions of the block are executed. The proposal suggests to change the system transaction so that it adds the current block's SoR_A to the SoR history contract (it also removes the oldest block's SoR_A).
  3. an on-chain implementation of a PRG is required. A PRG is initialized with an SoR and can generate a sequence of random numbers for an application. It's up to the application to use a suitable PRG instance. At least one recommended PRG implementation should be provided as part of the FLIP implementation (precise instance to be determined).

Once the changes above are available, a commit-reveal scheme can be implemented as follows. The coin toss example described earlier will be used for illustration:

  • when a user submits a bidding transaction, the bid amount is transferred to the coin toss contract, and the block height where the bid was made is stored. This is a commitment by the user to use the SoR at the current block. Note that the current block's SoR_A isn't known to the transaction execution environment, and therefore the transaction has no way to inspect the random outcome and predict the coin toss result. The current block's SoR_A is only available once added to the history core-contract, which only happens at the end of the block's execution. The user may also commit to using an SoR of a future block, which is still unknown at the time the bid is made.
  • the coin toss contract may grant the user a limited window of time (i.e a block height range) to reveal the results and claim any winnings. Failing to do so, the bid amount remains in the coin toss contract.
  • the user can submit a second transaction to call the coin toss contract and resolve the bid. The coin toss contract looks up the committed block height of the user and checks it has already passed. The contract uses the block height to query the past-block's SoR_A on the core-contract RandomBeaconHistory.
  • the coin toss contract uses a PRG seeded by the queried SoR_A and diversified using a specific information to the use-case (a user ID or resource ID for instance). Diversification does not add new entropy, but it avoids generating the same outcome for different use-cases. If a diversifier (or salt) isn't used, all users that committed a bid on the same block would either win or lose.
  • The PRG is used to generate the random result and resolve the bid. Note that the user can make the transaction abort after inspecting a losing result. However, the bid amount would be lost anyway when the allocated window expires.

Notes:

  • SoR is public data that is part of the block's payload in Flow. However, block history in Flow may be truncated at each Epoch and consensus nodes are not required to keep the full block history prior to the current Epoch. Storing the history in the execution state is a safe way to track past-Epoch SoRs since the execution state lives beyond Epochs and Sporks boundaries and is fork-aware. Correctness of the execution state is guaranteed by the verification and sealing processes. This is the reason the proposal suggests to keep the SoR history on a core-contract.
  • With a block rate of 1/second, the minimum raw data storage (not including encoding overhead) is evaluated at 962 MB per year. This is an acceptable overhead given the current execution state size. The SoR history size remains a small percentage of the overall state size.
  • With the proposal above, there will be two ways to use randomness on-chain. One provided by Cadence's unsafeRandom (derived from the current block's SoR, let's call it SoR_B), and another provided by the RandomBeaconHistory core-contract (past blocks SoR_A via the new FVM function). It is important that the protocol uses independent SoRs (SoR_A and SoR_B), although both are derived from the unique Flow protocol distributed beacon. If SoR_A and SoR_B are equal, a user could use the Cadence function unsafeRandom in the commitment phase to predict the randoms that will be drawn in the future reveal transaction. The independence of SoRs is already enforced because SoR_B is diversified per transaction, but extra safety diversification should be added when extracting the execution SoRs(SoR_A and SoR_B).

Drawbacks

  • commit-reveal protocols are implemented over two rounds, which represents a friction for the application user and adds complexity for contract developers.
  • once the bets are committing, the amount remains in escrow on the application contract. This can open back-doors for a malicious application to run away with the bid amount by updating the contract. A frozen contract (all the account keys get revoked) is an option to get users to trust the application.

Alternatives Considered

  • a one-round solution would be to implement a commit-reveal through one user action only: a user sends a commit transaction calling the application contract. The commit triggers the application contract to defer a transaction to run in the future (same block or subsequent one). The deferred transaction is non-revertible by the user. This alternative requires Cadence to support deferred actions which is not available at the moment.

  • another way to use randomness is to implement a commit-reveal scheme over two rounds, but with hiding the random result from the user transaction environment: When the user submits a bidding transaction, the application contract executes the randomized algorithm (coin toss for instance) and returns the result encapsulated into a Cadence object with private attributes. The user transaction should have no way to inspect the the private attributes in order to potentially abort the transaction. The user still needs to submit a second transaction to resolve the results of the Cadence object. Although this method avoids having the bid amount in escrow on the application contract, it makes strong assumptions about a Cadence object. Any information leakage of the private attributes (gas consumption, encoding size..) into the user commit transaction may result in revealing the hidden result. There are no guarantees that the transaction execution environment will not be able to differentiate private attributes in the future, even though the method may be safe today. Another advantage of this method is that it works well with the current protocol infrastructure and does not require extra tools.

Performance Implications

  • There is no timing and gas impact other then the PRG is being executed on-chain.
  • The core-contract will have to store the SoR history. This is estimated to be less than 1GB per year (not counting the encoding overhead).

Dependencies

None

Engineering Impact

The points 1, 2 and 3 in the design proposal need to be built as part of this proposal.

Tutorials and Examples

Here is an example of a coin toss contract with one function to commit a bid, and another function to resolve the bid:

import RandomBeaconHistory from "RandomBeaconHistory"
import PRG from "PRG"
// PRG implementation is not provided by the FLIP, we assume this contract
// imports a suitable PRG implementation

fun commitCointoss(bet: @FlowToken.Vault): @Receipt {
	let receipt <- create Receipt(
				betAmount: bet.balance,
				// commit to use randomness at the current block (still unknown)
				commitBlock: getCurrentBlock().height
	)
	// commit the bet
	// `self.reserve` is a `@FlowToken.Vault` field defined on the app contract
	//  and represents a pool of funds
	self.reserve.deposit(from: <-bet)
    return <- receipt
}

fun revealCointoss(receipt: @Receipt): @FlowToken.Vault {
	let currentBlock = getCurrentBlock().height
	if receipt.commitBlock >= currentBlock {
		panic("cannot reveal yet")
	}
	
	let winnings = receipt.betAmount * 2
	let coin = randomCoin(atBlock: receipt.commitBlock, salt: receipt.id)
	destroy receipt

	if coin == 1 {
		return <-FlowToken.createEmptyVault()
	}
	
	// `self.reserve` is a `@FlowToken.Vault` field defined on the app contract
	//  and represents a pool of funds
	return <-self.reserve.withdraw(amount: winnings)
}

fun randomCoin(atBlockHeight blockHeight: UInt64, salt: UInt64): UInt8 {
	// query the Random Beacon history core-contract.
	// if `blockHeight` is the current block height, `sourceOfRandomness` errors.
	let sourceOfRandomness = RandomBeaconHistory.sourceOfRandomness(atBlockHeight: blockHeight)

	// instantiate a PRG object using external `createPRG` that takes a `seed` 
	// and `salt` and returns a pseudo-random-generator object.
	let prg = PRG.createPRG(sourceOfRandomness, salt)

	// derive a 64-bit random using the object `prg`
	let rand = prg.Uint64()
	return UInt8(rand & 1)
}