From 261eebafc7694fb25fe8f2e77281463cfecddf35 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Wed, 31 Mar 2021 12:09:21 +0530 Subject: [PATCH 1/2] Add EIP-16 (oracle-pool contract spec) --- eip-0016.md | 301 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 eip-0016.md diff --git a/eip-0016.md b/eip-0016.md new file mode 100644 index 00000000..1bf9d339 --- /dev/null +++ b/eip-0016.md @@ -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 + } From f80b1c83c5d9f668ce8ad77136aa0bfb19493685 Mon Sep 17 00:00:00 2001 From: ScalaHub <23208922+scalahub@users.noreply.github.com> Date: Tue, 7 Sep 2021 20:57:48 +0530 Subject: [PATCH 2/2] Add EIP-0016 to list of EIPs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2a6b86f8..042858f3 100644 --- a/README.md +++ b/README.md @@ -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 |