Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EIP-0016: Oracle-Pool contracts specification #29

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ Please check out existing EIPs, such as [EIP-1](eip-0001.md), to understand the
| [EIP-0004](eip-0004.md) | Assets standard |
| [EIP-0005](eip-0005.md) | Contract Template |
| [EIP-0006](eip-0006.md) | Informal Smart Contract Protocol Specification Format |
| [EIP-0016](eip-0016.md) | Oracle pool 1.0 |
301 changes: 301 additions & 0 deletions eip-0016.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
Oracle-Pool Contract standard
=========================================

* Author: kushti, scalahub, Robert Kornacki
* Status: Proposed
* Created: 28-Mar-2021
* Last edited: 28-Mar-2021
* License: CC0
* Track: Applications, Standards


Motivation
----------

This Ergo Improvement Proposal defines the Oracle-Pool contracts actually deployed on the blockchain and used via known user interfaces.
Thus, if any of the following scripts get modified in the deployment, this EIP is to updated as well.

The Oracle-Pool dApp consists of the following scripts. For details of the scripts, refer to the oracle-pool documentation.

Live Epoch Script
-----------------

{ // This box:
// R4: The latest finalized datapoint (from the previous epoch)
// R5: Block height that the current epoch will finish on
// R6: Address of the "Epoch Preparation" stage contract.

// Oracle box:
// R4: Public key (group element)
// R5: Epoch box Id (this box's Id)
// R6: Data point

// Base-64 version of the oracle (participant) token 8c27dd9d8a35aac1e3167d58858c0a8b4059b277da790552e37eba22df9b9035
// Got via http://tomeko.net/online_tools/hex_to_base64.php
val oracleTokenId = fromBase64("jCfdnYo1qsHjFn1YhYwKi0BZsnfaeQVS4366It+bkDU=")

// Note that in the next update, the current reward of 250000 should be increased to at least 5000000 to cover various costs

val oracleBoxes = CONTEXT.dataInputs.filter{(b:Box) =>
b.R5[Coll[Byte]].get == SELF.id &&
b.tokens(0)._1 == oracleTokenId
}

val pubKey = oracleBoxes.map{(b:Box) => proveDlog(b.R4[GroupElement].get)}(OUTPUTS(1).R4[Int].get)

val sum = oracleBoxes.fold(0L, { (t:Long, b: Box) => t + b.R6[Long].get })

val average = sum / oracleBoxes.size

val firstOracleDataPoint = oracleBoxes(0).R6[Long].get

def getPrevOracleDataPoint(index:Int) = if (index <= 0) firstOracleDataPoint else oracleBoxes(index - 1).R6[Long].get

val rewardAndOrderingCheck = oracleBoxes.fold((1, true), {
(t:(Int, Boolean), b:Box) =>
val currOracleDataPoint = b.R6[Long].get
val prevOracleDataPoint = getPrevOracleDataPoint(t._1 - 1)

(t._1 + 1, t._2 &&
OUTPUTS(t._1).propositionBytes == proveDlog(b.R4[GroupElement].get).propBytes &&
OUTPUTS(t._1).value >= 250000 && // oracleReward = 250000
prevOracleDataPoint >= currOracleDataPoint
)
}
)

val lastDataPoint = getPrevOracleDataPoint(rewardAndOrderingCheck._1 - 1)
val firstDataPoint = oracleBoxes(0).R6[Long].get

val delta = firstDataPoint * 5 / 100 // maxDeviation = 5

val epochPrepScriptHash = SELF.R6[Coll[Byte]].get

sigmaProp(
blake2b256(OUTPUTS(0).propositionBytes) == epochPrepScriptHash &&
oracleBoxes.size >= 4 && // minOracleBoxes = 4
OUTPUTS(0).tokens == SELF.tokens &&
OUTPUTS(0).R4[Long].get == average &&
OUTPUTS(0).R5[Int].get == SELF.R5[Int].get + 6 && // epochPeriod = 6 = 4 (live) + 2 (prep) blocks
OUTPUTS(0).value >= SELF.value - (oracleBoxes.size + 1) * 250000 && // oracleReward = 250000
rewardAndOrderingCheck._2 &&
lastDataPoint >= firstDataPoint - delta
) && pubKey
}

Epoch Preparation Script
------------------------

