From 3efd0c6d6fd72b640dcda3a256aba729d1d26679 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Wed, 1 Sep 2021 17:27:57 +0530 Subject: [PATCH 01/43] EIP-0022 md file for oracle pool 2.0 --- eip-0022.md | 247 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 eip-0022.md diff --git a/eip-0022.md b/eip-0022.md new file mode 100644 index 00000000..96d8e1f5 --- /dev/null +++ b/eip-0022.md @@ -0,0 +1,247 @@ +# Oracle Pool v2.0 [WIP] + +Below is a summary of the main new features in v2.0 and how it differs from v1.0 + +1. Single pool address (only one type of box). This version of the pool will have only one address, the *pool address*. +2. Data-point boxes will be considered as inputs rather than data-inputs. These inputs will be spent, and a copy of the box with the reward will be created. +3. Reward in tokens not Ergs. The posting reward will be in the form of tokens, which can be redeemed separately. +4. Reward accumulated in data point boxes to prevent dust. We will not be creating a new box for rewarding each posting. Instead, the rewards will be accumulated in data-point boxes. +5. When creating a data-point box, the pool box is not needed as data input. Creating a data-point will be decoupled from the pool, and will not require the bool box as data-input. +6. Update mechanism as before. We will have the same update mechanism as in v1.0 +7. Transferable participant tokens. Participant tokens are free to be transferred between public keys +8. Longer epoch period (1 hour or 30 blocks). +9. No separate funding box. The pool box emits only reward tokens and won't be handing out Ergs. Thus, there won't be a separate funding process required. +10. Reward mechanism separate from pool. The process of redeeming the reward tokens is not part of the protocol. + +## Reward Mechanism + +In v1.0, the pool was responsbible for rewarding each participant for posting a data-point. In v2.0, the pool simply +certifies that a data-point was posted and a separate reward mechanism is proposed. This keeps the contract smaller and more flexible. + +The certificates are in the form of tokens emitted by the pool box. Once there are sufficient number of such tokens, a participant +can exchange or burn them in return for a reward. We also give a sample token exchange contract. + +**Note**: The reward mechanism needs more work. +Is there a need to lock the reward tokens and make them reedemable only by a certain contract? + +## Participant contract + +```scala +{ // This box (participant box) + // R4 data point + // R5 box id of pool box + // R6 public key + + // tokens(0) participant token (one) + // tokens(1) reward tokens collected (one or more) + // + // When initializing the box, there must be one reward token. When claiming reward, one token must be left unclaimed + + val poolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual + val minStorageRent = 100000000 + val selfPubKey = SELF.R6[GroupElement].get + val selfIndex = getVar[Int](0).get + val output = OUTPUTS(selfIndex) + val outPubKey = output.R6[GroupElement].get // output must have a public key (not necessarily the same) + + val isSimpleCopy = output.tokens(0) == SELF.tokens(0) && // participant token is preserved + output.tokens(1)._1 == SELF.tokens(1)._1 && // reward tokenId is preserved + output.tokens.size == 2 && // exactly two token types + output.propositionBytes == SELF.propositionBytes && // script preserved + output.R7[Any].isDefined == false // no more registers + + val collection = INPUTS(0).tokens(0)._1 == poolNFT && // first input must be pool box + output.tokens(1)._2 > SELF.tokens(1)._2 && // at least one reward token must be added + outPubKey == selfPubKey && + output.value >= SELF.value // nanoErgs value preserved + + val owner = proveDlog(selfPubKey) && + output.value >= minStorageRent + + // owner can choose to transfer to another public key by setting different value in R6 + isSimpleCopy && (owner || collection) +} +``` +## Pool Contract + +```scala +{ // This box (pool box) + // R4 Current data point (Long) + // epoch start height is stored in creation Height (R3) + // + // tokens(0) pool token (NFT) + // tokens(1) reward tokens to be emitted (several) + // + // When initializing the box, there must be one reward token. When claiming reward, one token must be left unclaimed + // + + val participantTokenId = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual + val updateNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual + + val poolAction = if (getVar[Any](0).isDefined) { + val spenderIndex = getVar[Int](0).get // the index of the data-point box (NOT input!) belonging to spender + + val epochLength = 30 // 1 hour + val minStartHeight = HEIGHT - epochLength + val minDataPoints = 4 + val buffer = 4 + val rewardTokens = SELF.tokens(1) + val maxDeviationPercent = 5 // percent + + def isValidDataPoint(b: Box) = if (b.R5[Any].isDefined) { + b.creationInfo._1 >= minStartHeight && // data point must not be too old + b.tokens(0)._1 == participantTokenId && // first token id must be of participant token + b.R5[Coll[Byte]].get == SELF.id // it must correspond to this epoch + } else false + + val dataPoints = INPUTS.filter(isValidDataPoint) + val pubKey = dataPoints(spenderIndex).R6[GroupElement].get + + val output = OUTPUTS(0) + + val enoughDataPoints = dataPoints.size >= minDataPoints + val rewardEmitted = dataPoints.size * 2 // one extra token for each collected box as reward to collector + val epochOver = SELF.creationInfo._1 <= minStartHeight + + val startData = 1L // we don't allow 0 data points + val startSum = 0L + // we expected datapoints to be sorted in INCREASING order + + val lastSortedSum = dataPoints.fold((startData, (true, startSum)), { + (t: (Long, (Boolean, Long)), b: Box) => + val currData = b.R4[Long].get + val prevData = t._1 + val wasSorted = t._2._1 + val oldSum = t._2._2 + val newSum = oldSum + currData // we don't have to worry about overflow, as it causes script to fail + + val isSorted = wasSorted && prevData <= currData + + (currData, (isSorted, newSum)) + } + ) + + val lastData = lastSortedSum._1 + val isSorted = lastSortedSum._2._1 + val sum = lastSortedSum._2._2 + val average = sum / dataPoints.size + + val maxDelta = lastData * maxDeviationPercent / 100 + val firstData = dataPoints(0).R4[Long].get + + sigmaProp(proveDlog(pubKey)) && + enoughDataPoints && + isSorted && + lastData - firstData <= maxDelta && + output.R4[Long].get == average && + output.tokens(0) == SELF.tokens(0) && // pool NFT preserved + output.tokens(1)._1 == SELF.tokens(1)._1 && // reward token id preserved + output.tokens(1)._2 == SELF.tokens(1)._2 - rewardEmitted && // reward token amount correctly reduced + output.tokens.size == 2 && // no more tokens + output.propositionBytes == SELF.propositionBytes && // script preserved + output.value >= SELF.value && // Ergs preserved && + output.creationInfo._1 >= HEIGHT - buffer // ensure that new box has correct start epoch height + } else false + + val updateAction = INPUTS(0).tokens(0)._1 == updateNFT + + poolAction || updateAction +} +``` +## Ballot Contract [WIP] + +```scala +{ // 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("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual + + 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 Contract [WIP] + +```scala +{ // 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]] + + val poolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual + + val ballotTokenId = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual + + // 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 ab5b37aa1b8ee9b6f6c3fe3ae031e782d8d70e97 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:52:58 +0530 Subject: [PATCH 02/43] Rename EIP-22 to EIP-23 22 is already taken by auction contracts --- eip-0022.md => eip-0023.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) rename eip-0022.md => eip-0023.md (93%) diff --git a/eip-0022.md b/eip-0023.md similarity index 93% rename from eip-0022.md rename to eip-0023.md index 96d8e1f5..fdeee379 100644 --- a/eip-0022.md +++ b/eip-0023.md @@ -1,5 +1,14 @@ # Oracle Pool v2.0 [WIP] +This is a proposed update to the oracle pool 1.0 currently deployed and documented in EIP16 (https://github.com/ergoplatform/eips/blob/eip16/eip-0016.md) +Oracle pool 1.0 has some drawbacks: +1. Rewards generate a lot of dust +2. Current rewards are too low (related to 1) +3. There are two types of oracle pool boxes. This makes dApps and update mechanism more complex. +4. Participant tokens are non-transferable, and so oracles are locked permanently + +The new version 2.0 aims to address the above comments + Below is a summary of the main new features in v2.0 and how it differs from v1.0 1. Single pool address (only one type of box). This version of the pool will have only one address, the *pool address*. @@ -15,8 +24,8 @@ Below is a summary of the main new features in v2.0 and how it differs from v1.0 ## Reward Mechanism -In v1.0, the pool was responsbible for rewarding each participant for posting a data-point. In v2.0, the pool simply -certifies that a data-point was posted and a separate reward mechanism is proposed. This keeps the contract smaller and more flexible. +In v1.0, the pool was responsible for rewarding each participant for posting a data-point. In v2.0, the pool simply +certifies that a data-point was posted, and a separate reward mechanism is proposed. This keeps the contract smaller and more flexible. The certificates are in the form of tokens emitted by the pool box. Once there are sufficient number of such tokens, a participant can exchange or burn them in return for a reward. We also give a sample token exchange contract. From adc45446a7c2ca3c7b656843ef0e401f301cbb86 Mon Sep 17 00:00:00 2001 From: ScalaHub <23208922+scalahub@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:56:56 +0530 Subject: [PATCH 03/43] Update eip-0023.md --- eip-0023.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eip-0023.md b/eip-0023.md index fdeee379..de325fde 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -4,7 +4,7 @@ This is a proposed update to the oracle pool 1.0 currently deployed and document Oracle pool 1.0 has some drawbacks: 1. Rewards generate a lot of dust 2. Current rewards are too low (related to 1) -3. There are two types of oracle pool boxes. This makes dApps and update mechanism more complex. +3. There are two types of oracle pool boxes. This makes dApps and update mechanism more complex 4. Participant tokens are non-transferable, and so oracles are locked permanently The new version 2.0 aims to address the above comments From ddbfbfc8500c498dfc29aaf6838b9946624677ec Mon Sep 17 00:00:00 2001 From: ScalaHub <23208922+scalahub@users.noreply.github.com> Date: Tue, 7 Sep 2021 20:56:53 +0530 Subject: [PATCH 04/43] Add EIP-23 to index of EIPs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a6257f52..5e278717 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,4 @@ Please check out existing EIPs, such as [EIP-1](eip-0001.md), to understand the | [EIP-0005](eip-0005.md) | Contract Template | | [EIP-0006](eip-0006.md) | Informal Smart Contract Protocol Specification Format | | [EIP-0017](eip-0017.md) | Proxy Contracts | +| [EIP-0023](eip-0023.md) | Oracle pool 2.0 | From 52c467eb63d237f098fa7a0dff7c65ae17ab6810 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Sun, 12 Sep 2021 04:57:03 +0530 Subject: [PATCH 05/43] Add description of oracle pool 2.0 --- eip-0023.md | 273 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 184 insertions(+), 89 deletions(-) diff --git a/eip-0023.md b/eip-0023.md index de325fde..adea3cdc 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -1,7 +1,8 @@ -# Oracle Pool v2.0 [WIP] +# Oracle Pool v2.0 This is a proposed update to the oracle pool 1.0 currently deployed and documented in EIP16 (https://github.com/ergoplatform/eips/blob/eip16/eip-0016.md) Oracle pool 1.0 has some drawbacks: + 1. Rewards generate a lot of dust 2. Current rewards are too low (related to 1) 3. There are two types of oracle pool boxes. This makes dApps and update mechanism more complex @@ -30,47 +31,100 @@ certifies that a data-point was posted, and a separate reward mechanism is propo The certificates are in the form of tokens emitted by the pool box. Once there are sufficient number of such tokens, a participant can exchange or burn them in return for a reward. We also give a sample token exchange contract. -**Note**: The reward mechanism needs more work. -Is there a need to lock the reward tokens and make them reedemable only by a certain contract? +## High level design -## Participant contract +In this section we describe the high level design of the system. This will include the tokens used, the contracts and the transactions. -```scala -{ // This box (participant box) - // R4 data point - // R5 box id of pool box - // R6 public key +### Contracts and boxes +There are a total of 5 contracts and so 5 types of boxes as mentioned below - // tokens(0) participant token (one) - // tokens(1) reward tokens collected (one or more) - // - // When initializing the box, there must be one reward token. When claiming reward, one token must be left unclaimed +| Contract name | How many boxes? | Token in box(es) | Important Registers | Purpose | Transactions | +|---|---|---|---|---|---| +|Pool | 1 | Pool-NFT| R3: Creation height (Int)
R4: Rate (Long)
R5: Epoch counter (Int) | Publish pool rate for dApps | Refresh pool | +|Refresh| 1 | Refresh-NFT
Reward tokens| | Refresh pool box | Refresh pool
Update pool | +|Participant | 15 | Participant token
Reward tokens | R4: Published rate (Long)
R5: Pool box Id (Coll[Byte])
R6: Public key (GroupElement) |Publish data-point
Accumulate reward | Publish data-point
Refresh pool
Transfer token
Redeem reward | +|Update | 1 | Update-NFT | Updating refresh box | Update refresh box | +|Ballot | 15 | Ballot token | R4: New refresh box hash (Coll[Byte])
R5: Update box Id (Coll[Byte]) | Voting for updating refresh box | Vote for update
Update pool | - val poolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual - val minStorageRent = 100000000 - val selfPubKey = SELF.R6[GroupElement].get - val selfIndex = getVar[Int](0).get - val output = OUTPUTS(selfIndex) - val outPubKey = output.R6[GroupElement].get // output must have a public key (not necessarily the same) - val isSimpleCopy = output.tokens(0) == SELF.tokens(0) && // participant token is preserved - output.tokens(1)._1 == SELF.tokens(1)._1 && // reward tokenId is preserved - output.tokens.size == 2 && // exactly two token types - output.propositionBytes == SELF.propositionBytes && // script preserved - output.R7[Any].isDefined == false // no more registers +### Tokens - val collection = INPUTS(0).tokens(0)._1 == poolNFT && // first input must be pool box - output.tokens(1)._2 > SELF.tokens(1)._2 && // at least one reward token must be added - outPubKey == selfPubKey && - output.value >= SELF.value // nanoErgs value preserved +The system has the following types of tokens. Note that we use the term **NFT** to refer to any token that was issued in quantity 1. + +| Token | Issued quantity | purpose | where stored | +|---|---|---|---| +|Pool-NFT | 1 | Identify pool box | Pool box | +|Refresh-NFT | 1 | Identify refresh box | Refresh box | +|Update-NFT | 1 | Identify update box | Update box | +|Participant tokens | 15 | Identify each participant box | Participant boxes | +|Ballot tokens | 15 | Identify each ballot box | Ballot boxes | +|Reward tokens | 100 million | Reward participants | Refresh box
participant boxes | + +### Transactions + +Oracle pool 2.0 has the following transactions. +Each of the transactions below is also assumed to have the following boxes which will not be shown. + +1. Funding box: this will be used to fund the transaction, and will be the last input +2. Fee box: this will be the last box +3. Change box: this is optional, and if present, will be the second last box. + +| Trasactions | Boxes Involved | Purpose | +| --- | --- | --- | +| Refresh pool | Pool
Refresh
Participants | Refresh pool rate | +| Publish data point | Participant | Publish data point for refresh | +| Redeem reward | Participant | Redeem rewards obtained via publishing rate | +| Transfer token | Participant | Transfer participant token to another public key | +| Vote for update | Ballot | Vote for updating refresh box | +| Update refresh box | Update
Refresh
Ballots | Update refresh box | + +None of the transactions have data-inputs. + +## Transaction Structure + +### Refresh pool + +| Index | Input | Output | +|---|---|---| +| 0 | Pool | Pool | +| 1 | Refresh | Refresh | +| 2 | Participant 1 | Participant 1 | +| 3 | Participant 2 | Participant 2 | +| 4 | Participant 3 | Participant 3 | +| ... | ... | ... | + +The participant boxes **must be sorted in increasing order** of their R4 (Long) value. + +### Publish data-point + +| Index | Input | Output | +|---|---|---| +| 0 | Participant | Participant | + +### Transfer participant token + +| Index | Input | Output | +|---|---|---| +| 0 | Participant | Participant | + +### Vote for update + +| Index | Input | Output | +|---|---|---| +| 0 | Ballot | Ballot | + +### Update refresh box + +| Index | Input | Output | +|---|---|---| +| 0 | Update | Update | +| 1 | Refresh | Refresh | +| 2 | Ballot 1 | Ballot 1 | +| 3 | Ballot 2 | Ballot 2 | +| 4 | Ballot 3 | Ballot 3 | +| ... | ... | ... | - val owner = proveDlog(selfPubKey) && - output.value >= minStorageRent - // owner can choose to transfer to another public key by setting different value in R6 - isSimpleCopy && (owner || collection) -} -``` ## Pool Contract ```scala @@ -79,13 +133,23 @@ Is there a need to lock the reward tokens and make them reedemable only by a cer // epoch start height is stored in creation Height (R3) // // tokens(0) pool token (NFT) - // tokens(1) reward tokens to be emitted (several) - // - // When initializing the box, there must be one reward token. When claiming reward, one token must be left unclaimed + + val refreshNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual + + sigmaProp(INPUTS(1).tokens(0)._1 == refreshNFT) +} +``` +## Refresh Contract + +```scala +{ // This box (refresh box) + // tokens(0) reward tokens to be emitted (several) // + // When initializing the box, there must be one reward token. When claiming reward, one token must be left unclaimed val participantTokenId = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual val updateNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual + val poolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual val poolAction = if (getVar[Any](0).isDefined) { val spenderIndex = getVar[Int](0).get // the index of the data-point box (NOT input!) belonging to spender @@ -94,23 +158,24 @@ Is there a need to lock the reward tokens and make them reedemable only by a cer val minStartHeight = HEIGHT - epochLength val minDataPoints = 4 val buffer = 4 - val rewardTokens = SELF.tokens(1) val maxDeviationPercent = 5 // percent def isValidDataPoint(b: Box) = if (b.R5[Any].isDefined) { b.creationInfo._1 >= minStartHeight && // data point must not be too old b.tokens(0)._1 == participantTokenId && // first token id must be of participant token - b.R5[Coll[Byte]].get == SELF.id // it must correspond to this epoch + b.R5[Coll[Byte]].get == poolIn.id // it must correspond to this epoch } else false val dataPoints = INPUTS.filter(isValidDataPoint) val pubKey = dataPoints(spenderIndex).R6[GroupElement].get - val output = OUTPUTS(0) + val poolIn = INPUTS(0) + val poolOut = OUTPUTS(0) + val selfOut = OUTPUTS(1) val enoughDataPoints = dataPoints.size >= minDataPoints val rewardEmitted = dataPoints.size * 2 // one extra token for each collected box as reward to collector - val epochOver = SELF.creationInfo._1 <= minStartHeight + val epochOver = poolIn.creationInfo._1 <= minStartHeight val startData = 1L // we don't allow 0 data points val startSum = 0L @@ -138,18 +203,23 @@ Is there a need to lock the reward tokens and make them reedemable only by a cer val maxDelta = lastData * maxDeviationPercent / 100 val firstData = dataPoints(0).R4[Long].get - sigmaProp(proveDlog(pubKey)) && - enoughDataPoints && - isSorted && - lastData - firstData <= maxDelta && - output.R4[Long].get == average && - output.tokens(0) == SELF.tokens(0) && // pool NFT preserved - output.tokens(1)._1 == SELF.tokens(1)._1 && // reward token id preserved - output.tokens(1)._2 == SELF.tokens(1)._2 - rewardEmitted && // reward token amount correctly reduced - output.tokens.size == 2 && // no more tokens - output.propositionBytes == SELF.propositionBytes && // script preserved - output.value >= SELF.value && // Ergs preserved && - output.creationInfo._1 >= HEIGHT - buffer // ensure that new box has correct start epoch height + sigmaProp(proveDlog(pubKey)) && + enoughDataPoints && + isSorted && + lastData - firstData <= maxDelta && + poolIn.tokens(0)._1 == poolNFT && + poolOut.tokens == poolIn.tokens && // preserve pool tokens + poolOut.R4[Long].get == average && // rate + poolOut.R5[Int].get == poolIn.R5[Int].get + 1 && // counter + poolOut.propositionBytes == poolIn.propositionBytes && // preserve pool script + poolOut.value >= poolIn.value && + poolOut.creationInfo._1 >= HEIGHT - buffer && // ensure that new box has correct start epoch height + selfOut.tokens(0) == SELF.tokens(0) && // refresh NFT preserved + selfOut.tokens(1)._1 == SELF.tokens(1)._1 && // reward token id preserved + selfOut.tokens(1)._2 == SELF.tokens(1)._2 - rewardEmitted && // reward token amount correctly reduced + selfOut.tokens.size == 2 && // no more tokens + selfOut.propositionBytes == SELF.propositionBytes && // script preserved + selfOut.value >= SELF.value // Ergs preserved && } else false val updateAction = INPUTS(0).tokens(0)._1 == updateNFT @@ -157,14 +227,53 @@ Is there a need to lock the reward tokens and make them reedemable only by a cer poolAction || updateAction } ``` -## Ballot Contract [WIP] + +## Participant contract + +```scala +{ // This box (participant box) + // R4 data point (Long) + // R5 box id of pool box (Coll[Byte]) + // R6 public key (GroupElement) + + // tokens(0) participant token (one) + // tokens(1) reward tokens collected (one or more) + // + // When initializing the box, there must be one reward token. When claiming reward, one token must be left unclaimed + + val poolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual + val minStorageRent = 100000000 + val selfPubKey = SELF.R6[GroupElement].get + val selfIndex = getVar[Int](0).get + val output = OUTPUTS(selfIndex) + + val isSimpleCopy = output.tokens(0) == SELF.tokens(0) && // participant token is preserved + output.tokens(1)._1 == SELF.tokens(1)._1 && // reward tokenId is preserved + output.tokens.size == 2 && // exactly two token types + output.propositionBytes == SELF.propositionBytes && // script preserved + output.R6[GroupElement].isDefined && // output must have a public key (not necessarily the same) + output.R7[Any].isDefined == false // no more registers + + val collection = INPUTS(0).tokens(0)._1 == poolNFT && // first input must be pool box + output.tokens(1)._2 > SELF.tokens(1)._2 && // at least one reward token must be added + output.R6[GroupElement].get == selfPubKey && // for collection preserve public key + output.value >= SELF.value // nanoErgs value preserved + + val owner = proveDlog(selfPubKey) && + output.value >= minStorageRent + + // owner can choose to transfer to another public key by setting different value in R6 + isSimpleCopy && (owner || collection) +} +``` + +## Ballot Contract ```scala { // 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]] + // R5 the box id of the update box [Coll[Byte]] + // R6 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 @@ -172,11 +281,10 @@ Is there a need to lock the reward tokens and make them reedemable only by a cer val pubKey = SELF.R4[GroupElement].get - val index = INPUTS.indexOf(SELF, 0) - - val output = OUTPUTS(index) + val output = OUTPUTS(0) - val isBasicCopy = output.R4[GroupElement].get == pubKey && + val isBasicCopy = SELF.id == INPUTS(0).id && + output.R4[GroupElement].isDefined && output.propositionBytes == SELF.propositionBytes && output.tokens == SELF.tokens && output.value >= 10000000 // minStorageRent @@ -185,6 +293,7 @@ Is there a need to lock the reward tokens and make them reedemable only by a cer isBasicCopy && ( proveDlog(pubKey) || ( INPUTS(0).tokens(0)._1 == updateNFT && + output.R4[GroupElement].get == pubKey && output.value >= SELF.value ) ) @@ -192,7 +301,7 @@ Is there a need to lock the reward tokens and make them reedemable only by a cer } ``` -## Update Contract [WIP] +## Update Contract ```scala { // This box (update box): @@ -200,11 +309,10 @@ Is there a need to lock the reward tokens and make them reedemable only by a cer // // 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]] + // R5 the box id of this box [Coll[Byte]] + // R6 the value voted for [Coll[Byte]] - val poolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual + val refreshNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual val ballotTokenId = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual @@ -212,45 +320,32 @@ Is there a need to lock the reward tokens and make them reedemable only by a cer 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 + val refreshBoxIn = INPUTS(1) // pool box is 2nd input + val refreshBoxOut = 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 refreshBoxOutHash = blake2b256(refreshBoxOut.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 validRefreshIn = refreshBoxIn.tokens(0)._1 == refreshNFT + val validRefreshOut = refreshBoxIn.tokens == refreshBoxOut.tokens && + refreshBoxIn.value == refreshBoxOut.value 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) + SELF.value <= updateBoxOut.value 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 + b.R5[Coll[Byte]].get == SELF.id && // ensure vote corresponds to this box **** + b.R6[Coll[Byte]].get == refreshBoxOutHash // 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 + sigmaProp(validRefreshIn && validRefreshOut && validUpdateIn && validUpdateOut && votesCount >= 8) // minVotes = 8 } ``` From ee92d8c79ff88b72b29b8b1ac1154529aad6e085 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Sun, 12 Sep 2021 05:16:24 +0530 Subject: [PATCH 06/43] Store participant public keys in R4 --- eip-0023.md | 71 +++++++++++++++++++++++++++-------------------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/eip-0023.md b/eip-0023.md index adea3cdc..01a58fc4 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -1,16 +1,17 @@ # Oracle Pool v2.0 -This is a proposed update to the oracle pool 1.0 currently deployed and documented in EIP16 (https://github.com/ergoplatform/eips/blob/eip16/eip-0016.md) -Oracle pool 1.0 has some drawbacks: +This is a proposed update to the oracle pool 1.0 currently deployed and documented in [EIP16](https://github.com/ergoplatform/eips/blob/eip16/eip-0016.md). + +Oracle pool 1.0 has the following drawbacks: 1. Rewards generate a lot of dust 2. Current rewards are too low (related to 1) 3. There are two types of oracle pool boxes. This makes dApps and update mechanism more complex -4. Participant tokens are non-transferable, and so oracles are locked permanently +4. Participant tokens are non-transferable, and so oracles are locked permanently. The same goes with ballot tokens. -The new version 2.0 aims to address the above comments +Oracle pool 2.0 aims to address the above. -Below is a summary of the main new features in v2.0 and how it differs from v1.0 +Below is a summary of the main new features in 2.0 and how it differs from 1.0. 1. Single pool address (only one type of box). This version of the pool will have only one address, the *pool address*. 2. Data-point boxes will be considered as inputs rather than data-inputs. These inputs will be spent, and a copy of the box with the reward will be created. @@ -22,34 +23,22 @@ Below is a summary of the main new features in v2.0 and how it differs from v1.0 8. Longer epoch period (1 hour or 30 blocks). 9. No separate funding box. The pool box emits only reward tokens and won't be handing out Ergs. Thus, there won't be a separate funding process required. 10. Reward mechanism separate from pool. The process of redeeming the reward tokens is not part of the protocol. +11. Pool box is separated from the logic of pool management, which is captured instead in a **refresh** box. This makes the pool box very small for use in other dApps. ## Reward Mechanism -In v1.0, the pool was responsible for rewarding each participant for posting a data-point. In v2.0, the pool simply -certifies that a data-point was posted, and a separate reward mechanism is proposed. This keeps the contract smaller and more flexible. +In 1.0, the pool was responsible for rewarding each participant for posting a data-point. In 2.0, the pool simply certifies that a data-point was posted, and a separate reward mechanism is proposed. This keeps the contract smaller and more flexible. The certificates are in the form of tokens emitted by the pool box. Once there are sufficient number of such tokens, a participant can exchange or burn them in return for a reward. We also give a sample token exchange contract. -## High level design +## Design In this section we describe the high level design of the system. This will include the tokens used, the contracts and the transactions. -### Contracts and boxes -There are a total of 5 contracts and so 5 types of boxes as mentioned below - -| Contract name | How many boxes? | Token in box(es) | Important Registers | Purpose | Transactions | -|---|---|---|---|---|---| -|Pool | 1 | Pool-NFT| R3: Creation height (Int)
R4: Rate (Long)
R5: Epoch counter (Int) | Publish pool rate for dApps | Refresh pool | -|Refresh| 1 | Refresh-NFT
Reward tokens| | Refresh pool box | Refresh pool
Update pool | -|Participant | 15 | Participant token
Reward tokens | R4: Published rate (Long)
R5: Pool box Id (Coll[Byte])
R6: Public key (GroupElement) |Publish data-point
Accumulate reward | Publish data-point
Refresh pool
Transfer token
Redeem reward | -|Update | 1 | Update-NFT | Updating refresh box | Update refresh box | -|Ballot | 15 | Ballot token | R4: New refresh box hash (Coll[Byte])
R5: Update box Id (Coll[Byte]) | Voting for updating refresh box | Vote for update
Update pool | - - ### Tokens -The system has the following types of tokens. Note that we use the term **NFT** to refer to any token that was issued in quantity 1. +The system has the following types of tokens. Note that we use the term **NFT** to refer to any token that was issued in quantity 1. | Token | Issued quantity | purpose | where stored | |---|---|---|---| @@ -58,7 +47,19 @@ The system has the following types of tokens. Note that we use the term **NFT** |Update-NFT | 1 | Identify update box | Update box | |Participant tokens | 15 | Identify each participant box | Participant boxes | |Ballot tokens | 15 | Identify each ballot box | Ballot boxes | -|Reward tokens | 100 million | Reward participants | Refresh box
participant boxes | +|Reward tokens | 100 million | Reward participants | Refresh box
Participant boxes | + +### Contracts and boxes +There are a total of 5 contracts and so 5 types of boxes as mentioned below + +| Contract name | How many boxes? | Token in boxes | Important Registers | Purpose | Transactions | +|---|---|---|---|---|---| +|Pool | 1 | Pool-NFT| R3: Creation height (Int)
R4: Rate (Long)
R5: Epoch counter (Int) | Publish pool rate for dApps | Refresh pool | +|Refresh| 1 | Refresh-NFT
Reward tokens| | Refresh pool box | Refresh pool
Update pool | +|Participant | 15 | Participant token
Reward tokens | R4: Public key (GroupElement)
R5: Pool box Id (Coll[Byte])
R6: Published rate (Long) |Publish data-point
Accumulate reward | Publish data-point
Refresh pool
Transfer token
Redeem reward | +|Update | 1 | Update-NFT | Updating refresh box | Update refresh box | +|Ballot | 15 | Ballot token | R4: Public key (GroupElement)
R5: Update box Id (Coll[Byte])
R6: New refresh box hash (Coll[Byte]) | Voting for updating refresh box | Vote for update
Update pool | + ### Transactions @@ -160,6 +161,10 @@ The participant boxes **must be sorted in increasing order** of their R4 (Long) val buffer = 4 val maxDeviationPercent = 5 // percent + val poolIn = INPUTS(0) + val poolOut = OUTPUTS(0) + val selfOut = OUTPUTS(1) + def isValidDataPoint(b: Box) = if (b.R5[Any].isDefined) { b.creationInfo._1 >= minStartHeight && // data point must not be too old b.tokens(0)._1 == participantTokenId && // first token id must be of participant token @@ -167,11 +172,7 @@ The participant boxes **must be sorted in increasing order** of their R4 (Long) } else false val dataPoints = INPUTS.filter(isValidDataPoint) - val pubKey = dataPoints(spenderIndex).R6[GroupElement].get - - val poolIn = INPUTS(0) - val poolOut = OUTPUTS(0) - val selfOut = OUTPUTS(1) + val pubKey = dataPoints(spenderIndex).R4[GroupElement].get val enoughDataPoints = dataPoints.size >= minDataPoints val rewardEmitted = dataPoints.size * 2 // one extra token for each collected box as reward to collector @@ -183,7 +184,7 @@ The participant boxes **must be sorted in increasing order** of their R4 (Long) val lastSortedSum = dataPoints.fold((startData, (true, startSum)), { (t: (Long, (Boolean, Long)), b: Box) => - val currData = b.R4[Long].get + val currData = b.R6[Long].get val prevData = t._1 val wasSorted = t._2._1 val oldSum = t._2._2 @@ -201,7 +202,7 @@ The participant boxes **must be sorted in increasing order** of their R4 (Long) val average = sum / dataPoints.size val maxDelta = lastData * maxDeviationPercent / 100 - val firstData = dataPoints(0).R4[Long].get + val firstData = dataPoints(0).R6[Long].get sigmaProp(proveDlog(pubKey)) && enoughDataPoints && @@ -232,9 +233,9 @@ The participant boxes **must be sorted in increasing order** of their R4 (Long) ```scala { // This box (participant box) - // R4 data point (Long) + // R4 public key (GroupElement) // R5 box id of pool box (Coll[Byte]) - // R6 public key (GroupElement) + // R6 data point (Long) // tokens(0) participant token (one) // tokens(1) reward tokens collected (one or more) @@ -243,7 +244,7 @@ The participant boxes **must be sorted in increasing order** of their R4 (Long) val poolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual val minStorageRent = 100000000 - val selfPubKey = SELF.R6[GroupElement].get + val selfPubKey = SELF.R4[GroupElement].get val selfIndex = getVar[Int](0).get val output = OUTPUTS(selfIndex) @@ -251,13 +252,13 @@ The participant boxes **must be sorted in increasing order** of their R4 (Long) output.tokens(1)._1 == SELF.tokens(1)._1 && // reward tokenId is preserved output.tokens.size == 2 && // exactly two token types output.propositionBytes == SELF.propositionBytes && // script preserved - output.R6[GroupElement].isDefined && // output must have a public key (not necessarily the same) - output.R7[Any].isDefined == false // no more registers + output.R4[GroupElement].isDefined // output must have a public key (not necessarily the same) val collection = INPUTS(0).tokens(0)._1 == poolNFT && // first input must be pool box output.tokens(1)._2 > SELF.tokens(1)._2 && // at least one reward token must be added - output.R6[GroupElement].get == selfPubKey && // for collection preserve public key + output.R4[GroupElement].get == selfPubKey && // for collection preserve public key output.value >= SELF.value // nanoErgs value preserved + output.R5[Any].isDefined == false // no more registers val owner = proveDlog(selfPubKey) && output.value >= minStorageRent From d35140e579062d8013418988538ea2e18f6acc6f Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Mon, 13 Sep 2021 00:56:08 +0530 Subject: [PATCH 07/43] Add counter to pool box and fix typos --- eip-0023.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/eip-0023.md b/eip-0023.md index 01a58fc4..221d5d69 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -24,6 +24,7 @@ Below is a summary of the main new features in 2.0 and how it differs from 1.0. 9. No separate funding box. The pool box emits only reward tokens and won't be handing out Ergs. Thus, there won't be a separate funding process required. 10. Reward mechanism separate from pool. The process of redeeming the reward tokens is not part of the protocol. 11. Pool box is separated from the logic of pool management, which is captured instead in a **refresh** box. This makes the pool box very small for use in other dApps. +12. Pool box will additionally store a counter that is incremented on each collection. ## Reward Mechanism @@ -57,20 +58,20 @@ There are a total of 5 contracts and so 5 types of boxes as mentioned below |Pool | 1 | Pool-NFT| R3: Creation height (Int)
R4: Rate (Long)
R5: Epoch counter (Int) | Publish pool rate for dApps | Refresh pool | |Refresh| 1 | Refresh-NFT
Reward tokens| | Refresh pool box | Refresh pool
Update pool | |Participant | 15 | Participant token
Reward tokens | R4: Public key (GroupElement)
R5: Pool box Id (Coll[Byte])
R6: Published rate (Long) |Publish data-point
Accumulate reward | Publish data-point
Refresh pool
Transfer token
Redeem reward | -|Update | 1 | Update-NFT | Updating refresh box | Update refresh box | +|Update | 1 | Update-NFT | | Updating refresh box | Update refresh box | |Ballot | 15 | Ballot token | R4: Public key (GroupElement)
R5: Update box Id (Coll[Byte])
R6: New refresh box hash (Coll[Byte]) | Voting for updating refresh box | Vote for update
Update pool | ### Transactions Oracle pool 2.0 has the following transactions. -Each of the transactions below is also assumed to have the following boxes which will not be shown. +Each of the transactions below also contain the following boxes which will not be shown. -1. Funding box: this will be used to fund the transaction, and will be the last input -2. Fee box: this will be the last box -3. Change box: this is optional, and if present, will be the second last box. +1. Funding input box: this will be used to fund the transaction, and will be the last input. +2. Fee output box: this will be the last output. +3. Change output box: this is optional, and if present, will be the second-last output. -| Trasactions | Boxes Involved | Purpose | +| Transaction | Boxes Involved | Purpose | | --- | --- | --- | | Refresh pool | Pool
Refresh
Participants | Refresh pool rate | | Publish data point | Participant | Publish data point for refresh | @@ -130,8 +131,9 @@ The participant boxes **must be sorted in increasing order** of their R4 (Long) ```scala { // This box (pool box) - // R4 Current data point (Long) // epoch start height is stored in creation Height (R3) + // R4 Current data point (Long) + // R5 Epoch counter (Int) // // tokens(0) pool token (NFT) From 95a2da097a00c9e2bcc54d26055bf0fc570deeca Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Mon, 13 Sep 2021 14:14:49 +0530 Subject: [PATCH 08/43] Update the pool box instead of refresh box --- eip-0023.md | 274 +++++++++++++++++++++++++++------------------------- 1 file changed, 142 insertions(+), 132 deletions(-) diff --git a/eip-0023.md b/eip-0023.md index 221d5d69..94690f55 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -48,19 +48,18 @@ The system has the following types of tokens. Note that we use the term **NFT** |Update-NFT | 1 | Identify update box | Update box | |Participant tokens | 15 | Identify each participant box | Participant boxes | |Ballot tokens | 15 | Identify each ballot box | Ballot boxes | -|Reward tokens | 100 million | Reward participants | Refresh box
Participant boxes | +|Reward tokens | 100 million | Reward participants | Refresh box
Participant boxes | ### Contracts and boxes There are a total of 5 contracts and so 5 types of boxes as mentioned below -| Contract name | How many boxes? | Token in boxes | Important Registers | Purpose | Transactions | +| Contract | Num boxes | Tokens | Registers used | Purpose | Transactions | |---|---|---|---|---|---| -|Pool | 1 | Pool-NFT| R3: Creation height (Int)
R4: Rate (Long)
R5: Epoch counter (Int) | Publish pool rate for dApps | Refresh pool | -|Refresh| 1 | Refresh-NFT
Reward tokens| | Refresh pool box | Refresh pool
Update pool | -|Participant | 15 | Participant token
Reward tokens | R4: Public key (GroupElement)
R5: Pool box Id (Coll[Byte])
R6: Published rate (Long) |Publish data-point
Accumulate reward | Publish data-point
Refresh pool
Transfer token
Redeem reward | -|Update | 1 | Update-NFT | | Updating refresh box | Update refresh box | -|Ballot | 15 | Ballot token | R4: Public key (GroupElement)
R5: Update box Id (Coll[Byte])
R6: New refresh box hash (Coll[Byte]) | Voting for updating refresh box | Vote for update
Update pool | - +|Pool | 1 | Pool-NFT| R3: Creation height (Int)
R4: Rate (Long)
R5: Epoch counter (Int) | Publish pool rate for dApps | Refresh pool
Update pool | +|Refresh| 1 | Refresh-NFT
Reward tokens| | Refresh pool box
Emit reward tokens| Refresh pool | +|Participant | 15 | Participant token
Reward tokens | R4: Public key (GroupElement)
R5: Pool box Id (Coll[Byte])
R6: Published rate (Long) | Publish data-point
Accumulate reward tokens | Publish data-point,
Refresh pool,
Transfer participant token,
Redeem reward tokens| +|Update | 1 | Update-NFT | | Updating pool box | Update pool box | +|Ballot | 15 | Ballot token | R4: Public key (GroupElement)
R5: Update box Id (Coll[Byte])
R6: New pool box hash (Coll[Byte]) | Voting for updating pool box | Vote for update
Update pool box | ### Transactions @@ -73,12 +72,12 @@ Each of the transactions below also contain the following boxes which will not b | Transaction | Boxes Involved | Purpose | | --- | --- | --- | -| Refresh pool | Pool
Refresh
Participants | Refresh pool rate | -| Publish data point | Participant | Publish data point for refresh | -| Redeem reward | Participant | Redeem rewards obtained via publishing rate | -| Transfer token | Participant | Transfer participant token to another public key | -| Vote for update | Ballot | Vote for updating refresh box | -| Update refresh box | Update
Refresh
Ballots | Update refresh box | +| Refresh pool | Pool
Refresh
Participants | Refresh pool box | +| Publish data point | Participant | Publish data point | +| Redeem reward | Participant | Redeem rewards | +| Transfer token | Participant | Transfer participant token | +| Vote for update | Ballot | Vote for updating pool box | +| Update pool | Pool
Update
Ballots | Update pool box | None of the transactions have data-inputs. @@ -95,7 +94,7 @@ None of the transactions have data-inputs. | 4 | Participant 3 | Participant 3 | | ... | ... | ... | -The participant boxes **must be sorted in increasing order** of their R4 (Long) value. +The participant boxes **must be sorted in increasing order** of their R6 (Long) value. ### Publish data-point @@ -119,13 +118,14 @@ The participant boxes **must be sorted in increasing order** of their R4 (Long) | Index | Input | Output | |---|---|---| -| 0 | Update | Update | -| 1 | Refresh | Refresh | +| 0 | Pool | Pool | +| 1 | Update | Update | | 2 | Ballot 1 | Ballot 1 | | 3 | Ballot 2 | Ballot 2 | | 4 | Ballot 3 | Ballot 3 | | ... | ... | ... | +# Contracts ## Pool Contract @@ -137,9 +137,11 @@ The participant boxes **must be sorted in increasing order** of their R4 (Long) // // tokens(0) pool token (NFT) + val otherTokenId = INPUTS(1).tokens(0)._1 val refreshNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual + val updateNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual - sigmaProp(INPUTS(1).tokens(0)._1 == refreshNFT) + sigmaProp(otherTokenId == refreshNFT || otherTokenId == updateNFT) } ``` ## Refresh Contract @@ -151,40 +153,38 @@ The participant boxes **must be sorted in increasing order** of their R4 (Long) // When initializing the box, there must be one reward token. When claiming reward, one token must be left unclaimed val participantTokenId = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual - val updateNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual val poolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual - val poolAction = if (getVar[Any](0).isDefined) { - val spenderIndex = getVar[Int](0).get // the index of the data-point box (NOT input!) belonging to spender - - val epochLength = 30 // 1 hour - val minStartHeight = HEIGHT - epochLength - val minDataPoints = 4 - val buffer = 4 - val maxDeviationPercent = 5 // percent - - val poolIn = INPUTS(0) - val poolOut = OUTPUTS(0) - val selfOut = OUTPUTS(1) - - def isValidDataPoint(b: Box) = if (b.R5[Any].isDefined) { - b.creationInfo._1 >= minStartHeight && // data point must not be too old - b.tokens(0)._1 == participantTokenId && // first token id must be of participant token - b.R5[Coll[Byte]].get == poolIn.id // it must correspond to this epoch - } else false + val spenderIndex = getVar[Int](0).get // the index of the data-point box (NOT input!) belonging to spender + + val epochLength = 30 // 1 hour + val minStartHeight = HEIGHT - epochLength + val minDataPoints = 4 + val buffer = 4 + val maxDeviationPercent = 5 // percent + + val poolIn = INPUTS(0) + val poolOut = OUTPUTS(0) + val selfOut = OUTPUTS(1) + + def isValidDataPoint(b: Box) = if (b.R5[Any].isDefined) { + b.creationInfo._1 >= minStartHeight && // data point must not be too old + b.tokens(0)._1 == participantTokenId && // first token id must be of participant token + b.R5[Coll[Byte]].get == poolIn.id // it must correspond to this epoch + } else false - val dataPoints = INPUTS.filter(isValidDataPoint) - val pubKey = dataPoints(spenderIndex).R4[GroupElement].get + val dataPoints = INPUTS.filter(isValidDataPoint) + val pubKey = dataPoints(spenderIndex).R4[GroupElement].get - val enoughDataPoints = dataPoints.size >= minDataPoints - val rewardEmitted = dataPoints.size * 2 // one extra token for each collected box as reward to collector - val epochOver = poolIn.creationInfo._1 <= minStartHeight + val enoughDataPoints = dataPoints.size >= minDataPoints + val rewardEmitted = dataPoints.size * 2 // one extra token for each collected box as reward to collector + val epochOver = poolIn.creationInfo._1 < minStartHeight - val startData = 1L // we don't allow 0 data points - val startSum = 0L - // we expected datapoints to be sorted in INCREASING order - - val lastSortedSum = dataPoints.fold((startData, (true, startSum)), { + val startData = 1L // we don't allow 0 data points + val startSum = 0L + // we expect data-points to be sorted in INCREASING order + + val lastSortedSum = dataPoints.fold((startData, (true, startSum)), { (t: (Long, (Boolean, Long)), b: Box) => val currData = b.R6[Long].get val prevData = t._1 @@ -195,39 +195,34 @@ The participant boxes **must be sorted in increasing order** of their R4 (Long) val isSorted = wasSorted && prevData <= currData (currData, (isSorted, newSum)) - } - ) + } + ) - val lastData = lastSortedSum._1 - val isSorted = lastSortedSum._2._1 - val sum = lastSortedSum._2._2 - val average = sum / dataPoints.size - - val maxDelta = lastData * maxDeviationPercent / 100 - val firstData = dataPoints(0).R6[Long].get - - sigmaProp(proveDlog(pubKey)) && - enoughDataPoints && - isSorted && - lastData - firstData <= maxDelta && - poolIn.tokens(0)._1 == poolNFT && - poolOut.tokens == poolIn.tokens && // preserve pool tokens - poolOut.R4[Long].get == average && // rate - poolOut.R5[Int].get == poolIn.R5[Int].get + 1 && // counter - poolOut.propositionBytes == poolIn.propositionBytes && // preserve pool script - poolOut.value >= poolIn.value && - poolOut.creationInfo._1 >= HEIGHT - buffer && // ensure that new box has correct start epoch height - selfOut.tokens(0) == SELF.tokens(0) && // refresh NFT preserved - selfOut.tokens(1)._1 == SELF.tokens(1)._1 && // reward token id preserved - selfOut.tokens(1)._2 == SELF.tokens(1)._2 - rewardEmitted && // reward token amount correctly reduced - selfOut.tokens.size == 2 && // no more tokens - selfOut.propositionBytes == SELF.propositionBytes && // script preserved - selfOut.value >= SELF.value // Ergs preserved && - } else false - - val updateAction = INPUTS(0).tokens(0)._1 == updateNFT - - poolAction || updateAction + val lastData = lastSortedSum._1 + val isSorted = lastSortedSum._2._1 + val sum = lastSortedSum._2._2 + val average = sum / dataPoints.size + + val maxDelta = lastData * maxDeviationPercent / 100 + val firstData = dataPoints(0).R6[Long].get + + sigmaProp(proveDlog(pubKey)) && + enoughDataPoints && + isSorted && + lastData - firstData <= maxDelta && + poolIn.tokens(0)._1 == poolNFT && + poolOut.tokens == poolIn.tokens && // preserve pool tokens + poolOut.R4[Long].get == average && // rate + poolOut.R5[Int].get == poolIn.R5[Int].get + 1 && // counter + poolOut.propositionBytes == poolIn.propositionBytes && // preserve pool script + poolOut.value >= poolIn.value && + poolOut.creationInfo._1 >= HEIGHT - buffer && // ensure that new box has correct start epoch height + selfOut.tokens(0) == SELF.tokens(0) && // refresh NFT preserved + selfOut.tokens(1)._1 == SELF.tokens(1)._1 && // reward token id preserved + selfOut.tokens(1)._2 == SELF.tokens(1)._2 - rewardEmitted && // reward token amount correctly reduced + selfOut.tokens.size == 2 && // no more tokens + selfOut.propositionBytes == SELF.propositionBytes && // script preserved + selfOut.value >= SELF.value } ``` @@ -243,29 +238,38 @@ The participant boxes **must be sorted in increasing order** of their R4 (Long) // tokens(1) reward tokens collected (one or more) // // When initializing the box, there must be one reward token. When claiming reward, one token must be left unclaimed + // + // We will connect this box to pool NFT in input #0 (and not the refresh NFT in input #1). + // This way, we can continue to use the same box after updating pool + // This *could* allow the participant box to be spent during an update + // (when input #2 contains the update NFT instead of the refresh NFT) + // However, this is not an issue because the update contract ensures that tokens and registers (except script) of the pool box are preserved val poolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual - val minStorageRent = 100000000 + + val otherTokenId = INPUTS(0).tokens(0)._1 + + val minStorageRent = 10000000L val selfPubKey = SELF.R4[GroupElement].get - val selfIndex = getVar[Int](0).get - val output = OUTPUTS(selfIndex) - - val isSimpleCopy = output.tokens(0) == SELF.tokens(0) && // participant token is preserved - output.tokens(1)._1 == SELF.tokens(1)._1 && // reward tokenId is preserved - output.tokens.size == 2 && // exactly two token types - output.propositionBytes == SELF.propositionBytes && // script preserved - output.R4[GroupElement].isDefined // output must have a public key (not necessarily the same) + val outIndex = getVar[Int](0).get + val output = OUTPUTS(outIndex) + + val isSimpleCopy = output.tokens(0) == SELF.tokens(0) && // participant token is preserved + output.tokens(1)._1 == SELF.tokens(1)._1 && // reward tokenId is preserved + output.tokens.size == 2 && // no more tokens + output.propositionBytes == SELF.propositionBytes && // script preserved + output.R4[GroupElement].isDefined && // output must have a public key (not necessarily the same) + output.value >= minStorageRent // ensure sufficient Ergs to ensure no garbage collection - val collection = INPUTS(0).tokens(0)._1 == poolNFT && // first input must be pool box - output.tokens(1)._2 > SELF.tokens(1)._2 && // at least one reward token must be added - output.R4[GroupElement].get == selfPubKey && // for collection preserve public key - output.value >= SELF.value // nanoErgs value preserved - output.R5[Any].isDefined == false // no more registers + val collection = otherTokenId == poolNFT && // first input must be pool box + output.tokens(1)._2 > SELF.tokens(1)._2 && // at least one reward token must be added + output.R4[GroupElement].get == selfPubKey && // for collection preserve public key + output.value >= SELF.value && // nanoErgs value preserved + output.R5[Any].isDefined == false // no more registers, preserving only R4, the group element - val owner = proveDlog(selfPubKey) && - output.value >= minStorageRent + val owner = proveDlog(selfPubKey) - // owner can choose to transfer to another public key by setting different value in R6 + // owner can choose to transfer to another public key by setting different value in R4 isSimpleCopy && (owner || collection) } ``` @@ -282,25 +286,26 @@ The participant boxes **must be sorted in increasing order** of their R4 (Long) // Got via http://tomeko.net/online_tools/hex_to_base64.php val updateNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual - val pubKey = SELF.R4[GroupElement].get + val minStorageRent = 10000000L - val output = OUTPUTS(0) + val selfPubKey = SELF.R4[GroupElement].get + val otherTokenId = INPUTS(0).tokens(0)._1 - val isBasicCopy = SELF.id == INPUTS(0).id && - output.R4[GroupElement].isDefined && - output.propositionBytes == SELF.propositionBytes && - output.tokens == SELF.tokens && - output.value >= 10000000 // minStorageRent + val outIndex = getVar[Int](0).get + val output = OUTPUTS(outIndex) - sigmaProp( - isBasicCopy && ( - proveDlog(pubKey) || ( - INPUTS(0).tokens(0)._1 == updateNFT && - output.R4[GroupElement].get == pubKey && - output.value >= SELF.value - ) - ) - ) + val isSimpleCopy = output.R4[GroupElement].isDefined && // ballot boxes are transferable by setting different value here + output.propositionBytes == SELF.propositionBytes && + output.tokens == SELF.tokens && + output.value >= minStorageRent + + val update = otherTokenId == updateNFT && + output.R4[GroupElement].get == selfPubKey && + output.value >= SELF.value + + val owner = proveDlog(selfPubKey) + + isSimpleCopy && (owner || update) } ``` @@ -313,42 +318,47 @@ The participant boxes **must be sorted in increasing order** of their R4 (Long) // ballot boxes (Inputs) // R4 the pub key of voter [GroupElement] (not used here) // R5 the box id of this box [Coll[Byte]] - // R6 the value voted for [Coll[Byte]] + // R6 the value voted for [Coll[Byte]] (hash of the new pool box script) - val refreshNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual + val poolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual val ballotTokenId = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual - // 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 refreshBoxIn = INPUTS(1) // pool box is 2nd input - val refreshBoxOut = OUTPUTS(1) // copy of pool box is the 2nd output + val minVotes = 8 + val poolBoxIn = INPUTS(0) // pool box is 1st input + val poolBoxOut = OUTPUTS(0) // copy of pool box is the 1st output + + val updateBoxOut = OUTPUTS(1) // copy of this box is the 2nd output + // compute the hash of the pool output box. This should be the value voted for - val refreshBoxOutHash = blake2b256(refreshBoxOut.propositionBytes) + val poolBoxOutHash = blake2b256(poolBoxOut.propositionBytes) - val validRefreshIn = refreshBoxIn.tokens(0)._1 == refreshNFT - val validRefreshOut = refreshBoxIn.tokens == refreshBoxOut.tokens && - refreshBoxIn.value == refreshBoxOut.value +// val poolBoxInHash = blake2b256(poolBoxIn.propositionBytes) + + val validPoolIn = poolBoxIn.tokens(0)._1 == poolNFT + + val validPoolOut = poolBoxIn.propositionBytes != poolBoxOut.propositionBytes && // script should not be preserved + poolBoxIn.tokens == poolBoxOut.tokens && // tokens preserved + poolBoxIn.value == poolBoxOut.value && // value preserved + poolBoxIn.R4[Long] == poolBoxOut.R4[Long] && // rate preserved + poolBoxIn.R5[Int] == poolBoxOut.R5[Int] // counter preserved - val validUpdateOut = SELF.tokens == updateBoxOut.tokens && - SELF.propositionBytes == updateBoxOut.propositionBytes && - SELF.value <= updateBoxOut.value + val validUpdateOut = updateBoxOut.tokens == SELF.tokens && + updateBoxOut.propositionBytes == SELF.propositionBytes && + updateBoxOut.value >= SELF.value - def isValidBallot(b:Box) = { - b.tokens.size > 0 && + def isValidBallot(b:Box) = if (b.tokens.size > 0) { b.tokens(0)._1 == ballotTokenId && b.R5[Coll[Byte]].get == SELF.id && // ensure vote corresponds to this box **** - b.R6[Coll[Byte]].get == refreshBoxOutHash // check value voted for - } + b.R6[Coll[Byte]].get == poolBoxOutHash // check value voted for + } else false val ballotBoxes = INPUTS.filter(isValidBallot) val votesCount = ballotBoxes.fold(0L, {(accum: Long, b: Box) => accum + b.tokens(0)._2}) - sigmaProp(validRefreshIn && validRefreshOut && validUpdateIn && validUpdateOut && votesCount >= 8) // minVotes = 8 + sigmaProp(validPoolIn && validPoolOut && validUpdateOut && votesCount >= minVotes) } ``` From 0ba8625b35704b36da28c1de3a524e83a84bba00 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Mon, 13 Sep 2021 17:25:03 +0530 Subject: [PATCH 09/43] Fix typos, streamline headings --- eip-0023.md | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/eip-0023.md b/eip-0023.md index 94690f55..93eb8157 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -17,7 +17,7 @@ Below is a summary of the main new features in 2.0 and how it differs from 1.0. 2. Data-point boxes will be considered as inputs rather than data-inputs. These inputs will be spent, and a copy of the box with the reward will be created. 3. Reward in tokens not Ergs. The posting reward will be in the form of tokens, which can be redeemed separately. 4. Reward accumulated in data point boxes to prevent dust. We will not be creating a new box for rewarding each posting. Instead, the rewards will be accumulated in data-point boxes. -5. When creating a data-point box, the pool box is not needed as data input. Creating a data-point will be decoupled from the pool, and will not require the bool box as data-input. +5. When creating a data-point box, the pool box is not needed as data input. Creating a data-point will be decoupled from the pool, and will not require the pool box as data-input. 6. Update mechanism as before. We will have the same update mechanism as in v1.0 7. Transferable participant tokens. Participant tokens are free to be transferred between public keys 8. Longer epoch period (1 hour or 30 blocks). @@ -33,11 +33,11 @@ In 1.0, the pool was responsible for rewarding each participant for posting a da The certificates are in the form of tokens emitted by the pool box. Once there are sufficient number of such tokens, a participant can exchange or burn them in return for a reward. We also give a sample token exchange contract. -## Design +[comment]: <> (## Design) -In this section we describe the high level design of the system. This will include the tokens used, the contracts and the transactions. +[comment]: <> (In this section we describe the high level design of the system. This will include the tokens used, the contracts and the transactions.) -### Tokens +## Tokens The system has the following types of tokens. Note that we use the term **NFT** to refer to any token that was issued in quantity 1. @@ -50,10 +50,11 @@ The system has the following types of tokens. Note that we use the term **NFT** |Ballot tokens | 15 | Identify each ballot box | Ballot boxes | |Reward tokens | 100 million | Reward participants | Refresh box
Participant boxes | -### Contracts and boxes -There are a total of 5 contracts and so 5 types of boxes as mentioned below +## Boxes -| Contract | Num boxes | Tokens | Registers used | Purpose | Transactions | +There are a total of 5 contracts, each corresponding to a box type + +| Box | Quantity | Tokens | Registers used | Purpose | Spending transactions | |---|---|---|---|---|---| |Pool | 1 | Pool-NFT| R3: Creation height (Int)
R4: Rate (Long)
R5: Epoch counter (Int) | Publish pool rate for dApps | Refresh pool
Update pool | |Refresh| 1 | Refresh-NFT
Reward tokens| | Refresh pool box
Emit reward tokens| Refresh pool | @@ -61,7 +62,7 @@ There are a total of 5 contracts and so 5 types of boxes as mentioned below |Update | 1 | Update-NFT | | Updating pool box | Update pool box | |Ballot | 15 | Ballot token | R4: Public key (GroupElement)
R5: Update box Id (Coll[Byte])
R6: New pool box hash (Coll[Byte]) | Voting for updating pool box | Vote for update
Update pool box | -### Transactions +## Transactions Oracle pool 2.0 has the following transactions. Each of the transactions below also contain the following boxes which will not be shown. @@ -81,8 +82,6 @@ Each of the transactions below also contain the following boxes which will not b None of the transactions have data-inputs. -## Transaction Structure - ### Refresh pool | Index | Input | Output | @@ -125,9 +124,9 @@ The participant boxes **must be sorted in increasing order** of their R6 (Long) | 4 | Ballot 3 | Ballot 3 | | ... | ... | ... | -# Contracts +## Contracts -## Pool Contract +### Pool Contract ```scala { // This box (pool box) @@ -144,7 +143,7 @@ The participant boxes **must be sorted in increasing order** of their R6 (Long) sigmaProp(otherTokenId == refreshNFT || otherTokenId == updateNFT) } ``` -## Refresh Contract +### Refresh Contract ```scala { // This box (refresh box) @@ -226,7 +225,7 @@ The participant boxes **must be sorted in increasing order** of their R6 (Long) } ``` -## Participant contract +### Participant contract ```scala { // This box (participant box) @@ -274,7 +273,7 @@ The participant boxes **must be sorted in increasing order** of their R6 (Long) } ``` -## Ballot Contract +### Ballot Contract ```scala { // This box (ballot box): From 2aecd3ceb0ee77ad7e8756d9cc9bf0d166ac8717 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Tue, 14 Sep 2021 17:14:56 +0530 Subject: [PATCH 10/43] Add transaction to extract reward tokens --- eip-0023.md | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/eip-0023.md b/eip-0023.md index 93eb8157..02f0ae7b 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -33,10 +33,6 @@ In 1.0, the pool was responsible for rewarding each participant for posting a da The certificates are in the form of tokens emitted by the pool box. Once there are sufficient number of such tokens, a participant can exchange or burn them in return for a reward. We also give a sample token exchange contract. -[comment]: <> (## Design) - -[comment]: <> (In this section we describe the high level design of the system. This will include the tokens used, the contracts and the transactions.) - ## Tokens The system has the following types of tokens. Note that we use the term **NFT** to refer to any token that was issued in quantity 1. @@ -58,9 +54,9 @@ There are a total of 5 contracts, each corresponding to a box type |---|---|---|---|---|---| |Pool | 1 | Pool-NFT| R3: Creation height (Int)
R4: Rate (Long)
R5: Epoch counter (Int) | Publish pool rate for dApps | Refresh pool
Update pool | |Refresh| 1 | Refresh-NFT
Reward tokens| | Refresh pool box
Emit reward tokens| Refresh pool | -|Participant | 15 | Participant token
Reward tokens | R4: Public key (GroupElement)
R5: Pool box Id (Coll[Byte])
R6: Published rate (Long) | Publish data-point
Accumulate reward tokens | Publish data-point,
Refresh pool,
Transfer participant token,
Redeem reward tokens| +|Participant | 15 | Participant token
Reward tokens | R4: Public key (GroupElement)
R5: Pool box Id (Coll[Byte])
R6: Published rate (Long) | Publish data-point
Accumulate reward tokens | Publish data-point,
Refresh pool,
Transfer participant token,
Extract reward tokens| |Update | 1 | Update-NFT | | Updating pool box | Update pool box | -|Ballot | 15 | Ballot token | R4: Public key (GroupElement)
R5: Update box Id (Coll[Byte])
R6: New pool box hash (Coll[Byte]) | Voting for updating pool box | Vote for update
Update pool box | +|Ballot | 15 | Ballot token | R4: Public key (GroupElement)
R5: Update box Id (Coll[Byte])
R6: New pool box hash (Coll[Byte]) | Voting for updating pool box | Vote for update
Update pool box
Transfer ballot token | ## Transactions @@ -75,10 +71,11 @@ Each of the transactions below also contain the following boxes which will not b | --- | --- | --- | | Refresh pool | Pool
Refresh
Participants | Refresh pool box | | Publish data point | Participant | Publish data point | -| Redeem reward | Participant | Redeem rewards | -| Transfer token | Participant | Transfer participant token | +| Extract reward tokens | Participant | Extract reward tokens to redeem via external mechanism | +| Transfer participant token | Participant | Transfer participant token to another public key| | Vote for update | Ballot | Vote for updating pool box | | Update pool | Pool
Update
Ballots | Update pool box | +| Transfer ballot token | Ballot | Transfer ballot token to another public key| None of the transactions have data-inputs. @@ -101,6 +98,13 @@ The participant boxes **must be sorted in increasing order** of their R6 (Long) |---|---|---| | 0 | Participant | Participant | +### Extract reward tokens + +| Index | Input | Output | +|---|---|---| +| 0 | Participant | Participant | +| 1 | | Box with freed reward tokens | + ### Transfer participant token | Index | Input | Output | @@ -124,6 +128,12 @@ The participant boxes **must be sorted in increasing order** of their R6 (Long) | 4 | Ballot 3 | Ballot 3 | | ... | ... | ... | +### Transfer ballot token + +| Index | Input | Output | +|---|---|---| +| 0 | Ballot | Ballot | + ## Contracts ### Pool Contract @@ -333,8 +343,6 @@ The participant boxes **must be sorted in increasing order** of their R6 (Long) // compute the hash of the pool output box. This should be the value voted for val poolBoxOutHash = blake2b256(poolBoxOut.propositionBytes) -// val poolBoxInHash = blake2b256(poolBoxIn.propositionBytes) - val validPoolIn = poolBoxIn.tokens(0)._1 == poolNFT val validPoolOut = poolBoxIn.propositionBytes != poolBoxOut.propositionBytes && // script should not be preserved @@ -350,7 +358,7 @@ The participant boxes **must be sorted in increasing order** of their R6 (Long) def isValidBallot(b:Box) = if (b.tokens.size > 0) { b.tokens(0)._1 == ballotTokenId && - b.R5[Coll[Byte]].get == SELF.id && // ensure vote corresponds to this box **** + b.R5[Coll[Byte]].get == SELF.id && // ensure vote corresponds to this box b.R6[Coll[Byte]].get == poolBoxOutHash // check value voted for } else false From 731e8cae66dc74285e8bd1403711767d43fdd45e Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Wed, 15 Sep 2021 18:44:20 +0530 Subject: [PATCH 11/43] Add description of refresh and oracle tx --- eip-0023.md | 123 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 95 insertions(+), 28 deletions(-) diff --git a/eip-0023.md b/eip-0023.md index 02f0ae7b..bf9ec336 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -2,12 +2,14 @@ This is a proposed update to the oracle pool 1.0 currently deployed and documented in [EIP16](https://github.com/ergoplatform/eips/blob/eip16/eip-0016.md). +## Introduction + Oracle pool 1.0 has the following drawbacks: 1. Rewards generate a lot of dust 2. Current rewards are too low (related to 1) -3. There are two types of oracle pool boxes. This makes dApps and update mechanism more complex -4. Participant tokens are non-transferable, and so oracles are locked permanently. The same goes with ballot tokens. +3. There are two types of pool boxes. This makes dApps and update mechanism more complex +4. Oracle tokens are non-transferable, and so oracles are locked permanently. The same goes with ballot tokens. Oracle pool 2.0 aims to address the above. @@ -19,18 +21,18 @@ Below is a summary of the main new features in 2.0 and how it differs from 1.0. 4. Reward accumulated in data point boxes to prevent dust. We will not be creating a new box for rewarding each posting. Instead, the rewards will be accumulated in data-point boxes. 5. When creating a data-point box, the pool box is not needed as data input. Creating a data-point will be decoupled from the pool, and will not require the pool box as data-input. 6. Update mechanism as before. We will have the same update mechanism as in v1.0 -7. Transferable participant tokens. Participant tokens are free to be transferred between public keys +7. Transferable oracle tokens. Oracle tokens are free to be transferred between public keys 8. Longer epoch period (1 hour or 30 blocks). 9. No separate funding box. The pool box emits only reward tokens and won't be handing out Ergs. Thus, there won't be a separate funding process required. 10. Reward mechanism separate from pool. The process of redeeming the reward tokens is not part of the protocol. 11. Pool box is separated from the logic of pool management, which is captured instead in a **refresh** box. This makes the pool box very small for use in other dApps. 12. Pool box will additionally store a counter that is incremented on each collection. -## Reward Mechanism +### Reward Mechanism -In 1.0, the pool was responsible for rewarding each participant for posting a data-point. In 2.0, the pool simply certifies that a data-point was posted, and a separate reward mechanism is proposed. This keeps the contract smaller and more flexible. +In 1.0, the pool was responsible for rewarding each oracle for posting a data-point. In 2.0, the pool simply certifies that a data-point was posted, and a separate reward mechanism is proposed. This keeps the contract smaller and more flexible. -The certificates are in the form of tokens emitted by the pool box. Once there are sufficient number of such tokens, a participant +The certificates are in the form of tokens emitted by the pool box. Once there are sufficient number of such tokens, a oracle can exchange or burn them in return for a reward. We also give a sample token exchange contract. ## Tokens @@ -42,9 +44,9 @@ The system has the following types of tokens. Note that we use the term **NFT** |Pool-NFT | 1 | Identify pool box | Pool box | |Refresh-NFT | 1 | Identify refresh box | Refresh box | |Update-NFT | 1 | Identify update box | Update box | -|Participant tokens | 15 | Identify each participant box | Participant boxes | +|Oracle tokens | 15 | Identify each oracle box | Oracle boxes | |Ballot tokens | 15 | Identify each ballot box | Ballot boxes | -|Reward tokens | 100 million | Reward participants | Refresh box
Participant boxes | +|Reward tokens | 100 million | Reward oracles | Refresh box
Oracle boxes | ## Boxes @@ -54,7 +56,7 @@ There are a total of 5 contracts, each corresponding to a box type |---|---|---|---|---|---| |Pool | 1 | Pool-NFT| R3: Creation height (Int)
R4: Rate (Long)
R5: Epoch counter (Int) | Publish pool rate for dApps | Refresh pool
Update pool | |Refresh| 1 | Refresh-NFT
Reward tokens| | Refresh pool box
Emit reward tokens| Refresh pool | -|Participant | 15 | Participant token
Reward tokens | R4: Public key (GroupElement)
R5: Pool box Id (Coll[Byte])
R6: Published rate (Long) | Publish data-point
Accumulate reward tokens | Publish data-point,
Refresh pool,
Transfer participant token,
Extract reward tokens| +|Oracle | 15 | Oracle token
Reward tokens | R4: Public key (GroupElement)
R5: Pool box Id (Coll[Byte])
R6: Published rate (Long) | Publish data-point
Accumulate reward tokens | Publish data-point,
Refresh pool,
Transfer oracle token,
Extract reward tokens| |Update | 1 | Update-NFT | | Updating pool box | Update pool box | |Ballot | 15 | Ballot token | R4: Public key (GroupElement)
R5: Update box Id (Coll[Byte])
R6: New pool box hash (Coll[Byte]) | Voting for updating pool box | Vote for update
Update pool box
Transfer ballot token | @@ -69,10 +71,10 @@ Each of the transactions below also contain the following boxes which will not b | Transaction | Boxes Involved | Purpose | | --- | --- | --- | -| Refresh pool | Pool
Refresh
Participants | Refresh pool box | -| Publish data point | Participant | Publish data point | -| Extract reward tokens | Participant | Extract reward tokens to redeem via external mechanism | -| Transfer participant token | Participant | Transfer participant token to another public key| +| Refresh pool | Pool
Refresh
Oracles | Refresh pool box | +| Publish data point | Oracle | Publish data point | +| Extract reward tokens | Oracle | Extract reward tokens to redeem via external mechanism | +| Transfer oracle token | Oracle | Transfer oracle token to another public key| | Vote for update | Ballot | Vote for updating pool box | | Update pool | Pool
Update
Ballots | Update pool box | | Transfer ballot token | Ballot | Transfer ballot token to another public key| @@ -85,31 +87,96 @@ None of the transactions have data-inputs. |---|---|---| | 0 | Pool | Pool | | 1 | Refresh | Refresh | -| 2 | Participant 1 | Participant 1 | -| 3 | Participant 2 | Participant 2 | -| 4 | Participant 3 | Participant 3 | +| 2 | Oracle 1 | Oracle 1 | +| 3 | Oracle 2 | Oracle 2 | +| 4 | Oracle 3 | Oracle 3 | | ... | ... | ... | -The participant boxes **must be sorted in increasing order** of their R6 (Long) value. +The purpose of this transaction is to take the average of the rates in all the oracle boxes and update the rate in the pool box whenever the +epoch gets over (i.e., the current height is > creation height + epoch length). +We consider such a pool box to be *stale* that needs to be refreshed. + +1. The first input is the pool box that simply requires the second input to be the refresh box (i.e., contain the refresh token) ([smart contract](#pool-contract)). +2. The second input is a refresh box that contains the following logic ([smart contract](#refresh-contract)): + - The first input is a stale pool box, that is a box with the pool token and creation height lower than the current height minus epoch length. + - This transaction can only be done by someone holding a oracle token and having published an oracle box (see below). + - Any value published within the last epoch length by someone holding the oracle token is considered *latest*. This is called an oracle box. + In particular, for the box to be considered an oracle box, the following must hold: + - It must have an oracle token at index 0. + - Register R4 must contain a group element. + - Register R5 must be the box id of the stale pool box. + - Register R6 must contain a long value, which will be assumed to be the rate. + - Its creation height must not be less than the current height minus epoch length. + - There must be at least a certain number of oracle boxes (currently 4) as inputs. + - The oracle boxes must be arranged in increasing order of their R6 values (rate). + - The first oracle box's rate must be within 5% of that of the last, and must be > 0. + - The first output must be a new pool box as follows: + - Registers R0 (value), R1 (script), R2 (tokens) are preserved from the old pool box. + - The creation height (stored in R3) must be at most 4 less than the current height. + - The rate (stored in R4) must be the average of the rates in all the oracle boxes. + - The epoch counter (stored in R5) must be incremented by 1. + - The second output must be a new refresh box as follows: + - Registers R0 (value), R1 (script), and the first token (refresh NFT) are preserved from the old refresh box. + - The quantity of the second token (reward) must be decremented by at most twice the number of valid rate boxes. + - Register R4 and onward are empty. +3. Each input oracle box has following logic ([smart contract](#oracle-contract)): + - The first input is a pool box (i.e., has the pool token). + - An output oracle box (acting as a copy of this box) must be created as follows: + - The following values are directly copied: R0 (nanoErgs), R1 (script), the first token (oracle token) (stored in R2) and R4 (GroupElement). + - The second token (reward token) is incremented by at least 1. + - Registers R5 and onward are empty. + +Suppose there are *n* valid oracle boxes, then there are 2*n* reward tokens released. +It is expected that whoever creates the refresh transaction (the *collector*) takes *n*+1 reward tokens, and the other oracles get 1 token each. +This gives incentive to use as many oracle boxes as possible during the refresh. ### Publish data-point | Index | Input | Output | |---|---|---| -| 0 | Participant | Participant | +| 0 | Oracle | Oracle | + +This allows an oracle to publish data-point for collection in next epoch. +This entails spending the oracle box and creating a new oracle box as per the [smart contract](#oracle-contract). + +1. The public key stored in R4 defines who can spend the oracle box. +2. The logic requires the new oracle box to be as follows: + - The script and the first token are copied from this box. + - R4 contains a group element. + - The second token is the reward token in some non-zero quantity. +3. The following rules (not enforced by the smart contract) to be followed. + - It should store the same group element in R4. + - It should store box id of the current pool box in R5. + - It should store the rate to publish in R6. + - It should ensure reward tokens are preserved. ### Extract reward tokens | Index | Input | Output | |---|---|---| -| 0 | Participant | Participant | +| 0 | Oracle | Oracle | | 1 | | Box with freed reward tokens | -### Transfer participant token +This is similar to the "Publish data-point" case, except that Step 3 is modified as follows. + +3. The following rules (not enforced by the smart contract) to be followed. + - It should store the same group element in R4. + - It should keep at least 1 reward token. + - It should store the balance reward tokens in some other box. + + +### Transfer oracle token | Index | Input | Output | |---|---|---| -| 0 | Participant | Participant | +| 0 | Oracle | Oracle | + +This is similar to the "Publish data-point" case, except that Step 3 is modified as follows. + +3. The following rules (not enforced by the smart contract) to be followed. + - It should store new owner's group element in R4. + - It should keep at least 1 reward token. + - It should store the balance reward tokens, if any, in some other box. ### Vote for update @@ -161,7 +228,7 @@ The participant boxes **must be sorted in increasing order** of their R6 (Long) // // When initializing the box, there must be one reward token. When claiming reward, one token must be left unclaimed - val participantTokenId = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual + val oracleTokenId = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual val poolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual val spenderIndex = getVar[Int](0).get // the index of the data-point box (NOT input!) belonging to spender @@ -178,7 +245,7 @@ The participant boxes **must be sorted in increasing order** of their R6 (Long) def isValidDataPoint(b: Box) = if (b.R5[Any].isDefined) { b.creationInfo._1 >= minStartHeight && // data point must not be too old - b.tokens(0)._1 == participantTokenId && // first token id must be of participant token + b.tokens(0)._1 == oracleTokenId && // first token id must be of oracle token b.R5[Coll[Byte]].get == poolIn.id // it must correspond to this epoch } else false @@ -235,22 +302,22 @@ The participant boxes **must be sorted in increasing order** of their R6 (Long) } ``` -### Participant contract +### Oracle contract ```scala -{ // This box (participant box) +{ // This box (oracle box) // R4 public key (GroupElement) // R5 box id of pool box (Coll[Byte]) // R6 data point (Long) - // tokens(0) participant token (one) + // tokens(0) oracle token (one) // tokens(1) reward tokens collected (one or more) // // When initializing the box, there must be one reward token. When claiming reward, one token must be left unclaimed // // We will connect this box to pool NFT in input #0 (and not the refresh NFT in input #1). // This way, we can continue to use the same box after updating pool - // This *could* allow the participant box to be spent during an update + // This *could* allow the oracle box to be spent during an update // (when input #2 contains the update NFT instead of the refresh NFT) // However, this is not an issue because the update contract ensures that tokens and registers (except script) of the pool box are preserved @@ -263,7 +330,7 @@ The participant boxes **must be sorted in increasing order** of their R6 (Long) val outIndex = getVar[Int](0).get val output = OUTPUTS(outIndex) - val isSimpleCopy = output.tokens(0) == SELF.tokens(0) && // participant token is preserved + val isSimpleCopy = output.tokens(0) == SELF.tokens(0) && // oracle token is preserved output.tokens(1)._1 == SELF.tokens(1)._1 && // reward tokenId is preserved output.tokens.size == 2 && // no more tokens output.propositionBytes == SELF.propositionBytes && // script preserved From 786cc9f263e2528574a060c67a058a2ab96d6036 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Thu, 30 Sep 2021 03:05:46 +0530 Subject: [PATCH 12/43] Keep epoch counter in R5 or oracle box --- eip-0023.md | 274 ++++++++++++++++++++++++++-------------------------- 1 file changed, 139 insertions(+), 135 deletions(-) diff --git a/eip-0023.md b/eip-0023.md index bf9ec336..ca335806 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -15,18 +15,21 @@ Oracle pool 2.0 aims to address the above. Below is a summary of the main new features in 2.0 and how it differs from 1.0. -1. Single pool address (only one type of box). This version of the pool will have only one address, the *pool address*. -2. Data-point boxes will be considered as inputs rather than data-inputs. These inputs will be spent, and a copy of the box with the reward will be created. -3. Reward in tokens not Ergs. The posting reward will be in the form of tokens, which can be redeemed separately. -4. Reward accumulated in data point boxes to prevent dust. We will not be creating a new box for rewarding each posting. Instead, the rewards will be accumulated in data-point boxes. -5. When creating a data-point box, the pool box is not needed as data input. Creating a data-point will be decoupled from the pool, and will not require the pool box as data-input. -6. Update mechanism as before. We will have the same update mechanism as in v1.0 -7. Transferable oracle tokens. Oracle tokens are free to be transferred between public keys -8. Longer epoch period (1 hour or 30 blocks). -9. No separate funding box. The pool box emits only reward tokens and won't be handing out Ergs. Thus, there won't be a separate funding process required. -10. Reward mechanism separate from pool. The process of redeeming the reward tokens is not part of the protocol. -11. Pool box is separated from the logic of pool management, which is captured instead in a **refresh** box. This makes the pool box very small for use in other dApps. -12. Pool box will additionally store a counter that is incremented on each collection. +- Single pool address: This version of the pool will have only one address, the *pool address*. +- Pool box will additionally store a counter that is incremented on each collection. +- Compact pool box: Pool box is separated from the logic of pool management, which is captured instead in a **refresh** box. This makes the pool box very small for use in other dApps. +- Reward in tokens not Ergs: The posting reward will be in the form of tokens, which can be redeemed separately. +- Reward accumulated in oracle boxes to prevent dust: We will not be creating a new box for rewarding each posting. +- Oracle boxes spent during collection: Because the rewards must be accumulated, the oracle boxes will be considered as inputs rather than data-inputs when collecting individual rates for averaging. + These inputs will be spent, and a copy of the box with the reward will be created. + This gives us the ability to accumulate rewards, while keeping the transaction size similar to when using them as data-inputs in v1.0. + + **Note:** The pool box will still be used as data input in other dApps. + +- Transferable oracle tokens: Oracle tokens are free to be transferred between public keys. +- We will have the same update mechanism as in v1.0. +- No separate funding box. The pool box emits only reward tokens and won't be handing out Ergs. Thus, there won't be a separate funding process required. +- Reward mechanism separate from pool. The process of redeeming the reward tokens is not part of the protocol. ### Reward Mechanism @@ -56,7 +59,7 @@ There are a total of 5 contracts, each corresponding to a box type |---|---|---|---|---|---| |Pool | 1 | Pool-NFT| R3: Creation height (Int)
R4: Rate (Long)
R5: Epoch counter (Int) | Publish pool rate for dApps | Refresh pool
Update pool | |Refresh| 1 | Refresh-NFT
Reward tokens| | Refresh pool box
Emit reward tokens| Refresh pool | -|Oracle | 15 | Oracle token
Reward tokens | R4: Public key (GroupElement)
R5: Pool box Id (Coll[Byte])
R6: Published rate (Long) | Publish data-point
Accumulate reward tokens | Publish data-point,
Refresh pool,
Transfer oracle token,
Extract reward tokens| +|Oracle | 15 | Oracle token
Reward tokens | R4: Public key (GroupElement)
R5: Epoch counter of pool box (Int)
R6: Published rate (Long) | Publish data-point
Accumulate reward tokens | Publish data-point,
Refresh pool,
Transfer oracle token,
Extract reward tokens| |Update | 1 | Update-NFT | | Updating pool box | Update pool box | |Ballot | 15 | Ballot token | R4: Public key (GroupElement)
R5: Update box Id (Coll[Byte])
R6: New pool box hash (Coll[Byte]) | Voting for updating pool box | Vote for update
Update pool box
Transfer ballot token | @@ -104,7 +107,7 @@ We consider such a pool box to be *stale* that needs to be refreshed. In particular, for the box to be considered an oracle box, the following must hold: - It must have an oracle token at index 0. - Register R4 must contain a group element. - - Register R5 must be the box id of the stale pool box. + - Register R5 must be the current epoch counter (from R5 of the stale pool box). - Register R6 must contain a long value, which will be assumed to be the rate. - Its creation height must not be less than the current height minus epoch length. - There must be at least a certain number of oracle boxes (currently 4) as inputs. @@ -146,7 +149,7 @@ This entails spending the oracle box and creating a new oracle box as per the [s - The second token is the reward token in some non-zero quantity. 3. The following rules (not enforced by the smart contract) to be followed. - It should store the same group element in R4. - - It should store box id of the current pool box in R5. + - It should store epoch counter of the current pool box in R5. - It should store the rate to publish in R6. - It should ensure reward tokens are preserved. @@ -174,7 +177,7 @@ This is similar to the "Publish data-point" case, except that Step 3 is modified This is similar to the "Publish data-point" case, except that Step 3 is modified as follows. 3. The following rules (not enforced by the smart contract) to be followed. - - It should store new owner's group element in R4. + - It should store the new owner's group element in R4. - It should keep at least 1 reward token. - It should store the balance reward tokens, if any, in some other box. @@ -206,99 +209,102 @@ This is similar to the "Publish data-point" case, except that Step 3 is modified ### Pool Contract ```scala -{ // This box (pool box) - // epoch start height is stored in creation Height (R3) - // R4 Current data point (Long) - // R5 Epoch counter (Int) - // - // tokens(0) pool token (NFT) - - val otherTokenId = INPUTS(1).tokens(0)._1 - val refreshNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual - val updateNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual - - sigmaProp(otherTokenId == refreshNFT || otherTokenId == updateNFT) +{ + // This box (pool box) + // epoch start height is stored in creation Height (R3) + // R4 Current data point (Long) + // + // tokens(0) pool token (NFT) + + val otherTokenId = INPUTS(1).tokens(0)._1 + val refreshNFT = fromBase64("VGpXblpyNHU3eCFBJUQqRy1LYU5kUmdVa1hwMnM1djg=") // TODO replace with actual + val updateNFT = fromBase64("YlFlVGhXbVpxNHQ3dyF6JUMqRi1KQE5jUmZValhuMnI=") // TODO replace with actual + + sigmaProp(otherTokenId == refreshNFT || otherTokenId == updateNFT) } ``` ### Refresh Contract ```scala { // This box (refresh box) - // tokens(0) reward tokens to be emitted (several) - // - // When initializing the box, there must be one reward token. When claiming reward, one token must be left unclaimed - - val oracleTokenId = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual - val poolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual - - val spenderIndex = getVar[Int](0).get // the index of the data-point box (NOT input!) belonging to spender - - val epochLength = 30 // 1 hour - val minStartHeight = HEIGHT - epochLength - val minDataPoints = 4 - val buffer = 4 - val maxDeviationPercent = 5 // percent - - val poolIn = INPUTS(0) - val poolOut = OUTPUTS(0) - val selfOut = OUTPUTS(1) - - def isValidDataPoint(b: Box) = if (b.R5[Any].isDefined) { - b.creationInfo._1 >= minStartHeight && // data point must not be too old - b.tokens(0)._1 == oracleTokenId && // first token id must be of oracle token - b.R5[Coll[Byte]].get == poolIn.id // it must correspond to this epoch - } else false - - val dataPoints = INPUTS.filter(isValidDataPoint) - val pubKey = dataPoints(spenderIndex).R4[GroupElement].get - - val enoughDataPoints = dataPoints.size >= minDataPoints - val rewardEmitted = dataPoints.size * 2 // one extra token for each collected box as reward to collector - val epochOver = poolIn.creationInfo._1 < minStartHeight - - val startData = 1L // we don't allow 0 data points - val startSum = 0L - // we expect data-points to be sorted in INCREASING order - - val lastSortedSum = dataPoints.fold((startData, (true, startSum)), { - (t: (Long, (Boolean, Long)), b: Box) => - val currData = b.R6[Long].get - val prevData = t._1 - val wasSorted = t._2._1 - val oldSum = t._2._2 - val newSum = oldSum + currData // we don't have to worry about overflow, as it causes script to fail - - val isSorted = wasSorted && prevData <= currData - - (currData, (isSorted, newSum)) - } - ) - - val lastData = lastSortedSum._1 - val isSorted = lastSortedSum._2._1 - val sum = lastSortedSum._2._2 - val average = sum / dataPoints.size - - val maxDelta = lastData * maxDeviationPercent / 100 - val firstData = dataPoints(0).R6[Long].get - - sigmaProp(proveDlog(pubKey)) && - enoughDataPoints && - isSorted && - lastData - firstData <= maxDelta && - poolIn.tokens(0)._1 == poolNFT && - poolOut.tokens == poolIn.tokens && // preserve pool tokens - poolOut.R4[Long].get == average && // rate - poolOut.R5[Int].get == poolIn.R5[Int].get + 1 && // counter - poolOut.propositionBytes == poolIn.propositionBytes && // preserve pool script - poolOut.value >= poolIn.value && - poolOut.creationInfo._1 >= HEIGHT - buffer && // ensure that new box has correct start epoch height - selfOut.tokens(0) == SELF.tokens(0) && // refresh NFT preserved - selfOut.tokens(1)._1 == SELF.tokens(1)._1 && // reward token id preserved - selfOut.tokens(1)._2 == SELF.tokens(1)._2 - rewardEmitted && // reward token amount correctly reduced - selfOut.tokens.size == 2 && // no more tokens - selfOut.propositionBytes == SELF.propositionBytes && // script preserved - selfOut.value >= SELF.value + // tokens(0) reward tokens to be emitted (several) + // + // When initializing the box, there must be one reward token. When claiming reward, one token must be left unclaimed + + val oracleTokenId = fromBase64("KkctSmFOZFJnVWtYcDJzNXY4eS9CP0UoSCtNYlBlU2g=") // TODO replace with actual + val poolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // TODO replace with actual + + val spenderIndex = getVar[Int](0).get // the index of the data-point box (NOT input!) belonging to spender + + val epochLength = 30 + val minStartHeight = HEIGHT - epochLength + val minDataPoints = 4 + val buffer = 4 + val maxDeviationPercent = 5 // percent + + val poolIn = INPUTS(0) + val poolOut = OUTPUTS(0) + val selfOut = OUTPUTS(1) + + def isValidDataPoint(b: Box) = if (b.R6[Long].isDefined) { + b.creationInfo._1 >= minStartHeight && // data point must not be too old + b.tokens(0)._1 == oracleTokenId && // first token id must be of oracle token + b.R5[Int].get == poolIn.R5[Int].get // it must correspond to this epoch + } else false + + val dataPoints = INPUTS.filter(isValidDataPoint) + val pubKey = dataPoints(spenderIndex).R4[GroupElement].get + + val enoughDataPoints = dataPoints.size >= minDataPoints + val rewardEmitted = dataPoints.size * 2 // one extra token for each collected box as reward to collector + val epochOver = poolIn.creationInfo._1 < minStartHeight + + val startData = 1L // we don't allow 0 data points + val startSum = 0L + // we expect data-points to be sorted in INCREASING order + + val lastSortedSum = dataPoints.fold((startData, (true, startSum)), { + (t: (Long, (Boolean, Long)), b: Box) => + val currData = b.R6[Long].get + val prevData = t._1 + val wasSorted = t._2._1 + val oldSum = t._2._2 + val newSum = oldSum + currData // we don't have to worry about overflow, as it causes script to fail + + val isSorted = wasSorted && prevData <= currData + + (currData, (isSorted, newSum)) + } + ) + + val lastData = lastSortedSum._1 + val isSorted = lastSortedSum._2._1 + val sum = lastSortedSum._2._2 + val average = sum / dataPoints.size + + val maxDelta = lastData * maxDeviationPercent / 100 + val firstData = dataPoints(0).R6[Long].get + + proveDlog(pubKey) && + epochOver && + enoughDataPoints && + isSorted && + lastData - firstData <= maxDelta && + poolIn.tokens(0)._1 == poolNFT && + poolOut.tokens == poolIn.tokens && // preserve pool tokens + poolOut.R4[Long].get == average && // rate + poolOut.R5[Int].get == poolIn.R5[Int].get + 1 && // counter + ! (poolOut.R6[Any].isDefined) && + poolOut.propositionBytes == poolIn.propositionBytes && // preserve pool script + poolOut.value >= poolIn.value && + poolOut.creationInfo._1 >= HEIGHT - buffer && // ensure that new box has correct start epoch height + selfOut.tokens(0) == SELF.tokens(0) && // refresh NFT preserved + selfOut.tokens(1)._1 == SELF.tokens(1)._1 && // reward token id preserved + selfOut.tokens(1)._2 >= SELF.tokens(1)._2 - rewardEmitted && // reward token amount correctly reduced + selfOut.tokens.size == 2 && // no more tokens + ! (selfOut.R4[Any].isDefined) && + selfOut.propositionBytes == SELF.propositionBytes && // script preserved + selfOut.value >= SELF.value } ``` @@ -306,9 +312,9 @@ This is similar to the "Publish data-point" case, except that Step 3 is modified ```scala { // This box (oracle box) - // R4 public key (GroupElement) - // R5 box id of pool box (Coll[Byte]) - // R6 data point (Long) + // R4 public key (GroupElement) + // R5 epoch counter of current epoch (Int) + // R6 data point (Long) or empty // tokens(0) oracle token (one) // tokens(1) reward tokens collected (one or more) @@ -321,7 +327,7 @@ This is similar to the "Publish data-point" case, except that Step 3 is modified // (when input #2 contains the update NFT instead of the refresh NFT) // However, this is not an issue because the update contract ensures that tokens and registers (except script) of the pool box are preserved - val poolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual + val poolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // TODO replace with actual val otherTokenId = INPUTS(0).tokens(0)._1 @@ -354,34 +360,32 @@ This is similar to the "Publish data-point" case, except that Step 3 is modified ```scala { // This box (ballot box): - // R4 the group element of the owner of the ballot token [GroupElement] - // R5 the box id of the update box [Coll[Byte]] - // R6 the value voted for [Coll[Byte]] + // R4 the group element of the owner of the ballot token [GroupElement] + // R5 the box id of the update box [Coll[Byte]] + // R6 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("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual + val updateNFT = fromBase64("YlFlVGhXbVpxNHQ3dyF6JUMqRi1KQE5jUmZValhuMnI=") // TODO replace with actual - val minStorageRent = 10000000L - - val selfPubKey = SELF.R4[GroupElement].get - val otherTokenId = INPUTS(0).tokens(0)._1 - - val outIndex = getVar[Int](0).get - val output = OUTPUTS(outIndex) - - val isSimpleCopy = output.R4[GroupElement].isDefined && // ballot boxes are transferable by setting different value here - output.propositionBytes == SELF.propositionBytes && - output.tokens == SELF.tokens && - output.value >= minStorageRent - - val update = otherTokenId == updateNFT && - output.R4[GroupElement].get == selfPubKey && - output.value >= SELF.value - - val owner = proveDlog(selfPubKey) - - isSimpleCopy && (owner || update) + val minStorageRent = 10000000L + + val selfPubKey = SELF.R4[GroupElement].get + val otherTokenId = INPUTS(0).tokens(0)._1 + + val outIndex = getVar[Int](0).get + val output = OUTPUTS(outIndex) + + val isSimpleCopy = output.R4[GroupElement].isDefined && // ballot boxes are transferable by setting different value here + output.propositionBytes == SELF.propositionBytes && + output.tokens == SELF.tokens && + output.value >= minStorageRent + + val update = otherTokenId == updateNFT && + output.R4[GroupElement].get == selfPubKey && + output.value >= SELF.value + + val owner = proveDlog(selfPubKey) + + isSimpleCopy && (owner || update) } ``` @@ -396,9 +400,9 @@ This is similar to the "Publish data-point" case, except that Step 3 is modified // R5 the box id of this box [Coll[Byte]] // R6 the value voted for [Coll[Byte]] (hash of the new pool box script) - val poolNFT = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual + val poolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // TODO replace with actual - val ballotTokenId = fromBase64("ERERERERERERERERERERERERERERERERERERERERERE=") // TODO replace with actual + val ballotTokenId = fromBase64("P0QoRy1LYVBkU2dWa1lwM3M2djl5JEImRSlIQE1iUWU=") // TODO replace with actual val minVotes = 8 From 0faaa3a2a043158a01b96ed9d3e54f8f46532452 Mon Sep 17 00:00:00 2001 From: ScalaHub <23208922+scalahub@users.noreply.github.com> Date: Thu, 30 Sep 2021 11:10:45 +0530 Subject: [PATCH 13/43] Update eip-0023.md --- eip-0023.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eip-0023.md b/eip-0023.md index ca335806..994c09a5 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -213,7 +213,8 @@ This is similar to the "Publish data-point" case, except that Step 3 is modified // This box (pool box) // epoch start height is stored in creation Height (R3) // R4 Current data point (Long) - // + // R5 Current epoch counter (Int) + // tokens(0) pool token (NFT) val otherTokenId = INPUTS(1).tokens(0)._1 From 3eb4c2be4edb03e92c2ef6f34ade9af1ce12dc87 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Mon, 4 Oct 2021 01:05:38 +0530 Subject: [PATCH 14/43] Update box creation height in R5 of ballot --- eip-0023.md | 252 +++++++++++++++++++++++++++++----------------------- 1 file changed, 140 insertions(+), 112 deletions(-) diff --git a/eip-0023.md b/eip-0023.md index 994c09a5..35606f8c 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -61,7 +61,7 @@ There are a total of 5 contracts, each corresponding to a box type |Refresh| 1 | Refresh-NFT
Reward tokens| | Refresh pool box
Emit reward tokens| Refresh pool | |Oracle | 15 | Oracle token
Reward tokens | R4: Public key (GroupElement)
R5: Epoch counter of pool box (Int)
R6: Published rate (Long) | Publish data-point
Accumulate reward tokens | Publish data-point,
Refresh pool,
Transfer oracle token,
Extract reward tokens| |Update | 1 | Update-NFT | | Updating pool box | Update pool box | -|Ballot | 15 | Ballot token | R4: Public key (GroupElement)
R5: Update box Id (Coll[Byte])
R6: New pool box hash (Coll[Byte]) | Voting for updating pool box | Vote for update
Update pool box
Transfer ballot token | +|Ballot | 15 | Ballot token | R4: Public key (GroupElement)
R5: Update box creation height (Int)
R6: New pool box hash (Coll[Byte]) | Voting for updating pool box | Vote for update
Update pool box
Transfer ballot token | ## Transactions @@ -160,7 +160,9 @@ This entails spending the oracle box and creating a new oracle box as per the [s | 0 | Oracle | Oracle | | 1 | | Box with freed reward tokens | -This is similar to the "Publish data-point" case, except that Step 3 is modified as follows. +This allows an oracle to extract tokens obtained from publishing data point to redeem them elsewhere. + +The transaction is similar to the "Publish data-point" case, except that Step 3 is modified as follows. 3. The following rules (not enforced by the smart contract) to be followed. - It should store the same group element in R4. @@ -174,7 +176,9 @@ This is similar to the "Publish data-point" case, except that Step 3 is modified |---|---|---| | 0 | Oracle | Oracle | -This is similar to the "Publish data-point" case, except that Step 3 is modified as follows. +This is used to transfer the ownership of an oracle token to another public key. + +The transaction is similar to the "Publish data-point" case, except that Step 3 is modified as follows. 3. The following rules (not enforced by the smart contract) to be followed. - It should store the new owner's group element in R4. @@ -183,15 +187,34 @@ This is similar to the "Publish data-point" case, except that Step 3 is modified ### Vote for update +This is used by ballot token holders to vote for updating the pool box address. + | Index | Input | Output | |---|---|---| | 0 | Ballot | Ballot | -### Update refresh box +The input and output are ballot boxes such that the following holds: + - There is a group element in R4 (enforced by smart contract). + - R5 contains the creation height of the current update box. + - R6 contains the hash of the address of the new pool box. + +### Update Pool box + +This updates the pool box (i.e., transfers the poolNFT to a new address). + +The ballots used in inputs must satisfy the following [smart contract](#ballot-contract): + - There must be an output ballot box with the same public key in R4 and rest registers empty. + +The update box additionally ensures following: + - Each ballot box has R4 containing the has of the new pool box script (address). + - Each ballot box has R5 containing this box's creation height. + - There is a copy of the update box in outputs with everything preserved but with larger creation height. + - There are at least a threshold number of votes. + - The registers and token in the pool box are preserved in the new pool box. | Index | Input | Output | |---|---|---| -| 0 | Pool | Pool | +| 0 | Pool | New Pool | | 1 | Update | Update | | 2 | Ballot 1 | Ballot 1 | | 3 | Ballot 2 | Ballot 2 | @@ -200,6 +223,8 @@ This is similar to the "Publish data-point" case, except that Step 3 is modified ### Transfer ballot token +This is similar to the transfer oracle token, except that it is used to transfer ownership of a ballot token to another public key. + | Index | Input | Output | |---|---|---| | 0 | Ballot | Ballot | @@ -210,102 +235,100 @@ This is similar to the "Publish data-point" case, except that Step 3 is modified ```scala { - // This box (pool box) - // epoch start height is stored in creation Height (R3) - // R4 Current data point (Long) - // R5 Current epoch counter (Int) + // This box (pool box) + // epoch start height is stored in creation Height (R3) + // R4 Current data point (Long) + // R5 Current epoch counter (Int) + // tokens(0) pool token (NFT) - // tokens(0) pool token (NFT) + val otherTokenId = INPUTS(1).tokens(0)._1 + val refreshNFT = fromBase64("VGpXblpyNHU3eCFBJUQqRy1LYU5kUmdVa1hwMnM1djg=") // TODO replace with actual + val updateNFT = fromBase64("YlFlVGhXbVpxNHQ3dyF6JUMqRi1KQE5jUmZValhuMnI=") // TODO replace with actual - val otherTokenId = INPUTS(1).tokens(0)._1 - val refreshNFT = fromBase64("VGpXblpyNHU3eCFBJUQqRy1LYU5kUmdVa1hwMnM1djg=") // TODO replace with actual - val updateNFT = fromBase64("YlFlVGhXbVpxNHQ3dyF6JUMqRi1KQE5jUmZValhuMnI=") // TODO replace with actual - - sigmaProp(otherTokenId == refreshNFT || otherTokenId == updateNFT) + sigmaProp(otherTokenId == refreshNFT || otherTokenId == updateNFT) } ``` ### Refresh Contract ```scala { // This box (refresh box) - // tokens(0) reward tokens to be emitted (several) - // - // When initializing the box, there must be one reward token. When claiming reward, one token must be left unclaimed - - val oracleTokenId = fromBase64("KkctSmFOZFJnVWtYcDJzNXY4eS9CP0UoSCtNYlBlU2g=") // TODO replace with actual - val poolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // TODO replace with actual - - val spenderIndex = getVar[Int](0).get // the index of the data-point box (NOT input!) belonging to spender - - val epochLength = 30 - val minStartHeight = HEIGHT - epochLength - val minDataPoints = 4 - val buffer = 4 - val maxDeviationPercent = 5 // percent - - val poolIn = INPUTS(0) - val poolOut = OUTPUTS(0) - val selfOut = OUTPUTS(1) - - def isValidDataPoint(b: Box) = if (b.R6[Long].isDefined) { - b.creationInfo._1 >= minStartHeight && // data point must not be too old - b.tokens(0)._1 == oracleTokenId && // first token id must be of oracle token - b.R5[Int].get == poolIn.R5[Int].get // it must correspond to this epoch - } else false - - val dataPoints = INPUTS.filter(isValidDataPoint) - val pubKey = dataPoints(spenderIndex).R4[GroupElement].get - - val enoughDataPoints = dataPoints.size >= minDataPoints - val rewardEmitted = dataPoints.size * 2 // one extra token for each collected box as reward to collector - val epochOver = poolIn.creationInfo._1 < minStartHeight - - val startData = 1L // we don't allow 0 data points - val startSum = 0L - // we expect data-points to be sorted in INCREASING order - - val lastSortedSum = dataPoints.fold((startData, (true, startSum)), { - (t: (Long, (Boolean, Long)), b: Box) => - val currData = b.R6[Long].get - val prevData = t._1 - val wasSorted = t._2._1 - val oldSum = t._2._2 - val newSum = oldSum + currData // we don't have to worry about overflow, as it causes script to fail - - val isSorted = wasSorted && prevData <= currData - - (currData, (isSorted, newSum)) - } - ) - - val lastData = lastSortedSum._1 - val isSorted = lastSortedSum._2._1 - val sum = lastSortedSum._2._2 - val average = sum / dataPoints.size - - val maxDelta = lastData * maxDeviationPercent / 100 - val firstData = dataPoints(0).R6[Long].get - - proveDlog(pubKey) && - epochOver && - enoughDataPoints && - isSorted && - lastData - firstData <= maxDelta && - poolIn.tokens(0)._1 == poolNFT && - poolOut.tokens == poolIn.tokens && // preserve pool tokens - poolOut.R4[Long].get == average && // rate - poolOut.R5[Int].get == poolIn.R5[Int].get + 1 && // counter - ! (poolOut.R6[Any].isDefined) && - poolOut.propositionBytes == poolIn.propositionBytes && // preserve pool script - poolOut.value >= poolIn.value && - poolOut.creationInfo._1 >= HEIGHT - buffer && // ensure that new box has correct start epoch height - selfOut.tokens(0) == SELF.tokens(0) && // refresh NFT preserved - selfOut.tokens(1)._1 == SELF.tokens(1)._1 && // reward token id preserved - selfOut.tokens(1)._2 >= SELF.tokens(1)._2 - rewardEmitted && // reward token amount correctly reduced - selfOut.tokens.size == 2 && // no more tokens - ! (selfOut.R4[Any].isDefined) && - selfOut.propositionBytes == SELF.propositionBytes && // script preserved - selfOut.value >= SELF.value + // tokens(0) reward tokens to be emitted (several) + // + // When initializing the box, there must be one reward token. When claiming reward, one token must be left unclaimed + + val oracleTokenId = fromBase64("KkctSmFOZFJnVWtYcDJzNXY4eS9CP0UoSCtNYlBlU2g=") // TODO replace with actual + val poolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // TODO replace with actual + val epochLength = 30 // TODO replace with actual + val minDataPoints = 4 // TODO replace with actual + val buffer = 4 // TODO replace with actual + val maxDeviationPercent = 5 // percent // TODO replace with actual + + val minStartHeight = HEIGHT - epochLength + val spenderIndex = getVar[Int](0).get // the index of the data-point box (NOT input!) belonging to spender + + val poolIn = INPUTS(0) + val poolOut = OUTPUTS(0) + val selfOut = OUTPUTS(1) + + def isValidDataPoint(b: Box) = if (b.R6[Long].isDefined) { + b.creationInfo._1 >= minStartHeight && // data point must not be too old + b.tokens(0)._1 == oracleTokenId && // first token id must be of oracle token + b.R5[Int].get == poolIn.R5[Int].get // it must correspond to this epoch + } else false + + val dataPoints = INPUTS.filter(isValidDataPoint) + val pubKey = dataPoints(spenderIndex).R4[GroupElement].get + + val enoughDataPoints = dataPoints.size >= minDataPoints + val rewardEmitted = dataPoints.size * 2 // one extra token for each collected box as reward to collector + val epochOver = poolIn.creationInfo._1 < minStartHeight + + val startData = 1L // we don't allow 0 data points + val startSum = 0L + // we expect data-points to be sorted in INCREASING order + + val lastSortedSum = dataPoints.fold((startData, (true, startSum)), { + (t: (Long, (Boolean, Long)), b: Box) => + val currData = b.R6[Long].get + val prevData = t._1 + val wasSorted = t._2._1 + val oldSum = t._2._2 + val newSum = oldSum + currData // we don't have to worry about overflow, as it causes script to fail + + val isSorted = wasSorted && prevData <= currData + + (currData, (isSorted, newSum)) + } + ) + + val lastData = lastSortedSum._1 + val isSorted = lastSortedSum._2._1 + val sum = lastSortedSum._2._2 + val average = sum / dataPoints.size + + val maxDelta = lastData * maxDeviationPercent / 100 + val firstData = dataPoints(0).R6[Long].get + + proveDlog(pubKey) && + epochOver && + enoughDataPoints && + isSorted && + lastData - firstData <= maxDelta && + poolIn.tokens(0)._1 == poolNFT && + poolOut.tokens == poolIn.tokens && // preserve pool tokens + poolOut.R4[Long].get == average && // rate + poolOut.R5[Int].get == poolIn.R5[Int].get + 1 && // counter + ! (poolOut.R6[Any].isDefined) && + poolOut.propositionBytes == poolIn.propositionBytes && // preserve pool script + poolOut.value >= poolIn.value && + poolOut.creationInfo._1 >= HEIGHT - buffer && // ensure that new box has correct start epoch height + selfOut.tokens(0) == SELF.tokens(0) && // refresh NFT preserved + selfOut.tokens(1)._1 == SELF.tokens(1)._1 && // reward token id preserved + selfOut.tokens(1)._2 >= SELF.tokens(1)._2 - rewardEmitted && // reward token amount correctly reduced + selfOut.tokens.size == 2 && // no more tokens + ! (selfOut.R4[Any].isDefined) && + selfOut.propositionBytes == SELF.propositionBytes && // script preserved + selfOut.value >= SELF.value } ``` @@ -348,7 +371,7 @@ This is similar to the "Publish data-point" case, except that Step 3 is modified output.tokens(1)._2 > SELF.tokens(1)._2 && // at least one reward token must be added output.R4[GroupElement].get == selfPubKey && // for collection preserve public key output.value >= SELF.value && // nanoErgs value preserved - output.R5[Any].isDefined == false // no more registers, preserving only R4, the group element + ! (output.R5[Any].isDefined) // no more registers, preserving only R4, the group element val owner = proveDlog(selfPubKey) @@ -362,15 +385,15 @@ This is similar to the "Publish data-point" case, except that Step 3 is modified ```scala { // This box (ballot box): // R4 the group element of the owner of the ballot token [GroupElement] - // R5 the box id of the update box [Coll[Byte]] + // R5 the creation height of the update box [Int] // R6 the value voted for [Coll[Byte]] val updateNFT = fromBase64("YlFlVGhXbVpxNHQ3dyF6JUMqRi1KQE5jUmZValhuMnI=") // TODO replace with actual - val minStorageRent = 10000000L + val minStorageRent = 10000000L // TODO replace with actual val selfPubKey = SELF.R4[GroupElement].get - val otherTokenId = INPUTS(0).tokens(0)._1 + val otherTokenId = INPUTS(1).tokens(0)._1 val outIndex = getVar[Int](0).get val output = OUTPUTS(outIndex) @@ -382,12 +405,14 @@ This is similar to the "Publish data-point" case, except that Step 3 is modified val update = otherTokenId == updateNFT && output.R4[GroupElement].get == selfPubKey && - output.value >= SELF.value + output.value >= SELF.value && + ! (output.R5[Any].isDefined) val owner = proveDlog(selfPubKey) isSimpleCopy && (owner || update) } + ``` ## Update Contract @@ -398,40 +423,43 @@ This is similar to the "Publish data-point" case, except that Step 3 is modified // // ballot boxes (Inputs) // R4 the pub key of voter [GroupElement] (not used here) - // R5 the box id of this box [Coll[Byte]] + // R5 the creation height of this box [Int] // R6 the value voted for [Coll[Byte]] (hash of the new pool box script) val poolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // TODO replace with actual val ballotTokenId = fromBase64("P0QoRy1LYVBkU2dWa1lwM3M2djl5JEImRSlIQE1iUWU=") // TODO replace with actual - val minVotes = 8 + val minVotes = 6 // TODO replace with actual - val poolBoxIn = INPUTS(0) // pool box is 1st input - val poolBoxOut = OUTPUTS(0) // copy of pool box is the 1st output + val poolIn = INPUTS(0) // pool box is 1st input + val poolOut = OUTPUTS(0) // copy of pool box is the 1st output val updateBoxOut = OUTPUTS(1) // copy of this 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 poolOutHash = blake2b256(poolOut.propositionBytes) - val validPoolIn = poolBoxIn.tokens(0)._1 == poolNFT + val validPoolIn = poolIn.tokens(0)._1 == poolNFT - val validPoolOut = poolBoxIn.propositionBytes != poolBoxOut.propositionBytes && // script should not be preserved - poolBoxIn.tokens == poolBoxOut.tokens && // tokens preserved - poolBoxIn.value == poolBoxOut.value && // value preserved - poolBoxIn.R4[Long] == poolBoxOut.R4[Long] && // rate preserved - poolBoxIn.R5[Int] == poolBoxOut.R5[Int] // counter preserved + val validPoolOut = poolIn.propositionBytes != poolOut.propositionBytes && // script should not be preserved + poolIn.tokens == poolOut.tokens && // tokens preserved + poolIn.value == poolOut.value && // value preserved + poolIn.R4[Long] == poolOut.R4[Long] && // rate preserved + poolIn.R5[Int] == poolOut.R5[Int] && // counter preserved + ! (poolOut.R6[Any].isDefined) val validUpdateOut = updateBoxOut.tokens == SELF.tokens && updateBoxOut.propositionBytes == SELF.propositionBytes && - updateBoxOut.value >= SELF.value + updateBoxOut.value >= SELF.value && + updateBoxOut.creationInfo._1 > SELF.creationInfo._1 && + ! (updateBoxOut.R4[Any].isDefined) def isValidBallot(b:Box) = if (b.tokens.size > 0) { b.tokens(0)._1 == ballotTokenId && - b.R5[Coll[Byte]].get == SELF.id && // ensure vote corresponds to this box - b.R6[Coll[Byte]].get == poolBoxOutHash // check value voted for + b.R5[Int].get == SELF.creationInfo._1 && // ensure vote corresponds to this box by checking creation height + b.R6[Coll[Byte]].get == poolOutHash // check value voted for } else false val ballotBoxes = INPUTS.filter(isValidBallot) From 92e4bf319791d2ebd28a2eac81f39ac303c4cae3 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Mon, 4 Oct 2021 14:46:47 +0530 Subject: [PATCH 15/43] Add description of the update mechanism --- eip-0023.md | 63 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/eip-0023.md b/eip-0023.md index 35606f8c..1b45d943 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -15,21 +15,21 @@ Oracle pool 2.0 aims to address the above. Below is a summary of the main new features in 2.0 and how it differs from 1.0. -- Single pool address: This version of the pool will have only one address, the *pool address*. -- Pool box will additionally store a counter that is incremented on each collection. -- Compact pool box: Pool box is separated from the logic of pool management, which is captured instead in a **refresh** box. This makes the pool box very small for use in other dApps. -- Reward in tokens not Ergs: The posting reward will be in the form of tokens, which can be redeemed separately. -- Reward accumulated in oracle boxes to prevent dust: We will not be creating a new box for rewarding each posting. -- Oracle boxes spent during collection: Because the rewards must be accumulated, the oracle boxes will be considered as inputs rather than data-inputs when collecting individual rates for averaging. +- **Single pool address**: This version of the pool will have only one address, the *pool address*. +- **Epoch counter**: Pool box will additionally store a counter that is incremented on each collection. This will allow more sophisticated dApps. +- **Compact pool box**: Pool box is separated from the logic of pool management, which is captured instead in a **refresh** box. This makes the pool box very small for use in other dApps. +- **Refresh box**: The refresh box is used for collecting data-points, refreshing the pool box and emitting oracle rewards. +- **Reward in tokens**: The posting reward will be in the form of tokens instead of Ergs. These tokens can be redeemed separately, which is not part of the protocol. +- **No separate funding box**: The pool box emits only reward tokens and won't be handing out Ergs. Thus, there won't be a separate funding process required. +- **Reward accumulated**: We will not be creating a new box for rewarding each posting to prevent dust. Instead, the rewards will be accumulated directly in the oracle boxes. +- **Oracle boxes spent in collection**: Because the rewards must be accumulated, the oracle boxes will be considered as inputs rather than data-inputs when collecting individual rates for averaging. These inputs will be spent, and a copy of the box with the reward will be created. This gives us the ability to accumulate rewards, while keeping the transaction size similar to when using them as data-inputs in v1.0. + Additionally, this allows us to outsource part of the reward logic to the oracle boxes. **Note:** The pool box will still be used as data input in other dApps. - -- Transferable oracle tokens: Oracle tokens are free to be transferred between public keys. -- We will have the same update mechanism as in v1.0. -- No separate funding box. The pool box emits only reward tokens and won't be handing out Ergs. Thus, there won't be a separate funding process required. -- Reward mechanism separate from pool. The process of redeeming the reward tokens is not part of the protocol. +- **Transferable oracle tokens**: Oracle tokens are free to be transferred between public keys. +- **Similar update mechanism**: We will have the similar update mechanism as in v1.0 (threshold number of ballot token holders must vote for an update). ### Reward Mechanism @@ -38,6 +38,35 @@ In 1.0, the pool was responsible for rewarding each oracle for posting a data-po The certificates are in the form of tokens emitted by the pool box. Once there are sufficient number of such tokens, a oracle can exchange or burn them in return for a reward. We also give a sample token exchange contract. +### Refresh Mechanism + +In 1.0, the pool was refreshed by moving from the epoch-preparation phase to the live epoch phase. +In 2.0, although there is a single pool box, the logic of pool refresh is stored in a separate box, the **refresh box**. +The refresh box also contains the reward tokens to be emitted. The refresh box is identified by a separate token, the *refresh-NFT*, which is referenced in the pool and oracle contracts. + +### Update Mechanism + +The update mechanism is similar to that in 1.0. A threshold number (currently 6) of ballots must be cast for a proposed update. Such ballots are stored in a **ballot box** and identified by a **ballot token**. +The ballot token holders and oracle token holders need not be the same people. +The logic for updating the pool is stored in an **update box**, which identified by the **update-NFT**. This NFT is referenced in the pool and ballot boxes. + +**Note** The update box logic acts on the pool box and not the refresh box. + +In order to keep the pool box as compact as possible, a lot of parameters are hard-wired in the refresh contract. Consequently, even small updates in the pool require +almost all other boxes and tokens to be updated. In particular, the following rules apply: + +| When updating | This must be updated | This need not be updated | +|---|---|---| +|Refresh-NFT | Refresh box
Reward tokens
Oracle tokens
Oracle boxes | Update-NFT
Update box
Ballot tokens
Ballot boxes | +|Update-NFT | Update box
Ballot tokens
Ballot boxes | Refresh-NFT
Refresh box
Oracle tokens
Oracle boxes| +|Pool params (like epoch length) | Refresh-NFT
Same as in refresh-NFT| Same as in refresh-NFT | +|Oracle tokens | Refresh-NFT
Same as in refresh-NFT| Same as in refresh-NFT | +|Reward tokens | Refresh-NFT
Same as in refresh-NFT| Same as in refresh-NFT | +|Ballot tokens | Update-NFT
Same as in update-NFT| Same as in update-NFT | + +After an update, the older versions of the boxes will be left for garbage collection via storage-rent, and their tokens will become free for miners to take. +Hence, it is beneficial to update all the reletated tokens anyway. + ## Tokens The system has the following types of tokens. Note that we use the term **NFT** to refer to any token that was issued in quantity 1. @@ -55,13 +84,13 @@ The system has the following types of tokens. Note that we use the term **NFT** There are a total of 5 contracts, each corresponding to a box type -| Box | Quantity | Tokens | Registers used | Purpose | Spending transactions | +| Box | Quantity | Tokens | Additional registers used | Purpose | Spending transactions | |---|---|---|---|---|---| -|Pool | 1 | Pool-NFT| R3: Creation height (Int)
R4: Rate (Long)
R5: Epoch counter (Int) | Publish pool rate for dApps | Refresh pool
Update pool | +|Pool | 1 | Pool-NFT| R4: Rate (Long)
R5: Epoch counter (Int) | Publish pool rate for dApps | Refresh pool
Update pool | |Refresh| 1 | Refresh-NFT
Reward tokens| | Refresh pool box
Emit reward tokens| Refresh pool | -|Oracle | 15 | Oracle token
Reward tokens | R4: Public key (GroupElement)
R5: Epoch counter of pool box (Int)
R6: Published rate (Long) | Publish data-point
Accumulate reward tokens | Publish data-point,
Refresh pool,
Transfer oracle token,
Extract reward tokens| +|Oracle | 15 | Oracle token
Reward tokens | R4: Public key (GroupElement)
R5: Epoch counter of pool box (Int)
R6: Published rate (Long) | Publish data-point
Accumulate reward tokens | Publish data-point,
Refresh pool,
Transfer oracle token,
Extract reward tokens| |Update | 1 | Update-NFT | | Updating pool box | Update pool box | -|Ballot | 15 | Ballot token | R4: Public key (GroupElement)
R5: Update box creation height (Int)
R6: New pool box hash (Coll[Byte]) | Voting for updating pool box | Vote for update
Update pool box
Transfer ballot token | +|Ballot | 15 | Ballot token | R4: Public key (GroupElement)
R5: Update box creation height (Int)
R6: New pool box hash (Coll[Byte]) | Voting for updating pool box | Vote for update
Update pool box
Transfer ballot token | ## Transactions @@ -205,13 +234,15 @@ This updates the pool box (i.e., transfers the poolNFT to a new address). The ballots used in inputs must satisfy the following [smart contract](#ballot-contract): - There must be an output ballot box with the same public key in R4 and rest registers empty. -The update box additionally ensures following: +The update box additionally ensures following [smart contract](#update-contract): - Each ballot box has R4 containing the has of the new pool box script (address). - Each ballot box has R5 containing this box's creation height. - There is a copy of the update box in outputs with everything preserved but with larger creation height. - There are at least a threshold number of votes. - The registers and token in the pool box are preserved in the new pool box. +We store the update box's creation height in the ballot's R5 to ensure that a vote is actually for the current update box. I + | Index | Input | Output | |---|---|---| | 0 | Pool | New Pool | From 2d21b68e3958577f6c5ccb95261809614786d2fa Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Mon, 4 Oct 2021 21:08:23 +0530 Subject: [PATCH 16/43] Fix typos, add cross-references to contracts --- eip-0023.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/eip-0023.md b/eip-0023.md index 1b45d943..85c27661 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -65,7 +65,7 @@ almost all other boxes and tokens to be updated. In particular, the following ru |Ballot tokens | Update-NFT
Same as in update-NFT| Same as in update-NFT | After an update, the older versions of the boxes will be left for garbage collection via storage-rent, and their tokens will become free for miners to take. -Hence, it is beneficial to update all the reletated tokens anyway. +Hence, it is beneficial to update all the related tokens anyway. ## Tokens @@ -191,7 +191,7 @@ This entails spending the oracle box and creating a new oracle box as per the [s This allows an oracle to extract tokens obtained from publishing data point to redeem them elsewhere. -The transaction is similar to the "Publish data-point" case, except that Step 3 is modified as follows. +The transaction is similar to the [publish data-point](#publish-data-point) transaction, except that Step 3 is modified as follows. 3. The following rules (not enforced by the smart contract) to be followed. - It should store the same group element in R4. @@ -207,7 +207,7 @@ The transaction is similar to the "Publish data-point" case, except that Step 3 This is used to transfer the ownership of an oracle token to another public key. -The transaction is similar to the "Publish data-point" case, except that Step 3 is modified as follows. +The transaction is similar to the [publish data-point](#publish-data-point) transaction, except that Step 3 is modified as follows. 3. The following rules (not enforced by the smart contract) to be followed. - It should store the new owner's group element in R4. @@ -222,8 +222,10 @@ This is used by ballot token holders to vote for updating the pool box address. |---|---|---| | 0 | Ballot | Ballot | -The input and output are ballot boxes such that the following holds: - - There is a group element in R4 (enforced by smart contract). +The input and output are ballot boxes such that the following holds as per the [smart contract](#ballot-contract): + - There is a group element in R4. + +The following (not enforced by the contract) must be done for a proper vote: - R5 contains the creation height of the current update box. - R6 contains the hash of the address of the new pool box. @@ -231,17 +233,17 @@ The input and output are ballot boxes such that the following holds: This updates the pool box (i.e., transfers the poolNFT to a new address). -The ballots used in inputs must satisfy the following [smart contract](#ballot-contract): +The ballots used in inputs must satisfy the following as per the [smart contract](#ballot-contract): - There must be an output ballot box with the same public key in R4 and rest registers empty. -The update box additionally ensures following [smart contract](#update-contract): +The update box additionally ensures following as per the [smart contract](#update-contract): - Each ballot box has R4 containing the has of the new pool box script (address). - Each ballot box has R5 containing this box's creation height. - There is a copy of the update box in outputs with everything preserved but with larger creation height. - There are at least a threshold number of votes. - The registers and token in the pool box are preserved in the new pool box. -We store the update box's creation height in the ballot's R5 to ensure that a vote is actually for the current update box. I +We store the update box's creation height in the ballot's R5 to ensure that a vote is actually for the current update box. | Index | Input | Output | |---|---|---| @@ -254,7 +256,7 @@ We store the update box's creation height in the ballot's R5 to ensure that a vo ### Transfer ballot token -This is similar to the transfer oracle token, except that it is used to transfer ownership of a ballot token to another public key. +This is similar to the [transfer oracle token](#transfer-oracle-token) and [vote for update](#vote-for-update) transactions, except that it is used to transfer ownership of a ballot token to another public key. | Index | Input | Output | |---|---|---| @@ -446,7 +448,7 @@ This is similar to the transfer oracle token, except that it is used to transfer ``` -## Update Contract +### Update Contract ```scala { // This box (update box): From 1e4748e2409ff7e4868931c4e3f18058a48ea16a Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Tue, 5 Oct 2021 00:36:24 +0530 Subject: [PATCH 17/43] Fix description of update mechanism --- eip-0023.md | 434 ++++++++++++++++++++++++++-------------------------- 1 file changed, 219 insertions(+), 215 deletions(-) diff --git a/eip-0023.md b/eip-0023.md index 85c27661..44c7071d 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -1,19 +1,18 @@ # Oracle Pool v2.0 -This is a proposed update to the oracle pool 1.0 currently deployed and documented in [EIP16](https://github.com/ergoplatform/eips/blob/eip16/eip-0016.md). +This is a proposed update to the oracle pool v1.0 currently deployed and documented in [EIP16](https://github.com/ergoplatform/eips/blob/eip16/eip-0016.md). ## Introduction - -Oracle pool 1.0 has the following drawbacks: +In order to motivate the changes proposed in this document, consider the drawbacks of Oracle pool v1.0: 1. Rewards generate a lot of dust 2. Current rewards are too low (related to 1) 3. There are two types of pool boxes. This makes dApps and update mechanism more complex 4. Oracle tokens are non-transferable, and so oracles are locked permanently. The same goes with ballot tokens. -Oracle pool 2.0 aims to address the above. +Oracle pool v2.0 aims to address the above. -Below is a summary of the main new features in 2.0 and how it differs from 1.0. +Below is a summary of the main new features in v2.0 and how it differs from v1.0. - **Single pool address**: This version of the pool will have only one address, the *pool address*. - **Epoch counter**: Pool box will additionally store a counter that is incremented on each collection. This will allow more sophisticated dApps. @@ -30,43 +29,29 @@ Below is a summary of the main new features in 2.0 and how it differs from 1.0. **Note:** The pool box will still be used as data input in other dApps. - **Transferable oracle tokens**: Oracle tokens are free to be transferred between public keys. - **Similar update mechanism**: We will have the similar update mechanism as in v1.0 (threshold number of ballot token holders must vote for an update). +- **Transferable ballot tokens**: Similar to oracle tokens, the ballot tokens are free to be transferred between public keys. ### Reward Mechanism -In 1.0, the pool was responsible for rewarding each oracle for posting a data-point. In 2.0, the pool simply certifies that a data-point was posted, and a separate reward mechanism is proposed. This keeps the contract smaller and more flexible. +In v1.0, the pool was responsible for rewarding each oracle for posting a data-point. In v2.0, the pool simply certifies that a data-point was posted, and a separate reward mechanism is proposed. This keeps the contract smaller and more flexible. The certificates are in the form of tokens emitted by the pool box. Once there are sufficient number of such tokens, a oracle can exchange or burn them in return for a reward. We also give a sample token exchange contract. ### Refresh Mechanism -In 1.0, the pool was refreshed by moving from the epoch-preparation phase to the live epoch phase. -In 2.0, although there is a single pool box, the logic of pool refresh is stored in a separate box, the **refresh box**. +In v1.0, the pool was refreshed by moving from the epoch-preparation phase to the live epoch phase. +In v2.0, although there is a single pool box, the logic of pool refresh is stored in a separate box, the **refresh box**. The refresh box also contains the reward tokens to be emitted. The refresh box is identified by a separate token, the *refresh-NFT*, which is referenced in the pool and oracle contracts. ### Update Mechanism -The update mechanism is similar to that in 1.0. A threshold number (currently 6) of ballots must be cast for a proposed update. Such ballots are stored in a **ballot box** and identified by a **ballot token**. +The update mechanism is similar to that in v1.0. A threshold number (currently 6) of ballots must be cast for a proposed update. Such ballots are stored in a **ballot box** and identified by a **ballot token**. The ballot token holders and oracle token holders need not be the same people. The logic for updating the pool is stored in an **update box**, which identified by the **update-NFT**. This NFT is referenced in the pool and ballot boxes. **Note** The update box logic acts on the pool box and not the refresh box. -In order to keep the pool box as compact as possible, a lot of parameters are hard-wired in the refresh contract. Consequently, even small updates in the pool require -almost all other boxes and tokens to be updated. In particular, the following rules apply: - -| When updating | This must be updated | This need not be updated | -|---|---|---| -|Refresh-NFT | Refresh box
Reward tokens
Oracle tokens
Oracle boxes | Update-NFT
Update box
Ballot tokens
Ballot boxes | -|Update-NFT | Update box
Ballot tokens
Ballot boxes | Refresh-NFT
Refresh box
Oracle tokens
Oracle boxes| -|Pool params (like epoch length) | Refresh-NFT
Same as in refresh-NFT| Same as in refresh-NFT | -|Oracle tokens | Refresh-NFT
Same as in refresh-NFT| Same as in refresh-NFT | -|Reward tokens | Refresh-NFT
Same as in refresh-NFT| Same as in refresh-NFT | -|Ballot tokens | Update-NFT
Same as in update-NFT| Same as in update-NFT | - -After an update, the older versions of the boxes will be left for garbage collection via storage-rent, and their tokens will become free for miners to take. -Hence, it is beneficial to update all the related tokens anyway. - ## Tokens The system has the following types of tokens. Note that we use the term **NFT** to refer to any token that was issued in quantity 1. @@ -92,175 +77,7 @@ There are a total of 5 contracts, each corresponding to a box type |Update | 1 | Update-NFT | | Updating pool box | Update pool box | |Ballot | 15 | Ballot token | R4: Public key (GroupElement)
R5: Update box creation height (Int)
R6: New pool box hash (Coll[Byte]) | Voting for updating pool box | Vote for update
Update pool box
Transfer ballot token | -## Transactions - -Oracle pool 2.0 has the following transactions. -Each of the transactions below also contain the following boxes which will not be shown. - -1. Funding input box: this will be used to fund the transaction, and will be the last input. -2. Fee output box: this will be the last output. -3. Change output box: this is optional, and if present, will be the second-last output. - -| Transaction | Boxes Involved | Purpose | -| --- | --- | --- | -| Refresh pool | Pool
Refresh
Oracles | Refresh pool box | -| Publish data point | Oracle | Publish data point | -| Extract reward tokens | Oracle | Extract reward tokens to redeem via external mechanism | -| Transfer oracle token | Oracle | Transfer oracle token to another public key| -| Vote for update | Ballot | Vote for updating pool box | -| Update pool | Pool
Update
Ballots | Update pool box | -| Transfer ballot token | Ballot | Transfer ballot token to another public key| - -None of the transactions have data-inputs. - -### Refresh pool - -| Index | Input | Output | -|---|---|---| -| 0 | Pool | Pool | -| 1 | Refresh | Refresh | -| 2 | Oracle 1 | Oracle 1 | -| 3 | Oracle 2 | Oracle 2 | -| 4 | Oracle 3 | Oracle 3 | -| ... | ... | ... | - -The purpose of this transaction is to take the average of the rates in all the oracle boxes and update the rate in the pool box whenever the -epoch gets over (i.e., the current height is > creation height + epoch length). -We consider such a pool box to be *stale* that needs to be refreshed. - -1. The first input is the pool box that simply requires the second input to be the refresh box (i.e., contain the refresh token) ([smart contract](#pool-contract)). -2. The second input is a refresh box that contains the following logic ([smart contract](#refresh-contract)): - - The first input is a stale pool box, that is a box with the pool token and creation height lower than the current height minus epoch length. - - This transaction can only be done by someone holding a oracle token and having published an oracle box (see below). - - Any value published within the last epoch length by someone holding the oracle token is considered *latest*. This is called an oracle box. - In particular, for the box to be considered an oracle box, the following must hold: - - It must have an oracle token at index 0. - - Register R4 must contain a group element. - - Register R5 must be the current epoch counter (from R5 of the stale pool box). - - Register R6 must contain a long value, which will be assumed to be the rate. - - Its creation height must not be less than the current height minus epoch length. - - There must be at least a certain number of oracle boxes (currently 4) as inputs. - - The oracle boxes must be arranged in increasing order of their R6 values (rate). - - The first oracle box's rate must be within 5% of that of the last, and must be > 0. - - The first output must be a new pool box as follows: - - Registers R0 (value), R1 (script), R2 (tokens) are preserved from the old pool box. - - The creation height (stored in R3) must be at most 4 less than the current height. - - The rate (stored in R4) must be the average of the rates in all the oracle boxes. - - The epoch counter (stored in R5) must be incremented by 1. - - The second output must be a new refresh box as follows: - - Registers R0 (value), R1 (script), and the first token (refresh NFT) are preserved from the old refresh box. - - The quantity of the second token (reward) must be decremented by at most twice the number of valid rate boxes. - - Register R4 and onward are empty. -3. Each input oracle box has following logic ([smart contract](#oracle-contract)): - - The first input is a pool box (i.e., has the pool token). - - An output oracle box (acting as a copy of this box) must be created as follows: - - The following values are directly copied: R0 (nanoErgs), R1 (script), the first token (oracle token) (stored in R2) and R4 (GroupElement). - - The second token (reward token) is incremented by at least 1. - - Registers R5 and onward are empty. - -Suppose there are *n* valid oracle boxes, then there are 2*n* reward tokens released. -It is expected that whoever creates the refresh transaction (the *collector*) takes *n*+1 reward tokens, and the other oracles get 1 token each. -This gives incentive to use as many oracle boxes as possible during the refresh. - -### Publish data-point - -| Index | Input | Output | -|---|---|---| -| 0 | Oracle | Oracle | - -This allows an oracle to publish data-point for collection in next epoch. -This entails spending the oracle box and creating a new oracle box as per the [smart contract](#oracle-contract). - -1. The public key stored in R4 defines who can spend the oracle box. -2. The logic requires the new oracle box to be as follows: - - The script and the first token are copied from this box. - - R4 contains a group element. - - The second token is the reward token in some non-zero quantity. -3. The following rules (not enforced by the smart contract) to be followed. - - It should store the same group element in R4. - - It should store epoch counter of the current pool box in R5. - - It should store the rate to publish in R6. - - It should ensure reward tokens are preserved. - -### Extract reward tokens - -| Index | Input | Output | -|---|---|---| -| 0 | Oracle | Oracle | -| 1 | | Box with freed reward tokens | - -This allows an oracle to extract tokens obtained from publishing data point to redeem them elsewhere. - -The transaction is similar to the [publish data-point](#publish-data-point) transaction, except that Step 3 is modified as follows. - -3. The following rules (not enforced by the smart contract) to be followed. - - It should store the same group element in R4. - - It should keep at least 1 reward token. - - It should store the balance reward tokens in some other box. - - -### Transfer oracle token - -| Index | Input | Output | -|---|---|---| -| 0 | Oracle | Oracle | - -This is used to transfer the ownership of an oracle token to another public key. - -The transaction is similar to the [publish data-point](#publish-data-point) transaction, except that Step 3 is modified as follows. - -3. The following rules (not enforced by the smart contract) to be followed. - - It should store the new owner's group element in R4. - - It should keep at least 1 reward token. - - It should store the balance reward tokens, if any, in some other box. - -### Vote for update - -This is used by ballot token holders to vote for updating the pool box address. - -| Index | Input | Output | -|---|---|---| -| 0 | Ballot | Ballot | - -The input and output are ballot boxes such that the following holds as per the [smart contract](#ballot-contract): - - There is a group element in R4. - -The following (not enforced by the contract) must be done for a proper vote: - - R5 contains the creation height of the current update box. - - R6 contains the hash of the address of the new pool box. - -### Update Pool box - -This updates the pool box (i.e., transfers the poolNFT to a new address). - -The ballots used in inputs must satisfy the following as per the [smart contract](#ballot-contract): - - There must be an output ballot box with the same public key in R4 and rest registers empty. - -The update box additionally ensures following as per the [smart contract](#update-contract): - - Each ballot box has R4 containing the has of the new pool box script (address). - - Each ballot box has R5 containing this box's creation height. - - There is a copy of the update box in outputs with everything preserved but with larger creation height. - - There are at least a threshold number of votes. - - The registers and token in the pool box are preserved in the new pool box. - -We store the update box's creation height in the ballot's R5 to ensure that a vote is actually for the current update box. - -| Index | Input | Output | -|---|---|---| -| 0 | Pool | New Pool | -| 1 | Update | Update | -| 2 | Ballot 1 | Ballot 1 | -| 3 | Ballot 2 | Ballot 2 | -| 4 | Ballot 3 | Ballot 3 | -| ... | ... | ... | - -### Transfer ballot token - -This is similar to the [transfer oracle token](#transfer-oracle-token) and [vote for update](#vote-for-update) transactions, except that it is used to transfer ownership of a ballot token to another public key. - -| Index | Input | Output | -|---|---|---| -| 0 | Ballot | Ballot | +Before going into the transactions in the protocol, we first present the contracts for each of the above boxes. ## Contracts @@ -304,9 +121,9 @@ This is similar to the [transfer oracle token](#transfer-oracle-token) and [vote val selfOut = OUTPUTS(1) def isValidDataPoint(b: Box) = if (b.R6[Long].isDefined) { - b.creationInfo._1 >= minStartHeight && // data point must not be too old - b.tokens(0)._1 == oracleTokenId && // first token id must be of oracle token - b.R5[Int].get == poolIn.R5[Int].get // it must correspond to this epoch + b.creationInfo._1 >= minStartHeight && // data point must not be too old + b.tokens(0)._1 == oracleTokenId && // first token id must be of oracle token + b.R5[Int].get == poolIn.R5[Int].get // it must correspond to this epoch } else false val dataPoints = INPUTS.filter(isValidDataPoint) @@ -393,18 +210,18 @@ This is similar to the [transfer oracle token](#transfer-oracle-token) and [vote val outIndex = getVar[Int](0).get val output = OUTPUTS(outIndex) - val isSimpleCopy = output.tokens(0) == SELF.tokens(0) && // oracle token is preserved - output.tokens(1)._1 == SELF.tokens(1)._1 && // reward tokenId is preserved - output.tokens.size == 2 && // no more tokens - output.propositionBytes == SELF.propositionBytes && // script preserved - output.R4[GroupElement].isDefined && // output must have a public key (not necessarily the same) - output.value >= minStorageRent // ensure sufficient Ergs to ensure no garbage collection + val isSimpleCopy = output.tokens(0) == SELF.tokens(0) && // oracle token is preserved + output.tokens(1)._1 == SELF.tokens(1)._1 && // reward tokenId is preserved + output.tokens.size == 2 && // no more tokens + output.propositionBytes == SELF.propositionBytes && // script preserved + output.R4[GroupElement].isDefined && // output must have a public key (not necessarily the same) + output.value >= minStorageRent // ensure sufficient Ergs to ensure no garbage collection - val collection = otherTokenId == poolNFT && // first input must be pool box - output.tokens(1)._2 > SELF.tokens(1)._2 && // at least one reward token must be added - output.R4[GroupElement].get == selfPubKey && // for collection preserve public key - output.value >= SELF.value && // nanoErgs value preserved - ! (output.R5[Any].isDefined) // no more registers, preserving only R4, the group element + val collection = otherTokenId == poolNFT && // first input must be pool box + output.tokens(1)._2 > SELF.tokens(1)._2 && // at least one reward token must be added + output.R4[GroupElement].get == selfPubKey && // for collection preserve public key + output.value >= SELF.value && // nanoErgs value preserved + ! (output.R5[Any].isDefined) // no more registers, preserving only R4, the group element val owner = proveDlog(selfPubKey) @@ -431,12 +248,12 @@ This is similar to the [transfer oracle token](#transfer-oracle-token) and [vote val outIndex = getVar[Int](0).get val output = OUTPUTS(outIndex) - val isSimpleCopy = output.R4[GroupElement].isDefined && // ballot boxes are transferable by setting different value here + val isSimpleCopy = output.R4[GroupElement].isDefined && // ballot boxes are transferable by setting different value here output.propositionBytes == SELF.propositionBytes && output.tokens == SELF.tokens && output.value >= minStorageRent - val update = otherTokenId == updateNFT && + val update = otherTokenId == updateNFT && output.R4[GroupElement].get == selfPubKey && output.value >= SELF.value && ! (output.R5[Any].isDefined) @@ -475,11 +292,11 @@ This is similar to the [transfer oracle token](#transfer-oracle-token) and [vote val validPoolIn = poolIn.tokens(0)._1 == poolNFT - val validPoolOut = poolIn.propositionBytes != poolOut.propositionBytes && // script should not be preserved - poolIn.tokens == poolOut.tokens && // tokens preserved - poolIn.value == poolOut.value && // value preserved - poolIn.R4[Long] == poolOut.R4[Long] && // rate preserved - poolIn.R5[Int] == poolOut.R5[Int] && // counter preserved + val validPoolOut = poolIn.propositionBytes != poolOut.propositionBytes && // script should not be preserved + poolIn.tokens == poolOut.tokens && // tokens preserved + poolIn.value == poolOut.value && // value preserved + poolIn.R4[Long] == poolOut.R4[Long] && // rate preserved + poolIn.R5[Int] == poolOut.R5[Int] && // counter preserved ! (poolOut.R6[Any].isDefined) @@ -490,7 +307,7 @@ This is similar to the [transfer oracle token](#transfer-oracle-token) and [vote ! (updateBoxOut.R4[Any].isDefined) def isValidBallot(b:Box) = if (b.tokens.size > 0) { - b.tokens(0)._1 == ballotTokenId && + b.tokens(0)._1 == ballotTokenId && b.R5[Int].get == SELF.creationInfo._1 && // ensure vote corresponds to this box by checking creation height b.R6[Coll[Byte]].get == poolOutHash // check value voted for } else false @@ -502,3 +319,190 @@ This is similar to the [transfer oracle token](#transfer-oracle-token) and [vote sigmaProp(validPoolIn && validPoolOut && validUpdateOut && votesCount >= minVotes) } ``` +## Transactions + +Oracle pool v2.0 has the following transactions. +Each of the transactions below also contain the following boxes which will not be shown. + +1. Funding input box: this will be used to fund the transaction, and will be the last input. +2. Fee output box: this will be the last output. +3. Change output box: this is optional, and if present, will be the second-last output. + +| Transaction | Boxes Involved | Purpose | +| --- | --- | --- | +| Refresh pool | Pool
Refresh
Oracles | Refresh pool box | +| Publish data point | Oracle | Publish data point | +| Extract reward tokens | Oracle | Extract reward tokens to redeem via external mechanism | +| Transfer oracle token | Oracle | Transfer oracle token to another public key| +| Vote for update | Ballot | Vote for updating pool box | +| Update pool | Pool
Update
Ballots | Update pool box | +| Transfer ballot token | Ballot | Transfer ballot token to another public key| + +None of the transactions have data-inputs. + +### Refresh pool + +| Index | Input | Output | +|---|---|---| +| 0 | Pool | Pool | +| 1 | Refresh | Refresh | +| 2 | Oracle 1 | Oracle 1 | +| 3 | Oracle 2 | Oracle 2 | +| 4 | Oracle 3 | Oracle 3 | +| ... | ... | ... | + +The purpose of this transaction is to take the average of the rates in all the oracle boxes and update the rate in the pool box whenever the +epoch gets over (i.e., the current height is > creation height + epoch length). +We consider such a pool box to be *stale* that needs to be refreshed. + +1. The first input is the pool box that simply requires the second input to be the refresh box (i.e., contain the refresh token) ([smart contract](#pool-contract)). +2. The second input is a refresh box that contains the following logic ([smart contract](#refresh-contract)): + - The first input is a stale pool box, that is a box with the pool token and creation height lower than the current height minus epoch length. + - This transaction can only be done by someone holding a oracle token and having published an oracle box (see below). + - Any value published within the last epoch length by someone holding the oracle token is considered *latest*. This is called an oracle box. + In particular, for the box to be considered an oracle box, the following must hold: + - It must have an oracle token at index 0. + - Register R4 must contain a group element. + - Register R5 must be the current epoch counter (from R5 of the stale pool box). + - Register R6 must contain a long value, which will be assumed to be the rate. + - Its creation height must not be less than the current height minus epoch length. + - There must be at least a certain number of oracle boxes (currently 4) as inputs. + - The oracle boxes must be arranged in increasing order of their R6 values (rate). + - The first oracle box's rate must be within 5% of that of the last, and must be > 0. + - The first output must be a new pool box as follows: + - Registers R0 (value), R1 (script), R2 (tokens) are preserved from the old pool box. + - The creation height (stored in R3) must be at most 4 less than the current height. + - The rate (stored in R4) must be the average of the rates in all the oracle boxes. + - The epoch counter (stored in R5) must be incremented by 1. + - The second output must be a new refresh box as follows: + - Registers R0 (value), R1 (script), and the first token (refresh NFT) are preserved from the old refresh box. + - The quantity of the second token (reward) must be decremented by at most twice the number of valid rate boxes. + - Register R4 and onward are empty. +3. Each input oracle box has following logic ([smart contract](#oracle-contract)): + - The first input is a pool box (i.e., has the pool token). + - An output oracle box (acting as a copy of this box) must be created as follows: + - The following values are directly copied: R0 (nanoErgs), R1 (script), the first token (oracle token) (stored in R2) and R4 (GroupElement). + - The second token (reward token) is incremented by at least 1. + - Registers R5 and onward are empty. + +Suppose there are *n* valid oracle boxes, then there are 2*n* reward tokens released. +It is expected that whoever creates the refresh transaction (the *collector*) takes *n*+1 reward tokens, and the other oracles get 1 token each. +This gives incentive to use as many oracle boxes as possible during the refresh. + +### Publish data-point + +| Index | Input | Output | +|---|---|---| +| 0 | Oracle | Oracle | + +This allows an oracle to publish data-point for collection in next epoch. +This entails spending the oracle box and creating a new oracle box as per the [smart contract](#oracle-contract). + +1. The public key stored in R4 defines who can spend the oracle box. +2. The logic requires the new oracle box to be as follows: + - The script and the first token are copied from this box. + - R4 contains a group element. + - The second token is the reward token in some non-zero quantity. +3. The following rules (not enforced by the smart contract) to be followed. + - It should store the same group element in R4. + - It should store epoch counter of the current pool box in R5. + - It should store the rate to publish in R6. + - It should ensure reward tokens are preserved. + +### Extract reward tokens + +| Index | Input | Output | +|---|---|---| +| 0 | Oracle | Oracle | +| 1 | | Box with freed reward tokens | + +This allows an oracle to extract tokens obtained from publishing data point to redeem them elsewhere. + +The transaction is similar to the [publish data-point](#publish-data-point) transaction, except that Step 3 is modified as follows. + +3. The following rules (not enforced by the smart contract) to be followed. + - It should store the same group element in R4. + - It should keep at least 1 reward token. + - It should store the balance reward tokens in some other box. + + +### Transfer oracle token + +| Index | Input | Output | +|---|---|---| +| 0 | Oracle | Oracle | + +This is used to transfer the ownership of an oracle token to another public key. + +The transaction is similar to the [publish data-point](#publish-data-point) transaction, except that Step 3 is modified as follows. + +3. The following rules (not enforced by the smart contract) to be followed. + - It should store the new owner's group element in R4. + - It should keep at least 1 reward token. + - It should store the balance reward tokens, if any, in some other box. + +### Vote for update + +| Index | Input | Output | +|---|---|---| +| 0 | Ballot | Ballot | + +This is used by ballot token holders to vote for updating the pool box address. + +The input and output are ballot boxes such that the following holds as per the [smart contract](#ballot-contract): +- There is a group element in R4. + +The following (not enforced by the contract) must be done for a proper vote: +- R5 contains the creation height of the current update box. +- R6 contains the hash of the address of the new pool box. + +### Update Pool box + +| Index | Input | Output | +|---|---|---| +| 0 | Pool | New Pool | +| 1 | Update | Update | +| 2 | Ballot 1 | Ballot 1 | +| 3 | Ballot 2 | Ballot 2 | +| 4 | Ballot 3 | Ballot 3 | +| ... | ... | ... | + +This updates the pool box (i.e., transfers the poolNFT to a new address). + +The ballots used in inputs must satisfy the following as per the [smart contract](#ballot-contract): +- There must be an output ballot box with the same public key in R4 and rest registers empty. + +The update box additionally ensures following as per the [smart contract](#update-contract): +- Each ballot box has R4 containing the has of the new pool box script (address). +- Each ballot box has R5 containing this box's creation height. +- There is a copy of the update box in outputs with everything preserved but with larger creation height. +- There are at least a threshold number of votes. +- The registers and token in the pool box are preserved in the new pool box. + +We store the update box's creation height in the ballot's R5 to ensure that a vote is actually for the current update box. + +#### Update Rules + +In order to keep the [pool contract](#pool-contract) as compact as possible, we can see that almost all parameters are hard-wired in the [refresh contract](#refresh-contract). Consequently, even small updates in the pool require +almost all other boxes and tokens to be updated. In particular, the following rules apply: + +| When updating | This must be updated | This need not be updated | +|---|---|---| +|Refresh-NFT | Refresh box
Reward tokens
Oracle tokens
Oracle boxes | Update-NFT
Update box
Ballot tokens
Ballot boxes | +|Update-NFT | Update box
Ballot tokens
Ballot boxes | Refresh-NFT
Refresh box
Oracle tokens
Oracle boxes| +|Pool params (like epoch length) | Refresh-NFT
Same as in refresh-NFT| Same as in refresh-NFT | +|Oracle tokens | Refresh-NFT
Same as in refresh-NFT| Same as in refresh-NFT | +|Reward tokens | Refresh-NFT
Same as in refresh-NFT| Same as in refresh-NFT | +|Ballot tokens | Update-NFT
Same as in update-NFT| Same as in update-NFT | + +After an update, the older versions of the boxes will be left for garbage collection via storage-rent, and their tokens will become free for miners to take. +Hence, it is beneficial to update all the related tokens anyway. + + +### Transfer ballot token + +| Index | Input | Output | +|---|---|---| +| 0 | Ballot | Ballot | + +This is similar to the [transfer oracle token](#transfer-oracle-token) and [vote for update](#vote-for-update) transactions, except that it is used to transfer ownership of a ballot token to another public key. From 7c3a2a4d88bce31c92acc05e3d6224e6edc6ad14 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Wed, 13 Oct 2021 19:01:44 +0530 Subject: [PATCH 18/43] Oracle can change reward tokens in oracle box --- eip-0023.md | 142 ++++++++++++++++++++++++++-------------------------- 1 file changed, 72 insertions(+), 70 deletions(-) diff --git a/eip-0023.md b/eip-0023.md index 44c7071d..f4c1321e 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -83,7 +83,7 @@ Before going into the transactions in the protocol, we first present the contrac ### Pool Contract -```scala +``` { // This box (pool box) // epoch start height is stored in creation Height (R3) @@ -100,7 +100,7 @@ Before going into the transactions in the protocol, we first present the contrac ``` ### Refresh Contract -```scala +``` { // This box (refresh box) // tokens(0) reward tokens to be emitted (several) // @@ -121,9 +121,9 @@ Before going into the transactions in the protocol, we first present the contrac val selfOut = OUTPUTS(1) def isValidDataPoint(b: Box) = if (b.R6[Long].isDefined) { - b.creationInfo._1 >= minStartHeight && // data point must not be too old - b.tokens(0)._1 == oracleTokenId && // first token id must be of oracle token - b.R5[Int].get == poolIn.R5[Int].get // it must correspond to this epoch + b.creationInfo._1 >= minStartHeight && // data point must not be too old + b.tokens(0)._1 == oracleTokenId && // first token id must be of oracle token + b.R5[Int].get == poolIn.R5[Int].get // it must correspond to this epoch } else false val dataPoints = INPUTS.filter(isValidDataPoint) @@ -168,23 +168,20 @@ Before going into the transactions in the protocol, we first present the contrac poolOut.tokens == poolIn.tokens && // preserve pool tokens poolOut.R4[Long].get == average && // rate poolOut.R5[Int].get == poolIn.R5[Int].get + 1 && // counter - ! (poolOut.R6[Any].isDefined) && poolOut.propositionBytes == poolIn.propositionBytes && // preserve pool script poolOut.value >= poolIn.value && poolOut.creationInfo._1 >= HEIGHT - buffer && // ensure that new box has correct start epoch height selfOut.tokens(0) == SELF.tokens(0) && // refresh NFT preserved selfOut.tokens(1)._1 == SELF.tokens(1)._1 && // reward token id preserved selfOut.tokens(1)._2 >= SELF.tokens(1)._2 - rewardEmitted && // reward token amount correctly reduced - selfOut.tokens.size == 2 && // no more tokens - ! (selfOut.R4[Any].isDefined) && selfOut.propositionBytes == SELF.propositionBytes && // script preserved - selfOut.value >= SELF.value + selfOut.value >= SELF.value } ``` ### Oracle contract -```scala +``` { // This box (oracle box) // R4 public key (GroupElement) // R5 epoch counter of current epoch (Int) @@ -193,14 +190,21 @@ Before going into the transactions in the protocol, we first present the contrac // tokens(0) oracle token (one) // tokens(1) reward tokens collected (one or more) // - // When initializing the box, there must be one reward token. When claiming reward, one token must be left unclaimed + // When publishing a datapoint, there must be at least one reward token at index 1 // - // We will connect this box to pool NFT in input #0 (and not the refresh NFT in input #1). + // We will connect this box to pool NFT in input #0 (and not the refresh NFT in input #1) // This way, we can continue to use the same box after updating pool - // This *could* allow the oracle box to be spent during an update - // (when input #2 contains the update NFT instead of the refresh NFT) + // This *could* allow the oracle box to be spent during an update // However, this is not an issue because the update contract ensures that tokens and registers (except script) of the pool box are preserved + // Private key holder can do following things: + // 1. Change group element (public key) stored in R4 + // 2. Store any value of type in or delete any value from R4 to R9 + // 3. Store any token or none at 2nd index + + // In order to connect this oracle box to a different refreshNFT after an update, + // the oracle should keep at least one new reward token at index 1 when publishing data-point + val poolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // TODO replace with actual val otherTokenId = INPUTS(0).tokens(0)._1 @@ -209,19 +213,18 @@ Before going into the transactions in the protocol, we first present the contrac val selfPubKey = SELF.R4[GroupElement].get val outIndex = getVar[Int](0).get val output = OUTPUTS(outIndex) - - val isSimpleCopy = output.tokens(0) == SELF.tokens(0) && // oracle token is preserved - output.tokens(1)._1 == SELF.tokens(1)._1 && // reward tokenId is preserved - output.tokens.size == 2 && // no more tokens - output.propositionBytes == SELF.propositionBytes && // script preserved - output.R4[GroupElement].isDefined && // output must have a public key (not necessarily the same) - output.value >= minStorageRent // ensure sufficient Ergs to ensure no garbage collection + + val isSimpleCopy = output.tokens(0) == SELF.tokens(0) && // oracle token is preserved + output.propositionBytes == SELF.propositionBytes && // script preserved + output.R4[GroupElement].isDefined && // output must have a public key (not necessarily the same) + output.value >= minStorageRent // ensure sufficient Ergs to ensure no garbage collection - val collection = otherTokenId == poolNFT && // first input must be pool box - output.tokens(1)._2 > SELF.tokens(1)._2 && // at least one reward token must be added - output.R4[GroupElement].get == selfPubKey && // for collection preserve public key - output.value >= SELF.value && // nanoErgs value preserved - ! (output.R5[Any].isDefined) // no more registers, preserving only R4, the group element + val collection = otherTokenId == poolNFT && // first input must be pool box + output.tokens(1)._1 == SELF.tokens(1)._1 && // reward tokenId is preserved (oracle should ensure this contains a reward token) + output.tokens(1)._2 > SELF.tokens(1)._2 && // at least one reward token must be added + output.R4[GroupElement].get == selfPubKey && // for collection preserve public key + output.value >= SELF.value && // nanoErgs value preserved + ! (output.R5[Any].isDefined) // no more registers; prevents box from being reused as a valid data-point val owner = proveDlog(selfPubKey) @@ -232,42 +235,42 @@ Before going into the transactions in the protocol, we first present the contrac ### Ballot Contract -```scala +``` { // This box (ballot box): - // R4 the group element of the owner of the ballot token [GroupElement] - // R5 the creation height of the update box [Int] - // R6 the value voted for [Coll[Byte]] - - val updateNFT = fromBase64("YlFlVGhXbVpxNHQ3dyF6JUMqRi1KQE5jUmZValhuMnI=") // TODO replace with actual - - val minStorageRent = 10000000L // TODO replace with actual - - val selfPubKey = SELF.R4[GroupElement].get - val otherTokenId = INPUTS(1).tokens(0)._1 - - val outIndex = getVar[Int](0).get - val output = OUTPUTS(outIndex) - - val isSimpleCopy = output.R4[GroupElement].isDefined && // ballot boxes are transferable by setting different value here - output.propositionBytes == SELF.propositionBytes && - output.tokens == SELF.tokens && - output.value >= minStorageRent + // R4 the group element of the owner of the ballot token [GroupElement] + // R5 the creation height of the update box [Int] + // R6 the value voted for [Coll[Byte]] - val update = otherTokenId == updateNFT && - output.R4[GroupElement].get == selfPubKey && - output.value >= SELF.value && - ! (output.R5[Any].isDefined) + val updateNFT = fromBase64("YlFlVGhXbVpxNHQ3dyF6JUMqRi1KQE5jUmZValhuMnI=") // TODO replace with actual - val owner = proveDlog(selfPubKey) - - isSimpleCopy && (owner || update) + val minStorageRent = 10000000L // TODO replace with actual + + val selfPubKey = SELF.R4[GroupElement].get + val otherTokenId = INPUTS(1).tokens(0)._1 + + val outIndex = getVar[Int](0).get + val output = OUTPUTS(outIndex) + + val isSimpleCopy = output.R4[GroupElement].isDefined && // ballot boxes are transferable by setting different value here + output.propositionBytes == SELF.propositionBytes && + output.tokens == SELF.tokens && + output.value >= minStorageRent + + val update = otherTokenId == updateNFT && // can only update when update box is the 2nd input + output.R4[GroupElement].get == selfPubKey && // public key is preserved + output.value >= SELF.value && // value preserved or increased + ! (output.R5[Any].isDefined) // no more registers; prevents box from being reused as a valid vote + + val owner = proveDlog(selfPubKey) + + // unlike in collection, here we don't require spender to be one of the ballot token holders + isSimpleCopy && (owner || update) } - ``` ### Update Contract -```scala +``` { // This box (update box): // Registers empty // @@ -292,11 +295,11 @@ Before going into the transactions in the protocol, we first present the contrac val validPoolIn = poolIn.tokens(0)._1 == poolNFT - val validPoolOut = poolIn.propositionBytes != poolOut.propositionBytes && // script should not be preserved - poolIn.tokens == poolOut.tokens && // tokens preserved - poolIn.value == poolOut.value && // value preserved - poolIn.R4[Long] == poolOut.R4[Long] && // rate preserved - poolIn.R5[Int] == poolOut.R5[Int] && // counter preserved + val validPoolOut = poolIn.propositionBytes != poolOut.propositionBytes && // script should not be preserved + poolIn.tokens == poolOut.tokens && // tokens preserved + poolIn.value == poolOut.value && // value preserved + poolIn.R4[Long] == poolOut.R4[Long] && // rate preserved + poolIn.R5[Int] == poolOut.R5[Int] && // counter preserved ! (poolOut.R6[Any].isDefined) @@ -309,7 +312,7 @@ Before going into the transactions in the protocol, we first present the contrac def isValidBallot(b:Box) = if (b.tokens.size > 0) { b.tokens(0)._1 == ballotTokenId && b.R5[Int].get == SELF.creationInfo._1 && // ensure vote corresponds to this box by checking creation height - b.R6[Coll[Byte]].get == poolOutHash // check value voted for + b.R6[Coll[Byte]].get == poolOutHash // check value voted for } else false val ballotBoxes = INPUTS.filter(isValidBallot) @@ -374,16 +377,15 @@ We consider such a pool box to be *stale* that needs to be refreshed. - The creation height (stored in R3) must be at most 4 less than the current height. - The rate (stored in R4) must be the average of the rates in all the oracle boxes. - The epoch counter (stored in R5) must be incremented by 1. + - Registers R6 and onward are empty. - The second output must be a new refresh box as follows: - Registers R0 (value), R1 (script), and the first token (refresh NFT) are preserved from the old refresh box. - The quantity of the second token (reward) must be decremented by at most twice the number of valid rate boxes. - - Register R4 and onward are empty. 3. Each input oracle box has following logic ([smart contract](#oracle-contract)): - The first input is a pool box (i.e., has the pool token). - An output oracle box (acting as a copy of this box) must be created as follows: - The following values are directly copied: R0 (nanoErgs), R1 (script), the first token (oracle token) (stored in R2) and R4 (GroupElement). - The second token (reward token) is incremented by at least 1. - - Registers R5 and onward are empty. Suppose there are *n* valid oracle boxes, then there are 2*n* reward tokens released. It is expected that whoever creates the refresh transaction (the *collector*) takes *n*+1 reward tokens, and the other oracles get 1 token each. @@ -483,17 +485,17 @@ We store the update box's creation height in the ballot's R5 to ensure that a vo #### Update Rules -In order to keep the [pool contract](#pool-contract) as compact as possible, we can see that almost all parameters are hard-wired in the [refresh contract](#refresh-contract). Consequently, even small updates in the pool require -almost all other boxes and tokens to be updated. In particular, the following rules apply: +In order to keep the [pool contract](#pool-contract) as compact as possible, many parameters are hard-wired in the [refresh contract](#refresh-contract). +Depending on what is to be updated, we may need to update other items as well. In particular, the following rules apply: | When updating | This must be updated | This need not be updated | |---|---|---| -|Refresh-NFT | Refresh box
Reward tokens
Oracle tokens
Oracle boxes | Update-NFT
Update box
Ballot tokens
Ballot boxes | -|Update-NFT | Update box
Ballot tokens
Ballot boxes | Refresh-NFT
Refresh box
Oracle tokens
Oracle boxes| -|Pool params (like epoch length) | Refresh-NFT
Same as in refresh-NFT| Same as in refresh-NFT | -|Oracle tokens | Refresh-NFT
Same as in refresh-NFT| Same as in refresh-NFT | -|Reward tokens | Refresh-NFT
Same as in refresh-NFT| Same as in refresh-NFT | -|Ballot tokens | Update-NFT
Same as in update-NFT| Same as in update-NFT | +|Pool params (like epoch length) | Refresh-NFT
Refresh box
Reward tokens | Update-NFT
Update box
Ballot tokens
Ballot boxes
Oracle tokens
Oracle boxes (with new reward token) | +|Refresh-NFT | Refresh box
Reward tokens | Update-NFT
Update box
Ballot tokens
Ballot boxes
Oracle tokens
Oracle boxes (with new reward token) | +|Update-NFT | Update box
Ballot tokens
Ballot boxes | Refresh-NFT
Refresh box
Reward tokens
Oracle tokens
Oracle boxes | +|Oracle tokens | Refresh-NFT
Refresh box
Reward tokens
Oracle boxes
| Update-NFT
Update box
Ballot tokens
Ballot boxes | +|Reward tokens | Refresh-NFT
Refresh box | Update-NFT
Update box
Ballot tokens
Ballot boxes
Oracle tokens
Oracle boxes | +|Ballot tokens | Update-NFT
Update box
Ballot boxes | Refresh-NFT
Refresh box
Reward tokens
Oracle tokens
Oracle boxes | After an update, the older versions of the boxes will be left for garbage collection via storage-rent, and their tokens will become free for miners to take. Hence, it is beneficial to update all the related tokens anyway. From 1a49f9e138959f70769b13c4ef83bf982b58d59b Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Mon, 25 Oct 2021 23:25:14 +0530 Subject: [PATCH 19/43] Ensure creation height is preserved in update --- eip-0023.md | 1 + 1 file changed, 1 insertion(+) diff --git a/eip-0023.md b/eip-0023.md index f4c1321e..b2768602 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -297,6 +297,7 @@ Before going into the transactions in the protocol, we first present the contrac val validPoolOut = poolIn.propositionBytes != poolOut.propositionBytes && // script should not be preserved poolIn.tokens == poolOut.tokens && // tokens preserved + poolIn.creationInfo._1 == poolOut.creationInfo._1 && // creation height preserved poolIn.value == poolOut.value && // value preserved poolIn.R4[Long] == poolOut.R4[Long] && // rate preserved poolIn.R5[Int] == poolOut.R5[Int] && // counter preserved From d12e62af80e1b771900715ff976489bc2e94dda6 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Thu, 28 Oct 2021 20:15:27 +0530 Subject: [PATCH 20/43] Add prerequisites for running own pool --- eip-0023.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/eip-0023.md b/eip-0023.md index b2768602..e16ef7f6 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -31,6 +31,25 @@ Below is a summary of the main new features in v2.0 and how it differs from v1.0 - **Similar update mechanism**: We will have the similar update mechanism as in v1.0 (threshold number of ballot token holders must vote for an update). - **Transferable ballot tokens**: Similar to oracle tokens, the ballot tokens are free to be transferred between public keys. +### Prerequisites + +The design below has some default parameters proposed for the ERG/USD oracle pool. + +If you want to run your own pool, for example, to serve a different pair, then the following parameters need to be decided. + +|Parameter|Symbol in code|Default value|Meaning| +|---|---|---|---| +|Epoch period|**epochLength**|30|The number of blocks for which a rate is locked in the pool box.
Each blocks is 2 mins on average
For a fast changing rate, use shorter value (such as 5)| +|Buffer| **buffer** | 4 | The max error allowed in the declared start height of new a epoch
Ideally, this should be the height at which tx gets mined
However, due to congestion, we allow this error margin| +|Total oracles| | 15 | The total number of oracles allowed to post data points
An oracle can post at most one data point at any time | +|Minimum data points| **minDataPoints** | 4 | The minimum number of fresh data points needed to refresh pool box
A data point is fresh if it has been posted in the last **e** blocks| +|Maximum deviation percent| **maxDeviationPercent** | 5 | Maximum allowed difference between the first and last data points
in terms of the first data point
Data points are sorted in increasing order| +|Total ballots| | 15 | The total number of people allowed to vote for an update | +|Minimum votes| **minVotes** | 6 | The minimum votes needed to update pool params (any of the above)| + +Once the parameters are decided, generate the [tokens](#tokens) in the correct quantity and update the values in the [contracts](#contracts). +Next, find the right number of trusted oracles and ballot holders and distribute the tokens to them. + ### Reward Mechanism In v1.0, the pool was responsible for rewarding each oracle for posting a data-point. In v2.0, the pool simply certifies that a data-point was posted, and a separate reward mechanism is proposed. This keeps the contract smaller and more flexible. From ac70adb1266da28f4638c205340b102d3a259de7 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Thu, 28 Oct 2021 20:18:00 +0530 Subject: [PATCH 21/43] Fix type in epochLength symbol usage --- eip-0023.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eip-0023.md b/eip-0023.md index e16ef7f6..86e13856 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -42,7 +42,7 @@ If you want to run your own pool, for example, to serve a different pair, then t |Epoch period|**epochLength**|30|The number of blocks for which a rate is locked in the pool box.
Each blocks is 2 mins on average
For a fast changing rate, use shorter value (such as 5)| |Buffer| **buffer** | 4 | The max error allowed in the declared start height of new a epoch
Ideally, this should be the height at which tx gets mined
However, due to congestion, we allow this error margin| |Total oracles| | 15 | The total number of oracles allowed to post data points
An oracle can post at most one data point at any time | -|Minimum data points| **minDataPoints** | 4 | The minimum number of fresh data points needed to refresh pool box
A data point is fresh if it has been posted in the last **e** blocks| +|Minimum data points| **minDataPoints** | 4 | The minimum number of fresh data points needed to refresh pool box
A data point is fresh if it has been posted in the last **epochLength** blocks| |Maximum deviation percent| **maxDeviationPercent** | 5 | Maximum allowed difference between the first and last data points
in terms of the first data point
Data points are sorted in increasing order| |Total ballots| | 15 | The total number of people allowed to vote for an update | |Minimum votes| **minVotes** | 6 | The minimum votes needed to update pool params (any of the above)| From bd7a6098db78d67ac2ca9ada4c09bfe709d5ac03 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Wed, 20 Apr 2022 02:01:44 +0530 Subject: [PATCH 22/43] Fix typo in eip-0023.md --- eip-0023.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eip-0023.md b/eip-0023.md index 86e13856..bfb4f3dd 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -39,7 +39,7 @@ If you want to run your own pool, for example, to serve a different pair, then t |Parameter|Symbol in code|Default value|Meaning| |---|---|---|---| -|Epoch period|**epochLength**|30|The number of blocks for which a rate is locked in the pool box.
Each blocks is 2 mins on average
For a fast changing rate, use shorter value (such as 5)| +|Epoch period|**epochLength**|30|The number of blocks for which a rate is locked in the pool box.
Each block is 2 mins on average
For a fast changing rate, use shorter value (such as 5)| |Buffer| **buffer** | 4 | The max error allowed in the declared start height of new a epoch
Ideally, this should be the height at which tx gets mined
However, due to congestion, we allow this error margin| |Total oracles| | 15 | The total number of oracles allowed to post data points
An oracle can post at most one data point at any time | |Minimum data points| **minDataPoints** | 4 | The minimum number of fresh data points needed to refresh pool box
A data point is fresh if it has been posted in the last **epochLength** blocks| From 196e89a8f98bc1611473f059a9e58b81ca7d18d2 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Fri, 17 Jun 2022 00:03:47 +0530 Subject: [PATCH 23/43] Store reward tokens in oracle pool box --- eip-0023.md | 104 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 64 insertions(+), 40 deletions(-) diff --git a/eip-0023.md b/eip-0023.md index bfb4f3dd..943f5d74 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -17,9 +17,10 @@ Below is a summary of the main new features in v2.0 and how it differs from v1.0 - **Single pool address**: This version of the pool will have only one address, the *pool address*. - **Epoch counter**: Pool box will additionally store a counter that is incremented on each collection. This will allow more sophisticated dApps. - **Compact pool box**: Pool box is separated from the logic of pool management, which is captured instead in a **refresh** box. This makes the pool box very small for use in other dApps. -- **Refresh box**: The refresh box is used for collecting data-points, refreshing the pool box and emitting oracle rewards. -- **Reward in tokens**: The posting reward will be in the form of tokens instead of Ergs. These tokens can be redeemed separately, which is not part of the protocol. -- **No separate funding box**: The pool box emits only reward tokens and won't be handing out Ergs. Thus, there won't be a separate funding process required. +- **Refresh box**: The refresh box is used for collecting data-points. +- **Reward in tokens**: The posting reward will be in the form of tokens instead of Ergs. These tokens can be redeemed separately, which is not part of the protocol. + The pool box emits such reward tokens. +- **No separate funding process**: The pool box emits only reward tokens and won't be handing out Ergs. Thus, there won't be a separate funding process required. - **Reward accumulated**: We will not be creating a new box for rewarding each posting to prevent dust. Instead, the rewards will be accumulated directly in the oracle boxes. - **Oracle boxes spent in collection**: Because the rewards must be accumulated, the oracle boxes will be considered as inputs rather than data-inputs when collecting individual rates for averaging. These inputs will be spent, and a copy of the box with the reward will be created. @@ -54,20 +55,28 @@ Next, find the right number of trusted oracles and ballot holders and distribute In v1.0, the pool was responsible for rewarding each oracle for posting a data-point. In v2.0, the pool simply certifies that a data-point was posted, and a separate reward mechanism is proposed. This keeps the contract smaller and more flexible. -The certificates are in the form of tokens emitted by the pool box. Once there are sufficient number of such tokens, a oracle -can exchange or burn them in return for a reward. We also give a sample token exchange contract. +The certificates are in the form of tokens emitted by the pool box. Thus, the pool box also contains the reward tokens to be emitted. + +Once there are sufficient number of such tokens, an oracle can exchange or burn them in return for a reward. We also give a sample token exchange contract. ### Refresh Mechanism In v1.0, the pool was refreshed by moving from the epoch-preparation phase to the live epoch phase. In v2.0, although there is a single pool box, the logic of pool refresh is stored in a separate box, the **refresh box**. -The refresh box also contains the reward tokens to be emitted. The refresh box is identified by a separate token, the *refresh-NFT*, which is referenced in the pool and oracle contracts. +The refresh box is identified by a separate token, the *refresh-NFT*, which is referenced in the pool and oracle contracts. ### Update Mechanism The update mechanism is similar to that in v1.0. A threshold number (currently 6) of ballots must be cast for a proposed update. Such ballots are stored in a **ballot box** and identified by a **ballot token**. The ballot token holders and oracle token holders need not be the same people. The logic for updating the pool is stored in an **update box**, which identified by the **update-NFT**. This NFT is referenced in the pool and ballot boxes. +The ballot box allows the owner to vote for 3 things: + +1. The script of the new pool box. +2. The reward token id to be stored in the new pool box. +3. The reward token amounts to be stored in the new pool box. + +Apart from this, the new pool box *must* preserve the remaining values from the old pool box. **Note** The update box logic acts on the pool box and not the refresh box. @@ -82,7 +91,7 @@ The system has the following types of tokens. Note that we use the term **NFT** |Update-NFT | 1 | Identify update box | Update box | |Oracle tokens | 15 | Identify each oracle box | Oracle boxes | |Ballot tokens | 15 | Identify each ballot box | Ballot boxes | -|Reward tokens | 100 million | Reward oracles | Refresh box
Oracle boxes | +|Reward tokens | 100 million | Reward oracles | Pool box
Oracle boxes | ## Boxes @@ -90,8 +99,8 @@ There are a total of 5 contracts, each corresponding to a box type | Box | Quantity | Tokens | Additional registers used | Purpose | Spending transactions | |---|---|---|---|---|---| -|Pool | 1 | Pool-NFT| R4: Rate (Long)
R5: Epoch counter (Int) | Publish pool rate for dApps | Refresh pool
Update pool | -|Refresh| 1 | Refresh-NFT
Reward tokens| | Refresh pool box
Emit reward tokens| Refresh pool | +|Pool | 1 | Pool-NFT
Reward tokens| R4: Rate (Long)
R5: Epoch counter (Int) | Publish pool rate for dApps
Emit reward tokens| Refresh pool
Update pool | +|Refresh| 1 | Refresh-NFT | | Refresh pool box | Refresh pool | |Oracle | 15 | Oracle token
Reward tokens | R4: Public key (GroupElement)
R5: Epoch counter of pool box (Int)
R6: Published rate (Long) | Publish data-point
Accumulate reward tokens | Publish data-point,
Refresh pool,
Transfer oracle token,
Extract reward tokens| |Update | 1 | Update-NFT | | Updating pool box | Update pool box | |Ballot | 15 | Ballot token | R4: Public key (GroupElement)
R5: Update box creation height (Int)
R6: New pool box hash (Coll[Byte]) | Voting for updating pool box | Vote for update
Update pool box
Transfer ballot token | @@ -108,7 +117,10 @@ Before going into the transactions in the protocol, we first present the contrac // epoch start height is stored in creation Height (R3) // R4 Current data point (Long) // R5 Current epoch counter (Int) + // // tokens(0) pool token (NFT) + // tokens(1) reward tokens + // When initializing the box, there must be one reward token. When claiming reward, one token must be left unclaimed val otherTokenId = INPUTS(1).tokens(0)._1 val refreshNFT = fromBase64("VGpXblpyNHU3eCFBJUQqRy1LYU5kUmdVa1hwMnM1djg=") // TODO replace with actual @@ -121,9 +133,8 @@ Before going into the transactions in the protocol, we first present the contrac ``` { // This box (refresh box) - // tokens(0) reward tokens to be emitted (several) // - // When initializing the box, there must be one reward token. When claiming reward, one token must be left unclaimed + // tokens(0) refresh token (NFT) val oracleTokenId = fromBase64("KkctSmFOZFJnVWtYcDJzNXY4eS9CP0UoSCtNYlBlU2g=") // TODO replace with actual val poolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // TODO replace with actual @@ -178,22 +189,23 @@ Before going into the transactions in the protocol, we first present the contrac val maxDelta = lastData * maxDeviationPercent / 100 val firstData = dataPoints(0).R6[Long].get - proveDlog(pubKey) && - epochOver && - enoughDataPoints && - isSorted && - lastData - firstData <= maxDelta && - poolIn.tokens(0)._1 == poolNFT && - poolOut.tokens == poolIn.tokens && // preserve pool tokens - poolOut.R4[Long].get == average && // rate - poolOut.R5[Int].get == poolIn.R5[Int].get + 1 && // counter - poolOut.propositionBytes == poolIn.propositionBytes && // preserve pool script - poolOut.value >= poolIn.value && - poolOut.creationInfo._1 >= HEIGHT - buffer && // ensure that new box has correct start epoch height - selfOut.tokens(0) == SELF.tokens(0) && // refresh NFT preserved - selfOut.tokens(1)._1 == SELF.tokens(1)._1 && // reward token id preserved - selfOut.tokens(1)._2 >= SELF.tokens(1)._2 - rewardEmitted && // reward token amount correctly reduced - selfOut.propositionBytes == SELF.propositionBytes && // script preserved + proveDlog(pubKey) && + epochOver && + enoughDataPoints && + isSorted && + lastData - firstData <= maxDelta && + poolIn.tokens(0)._1 == poolNFT && + poolOut.tokens(0) == poolIn.tokens(0) && // preserve pool NFT + poolOut.tokens(1)._1 == poolIn.tokens(1)._1 && // reward token id preserved + poolOut.tokens(1)._2 >= poolIn.tokens(1)._2 - rewardEmitted && // reward token amount correctly reduced + poolOut.tokens.size == poolIn.tokens.size && // cannot inject more tokens to pool box + poolOut.R4[Long].get == average && // rate + poolOut.R5[Int].get == poolIn.R5[Int].get + 1 && // counter + poolOut.propositionBytes == poolIn.propositionBytes && // preserve pool script + poolOut.value >= poolIn.value && + poolOut.creationInfo._1 >= HEIGHT - buffer && // ensure that new box has correct start epoch height + selfOut.tokens == SELF.tokens && // refresh NFT preserved + selfOut.propositionBytes == SELF.propositionBytes && // script preserved selfOut.value >= SELF.value } ``` @@ -259,6 +271,8 @@ Before going into the transactions in the protocol, we first present the contrac // R4 the group element of the owner of the ballot token [GroupElement] // R5 the creation height of the update box [Int] // R6 the value voted for [Coll[Byte]] + // R7 the reward token id [Coll[Byte]] + // R8 the reward token amount [Long] val updateNFT = fromBase64("YlFlVGhXbVpxNHQ3dyF6JUMqRi1KQE5jUmZValhuMnI=") // TODO replace with actual @@ -297,6 +311,8 @@ Before going into the transactions in the protocol, we first present the contrac // R4 the pub key of voter [GroupElement] (not used here) // R5 the creation height of this box [Int] // R6 the value voted for [Coll[Byte]] (hash of the new pool box script) + // R7 the reward token id in new box + // R8 the number of reward tokens in new box val poolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // TODO replace with actual @@ -311,11 +327,13 @@ Before going into the transactions in the protocol, we first present the contrac // compute the hash of the pool output box. This should be the value voted for val poolOutHash = blake2b256(poolOut.propositionBytes) + val rewardTokenId = poolOut.tokens(1)._1 + val rewardAmt = poolOut.tokens(1)._2 val validPoolIn = poolIn.tokens(0)._1 == poolNFT val validPoolOut = poolIn.propositionBytes != poolOut.propositionBytes && // script should not be preserved - poolIn.tokens == poolOut.tokens && // tokens preserved + poolIn.tokens(0) == poolOut.tokens(0) && // NFT preserved poolIn.creationInfo._1 == poolOut.creationInfo._1 && // creation height preserved poolIn.value == poolOut.value && // value preserved poolIn.R4[Long] == poolOut.R4[Long] && // rate preserved @@ -332,7 +350,9 @@ Before going into the transactions in the protocol, we first present the contrac def isValidBallot(b:Box) = if (b.tokens.size > 0) { b.tokens(0)._1 == ballotTokenId && b.R5[Int].get == SELF.creationInfo._1 && // ensure vote corresponds to this box by checking creation height - b.R6[Coll[Byte]].get == poolOutHash // check value voted for + b.R6[Coll[Byte]].get == poolOutHash && // check proposition voted for + b.R7[Coll[Byte]].get == rewardTokenId && // check rewardTokenId voted for + b.R8[Long].get == rewardAmt // check rewardTokenAmt voted for } else false val ballotBoxes = INPUTS.filter(isValidBallot) @@ -398,9 +418,9 @@ We consider such a pool box to be *stale* that needs to be refreshed. - The rate (stored in R4) must be the average of the rates in all the oracle boxes. - The epoch counter (stored in R5) must be incremented by 1. - Registers R6 and onward are empty. + - The quantity of the second token (reward) must be decremented by at most twice the number of valid rate boxes. - The second output must be a new refresh box as follows: - Registers R0 (value), R1 (script), and the first token (refresh NFT) are preserved from the old refresh box. - - The quantity of the second token (reward) must be decremented by at most twice the number of valid rate boxes. 3. Each input oracle box has following logic ([smart contract](#oracle-contract)): - The first input is a pool box (i.e., has the pool token). - An output oracle box (acting as a copy of this box) must be created as follows: @@ -475,8 +495,10 @@ The input and output are ballot boxes such that the following holds as per the [ - There is a group element in R4. The following (not enforced by the contract) must be done for a proper vote: -- R5 contains the creation height of the current update box. -- R6 contains the hash of the address of the new pool box. +- R5 of type `Int` contains the creation height of the current update box. +- R6 of type `Coll[Byte]` contains the hash of the address of the new pool box. +- R7 of type `Coll[Byte]` contains the reward token id for the new pool box. +- R8 of type `Int` contains the reward token amount for the new pool box. ### Update Pool box @@ -495,11 +517,13 @@ The ballots used in inputs must satisfy the following as per the [smart contract - There must be an output ballot box with the same public key in R4 and rest registers empty. The update box additionally ensures following as per the [smart contract](#update-contract): -- Each ballot box has R4 containing the has of the new pool box script (address). -- Each ballot box has R5 containing this box's creation height. +- Each ballot box has R5 containing the hash of the new pool box script (address). +- Each ballot box has R6 containing this box's creation height. +- Each ballot box has R7 containing the new pool box's reward token id. +- Each ballot box has R8 containing the new pool box's reward token amount. +- The remaining registers and tokens in the old pool box are preserved in the new pool box. - There is a copy of the update box in outputs with everything preserved but with larger creation height. - There are at least a threshold number of votes. -- The registers and token in the pool box are preserved in the new pool box. We store the update box's creation height in the ballot's R5 to ensure that a vote is actually for the current update box. @@ -508,13 +532,13 @@ We store the update box's creation height in the ballot's R5 to ensure that a vo In order to keep the [pool contract](#pool-contract) as compact as possible, many parameters are hard-wired in the [refresh contract](#refresh-contract). Depending on what is to be updated, we may need to update other items as well. In particular, the following rules apply: -| When updating | This must be updated | This need not be updated | +| To update | Also must update | Can preserve | |---|---|---| -|Pool params (like epoch length) | Refresh-NFT
Refresh box
Reward tokens | Update-NFT
Update box
Ballot tokens
Ballot boxes
Oracle tokens
Oracle boxes (with new reward token) | -|Refresh-NFT | Refresh box
Reward tokens | Update-NFT
Update box
Ballot tokens
Ballot boxes
Oracle tokens
Oracle boxes (with new reward token) | +|Pool params
(like epoch length) | Refresh-NFT
Refresh box | Reward tokens
Update-NFT
Update box
Ballot tokens
Ballot boxes
Oracle tokens
Oracle boxes | +|Refresh-NFT | Refresh box | Reward tokens
Update-NFT
Update box
Ballot tokens
Ballot boxes
Oracle tokens
Oracle boxes | |Update-NFT | Update box
Ballot tokens
Ballot boxes | Refresh-NFT
Refresh box
Reward tokens
Oracle tokens
Oracle boxes | -|Oracle tokens | Refresh-NFT
Refresh box
Reward tokens
Oracle boxes
| Update-NFT
Update box
Ballot tokens
Ballot boxes | -|Reward tokens | Refresh-NFT
Refresh box | Update-NFT
Update box
Ballot tokens
Ballot boxes
Oracle tokens
Oracle boxes | +|Oracle tokens | Refresh-NFT
Refresh box
Oracle boxes
| Reward tokens
Update-NFT
Update box
Ballot tokens
Ballot boxes | +|Reward tokens | Pool box| Refresh-NFT
Refresh box
Update-NFT
Update box
Ballot tokens
Ballot boxes
Oracle tokens
Oracle boxes | |Ballot tokens | Update-NFT
Update box
Ballot boxes | Refresh-NFT
Refresh box
Reward tokens
Oracle tokens
Oracle boxes | After an update, the older versions of the boxes will be left for garbage collection via storage-rent, and their tokens will become free for miners to take. From 80de6e97fa3ac35663fe6e53d09f59feaf99da64 Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Wed, 6 Jul 2022 16:20:07 +0530 Subject: [PATCH 24/43] Add table of contents based on headings --- eip-0023.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/eip-0023.md b/eip-0023.md index 943f5d74..8762f9b6 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -1,7 +1,37 @@ # Oracle Pool v2.0 +* Author: @scalahub +* Status: Proposed +* Created: 07-Sep-2021 +* License: CC0 +* Forking: not needed + This is a proposed update to the oracle pool v1.0 currently deployed and documented in [EIP16](https://github.com/ergoplatform/eips/blob/eip16/eip-0016.md). +## Contents +- [Introduction](#introduction) + - [Prerequisites](#prerequisites) + - [Reward Mechanism](#reward-mechanism) + - [Refresh Mechanism](#refresh-mechanism) + - [Update Mechanism](#update-mechanism) +- [Tokens](#tokens) +- [Boxes](#boxes) +- [Contracts](#contracts) + - [Pool Contract](#pool-contract) + - [Refresh Contract](#refresh-contract) + - [Oracle Contract](#oracle-contract) + - [Ballot Contract](#ballot-contract) + - [Update Contract](#update-contract) +- [Trasactions](#transactions) + - [Refresh Pool](#refresh-pool) + - [Publish Data Point](#publish-data-point) + - [Extract Reward Tokens](#extract-reward-tokens) + - [Transfer Oracle Token](#transfer-oracle-token) + - [Vote for Update](#vote-for-update) + - [Update Pool Box](#update-pool-box) + - [Update Rules](#update-rules) + - [Transfer Ballot Token](#transfer-ballot-token) + ## Introduction In order to motivate the changes proposed in this document, consider the drawbacks of Oracle pool v1.0: From a9e08d58c052bd876bd26cea244a3fb919e9b87f Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Mon, 11 Jul 2022 13:53:58 +0530 Subject: [PATCH 25/43] Use ballot contract from testing version v2b --- eip-0023.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/eip-0023.md b/eip-0023.md index 8762f9b6..c95e79a7 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -309,7 +309,6 @@ Before going into the transactions in the protocol, we first present the contrac val minStorageRent = 10000000L // TODO replace with actual val selfPubKey = SELF.R4[GroupElement].get - val otherTokenId = INPUTS(1).tokens(0)._1 val outIndex = getVar[Int](0).get val output = OUTPUTS(outIndex) @@ -319,7 +318,9 @@ Before going into the transactions in the protocol, we first present the contrac output.tokens == SELF.tokens && output.value >= minStorageRent - val update = otherTokenId == updateNFT && // can only update when update box is the 2nd input + val update = INPUTS.size > 1 && + INPUTS(1).tokens.size > 0 && + INPUTS(1).tokens(0)._1 == updateNFT && // can only update when update box is the 2nd input output.R4[GroupElement].get == selfPubKey && // public key is preserved output.value >= SELF.value && // value preserved or increased ! (output.R5[Any].isDefined) // no more registers; prevents box from being reused as a valid vote @@ -328,8 +329,7 @@ Before going into the transactions in the protocol, we first present the contrac // unlike in collection, here we don't require spender to be one of the ballot token holders isSimpleCopy && (owner || update) -} -``` +}``` ### Update Contract From 5d3c17883e2f8b3e94c4f99b3cfde216effee494 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Thu, 28 Jul 2022 10:20:51 +0300 Subject: [PATCH 26/43] fix markdown rendering --- eip-0023.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eip-0023.md b/eip-0023.md index c95e79a7..662e1628 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -329,7 +329,7 @@ Before going into the transactions in the protocol, we first present the contrac // unlike in collection, here we don't require spender to be one of the ballot token holders isSimpleCopy && (owner || update) -}``` +} ### Update Contract From 23e2d401b4d563639660c96f264fe96287db1a36 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Thu, 28 Jul 2022 10:26:03 +0300 Subject: [PATCH 27/43] properly fix md rendering; --- eip-0023.md | 1 + 1 file changed, 1 insertion(+) diff --git a/eip-0023.md b/eip-0023.md index 662e1628..f23932e3 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -330,6 +330,7 @@ Before going into the transactions in the protocol, we first present the contrac // unlike in collection, here we don't require spender to be one of the ballot token holders isSimpleCopy && (owner || update) } +``` ### Update Contract From abf27ef6ac266cde25f3e08563a711f32b489e6e Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Sat, 30 Jul 2022 04:06:48 +0530 Subject: [PATCH 28/43] Allow preserving pool script during update --- eip-0023.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/eip-0023.md b/eip-0023.md index f23932e3..7eca8746 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -363,8 +363,7 @@ Before going into the transactions in the protocol, we first present the contrac val validPoolIn = poolIn.tokens(0)._1 == poolNFT - val validPoolOut = poolIn.propositionBytes != poolOut.propositionBytes && // script should not be preserved - poolIn.tokens(0) == poolOut.tokens(0) && // NFT preserved + val validPoolOut = poolIn.tokens(0) == poolOut.tokens(0) && // NFT preserved poolIn.creationInfo._1 == poolOut.creationInfo._1 && // creation height preserved poolIn.value == poolOut.value && // value preserved poolIn.R4[Long] == poolOut.R4[Long] && // rate preserved From ffb7a76ee282b79f138a6268e7f00c0b86884f15 Mon Sep 17 00:00:00 2001 From: ScalaHub <23208922+scalahub@users.noreply.github.com> Date: Sat, 30 Jul 2022 04:09:58 +0530 Subject: [PATCH 29/43] Update author list for EIP23 (add greenhat) --- eip-0023.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eip-0023.md b/eip-0023.md index 7eca8746..ed08b142 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -1,6 +1,6 @@ # Oracle Pool v2.0 -* Author: @scalahub +* Author: @scalahub, @greenhat * Status: Proposed * Created: 07-Sep-2021 * License: CC0 From 01782c7c9ef95770bfcac3554aeea3783e406830 Mon Sep 17 00:00:00 2001 From: ScalaHub <23208922+scalahub@users.noreply.github.com> Date: Thu, 4 Aug 2022 18:35:54 +0530 Subject: [PATCH 30/43] Update eip-0023.md Co-authored-by: Denys Zadorozhnyi --- eip-0023.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eip-0023.md b/eip-0023.md index ed08b142..108e7cfc 100644 --- a/eip-0023.md +++ b/eip-0023.md @@ -1,6 +1,6 @@ # Oracle Pool v2.0 -* Author: @scalahub, @greenhat +* Author: @scalahub, @greenhat, @kettlebell, @SethDusek * Status: Proposed * Created: 07-Sep-2021 * License: CC0 From dabfb0a99c23ac6d11bce24608f610066ca33e30 Mon Sep 17 00:00:00 2001 From: Tim Ling <791016+kettlebell@users.noreply.github.com> Date: Wed, 31 Aug 2022 11:07:22 +1000 Subject: [PATCH 31/43] Split out EIP-23 contracts into separate files --- eip-0023/contracts/ballot_contract.es | 33 ++++ eip-0023/contracts/oracle_contract.es | 49 +++++ eip-0023/contracts/pool_contract.es | 16 ++ eip-0023/contracts/refresh_contract.es | 76 ++++++++ eip-0023/contracts/update_contract.es | 56 ++++++ eip-0023.md => eip-0023/eip-0023.md | 259 +------------------------ 6 files changed, 236 insertions(+), 253 deletions(-) create mode 100644 eip-0023/contracts/ballot_contract.es create mode 100644 eip-0023/contracts/oracle_contract.es create mode 100644 eip-0023/contracts/pool_contract.es create mode 100644 eip-0023/contracts/refresh_contract.es create mode 100644 eip-0023/contracts/update_contract.es rename eip-0023.md => eip-0023/eip-0023.md (62%) diff --git a/eip-0023/contracts/ballot_contract.es b/eip-0023/contracts/ballot_contract.es new file mode 100644 index 00000000..d9685e0d --- /dev/null +++ b/eip-0023/contracts/ballot_contract.es @@ -0,0 +1,33 @@ +{ // This box (ballot box): + // R4 the group element of the owner of the ballot token [GroupElement] + // R5 the creation height of the update box [Int] + // R6 the value voted for [Coll[Byte]] + // R7 the reward token id [Coll[Byte]] + // R8 the reward token amount [Long] + + val updateNFT = fromBase64("YlFlVGhXbVpxNHQ3dyF6JUMqRi1KQE5jUmZValhuMnI=") // TODO replace with actual + + val minStorageRent = 10000000L // TODO replace with actual + + val selfPubKey = SELF.R4[GroupElement].get + + val outIndex = getVar[Int](0).get + val output = OUTPUTS(outIndex) + + val isSimpleCopy = output.R4[GroupElement].isDefined && // ballot boxes are transferable by setting different value here + output.propositionBytes == SELF.propositionBytes && + output.tokens == SELF.tokens && + output.value >= minStorageRent + + val update = INPUTS.size > 1 && + INPUTS(1).tokens.size > 0 && + INPUTS(1).tokens(0)._1 == updateNFT && // can only update when update box is the 2nd input + output.R4[GroupElement].get == selfPubKey && // public key is preserved + output.value >= SELF.value && // value preserved or increased + ! (output.R5[Any].isDefined) // no more registers; prevents box from being reused as a valid vote + + val owner = proveDlog(selfPubKey) + + // unlike in collection, here we don't require spender to be one of the ballot token holders + isSimpleCopy && (owner || update) +} diff --git a/eip-0023/contracts/oracle_contract.es b/eip-0023/contracts/oracle_contract.es new file mode 100644 index 00000000..4606531e --- /dev/null +++ b/eip-0023/contracts/oracle_contract.es @@ -0,0 +1,49 @@ +{ // This box (oracle box) + // R4 public key (GroupElement) + // R5 epoch counter of current epoch (Int) + // R6 data point (Long) or empty + + // tokens(0) oracle token (one) + // tokens(1) reward tokens collected (one or more) + // + // When publishing a datapoint, there must be at least one reward token at index 1 + // + // We will connect this box to pool NFT in input #0 (and not the refresh NFT in input #1) + // This way, we can continue to use the same box after updating pool + // This *could* allow the oracle box to be spent during an update + // However, this is not an issue because the update contract ensures that tokens and registers (except script) of the pool box are preserved + + // Private key holder can do following things: + // 1. Change group element (public key) stored in R4 + // 2. Store any value of type in or delete any value from R4 to R9 + // 3. Store any token or none at 2nd index + + // In order to connect this oracle box to a different refreshNFT after an update, + // the oracle should keep at least one new reward token at index 1 when publishing data-point + + val poolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // TODO replace with actual + + val otherTokenId = INPUTS(0).tokens(0)._1 + + val minStorageRent = 10000000L + val selfPubKey = SELF.R4[GroupElement].get + val outIndex = getVar[Int](0).get + val output = OUTPUTS(outIndex) + + val isSimpleCopy = output.tokens(0) == SELF.tokens(0) && // oracle token is preserved + output.propositionBytes == SELF.propositionBytes && // script preserved + output.R4[GroupElement].isDefined && // output must have a public key (not necessarily the same) + output.value >= minStorageRent // ensure sufficient Ergs to ensure no garbage collection + + val collection = otherTokenId == poolNFT && // first input must be pool box + output.tokens(1)._1 == SELF.tokens(1)._1 && // reward tokenId is preserved (oracle should ensure this contains a reward token) + output.tokens(1)._2 > SELF.tokens(1)._2 && // at least one reward token must be added + output.R4[GroupElement].get == selfPubKey && // for collection preserve public key + output.value >= SELF.value && // nanoErgs value preserved + ! (output.R5[Any].isDefined) // no more registers; prevents box from being reused as a valid data-point + + val owner = proveDlog(selfPubKey) + + // owner can choose to transfer to another public key by setting different value in R4 + isSimpleCopy && (owner || collection) +} diff --git a/eip-0023/contracts/pool_contract.es b/eip-0023/contracts/pool_contract.es new file mode 100644 index 00000000..01f243c8 --- /dev/null +++ b/eip-0023/contracts/pool_contract.es @@ -0,0 +1,16 @@ +{ + // This box (pool box) + // epoch start height is stored in creation Height (R3) + // R4 Current data point (Long) + // R5 Current epoch counter (Int) + // + // tokens(0) pool token (NFT) + // tokens(1) reward tokens + // When initializing the box, there must be one reward token. When claiming reward, one token must be left unclaimed + + val otherTokenId = INPUTS(1).tokens(0)._1 + val refreshNFT = fromBase64("VGpXblpyNHU3eCFBJUQqRy1LYU5kUmdVa1hwMnM1djg=") // TODO replace with actual + val updateNFT = fromBase64("YlFlVGhXbVpxNHQ3dyF6JUMqRi1KQE5jUmZValhuMnI=") // TODO replace with actual + + sigmaProp(otherTokenId == refreshNFT || otherTokenId == updateNFT) +} diff --git a/eip-0023/contracts/refresh_contract.es b/eip-0023/contracts/refresh_contract.es new file mode 100644 index 00000000..cdd49628 --- /dev/null +++ b/eip-0023/contracts/refresh_contract.es @@ -0,0 +1,76 @@ +{ // This box (refresh box) + // + // tokens(0) refresh token (NFT) + + val oracleTokenId = fromBase64("KkctSmFOZFJnVWtYcDJzNXY4eS9CP0UoSCtNYlBlU2g=") // TODO replace with actual + val poolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // TODO replace with actual + val epochLength = 30 // TODO replace with actual + val minDataPoints = 4 // TODO replace with actual + val buffer = 4 // TODO replace with actual + val maxDeviationPercent = 5 // percent // TODO replace with actual + + val minStartHeight = HEIGHT - epochLength + val spenderIndex = getVar[Int](0).get // the index of the data-point box (NOT input!) belonging to spender + + val poolIn = INPUTS(0) + val poolOut = OUTPUTS(0) + val selfOut = OUTPUTS(1) + + def isValidDataPoint(b: Box) = if (b.R6[Long].isDefined) { + b.creationInfo._1 >= minStartHeight && // data point must not be too old + b.tokens(0)._1 == oracleTokenId && // first token id must be of oracle token + b.R5[Int].get == poolIn.R5[Int].get // it must correspond to this epoch + } else false + + val dataPoints = INPUTS.filter(isValidDataPoint) + val pubKey = dataPoints(spenderIndex).R4[GroupElement].get + + val enoughDataPoints = dataPoints.size >= minDataPoints + val rewardEmitted = dataPoints.size * 2 // one extra token for each collected box as reward to collector + val epochOver = poolIn.creationInfo._1 < minStartHeight + + val startData = 1L // we don't allow 0 data points + val startSum = 0L + // we expect data-points to be sorted in INCREASING order + + val lastSortedSum = dataPoints.fold((startData, (true, startSum)), { + (t: (Long, (Boolean, Long)), b: Box) => + val currData = b.R6[Long].get + val prevData = t._1 + val wasSorted = t._2._1 + val oldSum = t._2._2 + val newSum = oldSum + currData // we don't have to worry about overflow, as it causes script to fail + + val isSorted = wasSorted && prevData <= currData + + (currData, (isSorted, newSum)) + } + ) + + val lastData = lastSortedSum._1 + val isSorted = lastSortedSum._2._1 + val sum = lastSortedSum._2._2 + val average = sum / dataPoints.size + + val maxDelta = lastData * maxDeviationPercent / 100 + val firstData = dataPoints(0).R6[Long].get + + proveDlog(pubKey) && + epochOver && + enoughDataPoints && + isSorted && + lastData - firstData <= maxDelta && + poolIn.tokens(0)._1 == poolNFT && + poolOut.tokens(0) == poolIn.tokens(0) && // preserve pool NFT + poolOut.tokens(1)._1 == poolIn.tokens(1)._1 && // reward token id preserved + poolOut.tokens(1)._2 >= poolIn.tokens(1)._2 - rewardEmitted && // reward token amount correctly reduced + poolOut.tokens.size == poolIn.tokens.size && // cannot inject more tokens to pool box + poolOut.R4[Long].get == average && // rate + poolOut.R5[Int].get == poolIn.R5[Int].get + 1 && // counter + poolOut.propositionBytes == poolIn.propositionBytes && // preserve pool script + poolOut.value >= poolIn.value && + poolOut.creationInfo._1 >= HEIGHT - buffer && // ensure that new box has correct start epoch height + selfOut.tokens == SELF.tokens && // refresh NFT preserved + selfOut.propositionBytes == SELF.propositionBytes && // script preserved + selfOut.value >= SELF.value +} diff --git a/eip-0023/contracts/update_contract.es b/eip-0023/contracts/update_contract.es new file mode 100644 index 00000000..5595719f --- /dev/null +++ b/eip-0023/contracts/update_contract.es @@ -0,0 +1,56 @@ +{ // This box (update box): + // Registers empty + // + // ballot boxes (Inputs) + // R4 the pub key of voter [GroupElement] (not used here) + // R5 the creation height of this box [Int] + // R6 the value voted for [Coll[Byte]] (hash of the new pool box script) + // R7 the reward token id in new box + // R8 the number of reward tokens in new box + + val poolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // TODO replace with actual + + val ballotTokenId = fromBase64("P0QoRy1LYVBkU2dWa1lwM3M2djl5JEImRSlIQE1iUWU=") // TODO replace with actual + + val minVotes = 6 // TODO replace with actual + + val poolIn = INPUTS(0) // pool box is 1st input + val poolOut = OUTPUTS(0) // copy of pool box is the 1st output + + val updateBoxOut = OUTPUTS(1) // copy of this box is the 2nd output + + // compute the hash of the pool output box. This should be the value voted for + val poolOutHash = blake2b256(poolOut.propositionBytes) + val rewardTokenId = poolOut.tokens(1)._1 + val rewardAmt = poolOut.tokens(1)._2 + + val validPoolIn = poolIn.tokens(0)._1 == poolNFT + + val validPoolOut = poolIn.tokens(0) == poolOut.tokens(0) && // NFT preserved + poolIn.creationInfo._1 == poolOut.creationInfo._1 && // creation height preserved + poolIn.value == poolOut.value && // value preserved + poolIn.R4[Long] == poolOut.R4[Long] && // rate preserved + poolIn.R5[Int] == poolOut.R5[Int] && // counter preserved + ! (poolOut.R6[Any].isDefined) + + + val validUpdateOut = updateBoxOut.tokens == SELF.tokens && + updateBoxOut.propositionBytes == SELF.propositionBytes && + updateBoxOut.value >= SELF.value && + updateBoxOut.creationInfo._1 > SELF.creationInfo._1 && + ! (updateBoxOut.R4[Any].isDefined) + + def isValidBallot(b:Box) = if (b.tokens.size > 0) { + b.tokens(0)._1 == ballotTokenId && + b.R5[Int].get == SELF.creationInfo._1 && // ensure vote corresponds to this box by checking creation height + b.R6[Coll[Byte]].get == poolOutHash && // check proposition voted for + b.R7[Coll[Byte]].get == rewardTokenId && // check rewardTokenId voted for + b.R8[Long].get == rewardAmt // check rewardTokenAmt voted for + } else false + + val ballotBoxes = INPUTS.filter(isValidBallot) + + val votesCount = ballotBoxes.fold(0L, {(accum: Long, b: Box) => accum + b.tokens(0)._2}) + + sigmaProp(validPoolIn && validPoolOut && validUpdateOut && votesCount >= minVotes) +} diff --git a/eip-0023.md b/eip-0023/eip-0023.md similarity index 62% rename from eip-0023.md rename to eip-0023/eip-0023.md index 108e7cfc..cf0dbc30 100644 --- a/eip-0023.md +++ b/eip-0023/eip-0023.md @@ -139,259 +139,12 @@ Before going into the transactions in the protocol, we first present the contrac ## Contracts -### Pool Contract - -``` -{ - // This box (pool box) - // epoch start height is stored in creation Height (R3) - // R4 Current data point (Long) - // R5 Current epoch counter (Int) - // - // tokens(0) pool token (NFT) - // tokens(1) reward tokens - // When initializing the box, there must be one reward token. When claiming reward, one token must be left unclaimed - - val otherTokenId = INPUTS(1).tokens(0)._1 - val refreshNFT = fromBase64("VGpXblpyNHU3eCFBJUQqRy1LYU5kUmdVa1hwMnM1djg=") // TODO replace with actual - val updateNFT = fromBase64("YlFlVGhXbVpxNHQ3dyF6JUMqRi1KQE5jUmZValhuMnI=") // TODO replace with actual - - sigmaProp(otherTokenId == refreshNFT || otherTokenId == updateNFT) -} -``` -### Refresh Contract - -``` -{ // This box (refresh box) - // - // tokens(0) refresh token (NFT) - - val oracleTokenId = fromBase64("KkctSmFOZFJnVWtYcDJzNXY4eS9CP0UoSCtNYlBlU2g=") // TODO replace with actual - val poolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // TODO replace with actual - val epochLength = 30 // TODO replace with actual - val minDataPoints = 4 // TODO replace with actual - val buffer = 4 // TODO replace with actual - val maxDeviationPercent = 5 // percent // TODO replace with actual - - val minStartHeight = HEIGHT - epochLength - val spenderIndex = getVar[Int](0).get // the index of the data-point box (NOT input!) belonging to spender - - val poolIn = INPUTS(0) - val poolOut = OUTPUTS(0) - val selfOut = OUTPUTS(1) - - def isValidDataPoint(b: Box) = if (b.R6[Long].isDefined) { - b.creationInfo._1 >= minStartHeight && // data point must not be too old - b.tokens(0)._1 == oracleTokenId && // first token id must be of oracle token - b.R5[Int].get == poolIn.R5[Int].get // it must correspond to this epoch - } else false - - val dataPoints = INPUTS.filter(isValidDataPoint) - val pubKey = dataPoints(spenderIndex).R4[GroupElement].get - - val enoughDataPoints = dataPoints.size >= minDataPoints - val rewardEmitted = dataPoints.size * 2 // one extra token for each collected box as reward to collector - val epochOver = poolIn.creationInfo._1 < minStartHeight - - val startData = 1L // we don't allow 0 data points - val startSum = 0L - // we expect data-points to be sorted in INCREASING order - - val lastSortedSum = dataPoints.fold((startData, (true, startSum)), { - (t: (Long, (Boolean, Long)), b: Box) => - val currData = b.R6[Long].get - val prevData = t._1 - val wasSorted = t._2._1 - val oldSum = t._2._2 - val newSum = oldSum + currData // we don't have to worry about overflow, as it causes script to fail - - val isSorted = wasSorted && prevData <= currData - - (currData, (isSorted, newSum)) - } - ) - - val lastData = lastSortedSum._1 - val isSorted = lastSortedSum._2._1 - val sum = lastSortedSum._2._2 - val average = sum / dataPoints.size - - val maxDelta = lastData * maxDeviationPercent / 100 - val firstData = dataPoints(0).R6[Long].get - - proveDlog(pubKey) && - epochOver && - enoughDataPoints && - isSorted && - lastData - firstData <= maxDelta && - poolIn.tokens(0)._1 == poolNFT && - poolOut.tokens(0) == poolIn.tokens(0) && // preserve pool NFT - poolOut.tokens(1)._1 == poolIn.tokens(1)._1 && // reward token id preserved - poolOut.tokens(1)._2 >= poolIn.tokens(1)._2 - rewardEmitted && // reward token amount correctly reduced - poolOut.tokens.size == poolIn.tokens.size && // cannot inject more tokens to pool box - poolOut.R4[Long].get == average && // rate - poolOut.R5[Int].get == poolIn.R5[Int].get + 1 && // counter - poolOut.propositionBytes == poolIn.propositionBytes && // preserve pool script - poolOut.value >= poolIn.value && - poolOut.creationInfo._1 >= HEIGHT - buffer && // ensure that new box has correct start epoch height - selfOut.tokens == SELF.tokens && // refresh NFT preserved - selfOut.propositionBytes == SELF.propositionBytes && // script preserved - selfOut.value >= SELF.value -} -``` - -### Oracle contract - -``` -{ // This box (oracle box) - // R4 public key (GroupElement) - // R5 epoch counter of current epoch (Int) - // R6 data point (Long) or empty - - // tokens(0) oracle token (one) - // tokens(1) reward tokens collected (one or more) - // - // When publishing a datapoint, there must be at least one reward token at index 1 - // - // We will connect this box to pool NFT in input #0 (and not the refresh NFT in input #1) - // This way, we can continue to use the same box after updating pool - // This *could* allow the oracle box to be spent during an update - // However, this is not an issue because the update contract ensures that tokens and registers (except script) of the pool box are preserved - - // Private key holder can do following things: - // 1. Change group element (public key) stored in R4 - // 2. Store any value of type in or delete any value from R4 to R9 - // 3. Store any token or none at 2nd index - - // In order to connect this oracle box to a different refreshNFT after an update, - // the oracle should keep at least one new reward token at index 1 when publishing data-point - - val poolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // TODO replace with actual - - val otherTokenId = INPUTS(0).tokens(0)._1 - - val minStorageRent = 10000000L - val selfPubKey = SELF.R4[GroupElement].get - val outIndex = getVar[Int](0).get - val output = OUTPUTS(outIndex) - - val isSimpleCopy = output.tokens(0) == SELF.tokens(0) && // oracle token is preserved - output.propositionBytes == SELF.propositionBytes && // script preserved - output.R4[GroupElement].isDefined && // output must have a public key (not necessarily the same) - output.value >= minStorageRent // ensure sufficient Ergs to ensure no garbage collection - - val collection = otherTokenId == poolNFT && // first input must be pool box - output.tokens(1)._1 == SELF.tokens(1)._1 && // reward tokenId is preserved (oracle should ensure this contains a reward token) - output.tokens(1)._2 > SELF.tokens(1)._2 && // at least one reward token must be added - output.R4[GroupElement].get == selfPubKey && // for collection preserve public key - output.value >= SELF.value && // nanoErgs value preserved - ! (output.R5[Any].isDefined) // no more registers; prevents box from being reused as a valid data-point - - val owner = proveDlog(selfPubKey) - - // owner can choose to transfer to another public key by setting different value in R4 - isSimpleCopy && (owner || collection) -} -``` - -### Ballot Contract - -``` -{ // This box (ballot box): - // R4 the group element of the owner of the ballot token [GroupElement] - // R5 the creation height of the update box [Int] - // R6 the value voted for [Coll[Byte]] - // R7 the reward token id [Coll[Byte]] - // R8 the reward token amount [Long] - - val updateNFT = fromBase64("YlFlVGhXbVpxNHQ3dyF6JUMqRi1KQE5jUmZValhuMnI=") // TODO replace with actual - - val minStorageRent = 10000000L // TODO replace with actual - - val selfPubKey = SELF.R4[GroupElement].get - - val outIndex = getVar[Int](0).get - val output = OUTPUTS(outIndex) - - val isSimpleCopy = output.R4[GroupElement].isDefined && // ballot boxes are transferable by setting different value here - output.propositionBytes == SELF.propositionBytes && - output.tokens == SELF.tokens && - output.value >= minStorageRent - - val update = INPUTS.size > 1 && - INPUTS(1).tokens.size > 0 && - INPUTS(1).tokens(0)._1 == updateNFT && // can only update when update box is the 2nd input - output.R4[GroupElement].get == selfPubKey && // public key is preserved - output.value >= SELF.value && // value preserved or increased - ! (output.R5[Any].isDefined) // no more registers; prevents box from being reused as a valid vote - - val owner = proveDlog(selfPubKey) - - // unlike in collection, here we don't require spender to be one of the ballot token holders - isSimpleCopy && (owner || update) -} -``` - -### Update Contract - -``` -{ // This box (update box): - // Registers empty - // - // ballot boxes (Inputs) - // R4 the pub key of voter [GroupElement] (not used here) - // R5 the creation height of this box [Int] - // R6 the value voted for [Coll[Byte]] (hash of the new pool box script) - // R7 the reward token id in new box - // R8 the number of reward tokens in new box - - val poolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // TODO replace with actual - - val ballotTokenId = fromBase64("P0QoRy1LYVBkU2dWa1lwM3M2djl5JEImRSlIQE1iUWU=") // TODO replace with actual - - val minVotes = 6 // TODO replace with actual - - val poolIn = INPUTS(0) // pool box is 1st input - val poolOut = OUTPUTS(0) // copy of pool box is the 1st output - - val updateBoxOut = OUTPUTS(1) // copy of this box is the 2nd output - - // compute the hash of the pool output box. This should be the value voted for - val poolOutHash = blake2b256(poolOut.propositionBytes) - val rewardTokenId = poolOut.tokens(1)._1 - val rewardAmt = poolOut.tokens(1)._2 - - val validPoolIn = poolIn.tokens(0)._1 == poolNFT - - val validPoolOut = poolIn.tokens(0) == poolOut.tokens(0) && // NFT preserved - poolIn.creationInfo._1 == poolOut.creationInfo._1 && // creation height preserved - poolIn.value == poolOut.value && // value preserved - poolIn.R4[Long] == poolOut.R4[Long] && // rate preserved - poolIn.R5[Int] == poolOut.R5[Int] && // counter preserved - ! (poolOut.R6[Any].isDefined) - - - val validUpdateOut = updateBoxOut.tokens == SELF.tokens && - updateBoxOut.propositionBytes == SELF.propositionBytes && - updateBoxOut.value >= SELF.value && - updateBoxOut.creationInfo._1 > SELF.creationInfo._1 && - ! (updateBoxOut.R4[Any].isDefined) - - def isValidBallot(b:Box) = if (b.tokens.size > 0) { - b.tokens(0)._1 == ballotTokenId && - b.R5[Int].get == SELF.creationInfo._1 && // ensure vote corresponds to this box by checking creation height - b.R6[Coll[Byte]].get == poolOutHash && // check proposition voted for - b.R7[Coll[Byte]].get == rewardTokenId && // check rewardTokenId voted for - b.R8[Long].get == rewardAmt // check rewardTokenAmt voted for - } else false - - val ballotBoxes = INPUTS.filter(isValidBallot) - - val votesCount = ballotBoxes.fold(0L, {(accum: Long, b: Box) => accum + b.tokens(0)._2}) - - sigmaProp(validPoolIn && validPoolOut && validUpdateOut && votesCount >= minVotes) -} -``` +- [Pool Contract](contracts/pool_contract.es) +- [Refresh Contract](contracts/refresh_contract.es) +- [Oracle contract](contracts/oracle_contract.es) +- [Ballot Contract](contracts/ballot_contract.es) +- [Update Contract](contracts/update_contract.es) + ## Transactions Oracle pool v2.0 has the following transactions. From fec79b7e72c017d02aa799b3748ae845c02f8874 Mon Sep 17 00:00:00 2001 From: Tim Ling <791016+kettlebell@users.noreply.github.com> Date: Wed, 31 Aug 2022 11:38:20 +1000 Subject: [PATCH 32/43] Add encoded contract hashes to EIP-23 --- eip-0023/eip-0023.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/eip-0023/eip-0023.md b/eip-0023/eip-0023.md index cf0dbc30..1ddb1c8a 100644 --- a/eip-0023/eip-0023.md +++ b/eip-0023/eip-0023.md @@ -137,13 +137,13 @@ There are a total of 5 contracts, each corresponding to a box type Before going into the transactions in the protocol, we first present the contracts for each of the above boxes. -## Contracts +## Contracts with base-64-encoded hash of ergo-tree bytes -- [Pool Contract](contracts/pool_contract.es) -- [Refresh Contract](contracts/refresh_contract.es) -- [Oracle contract](contracts/oracle_contract.es) -- [Ballot Contract](contracts/ballot_contract.es) -- [Update Contract](contracts/update_contract.es) +- [Pool Contract](contracts/pool_contract.es) `8cJi+FGGU32jXyO8M2LeyWSWlerdcb1zxBWeZtyy7Y8=` +- [Refresh Contract](contracts/refresh_contract.es) `cs5c5QEirstI4ZlTyrbTjlPwWYHRW+QsedtpyOSBnH4=` +- [Oracle Contract](contracts/oracle_contract.es) `fhOYLO3s+NJCqTQDWUz0E+ffy2T1VG7ZnhSFs0RP948=` +- [Ballot Contract](contracts/ballot_contract.es) `2DnK+72bh+TxviNk8XfuYzLKtuF5jnqUJOzimt30NvI=` +- [Update Contract](contracts/update_contract.es) `0wFmk/1TNpgTsbzpWND3WLPbwQdD8E+TWDzZLaYv3nE=` ## Transactions From e485df29ed54778af0b4a67136a1516409341031 Mon Sep 17 00:00:00 2001 From: Tim Ling Date: Wed, 31 Aug 2022 20:43:47 +1000 Subject: [PATCH 33/43] Update eip-0023/eip-0023.md (add link to scastie) Co-authored-by: Denys Zadorozhnyi --- eip-0023/eip-0023.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eip-0023/eip-0023.md b/eip-0023/eip-0023.md index 1ddb1c8a..38a3009f 100644 --- a/eip-0023/eip-0023.md +++ b/eip-0023/eip-0023.md @@ -144,7 +144,7 @@ Before going into the transactions in the protocol, we first present the contrac - [Oracle Contract](contracts/oracle_contract.es) `fhOYLO3s+NJCqTQDWUz0E+ffy2T1VG7ZnhSFs0RP948=` - [Ballot Contract](contracts/ballot_contract.es) `2DnK+72bh+TxviNk8XfuYzLKtuF5jnqUJOzimt30NvI=` - [Update Contract](contracts/update_contract.es) `0wFmk/1TNpgTsbzpWND3WLPbwQdD8E+TWDzZLaYv3nE=` - +Use this Scastie playground to calculate the above hashes - [https://scastie.scala-lang.org/hnTEm2lJQPG1wRYCqPz8LQ](https://scastie.scala-lang.org/hnTEm2lJQPG1wRYCqPz8LQ) ## Transactions Oracle pool v2.0 has the following transactions. From c6f5425f325b1bcd0949806a9ea3857c45c58b1c Mon Sep 17 00:00:00 2001 From: scalahub <23208922+scalahub@users.noreply.github.com> Date: Thu, 24 Nov 2022 12:39:26 +0530 Subject: [PATCH 34/43] Allow changing pool creation height in update Rewrite confusing text in description --- eip-0023/contracts/update_contract.es | 3 +-- eip-0023/eip-0023.md | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/eip-0023/contracts/update_contract.es b/eip-0023/contracts/update_contract.es index 5595719f..cbc49736 100644 --- a/eip-0023/contracts/update_contract.es +++ b/eip-0023/contracts/update_contract.es @@ -27,8 +27,7 @@ val validPoolIn = poolIn.tokens(0)._1 == poolNFT val validPoolOut = poolIn.tokens(0) == poolOut.tokens(0) && // NFT preserved - poolIn.creationInfo._1 == poolOut.creationInfo._1 && // creation height preserved - poolIn.value == poolOut.value && // value preserved + poolIn.value == poolOut.value && // value preserved poolIn.R4[Long] == poolOut.R4[Long] && // rate preserved poolIn.R5[Int] == poolOut.R5[Int] && // counter preserved ! (poolOut.R6[Any].isDefined) diff --git a/eip-0023/eip-0023.md b/eip-0023/eip-0023.md index 38a3009f..ead017ab 100644 --- a/eip-0023/eip-0023.md +++ b/eip-0023/eip-0023.md @@ -197,7 +197,7 @@ We consider such a pool box to be *stale* that needs to be refreshed. - The first oracle box's rate must be within 5% of that of the last, and must be > 0. - The first output must be a new pool box as follows: - Registers R0 (value), R1 (script), R2 (tokens) are preserved from the old pool box. - - The creation height (stored in R3) must be at most 4 less than the current height. + - The creation height (stored in R3) must be at greater than or equal to the current height minus 4. - The rate (stored in R4) must be the average of the rates in all the oracle boxes. - The epoch counter (stored in R5) must be incremented by 1. - Registers R6 and onward are empty. @@ -304,7 +304,7 @@ The update box additionally ensures following as per the [smart contract](#updat - Each ballot box has R6 containing this box's creation height. - Each ballot box has R7 containing the new pool box's reward token id. - Each ballot box has R8 containing the new pool box's reward token amount. -- The remaining registers and tokens in the old pool box are preserved in the new pool box. +- The remaining registers (apart from creation height) and tokens in the old pool box are preserved in the new pool box. - There is a copy of the update box in outputs with everything preserved but with larger creation height. - There are at least a threshold number of votes. From cae50b722d6929c794847d21668500acb01f3c8c Mon Sep 17 00:00:00 2001 From: ScalaHub <23208922+scalahub@users.noreply.github.com> Date: Thu, 24 Nov 2022 19:25:14 +0530 Subject: [PATCH 35/43] Update eip-0023/eip-0023.md Co-authored-by: Denys Zadorozhnyi --- eip-0023/eip-0023.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eip-0023/eip-0023.md b/eip-0023/eip-0023.md index ead017ab..91320032 100644 --- a/eip-0023/eip-0023.md +++ b/eip-0023/eip-0023.md @@ -143,7 +143,7 @@ Before going into the transactions in the protocol, we first present the contrac - [Refresh Contract](contracts/refresh_contract.es) `cs5c5QEirstI4ZlTyrbTjlPwWYHRW+QsedtpyOSBnH4=` - [Oracle Contract](contracts/oracle_contract.es) `fhOYLO3s+NJCqTQDWUz0E+ffy2T1VG7ZnhSFs0RP948=` - [Ballot Contract](contracts/ballot_contract.es) `2DnK+72bh+TxviNk8XfuYzLKtuF5jnqUJOzimt30NvI=` -- [Update Contract](contracts/update_contract.es) `0wFmk/1TNpgTsbzpWND3WLPbwQdD8E+TWDzZLaYv3nE=` +- [Update Contract](contracts/update_contract.es) `3aIVTP5tRgCZHxaT3ZFw3XubRV5DJi0rKeo9bKVHlVw=` Use this Scastie playground to calculate the above hashes - [https://scastie.scala-lang.org/hnTEm2lJQPG1wRYCqPz8LQ](https://scastie.scala-lang.org/hnTEm2lJQPG1wRYCqPz8LQ) ## Transactions From b7138e7f0e162b2d8c21323e68c2cb2c64debf7f Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Thu, 16 Feb 2023 10:15:49 +0200 Subject: [PATCH 36/43] in update contract check reward tokens are either preserved or voted by ballot boxes; make reward token id and amount optional in ballot box; --- eip-0023/contracts/update_contract.es | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/eip-0023/contracts/update_contract.es b/eip-0023/contracts/update_contract.es index cbc49736..846e7ce8 100644 --- a/eip-0023/contracts/update_contract.es +++ b/eip-0023/contracts/update_contract.es @@ -5,8 +5,8 @@ // R4 the pub key of voter [GroupElement] (not used here) // R5 the creation height of this box [Int] // R6 the value voted for [Coll[Byte]] (hash of the new pool box script) - // R7 the reward token id in new box - // R8 the number of reward tokens in new box + // R7 the reward token id in new box (optional, if not present then reward token is preserved) + // R8 the number of reward tokens in new box (optional, if not present then reward token is preserved)) val poolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // TODO replace with actual @@ -39,12 +39,18 @@ updateBoxOut.creationInfo._1 > SELF.creationInfo._1 && ! (updateBoxOut.R4[Any].isDefined) + val rewardTokenPreserved = poolIn.tokens(1)._1 == rewardTokenId && // check reward token id is preserved + poolIn.tokens(1)._2 == rewardAmt // check reward token amt is preserved + def isValidBallot(b:Box) = if (b.tokens.size > 0) { + val validRewardToken = if (b.R7[Coll[Byte]].isDefined && b.R8[Long].isDefined) { + b.R7[Coll[Byte]].get == rewardTokenId && // check rewardTokenId voted for + b.R8[Long].get == rewardAmt // check rewardTokenAmt voted for + } else rewardTokenPreserved b.tokens(0)._1 == ballotTokenId && b.R5[Int].get == SELF.creationInfo._1 && // ensure vote corresponds to this box by checking creation height b.R6[Coll[Byte]].get == poolOutHash && // check proposition voted for - b.R7[Coll[Byte]].get == rewardTokenId && // check rewardTokenId voted for - b.R8[Long].get == rewardAmt // check rewardTokenAmt voted for + validRewardToken } else false val ballotBoxes = INPUTS.filter(isValidBallot) From f67124bb5c7ddb883d2e337c6cd44eb4306630aa Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Mon, 27 Feb 2023 11:08:45 +0200 Subject: [PATCH 37/43] update update contract hash according to changes in https://github.com/ergoplatform/eips/pull/89 --- eip-0023/eip-0023.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eip-0023/eip-0023.md b/eip-0023/eip-0023.md index 91320032..fd20b666 100644 --- a/eip-0023/eip-0023.md +++ b/eip-0023/eip-0023.md @@ -143,7 +143,7 @@ Before going into the transactions in the protocol, we first present the contrac - [Refresh Contract](contracts/refresh_contract.es) `cs5c5QEirstI4ZlTyrbTjlPwWYHRW+QsedtpyOSBnH4=` - [Oracle Contract](contracts/oracle_contract.es) `fhOYLO3s+NJCqTQDWUz0E+ffy2T1VG7ZnhSFs0RP948=` - [Ballot Contract](contracts/ballot_contract.es) `2DnK+72bh+TxviNk8XfuYzLKtuF5jnqUJOzimt30NvI=` -- [Update Contract](contracts/update_contract.es) `3aIVTP5tRgCZHxaT3ZFw3XubRV5DJi0rKeo9bKVHlVw=` +- [Update Contract](contracts/update_contract.es) `pQ7Dgjq1pUyISroP+RWEDf+kVNYAWjeFHzW+cpImhsQ=` Use this Scastie playground to calculate the above hashes - [https://scastie.scala-lang.org/hnTEm2lJQPG1wRYCqPz8LQ](https://scastie.scala-lang.org/hnTEm2lJQPG1wRYCqPz8LQ) ## Transactions From 821fc1f51e8a341f45c9b2e6268070bf3b9c751a Mon Sep 17 00:00:00 2001 From: ScalaHub <23208922+scalahub@users.noreply.github.com> Date: Sun, 9 Apr 2023 13:37:00 +0530 Subject: [PATCH 38/43] Fix link to EIP23 readme file --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d2dd0d5a..8dd03ea2 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Please check out existing EIPs, such as [EIP-1](eip-0001.md), to understand the | [EIP-0020](eip-0020.md) | ErgoPay Protocol | | [EIP-0021](eip-0021.md) | Genuine tokens verification | | [EIP-0022](eip-0022.md) | Auction Contract | -| [EIP-0023](eip-0023.md) | Oracle pool 2.0 | +| [EIP-0023](eip-0023/eip-0023.mdeip-0023.md) | Oracle pool 2.0 | | [EIP-0024](eip-0024.md) | Artwork contract | | [EIP-0025](eip-0025.md) | Payment Request URI | | [EIP-0027](eip-0027.md) | Emission Retargeting Soft-Fork | From 21be0b3f3dcd127c41eb45c620eb869e57b7b139 Mon Sep 17 00:00:00 2001 From: ScalaHub <23208922+scalahub@users.noreply.github.com> Date: Sun, 9 Apr 2023 13:38:20 +0530 Subject: [PATCH 39/43] Fix link to main readme file --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8dd03ea2..633eb0ab 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Please check out existing EIPs, such as [EIP-1](eip-0001.md), to understand the | [EIP-0020](eip-0020.md) | ErgoPay Protocol | | [EIP-0021](eip-0021.md) | Genuine tokens verification | | [EIP-0022](eip-0022.md) | Auction Contract | -| [EIP-0023](eip-0023/eip-0023.mdeip-0023.md) | Oracle pool 2.0 | +| [EIP-0023](eip-0023/eip-0023.md) | Oracle pool 2.0 | | [EIP-0024](eip-0024.md) | Artwork contract | | [EIP-0025](eip-0025.md) | Payment Request URI | | [EIP-0027](eip-0027.md) | Emission Retargeting Soft-Fork | From 2e14357204b9ffa9a2e48a7bb66904ad23e59829 Mon Sep 17 00:00:00 2001 From: ScalaHub <23208922+scalahub@users.noreply.github.com> Date: Sun, 9 Apr 2023 20:20:34 +0530 Subject: [PATCH 40/43] Remove text on sample token exchange contract We have not given a sample token exchange contract so we should remove the text. --- eip-0023/eip-0023.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eip-0023/eip-0023.md b/eip-0023/eip-0023.md index 91320032..71b519bf 100644 --- a/eip-0023/eip-0023.md +++ b/eip-0023/eip-0023.md @@ -87,7 +87,7 @@ In v1.0, the pool was responsible for rewarding each oracle for posting a data-p The certificates are in the form of tokens emitted by the pool box. Thus, the pool box also contains the reward tokens to be emitted. -Once there are sufficient number of such tokens, an oracle can exchange or burn them in return for a reward. We also give a sample token exchange contract. +Once there are sufficient number of such tokens, an oracle can exchange or burn them in return for a reward. ### Refresh Mechanism From 3bd226b08475abaffd0a885de17102378c856b86 Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 28 Jun 2023 12:01:23 +0300 Subject: [PATCH 41/43] let ballot token owner (R4 in ballot box) always spend the ballot box; --- eip-0023/contracts/ballot_contract.es | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/eip-0023/contracts/ballot_contract.es b/eip-0023/contracts/ballot_contract.es index d9685e0d..7e74a613 100644 --- a/eip-0023/contracts/ballot_contract.es +++ b/eip-0023/contracts/ballot_contract.es @@ -28,6 +28,5 @@ val owner = proveDlog(selfPubKey) - // unlike in collection, here we don't require spender to be one of the ballot token holders - isSimpleCopy && (owner || update) + owner || (isSimpleCopy && update) } From 9cbe17c75b7ef12cfea5df2873c76c931af5a69d Mon Sep 17 00:00:00 2001 From: Denys Zadorozhnyi Date: Wed, 28 Jun 2023 12:07:33 +0300 Subject: [PATCH 42/43] update ballot contract hash after changes introduced in https://github.com/ergoplatform/eips/pull/94/commits/3bd226b08475abaffd0a885de17102378c856b86 hash is calculated in https://scastie.scala-lang.org/W1KaudPGT2WBmJfHDsjlaw --- eip-0023/eip-0023.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eip-0023/eip-0023.md b/eip-0023/eip-0023.md index 2606fe27..da2eba1c 100644 --- a/eip-0023/eip-0023.md +++ b/eip-0023/eip-0023.md @@ -142,7 +142,7 @@ Before going into the transactions in the protocol, we first present the contrac - [Pool Contract](contracts/pool_contract.es) `8cJi+FGGU32jXyO8M2LeyWSWlerdcb1zxBWeZtyy7Y8=` - [Refresh Contract](contracts/refresh_contract.es) `cs5c5QEirstI4ZlTyrbTjlPwWYHRW+QsedtpyOSBnH4=` - [Oracle Contract](contracts/oracle_contract.es) `fhOYLO3s+NJCqTQDWUz0E+ffy2T1VG7ZnhSFs0RP948=` -- [Ballot Contract](contracts/ballot_contract.es) `2DnK+72bh+TxviNk8XfuYzLKtuF5jnqUJOzimt30NvI=` +- [Ballot Contract](contracts/ballot_contract.es) `x01xAvK0CrRCwj36vp/jon7NARR1rxplSwI5B20ZNyI=` - [Update Contract](contracts/update_contract.es) `pQ7Dgjq1pUyISroP+RWEDf+kVNYAWjeFHzW+cpImhsQ=` Use this Scastie playground to calculate the above hashes - [https://scastie.scala-lang.org/hnTEm2lJQPG1wRYCqPz8LQ](https://scastie.scala-lang.org/hnTEm2lJQPG1wRYCqPz8LQ) ## Transactions From 2b4a2e42d1586d8475a1b0265c015a7e4c9837a0 Mon Sep 17 00:00:00 2001 From: Alexander Chepurnoy Date: Wed, 15 Nov 2023 00:34:42 +0300 Subject: [PATCH 43/43] Update eip-0023/eip-0023.md Co-authored-by: Denys Zadorozhnyi --- eip-0023/eip-0023.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/eip-0023/eip-0023.md b/eip-0023/eip-0023.md index da2eba1c..cfe36480 100644 --- a/eip-0023/eip-0023.md +++ b/eip-0023/eip-0023.md @@ -263,8 +263,7 @@ The transaction is similar to the [publish data-point](#publish-data-point) tran 3. The following rules (not enforced by the smart contract) to be followed. - It should store the new owner's group element in R4. - - It should keep at least 1 reward token. - - It should store the balance reward tokens, if any, in some other box. + - It should check that there is only 1 reward token is in the oracle box and send it to the new owner along with oracle token. ### Vote for update