Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

EIP-37 implementation and activation #1845

Merged
merged 25 commits into from
Sep 27, 2022
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/resources/api/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: "3.0.2"

info:
version: "4.0.46"
version: "4.0.100"
title: Ergo Node API
description: API docs for Ergo Node. Models are shared between all Ergo products
contact:
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ scorex {
nodeName = "ergo-node"

# Network protocol version to be sent in handshakes
appVersion = 4.0.46
appVersion = 4.0.100

# Network agent name. May contain information about client code
# stack, starting from core code-base up to the end graphical interface.
Expand Down
7 changes: 6 additions & 1 deletion src/main/resources/mainnet.conf
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ ergo {
}
}
node {
mining = false

# start mining without waiting for a new block. helps in avoiding waiting period after node restart
offlineGeneration = true

# Optional and individual checkpoint.
# Before the height given (including it) validation of scripts is missed.
# This improving perfomance and memory usage during initial bootstrapping.
Expand All @@ -65,7 +70,7 @@ scorex {
network {
magicBytes = [1, 0, 2, 4]
bindAddress = "0.0.0.0:9030"
nodeName = "ergo-mainnet-4.0.46"
nodeName = "ergo-mainnet-4.0.100"
nodeName = ${?NODENAME}
knownPeers = [
"213.239.193.208:9030",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,25 @@ import org.ergoplatform.modifiers.history.header.Header
import org.ergoplatform.nodeView.history.ErgoHistory.{Difficulty, Height}
import org.ergoplatform.settings.ChainSettings
import scorex.util.ScorexLogging

import scala.concurrent.duration.FiniteDuration

class LinearDifficultyControl(val chainSettings: ChainSettings) extends ScorexLogging {

import LinearDifficultyControl._
class DifficultyAdjustment(val chainSettings: ChainSettings) extends ScorexLogging {

import DifficultyAdjustment._

val desiredInterval: FiniteDuration = chainSettings.blockInterval
val useLastEpochs: Int = chainSettings.useLastEpochs
val epochLength: Int = chainSettings.epochLength
val initialDifficulty: BigInt = chainSettings.initialDifficulty

require(useLastEpochs > 1, "useLastEpochs should always be > 1")
require(epochLength > 0, "epochLength should always be > 0")
require(epochLength < Int.MaxValue / useLastEpochs, s"epochLength $epochLength is too high for $useLastEpochs epochs")
require(chainSettings.epochLength > 0, "diff epoch length should always be > 0")
require(chainSettings.epochLength < Int.MaxValue / useLastEpochs, s"diff epoch length is too high for $useLastEpochs epochs")

/**
* @return heights of previous headers required for block recalculation
*/
def previousHeadersRequiredForRecalculation(height: Height): Seq[Int] = {
def previousHeadersRequiredForRecalculation(height: Height, epochLength: Int): Seq[Int] = {
if ((height - 1) % epochLength == 0 && epochLength > 1) {
(0 to useLastEpochs).map(i => (height - 1) - i * epochLength).filter(_ >= 0).reverse
} else if ((height - 1) % epochLength == 0 && height > epochLength * useLastEpochs) {
Expand All @@ -33,8 +32,57 @@ class LinearDifficultyControl(val chainSettings: ChainSettings) extends ScorexLo
}
}

/** @param previousHeaders should be last headers of the previous epochs */
def bitcoinCalculate(previousHeaders: Seq[Header], epochLength: Int): Difficulty = {
val hs = previousHeaders.takeRight(2)
bitcoinCalculate(hs(0), hs(1), epochLength: Int)
}

/**
* Calculate difficulty as done in Bitcoin (with no capping result)
*
* Please note this method does not normalize its result!
*
* @param start - last block of previous epoch
* @param end - last block of current epoch
*/
private def bitcoinCalculate(start: Header, end: Header, epochLength: Int): BigInt = {
end.requiredDifficulty * desiredInterval.toMillis * epochLength / (end.timestamp - start.timestamp)
}

/**
* Calculate difficulty for first block of a new epoch according to EIP-37
* @param previousHeaders - last headers of few epochs (8 in case of Ergo mainnet)
* @param epochLength - epoch length
*/
def eip37Calculate(previousHeaders: Seq[Header], epochLength: Int): Difficulty = {
require(previousHeaders.size >= 2, "at least two headers needed for diff recalc")
val lastDiff = previousHeaders.last.requiredDifficulty

val predictiveDiff = calculate(previousHeaders, epochLength)
val limitedPredictiveDiff = if (predictiveDiff > lastDiff) {
predictiveDiff.min(lastDiff * 3 / 2)
} else {
predictiveDiff.max(lastDiff / 2)
}
val classicDiff = bitcoinCalculate(previousHeaders, epochLength)
val avg = (classicDiff + limitedPredictiveDiff) / 2
val uncompressedDiff = if (avg > lastDiff) {
avg.min(lastDiff * 3 / 2)
} else {
avg.max(lastDiff / 2)
}
//todo: downgrade log level after testing
log.warn(s"Difficulty for ${previousHeaders.last.height + 1}: predictive $predictiveDiff, classic: $classicDiff, " +
s"resulting uncompressed: $uncompressedDiff")
// perform serialization cycle in order to normalize resulted difficulty
RequiredDifficulty.decodeCompactBits(
RequiredDifficulty.encodeCompactBits(uncompressedDiff)
)
}

@SuppressWarnings(Array("TraversableHead"))
def calculate(previousHeaders: Seq[Header]): Difficulty = {
def calculate(previousHeaders: Seq[Header], epochLength: Int): Difficulty = {
require(previousHeaders.nonEmpty, "PreviousHeaders should always contain at least 1 element")

val uncompressedDiff = {
Expand All @@ -48,7 +96,7 @@ class LinearDifficultyControl(val chainSettings: ChainSettings) extends ScorexLo
val diff = end.requiredDifficulty * desiredInterval.toMillis * epochLength / (end.timestamp - start.timestamp)
(end.height, diff)
}
val diff = interpolate(data)
val diff = interpolate(data, epochLength)
if (diff >= 1) diff else initialDifficulty
}
}
Expand All @@ -59,7 +107,7 @@ class LinearDifficultyControl(val chainSettings: ChainSettings) extends ScorexLo
}

//y = a + bx
private[difficulty] def interpolate(data: Seq[(Int, Difficulty)]): Difficulty = {
private[difficulty] def interpolate(data: Seq[(Int, Difficulty)], epochLength: Int): Difficulty = {
val size = data.size
if (size == 1) {
data.head._2
Expand All @@ -83,6 +131,6 @@ class LinearDifficultyControl(val chainSettings: ChainSettings) extends ScorexLo

}

object LinearDifficultyControl {
object DifficultyAdjustment {
val PrecisionConstant: Int = 1000000000
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.google.common.primitives.Ints
import org.ergoplatform.ErgoApp.CriticalSystemException
import org.ergoplatform.ErgoLikeContext.Height
import org.ergoplatform.mining.AutolykosPowScheme
import org.ergoplatform.mining.difficulty.LinearDifficultyControl
import org.ergoplatform.mining.difficulty.DifficultyAdjustment
import org.ergoplatform.modifiers.BlockSection
import org.ergoplatform.modifiers.history._
import org.ergoplatform.modifiers.history.header.Header
Expand All @@ -19,6 +19,7 @@ import scorex.core.consensus.ProgressInfo
import scorex.core.consensus.ModifierSemanticValidity
import scorex.core.utils.ScorexEncoding
import scorex.core.validation.{InvalidModifier, ModifierValidator, ValidationResult, ValidationState}
import scorex.crypto.hash.Blake2b256
import scorex.db.ByteArrayWrapper
import scorex.util._

Expand All @@ -42,7 +43,7 @@ trait HeadersProcessor extends ToDownloadProcessor with ScorexLogging with Score
// Maximum time in future block header may have
protected lazy val MaxTimeDrift: Long = 10 * chainSettings.blockInterval.toMillis

lazy val difficultyCalculator = new LinearDifficultyControl(chainSettings)
lazy val difficultyCalculator = new DifficultyAdjustment(chainSettings)

def isSemanticallyValid(modifierId: ModifierId): ModifierSemanticValidity

Expand Down Expand Up @@ -257,34 +258,82 @@ trait HeadersProcessor extends ToDownloadProcessor with ScorexLogging with Score

protected def heightIdsKey(height: Int): ByteArrayWrapper = ByteArrayWrapper(Algos.hash(Ints.toByteArray(height)))

private val EIP37VotingParameter: Byte = 6 // input cost, set 6 = 2100 for voting on EIP-37

private val eip37Key = Blake2b256.hash("eip37 activation height")

private def storeEip37ActivationHeight(eip37ActivationHeight: Int) = {
historyStorage.insert(eip37Key, Ints.toByteArray(eip37ActivationHeight))
}

private def eip37ActivationHeight: Option[Int] = {
historyStorage.get(scorex.util.bytesToId(eip37Key)).map(Ints.fromByteArray)
}

/**
* Calculate difficulty for the next block
*
* @param parent - latest block
* @return - difficulty for the next block
*/
def requiredDifficultyAfter(parent: Header): Difficulty = {
if (parent.height == settings.chainSettings.voting.version2ActivationHeight || parent.height + 1 == settings.chainSettings.voting.version2ActivationHeight) {
// Set difficulty for version 2 activation height (where specific difficulty is needed due to PoW change)
settings.chainSettings.initialDifficultyVersion2
val parentHeight = parent.height

val minActivationHeight = 843776
val maxActivationHeight = 843776 + 98304
val checkActivationPeriod = 128
val activationVotesChecked = 256
val activationThreshold = 232

// todo: this EIP-37 activation checking code could be removed after activation
if (settings.chainSettings.isMainnet &&
parentHeight > minActivationHeight &&
parentHeight <= maxActivationHeight &&
parentHeight % checkActivationPeriod == 0 &&
eip37ActivationHeight.isEmpty) {
val chain = headerChainBack(activationVotesChecked, parent, _ => false)
val eip37Activated = chain.headers.map(_.votes).map(_.contains(EIP37VotingParameter)).count(_ == true) >= activationThreshold
if (eip37Activated) {
storeEip37ActivationHeight(parentHeight + 1)
}
}

if (parentHeight > minActivationHeight && parentHeight + 1 >= eip37ActivationHeight.getOrElse(Int.MaxValue)) {
// by eip37VotedOn definition could be on mainnet only
val epochLength = 128 // epoch length after EIP-37 activation
if (parentHeight % epochLength == 0) {
val heights = difficultyCalculator.previousHeadersRequiredForRecalculation(parentHeight + 1, epochLength)
// todo: if parent is on best chain, read headers directly, not via headerChainBack
val chain = headerChainBack(heights.max - heights.min + 1, parent, _ => false)
aslesarenko marked this conversation as resolved.
Show resolved Hide resolved
val headers = chain.headers.filter(p => heights.contains(p.height))
difficultyCalculator.eip37Calculate(headers, epochLength)
} else {
parent.requiredDifficulty
}
} else {
val parentHeight = parent.height

if(parentHeight % settings.chainSettings.epochLength == 0) {
//todo: it is slow to read thousands headers from database for each header
//todo; consider caching here
//todo: https://github.com/ergoplatform/ergo/issues/872
val heights = difficultyCalculator.previousHeadersRequiredForRecalculation(parentHeight + 1)
.ensuring(_.last == parentHeight)
if (heights.lengthCompare(1) == 0) {
difficultyCalculator.calculate(Array(parent))
if (parentHeight == settings.chainSettings.voting.version2ActivationHeight ||
parent.height + 1 == settings.chainSettings.voting.version2ActivationHeight) {
// Set difficulty for version 2 activation height (where specific difficulty is needed due to PoW change)
settings.chainSettings.initialDifficultyVersion2
} else {
val epochLength = settings.chainSettings.epochLength

if (parentHeight % epochLength == 0) {
//todo: it is slow to read thousands headers from database for each header
//todo; consider caching here
//todo: https://github.com/ergoplatform/ergo/issues/872
val heights = difficultyCalculator.previousHeadersRequiredForRecalculation(parentHeight + 1, epochLength)
.ensuring(_.last == parentHeight)
if (heights.lengthCompare(1) == 0) {
difficultyCalculator.calculate(Array(parent), epochLength)
} else {
val chain = headerChainBack(heights.max - heights.min + 1, parent, _ => false)
val headers = chain.headers.filter(p => heights.contains(p.height))
difficultyCalculator.calculate(headers, epochLength)
}
} else {
val chain = headerChainBack(heights.max - heights.min + 1, parent, _ => false)
val headers = chain.headers.filter(p => heights.contains(p.height))
difficultyCalculator.calculate(headers)
parent.requiredDifficulty
}
} else {
parent.requiredDifficulty
}
}
}
aslesarenko marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/org/ergoplatform/settings/Constants.scala
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ object Constants {

val SoftForkEpochs = 32 //about 45.5 days

val TrueLeaf: ErgoTree = Values.TrueLeaf.toSigmaProp
val FalseLeaf: ErgoTree = Values.FalseLeaf.toSigmaProp
def TrueLeaf: ErgoTree = Values.TrueLeaf.toSigmaProp
def FalseLeaf: ErgoTree = Values.FalseLeaf.toSigmaProp

val StringEncoding = "UTF-8"

Expand Down
Loading