diff --git a/ergo-core/src/main/scala/org/ergoplatform/mining/AutolykosPowScheme.scala b/ergo-core/src/main/scala/org/ergoplatform/mining/AutolykosPowScheme.scala index 5dd710da26..d355b8d898 100644 --- a/ergo-core/src/main/scala/org/ergoplatform/mining/AutolykosPowScheme.scala +++ b/ergo-core/src/main/scala/org/ergoplatform/mining/AutolykosPowScheme.scala @@ -27,6 +27,10 @@ import scala.util.Try * Based on k-sum problem, so general idea is to find k numbers in a table of size N, such that * sum of numbers (or a hash of the sum) is less than target value. * + * There are two version of Autolykos PoW scheme, Autolykos v1 and v2. The main difference is that + * Autolykos v1 is (weakly) non-outsourceable, while v2 is outsourceable and also eliminates some vectors of + * optimizations a miner could follow. + * * See https://docs.ergoplatform.com/ErgoPow.pdf for details * * CPU Mining process is implemented in inefficient way and should not be used in real environment. @@ -58,7 +62,8 @@ class AutolykosPowScheme(val k: Int, val n: Int) extends ScorexLogging { val IncreasePeriodForN: Height = 50 * 1024 /** - * On this height, the table (`N` value) will stop to grow + * On this height, the table (`N` value) will stop to grow. + * Max N on and after this height would be 2,143,944,600 which is still less than 2^^31. */ val NIncreasementHeightMax: Height = 4198400 @@ -97,25 +102,37 @@ class AutolykosPowScheme(val k: Int, val n: Int) extends ScorexLogging { * Checks that `header` contains correct solution of the Autolykos PoW puzzle. */ def validate(header: Header): Try[Unit] = Try { - val b = getB(header.nBits) if (header.version == 1) { // for version 1, we check equality of left and right sides of the equation - require(checkPoWForVersion1(header, b), "Incorrect points") + require(checkPoWForVersion1(header), "Incorrect points") } else { - // for version 2, we're calculating hit and compare it with target - val hit = hitForVersion2(header) - require(hit < b, "h(f) < b condition not met") + require(checkPoWForVersion2(header), "h(f) < b condition not met") } } + /** + * Check PoW for Autolykos v2 header + * + * @param header - header to check PoW for + * @return whether PoW is valid or not + */ + def checkPoWForVersion2(header: Header): Boolean = { + val b = getB(header.nBits) + // for version 2, we're calculating hit and compare it with target + val hit = hitForVersion2(header) + hit < b + } + /** * Check PoW for Autolykos v1 header * * @param header - header to check PoW for - * @param b - PoW target * @return whether PoW is valid or not */ - def checkPoWForVersion1(header: Header, b: BigInt): Boolean = { + def checkPoWForVersion1(header: Header): Boolean = { + + val b = getB(header.nBits) // PoW target + val version = 1: Byte val msg = msgByHeader(header) @@ -152,7 +169,6 @@ class AutolykosPowScheme(val k: Int, val n: Int) extends ScorexLogging { * @return PoW hit */ def hitForVersion2(header: Header): BigInt = { - val version = 2: Byte val msg = msgByHeader(header) val nonce = header.powSolution.n @@ -161,6 +177,25 @@ class AutolykosPowScheme(val k: Int, val n: Int) extends ScorexLogging { val N = calcN(header) + hitForVersion2ForMessage(msg, nonce, h, N) + } + + /** + * Get a PoW hit for custom message (not necessarily a block header) with Autolykos v2. + * + * PoW then can can be checked as hit < b, where b is PoW target value + * + * @param msg - message to check PoW on + * @param nonce - PoW nonce + * @param h - for Ergo blockchain, this is height encoded as bytes. For other use-cases, could be + * unique value on each call or constant (in the latter case more pre-computations + * could be possible + * @param N - table size + * @return pow hit + */ + def hitForVersion2ForMessage(msg: Array[Byte], nonce: Array[Byte], h: Array[Byte], N: Int): BigInt = { + val version = 2: Byte // autolykos protocol version, used in genElement only + val prei8 = BigIntegers.fromUnsignedByteArray(hash(Bytes.concat(msg, nonce)).takeRight(8)) val i = BigIntegers.asUnsignedByteArray(4, prei8.mod(BigInt(N).underlying())) val f = Blake2b256(Bytes.concat(i, h, M)).drop(1) // .drop(1) is the same as takeRight(31) @@ -244,7 +279,9 @@ class AutolykosPowScheme(val k: Int, val n: Int) extends ScorexLogging { //Proving-related code which is not critical for consensus below /** - * Find a nonce from `minNonce` to `maxNonce`, such that header with the specified fields will contain + * Autolykos solver suitable for CPU-mining in testnet and devnets. + * + * Finds a nonce from `minNonce` to `maxNonce`, such that header with the specified fields will contain * correct solution of the Autolykos PoW puzzle. */ def prove(parentOpt: Option[Header], diff --git a/ergo-core/src/main/scala/org/ergoplatform/mining/NumericHash.scala b/ergo-core/src/main/scala/org/ergoplatform/mining/ModQHash.scala similarity index 91% rename from ergo-core/src/main/scala/org/ergoplatform/mining/NumericHash.scala rename to ergo-core/src/main/scala/org/ergoplatform/mining/ModQHash.scala index 0b4beb4bd0..1d4f360332 100644 --- a/ergo-core/src/main/scala/org/ergoplatform/mining/NumericHash.scala +++ b/ergo-core/src/main/scala/org/ergoplatform/mining/ModQHash.scala @@ -12,8 +12,10 @@ import scala.annotation.tailrec * in range from 0 to a maximum number divisible by q without remainder. * If yes, it returns the result mod q, otherwise make one more iteration using hash as an input. * This is done to ensure uniform distribution of the resulting numbers. + * + * Used in Autolykos v1 only! */ -class NumericHash(val q: BigInt) extends ScorexLogging with ScorexEncoding { +class ModQHash(val q: BigInt) extends ScorexLogging with ScorexEncoding { assert(q.bigInteger.bitLength() <= 256, "We use 256 bit hash here") // biggest number <= 2^256 that is divisible by q without remainder val validRange: BigInt = (BigInt(2).pow(256) / q) * q diff --git a/ergo-core/src/main/scala/org/ergoplatform/mining/mining.scala b/ergo-core/src/main/scala/org/ergoplatform/mining/mining.scala index e469225906..726ec9a808 100644 --- a/ergo-core/src/main/scala/org/ergoplatform/mining/mining.scala +++ b/ergo-core/src/main/scala/org/ergoplatform/mining/mining.scala @@ -19,14 +19,14 @@ package object mining { // and also to obtain target in both Autolykos v1 and v2 val q: BigInt = group.order - private val hashFn: NumericHash = new NumericHash(q) + private val modQHashFn: ModQHash = new ModQHash(q) /** * Hash function which output is in Zq. Used in Autolykos v.1 * @param in - input (bit-string) * @return - output(in Zq) */ - def hashModQ(in: Array[Byte]): BigInt = hashFn.hash(in) + def hashModQ(in: Array[Byte]): BigInt = modQHashFn.hash(in) /** * Convert byte array to unsigned integer