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

[6.0.0] Header.checkPow method #968

Merged
merged 21 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 = {
aslesarenko marked this conversation as resolved.
Show resolved Hide resolved

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
Loading