{
// This box:
// R4: The finalized data point from collection
// R5: Height the epoch will end

// Base-64 version of the hash of the live-epoch script (above) 77dffd47b690caa52fe13345aaf64ecdf7d55f2e7e3496e8206311f491aa46cd
val liveEpochScriptHash = fromBase64("d9/9R7aQyqUv4TNFqvZOzffVXy5+NJboIGMR9JGqRs0=")

// Base-64 version of the update NFT 720978c041239e7d6eb249d801f380557126f6324e12c5ba9172d820be2e1dde
// Got via http://tomeko.net/online_tools/hex_to_base64.php
val updateNFT = fromBase64("cgl4wEEjnn1usknYAfOAVXEm9jJOEsW6kXLYIL4uHd4=")

val canStartEpoch = HEIGHT > SELF.R5[Int].get - 4 // livePeriod = 4 blocks
val epochNotOver = HEIGHT < SELF.R5[Int].get
val epochOver = HEIGHT >= SELF.R5[Int].get
val enoughFunds = SELF.value >= 5000000 // minPoolBoxValue = 5000000

val maxNewEpochHeight = HEIGHT + 6 + 2 // epochPeriod = 6 = 4 (live) + 2 (prep) blocks; buffer = 2 blocks
val minNewEpochHeight = HEIGHT + 6 // epochPeriod = 6 = 4 (live) + 2 (prep) blocks

val poolAction = if (OUTPUTS(0).R6[Coll[Byte]].isDefined) {
val isliveEpochOutput = OUTPUTS(0).R6[Coll[Byte]].get == blake2b256(SELF.propositionBytes) &&
blake2b256(OUTPUTS(0).propositionBytes) == liveEpochScriptHash
( // start next epoch
epochNotOver && canStartEpoch && enoughFunds &&
OUTPUTS(0).tokens == SELF.tokens &&
OUTPUTS(0).value >= SELF.value &&
OUTPUTS(0).R4[Long].get == SELF.R4[Long].get &&
OUTPUTS(0).R5[Int].get == SELF.R5[Int].get &&
isliveEpochOutput
) || ( // create new epoch
epochOver &&
enoughFunds &&
OUTPUTS(0).tokens == SELF.tokens &&
OUTPUTS(0).value >= SELF.value &&
OUTPUTS(0).tokens == SELF.tokens &&
OUTPUTS(0).value >= SELF.value &&
OUTPUTS(0).R4[Long].get == SELF.R4[Long].get &&
OUTPUTS(0).R5[Int].get >= minNewEpochHeight &&
OUTPUTS(0).R5[Int].get <= maxNewEpochHeight &&
isliveEpochOutput
)
} else {
( // collect funds
OUTPUTS(0).propositionBytes == SELF.propositionBytes &&
OUTPUTS(0).tokens == SELF.tokens &&
OUTPUTS(0).value > SELF.value &&
OUTPUTS(0).R4[Long].get == SELF.R4[Long].get &&
OUTPUTS(0).R5[Int].get == SELF.R5[Int].get
)
}

val updateAction = INPUTS(0).tokens(0)._1 == updateNFT

sigmaProp(poolAction || updateAction)
}

DataPoint Script
----------------

{
// This box:
// R4: The address of the oracle (never allowed to change after bootstrap).
// R5: The box id of the latest Live Epoch box.
// R6: The oracle's datapoint.

// Base-64 version of the pool NFT 011d3364de07e5a26f0c4eef0852cddb387039a921b7154ef3cab22c6eda887f
// Got via http://tomeko.net/online_tools/hex_to_base64.php
val poolNFT = fromBase64("AR0zZN4H5aJvDE7vCFLN2zhwOakhtxVO88qyLG7aiH8=")

val pubKey = SELF.R4[GroupElement].get

val poolBox = CONTEXT.dataInputs(0)

// Allow datapoint box to contain box id of any box with pool NFT (i.e., either Live Epoch or Epoch Prep boxes)
// Earlier we additionally required that the box have the live epoch script.
// In summary:
// Earlier: (1st data-input has pool NFT) && (1st data-input has live epoch script)
// Now: (1st data-input has pool NFT)
//
val validPoolBox = poolBox.tokens(0)._1 == poolNFT

sigmaProp(
OUTPUTS(0).R4[GroupElement].get == pubKey &&
OUTPUTS(0).R5[Coll[Byte]].get == poolBox.id &&
OUTPUTS(0).R6[Long].get > 0 &&
OUTPUTS(0).propositionBytes == SELF.propositionBytes &&
OUTPUTS(0).tokens == SELF.tokens &&
validPoolBox
) && proveDlog(pubKey)
}

Pool Deposit Script
-------------------

{
val allFundingBoxes = INPUTS.filter{(b:Box) =>
b.propositionBytes == SELF.propositionBytes
}

// Base-64 version of the pool NFT 011d3364de07e5a26f0c4eef0852cddb387039a921b7154ef3cab22c6eda887f
// Got via http://tomeko.net/online_tools/hex_to_base64.php
val poolNFT = fromBase64("AR0zZN4H5aJvDE7vCFLN2zhwOakhtxVO88qyLG7aiH8=")

val totalFunds = allFundingBoxes.fold(0L, { (t:Long, b: Box) => t + b.value })

sigmaProp(
INPUTS(0).tokens(0)._1 == poolNFT &&
OUTPUTS(0).propositionBytes == INPUTS(0).propositionBytes &&
OUTPUTS(0).value >= INPUTS(0).value + totalFunds &&
OUTPUTS(0).tokens == INPUTS(0).tokens
)
}

