Skip to content

Commit

Permalink
Merge pull request #968 from ScorexFoundation/i958-2
Browse files Browse the repository at this point in the history
[6.0.0] Header.checkPow method
  • Loading branch information
kushti authored Sep 2, 2024
2 parents a988d90 + ea0890b commit 2cd57e1
Show file tree
Hide file tree
Showing 21 changed files with 451 additions and 57 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

yarn.lock
*.log
yarn.lock
docs/spec/out/
test-out/
flamegraphs/
Expand Down
5 changes: 5 additions & 0 deletions core/shared/src/main/scala/sigma/SigmaDsl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,11 @@ trait Header {
*/
def serializeWithoutPoW: Coll[Byte]

/**
* @return result of header's proof-of-work validation
*/
def checkPow: Boolean

}

/** Runtime representation of Context ErgoTree type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,9 @@ object ReflectionData {
},
mkMethod(clazz, "powDistance", Array[Class[_]]()) { (obj, _) =>
obj.asInstanceOf[Header].powDistance
},
mkMethod(clazz, "checkPow", Array[Class[_]]()) { (obj, _) =>
obj.asInstanceOf[Header].checkPow
}
)
)
Expand Down
84 changes: 84 additions & 0 deletions core/shared/src/main/scala/sigma/util/NBitsUtils.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package sigma.util

import java.math.BigInteger

object NBitsUtils {

/**
* <p>The "compact" format is a representation of a whole number N using an unsigned 32 bit number similar to a
* floating point format. The most significant 8 bits are the unsigned exponent of base 256. This exponent can
* be thought of as "number of bytes of N". The lower 23 bits are the mantissa. Bit number 24 (0x800000) represents
* the sign of N. Therefore, N = (-1^sign) * mantissa * 256^(exponent-3).</p>
*
* <p>Satoshi's original implementation used BN_bn2mpi() and BN_mpi2bn(). MPI uses the most significant bit of the
* first byte as sign. Thus 0x1234560000 is compact 0x05123456 and 0xc0de000000 is compact 0x0600c0de. Compact
* 0x05c0de00 would be -0x40de000000.</p>
*
* <p>Bitcoin only uses this "compact" format for encoding difficulty targets, which are unsigned 256bit quantities.
* Thus, all the complexities of the sign bit and using base 256 are probably an implementation accident.</p>
*/
def decodeCompactBits(compact: Long): BigInt = {
val size: Int = (compact >> 24).toInt & 0xFF
val bytes: Array[Byte] = new Array[Byte](4 + size)
bytes(3) = size.toByte
if (size >= 1) bytes(4) = ((compact >> 16) & 0xFF).toByte
if (size >= 2) bytes(5) = ((compact >> 8) & 0xFF).toByte
if (size >= 3) bytes(6) = (compact & 0xFF).toByte
decodeMPI(bytes)
}

/**
* @see Utils#decodeCompactBits(long)
*/
def encodeCompactBits(requiredDifficulty: BigInt): Long = {
val value = requiredDifficulty.bigInteger
var result: Long = 0L
var size: Int = value.toByteArray.length
if (size <= 3) {
result = value.longValue << 8 * (3 - size)
} else {
result = value.shiftRight(8 * (size - 3)).longValue
}
// The 0x00800000 bit denotes the sign.
// Thus, if it is already set, divide the mantissa by 256 and increase the exponent.
if ((result & 0x00800000L) != 0) {
result >>= 8
size += 1
}
result |= size << 24
val a: Int = if (value.signum == -1) 0x00800000 else 0
result |= a
result
}


/** Parse 4 bytes from the byte array (starting at the offset) as unsigned 32-bit integer in big endian format. */
def readUint32BE(bytes: Array[Byte]): Long = ((bytes(0) & 0xffL) << 24) | ((bytes(1) & 0xffL) << 16) | ((bytes(2) & 0xffL) << 8) | (bytes(3) & 0xffL)