Ballot Script
-------------

{ // This box (ballot box):
// R4 the group element of the owner of the ballot token [GroupElement]
// R5 dummy Int due to AOTC non-lazy evaluation (since pool box has Int at R5). Due to the line marked ****
// R6 the box id of the update box [Coll[Byte]]
// R7 the value voted for [Coll[Byte]]

// Base-64 version of the update NFT 720978c041239e7d6eb249d801f380557126f6324e12c5ba9172d820be2e1dde
// Got via http://tomeko.net/online_tools/hex_to_base64.php
val updateNFT = fromBase64("cgl4wEEjnn1usknYAfOAVXEm9jJOEsW6kXLYIL4uHd4=")

val pubKey = SELF.R4[GroupElement].get

val index = INPUTS.indexOf(SELF, 0)

val output = OUTPUTS(index)

val isBasicCopy = output.R4[GroupElement].get == pubKey &&
output.propositionBytes == SELF.propositionBytes &&
output.tokens == SELF.tokens &&
output.value >= 10000000 // minStorageRent

sigmaProp(
isBasicCopy && (
proveDlog(pubKey) || (
INPUTS(0).tokens(0)._1 == updateNFT &&
output.value >= SELF.value
)
)
)
}

Update Script
-------------

{ // This box (update box):
// Registers empty
//
// ballot boxes (Inputs)
// R4 the pub key of voter [GroupElement] (not used here)
// R5 dummy int due to AOTC non-lazy evaluation (from the line marked ****)
// R6 the box id of this box [Coll[Byte]]
// R7 the value voted for [Coll[Byte]]

// Base-64 version of the pool NFT 011d3364de07e5a26f0c4eef0852cddb387039a921b7154ef3cab22c6eda887f
// Got via http://tomeko.net/online_tools/hex_to_base64.php
val poolNFT = fromBase64("AR0zZN4H5aJvDE7vCFLN2zhwOakhtxVO88qyLG7aiH8=")

// Base-64 version of the ballot token ID 053fefab5477138b760bc7ae666c3e2b324d5ae937a13605cb766ec5222e5518
// Got via http://tomeko.net/online_tools/hex_to_base64.php
val ballotTokenId = fromBase64("BT/vq1R3E4t2C8euZmw+KzJNWuk3oTYFy3ZuxSIuVRg=")

// collect and update in one step
val updateBoxOut = OUTPUTS(0) // copy of this box is the 1st output
val validUpdateIn = SELF.id == INPUTS(0).id // this is 1st input

val poolBoxIn = INPUTS(1) // pool box is 2nd input
val poolBoxOut = OUTPUTS(1) // copy of pool box is the 2nd output

// compute the hash of the pool output box. This should be the value voted for
val poolBoxOutHash = blake2b256(poolBoxOut.propositionBytes)

val validPoolIn = poolBoxIn.tokens(0)._1 == poolNFT
val validPoolOut = poolBoxIn.tokens == poolBoxOut.tokens &&
poolBoxIn.value == poolBoxOut.value &&
poolBoxIn.R4[Long].get == poolBoxOut.R4[Long].get &&
poolBoxIn.R5[Int].get == poolBoxOut.R5[Int].get


val validUpdateOut = SELF.tokens == updateBoxOut.tokens &&
SELF.propositionBytes == updateBoxOut.propositionBytes &&
SELF.value >= updateBoxOut.value // ToDo: change in next update
// Above line contains a (non-critical) bug:
// Instead of
// SELF.value >= updateBoxOut.value
// we should have
// updateBoxOut.value >= SELF.value
//
// In the next oracle pool update, this should be fixed
// Until then, this has no impact because this box can only be spent in an update
// In summary, the next update will involve (at the minimum)
// 1. New update contract (with above bugfix)
// 2. New updateNFT (because the updateNFT is locked to this contract)

def isValidBallot(b:Box) = {
b.tokens.size > 0 &&
b.tokens(0)._1 == ballotTokenId &&
b.R6[Coll[Byte]].get == SELF.id && // ensure vote corresponds to this box ****
b.R7[Coll[Byte]].get == poolBoxOutHash // check value voted for
}

val ballotBoxes = INPUTS.filter(isValidBallot)

val votesCount = ballotBoxes.fold(0L, {(accum: Long, b: Box) => accum + b.tokens(0)._2})

sigmaProp(validPoolIn && validPoolOut && validUpdateIn && validUpdateOut && votesCount >= 8) // minVotes = 8
}