/**
* MPI encoded numbers are produced by the OpenSSL BN_bn2mpi function. They consist of
* a 4 byte big endian length field, followed by the stated number of bytes representing
* the number in big endian format (with a sign bit).
*
*/
private def decodeMPI(mpi: Array[Byte]): BigInteger = {

val length: Int = readUint32BE(mpi).toInt
val buf = new Array[Byte](length)
System.arraycopy(mpi, 4, buf, 0, length)

if (buf.length == 0) {
BigInteger.ZERO
} else {
val isNegative: Boolean = (buf(0) & 0x80) == 0x80
if (isNegative) buf(0) = (buf(0) & 0x7f).toByte
val result: BigInteger = new BigInteger(buf)
if (isNegative) {
result.negate
} else {
result
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ class HeaderWithoutPow(val version: Byte, // 1 byte
def toHeader(powSolution: AutolykosSolution, bytes: Array[Byte]): ErgoHeader =
ErgoHeader(version, parentId, ADProofsRoot, stateRoot, transactionsRoot, timestamp,
nBits, height, extensionRoot, powSolution, votes, unparsedBytes, bytes)

override def toString: String = {
s"HeaderWithoutPow($version, $parentId, ${bytesToId(ADProofsRoot)}, ${bytesToId(stateRoot)}, " +
s"${bytesToId(transactionsRoot)}, $timestamp, $nBits, $height, ${bytesToId(extensionRoot)}, ${bytesToId(votes)}, " +
s"${bytesToId(unparsedBytes)} )"
}
}

object HeaderWithoutPow {
Expand Down
22 changes: 19 additions & 3 deletions data/shared/src/main/scala/sigma/ast/methods.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1456,11 +1456,27 @@ case object SHeaderMethods extends MonoTypeMethods {
lazy val powDistanceMethod = propertyCall("powDistance", SBigInt, 14, FixedCost(JitCost(10)))
lazy val votesMethod = propertyCall("votes", SByteArray, 15, FixedCost(JitCost(10)))

protected override def getMethods() = super.getMethods() ++ Seq(
// cost of checkPoW is 700 as about 2*32 hashes required, and 1 hash (id) over short data costs 10
lazy val checkPowMethod = SMethod(
this, "checkPow", SFunc(Array(SHeader), SBoolean), 16, FixedCost(JitCost(700)))
.withIRInfo(MethodCallIrBuilder)
.withInfo(MethodCall, "Validate header's proof-of-work")

private lazy val v5Methods = super.getMethods() ++ Seq(
idMethod, versionMethod, parentIdMethod, ADProofsRootMethod, stateRootMethod, transactionsRootMethod,
timestampMethod, nBitsMethod, heightMethod, extensionRootMethod, minerPkMethod, powOnetimePkMethod,
powNonceMethod, powDistanceMethod, votesMethod
)
powNonceMethod, powDistanceMethod, votesMethod)

// 6.0 : checkPow method added
private lazy val v6Methods = v5Methods ++ Seq(checkPowMethod)

protected override def getMethods() = {
if (VersionContext.current.isV6SoftForkActivated) {
v6Methods
} else {
v5Methods
}
}
}

/** Type descriptor of `PreHeader` type of ErgoTree. */
Expand Down
10 changes: 6 additions & 4 deletions data/shared/src/main/scala/sigma/data/CHeader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import org.ergoplatform.{AutolykosSolution, ErgoHeader, HeaderWithoutPow, Header
import scorex.crypto.authds.ADDigest
import scorex.crypto.hash.Digest32
import scorex.util.{bytesToId, idToBytes}
import sigma.pow.Autolykos2PowValidation
import sigma.{AvlTree, BigInt, Coll, Colls, GroupElement, Header}

/** A default implementation of [[Header]] interface.
Expand Down Expand Up @@ -66,10 +67,11 @@ class CHeader(val ergoHeader: ErgoHeader) extends Header with WrapperOf[ErgoHead
override def wrappedValue: ErgoHeader = ergoHeader

override def serializeWithoutPoW: Coll[Byte] = {
val headerWithoutPow = HeaderWithoutPow(version, bytesToId(parentId.toArray), Digest32 @@ ADProofsRoot.toArray,
ADDigest @@ stateRoot.digest.toArray, Digest32 @@ transactionsRoot.toArray, timestamp,
nBits, height, Digest32 @@ extensionRoot.toArray, votes.toArray, unparsedBytes.toArray)
Colls.fromArray(HeaderWithoutPowSerializer.toBytes(headerWithoutPow))
Colls.fromArray(HeaderWithoutPowSerializer.toBytes(ergoHeader))
}

override def checkPow: Boolean = {
Autolykos2PowValidation.checkPoWForVersion2(this)
}

override def toString: String =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package sigma.eval

import sigma.{AvlTree, Coll, Context}
import sigma.{AvlTree, Coll, Context, Header}
import sigma.ast.{Constant, FixedCost, MethodCall, OperationCostInfo, OperationDesc, PerItemCost, SType, TypeBasedCost}
import sigma.data.KeyValueColl

Expand Down
176 changes: 176 additions & 0 deletions data/shared/src/main/scala/sigma/pow/Autolykos2PowValidation.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package sigma.pow


import scorex.crypto.hash.Blake2b256
import scorex.utils.{Bytes, Ints, Longs}
import sigma.Header
import sigma.crypto.{BcDlogGroup, BigIntegers, CryptoConstants}
import sigma.util.NBitsUtils

/**
* Functions used to validate Autolykos2 Proof-of-Work.
*/
object Autolykos2PowValidation {

type Height = Int

/**
* k value for k-sum problem Autolykos is based on (find k numbers in table on N size)
*/
private val k = 32

/**
* Initial size of N value for k-sum problem Autolykos is based on (find k numbers in table on N size).
* It grows from it since predefined block height in Autolykos 2.
*/
private val NStart = 26

/**
* Group order, used in Autolykos V.1 for non-outsourceability,
* and also to obtain target in both Autolykos v1 and v2
*/
private val q: BigInt = CryptoConstants.dlogGroup.order

/**
* Number of elements in a table to find k-sum problem solution on top of
*/
val NBase: Int = Math.pow(2, NStart.toDouble).toInt

/**
* Initial height since which table (`N` value) starting to increase by 5% per `IncreasePeriodForN` blocks
*/
val IncreaseStart: Height = 600 * 1024

/**
* Table size (`N`) increased every 50 * 1024 blocks
*/
val IncreasePeriodForN: Height = 50 * 1024

/**
* 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

/**
* Blake2b256 hash function invocation
* @param in - input bit-string
* @return - 256 bits (32 bytes) array
*/
def hash(in: Array[Byte]): Array[Byte] = Blake2b256.hash(in)

/**
* Convert byte array to unsigned integer
* @param in - byte array
* @return - unsigned integer
*/
def toBigInt(in: Array[Byte]): BigInt = BigInt(BigIntegers.fromUnsignedByteArray(in))

/**
* Constant data to be added to hash function to increase its calculation time
*/
val M: Array[Byte] = (0 until 1024).toArray.flatMap(i => Longs.toByteArray(i.toLong))

/**
* Calculates table size (N value) for a given height (moment of time)
*
* @see papers/yellow/pow/ErgoPow.tex for full description and test vectors
* @param headerHeight - height of a header to mine
* @return - N value
*/
def calcN(headerHeight: Height): Int = {
val height = Math.min(NIncreasementHeightMax, headerHeight)
if (height < IncreaseStart) {
NBase
} else {
val itersNumber = (height - IncreaseStart) / IncreasePeriodForN + 1
(1 to itersNumber).foldLeft(NBase) { case (step, _) =>
step / 100 * 105
}
}
}

def calcN(header: Header): Int = calcN(header.height)

/**
* Hash function that takes `m` and `nonceBytes` and returns a list of size `k` with numbers in
* [0,`N`)
*/
private def genIndexes(k: Int, seed: Array[Byte], N: Int): Seq[Int] = {
val hash = Blake2b256(seed)
val extendedHash = Bytes.concat(hash, hash.take(3))
(0 until k).map { i =>
BigInt(1, extendedHash.slice(i, i + 4)).mod(N).toInt
}
}.ensuring(_.length == k)

/**
* Generate element of Autolykos equation.
*/
private def genElementV2(indexBytes: Array[Byte], heightBytes: => Array[Byte]): BigInt = {
// Autolykos v. 2: H(j|h|M) (line 5 from the Algo 2 of the spec)
toBigInt(hash(Bytes.concat(indexBytes, heightBytes, M)).drop(1))
}

def hitForVersion2ForMessage(k: Int, msg: Array[Byte], nonce: Array[Byte], h: Array[Byte], N: Int): BigInt = {

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)
val seed = Bytes.concat(f, msg, nonce) // Autolykos v1, Alg. 2, line4:

val indexes = genIndexes(k, seed, N)
//pk and w not used in v2
val elems = indexes.map(idx => genElementV2(Ints.toByteArray(idx), h))
val f2 = elems.sum

// sum as byte array is always about 32 bytes
val array: Array[Byte] = BigIntegers.asUnsignedByteArray(32, f2.underlying())
val ha = hash(array)
toBigInt(ha)
}

/**
* Header digest ("message" for default GPU miners) a miner is working on
*/
def msgByHeader(h: Header): Array[Byte] = Blake2b256(h.serializeWithoutPoW.toArray)

/**
* Get hit for Autolykos v2 header (to test it then against PoW target)
*
* @param header - header to check PoW for
* @return PoW hit
*/
def hitForVersion2(header: Header): BigInt = {

val msg = msgByHeader(header)
val nonce = header.powNonce

val h = Ints.toByteArray(header.height) // used in AL v.2 only

val N = calcN(header)

hitForVersion2ForMessage(k, msg, nonce.toArray, h, N)
}

/**
* Get target `b` from encoded difficulty `nBits`
*/
def getB(nBits: Long): BigInt = {
q / NBitsUtils.decodeCompactBits(nBits)
}

/**
* 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
}

}
4 changes: 4 additions & 0 deletions docs/LangSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,10 @@ class Context {

/** Represents data of the block headers available in scripts. */
class Header {

/** Validate header's proof-of-work */
def checkPow: Boolean

/** Bytes representation of ModifierId of this Header */
def id: Coll[Byte]

Expand Down
Loading

0 comments on commit 2cd57e1

Please sign in to comment.