Skip to content
Open
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
5 changes: 0 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,3 @@
- Follow existing test patterns in similar files
- Type annotations for public methods
- Prefer immutable data structures and functional patterns

## Development Restrictions
- **Code Changes**: Only modify code in `src/test/` folders
- **Production Code**: Do not touch production code in `src/main/` directories
- **Test Focus**: All development work should be test-related only
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import scorex.crypto.authds.LeafData
import scorex.crypto.authds.merkle.{BatchMerkleProof, Leaf, MerkleProof, MerkleTree}
import scorex.crypto.hash.Digest32
import scorex.util.ModifierId
import scala.annotation.nowarn
import scala.collection.mutable
/**
* Extension block section with header id not provided
Expand Down Expand Up @@ -38,20 +37,18 @@ class ExtensionCandidate(val fields: Seq[(Array[Byte], Array[Byte])]) {
.flatMap(kv => merkleTree.proofByElement(Leaf[Digest32](LeafData @@ kv)(Algos.hash)))

/**
* Constructs BatchMerkleProof for a list of interlinks
* Note - only accounts for interlink vector fields in the extension
* Constructs a BatchMerkleProof for the requested extension fields
*
* @param keys - array of 2-byte keys
* @return BatchMerkleProof or None if keys not found
*/
@nowarn
def batchProofFor(keys: Array[Byte]*): Option[BatchMerkleProof[Digest32]] = {
val indices = keys.flatMap(key => fields.find(_._1 sameElements key)
.map(Extension.kvToLeaf)
.map(kv => Leaf[Digest32](LeafData @@ kv)(Algos.hash).hash)
.flatMap(leafData => interlinksMerkleTree.elementsHashIndex.get(
.flatMap(leafData => merkleTree.elementsHashIndex.get(
new mutable.WrappedArray.ofByte(leafData))))
if (indices.isEmpty) None else interlinksMerkleTree.proofByIndices(indices)(Algos.hash)
if (indices.isEmpty) None else merkleTree.proofByIndices(indices)(Algos.hash)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ class NipopowAlgos(val chainSettings: ChainSettings) {
*/
def maxLevelOf(header: Header): Int =
if (!header.isGenesis) {
val requiredTarget = org.ergoplatform.mining.q / DifficultySerializer.decodeCompactBits(header.nBits)
val decodedDifficulty = DifficultySerializer.decodeCompactBits(header.nBits)
require(decodedDifficulty > 0, "Decoded difficulty target must be positive")
val requiredTarget = org.ergoplatform.mining.q / decodedDifficulty
val realTarget = powScheme.powHit(header).doubleValue
val level = log2(requiredTarget.doubleValue) - log2(realTarget.doubleValue)
level.toInt
Expand Down Expand Up @@ -98,7 +100,7 @@ class NipopowAlgos(val chainSettings: ChainSettings) {
* end function
*/
def bestArg(chain: Seq[Header])(m: Int): Int = {
require(m >= 1, s"$m < 1")
PoPowParams.requireValidM(m)

@scala.annotation.tailrec
def loop(level: Int, acc: Seq[(Int, Int)] = Seq.empty): Seq[(Int, Int)] =
Expand Down Expand Up @@ -134,6 +136,7 @@ class NipopowAlgos(val chainSettings: ChainSettings) {
val k = params.k
val m = params.m

PoPowParams.requireValid(m, k)
require(chain.lengthCompare(k + m) >= 0, s"Can not prove chain of size < ${k + m}")
require(chain.head.header.isGenesis, "Can not prove non-anchored chain")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ case class NipopowProof(popowAlgos: NipopowAlgos,
*/
def isBetterThan(that: NipopowProof): Boolean = {
try {
if (this.isValid && that.isValid) {
if (this.m != that.m || this.k != that.k) {
false
} else if (this.isValid && that.isValid) {
popowAlgos.lowestCommonAncestor(headersChain, that.headersChain)
.map(h => headersChain.filter(_.height > h.height) -> that.headersChain.filter(_.height > h.height))
.exists({ case (thisDivergingChain, thatDivergingChain) =>
Expand All @@ -72,14 +74,22 @@ case class NipopowProof(popowAlgos: NipopowAlgos,
* @return true if the proof is valid
*/
lazy val isValid: Boolean = {
PoPowParams.isValid(m, k) &&
this.hasValidParams &&
this.hasValidConnections &&
this.hasValidHeights &&
this.hasValidProofs &&
this.hasValidDifficultyHeaders &&
this.hasValidPow
}

/**
* Checks proof parameters and the exact suffix cardinality before any
* parameter-dependent scoring or validation work.
*/
lazy val hasValidParams: Boolean = {
PoPowParams.areValid(m, k) && suffixTail.lengthCompare(k - 1) == 0
}

/**
* @return true if proof contains headers needed to check difficulty after the suffix,
* or if the proof is for non-continuous mode, false otherwise
Expand Down Expand Up @@ -192,9 +202,27 @@ object NipopowProof {

}

object NipopowProofSerializer {
private val MaxProofElements = PoPowParams.MaxProofElements

private def requireWithinLimit(value: Int, limit: Int, what: String): Unit =
require(value <= limit, s"$what $value exceeds sanity limit $limit")

private def readLengthPrefixed[T](r: Reader,
limit: Int,
what: String)
(parse: Array[Byte] => T): T = {
val declaredLength = r.getUInt().toIntExact
requireWithinLimit(declaredLength, limit, s"$what length")
parse(r.getBytes(declaredLength))
}
}

class NipopowProofSerializer(poPowAlgos: NipopowAlgos) extends ErgoSerializer[NipopowProof] {
import NipopowProofSerializer._

override def serialize(obj: NipopowProof, w: Writer): Unit = {
require(obj.hasValidParams, "Invalid NiPoPoW proof parameters or suffix length")
w.putUInt(obj.m.toLong)
w.putUInt(obj.k.toLong)
w.putUInt(obj.prefix.size.toLong)
Expand All @@ -218,19 +246,27 @@ class NipopowProofSerializer(poPowAlgos: NipopowAlgos) extends ErgoSerializer[Ni
override def parse(r: Reader): NipopowProof = {
val m = r.getUInt().toIntExact
val k = r.getUInt().toIntExact
PoPowParams.requireValid(m, k)
val prefixSize = r.getUInt().toIntExact
requireWithinLimit(prefixSize, MaxProofElements, "prefix count")
val prefix = (0 until prefixSize).map { _ =>
val size = r.getUInt().toIntExact
PoPowHeaderSerializer.parseBytes(r.getBytes(size))
readLengthPrefixed(r, PoPowHeaderSerializer.MaxSerializedBytes, "prefix element")(
PoPowHeaderSerializer.parseBytes)
}
val suffixHeadSize = r.getUInt().toIntExact
val suffixHead = PoPowHeaderSerializer.parseBytes(r.getBytes(suffixHeadSize))
val suffixHead = readLengthPrefixed(r, PoPowHeaderSerializer.MaxSerializedBytes, "suffix head")(
PoPowHeaderSerializer.parseBytes)
val suffixSize = r.getUInt().toIntExact
requireWithinLimit(suffixSize, MaxProofElements, "suffix count")
require(suffixSize == k - 1,
s"NiPoPoW suffix length ${suffixSize + 1} does not match k parameter $k")
val suffixTail = (0 until suffixSize).map { _ =>
val size = r.getUInt().toIntExact
HeaderSerializer.parseBytes(r.getBytes(size))
readLengthPrefixed(r, PoPowHeaderSerializer.MaxHeaderBytes, "suffix tail")(
HeaderSerializer.parseBytes)
}
val continuous = if (r.getByte() == 1) true else false
val continuousByte = r.getByte()
require(continuousByte == 0 || continuousByte == 1,
s"invalid NiPoPoW continuous mode byte ${continuousByte & 0xff}")
val continuous = continuousByte == 1
NipopowProof(poPowAlgos, m, k, prefix, suffixHead, suffixTail, continuous)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
package org.ergoplatform.modifiers.history.popow

import cats.syntax.either._
import sigmastate.utils.Helpers._
import cats.Traverse
import cats.implicits.{catsStdInstancesForEither, catsStdInstancesForList}
import io.circe.{Decoder, Encoder, Json}
import org.ergoplatform.core.BytesSerializable
import org.ergoplatform.modifiers.ErgoFullBlock
import org.ergoplatform.modifiers.history.extension.Extension.merkleTree
import org.ergoplatform.modifiers.history.extension.Extension
import org.ergoplatform.modifiers.history.header.{Header, HeaderSerializer}
import org.ergoplatform.settings.Algos
import org.ergoplatform.settings.Algos.HF
import org.ergoplatform.serialization.ErgoSerializer
import scorex.crypto.authds.Side
import scorex.crypto.authds.merkle.BatchMerkleProof
import scorex.crypto.authds.{LeafData, Side}
import scorex.crypto.authds.merkle.{BatchMerkleProof, Leaf}
import scorex.crypto.authds.merkle.serialization.BatchMerkleProofSerializer
import scorex.crypto.hash.Digest32
import scorex.util.Extensions._
import scorex.util.serialization.{Reader, Writer}
import scorex.util.{ModifierId, bytesToId, idToBytes}

import java.nio.ByteBuffer
import scala.util.Try

/**
Expand All @@ -42,7 +42,16 @@ case class PoPowHeader(header: Header,

def height: Int = header.height

def checkInterlinksProof(): Boolean = PoPowHeader.checkInterlinksProof(interlinks, interlinksProof)
def checkInterlinksProof(): Boolean = {
val proofIsEmpty = interlinksProof.indices.isEmpty && interlinksProof.proofs.isEmpty
if (header.isGenesis) {
interlinks.isEmpty && proofIsEmpty
} else if (!PoPowHeader.hasCanonicalInterlinkRuns(interlinks)) {
false
} else {
PoPowHeader.checkInterlinksProof(interlinks, interlinksProof, header.extensionRoot)
}
}
}

object PoPowHeader {
Expand All @@ -51,16 +60,61 @@ object PoPowHeader {

implicit val hf: HF = Algos.hash

private[popow] def hasCanonicalInterlinkRuns(interlinks: Seq[ModifierId]): Boolean = {
interlinks.headOption.exists { first =>
var current = first
var runLength = 1
var closedRuns = Set.empty[ModifierId]

interlinks.iterator.zipWithIndex.drop(1).forall { case (interlink, position) =>
if (interlink == current) {
if (runLength == 255) {
false
} else {
runLength += 1
true
}
} else if (position > 255) {
false
} else {
closedRuns += current
if (closedRuns.contains(interlink)) {
false
} else {
current = interlink
runLength = 1
true
}
}
}
}
}

/**
* Validates interlinks merkle root against provided proof
* Validates the exact packed interlink leaves against the full extension root
*/
def checkInterlinksProof(interlinks: Seq[ModifierId], proof: BatchMerkleProof[Digest32]): Boolean = {
if (interlinks.isEmpty && proof.indices.isEmpty && proof.proofs.isEmpty) {
true
def checkInterlinksProof(interlinks: Seq[ModifierId],
proof: BatchMerkleProof[Digest32],
extensionRoot: Digest32): Boolean = {
if (!PoPowHeaderSerializer.hasValidMerkleProofStructure(
proof.indices.map(_._1),
proof.proofs.size
)) {
false
} else {
val fields = NipopowAlgos.packInterlinks(interlinks)
val tree = merkleTree(fields)
proof.valid(tree.rootHash)
Try {
val expectedLeafHashes = NipopowAlgos.packInterlinks(interlinks)
.map(Extension.kvToLeaf)
.map(kv => Leaf[Digest32](LeafData @@ kv)(Algos.hash).hash)
val provenLeafHashes = proof.indices.map(_._2)

interlinks.nonEmpty &&
expectedLeafHashes.size == provenLeafHashes.size &&
expectedLeafHashes.zip(provenLeafHashes).forall { case (expected, proven) =>
expected sameElements proven
} &&
proof.valid(extensionRoot)
}.getOrElse(false)
}
}

Expand Down Expand Up @@ -142,9 +196,87 @@ object PoPowHeader {
object PoPowHeaderSerializer extends ErgoSerializer[PoPowHeader] {
import org.ergoplatform.sdk.wallet.Constants.ModifierIdLength

// Generous wire sanity limits shared with the sigma-rust NiPoPoW parser.
private[ergoplatform] final val MaxHeaderBytes = 10000
private[ergoplatform] final val MaxInterlinks = 10000
private[ergoplatform] final val MaxMerkleProofBytes = 1000000
private[ergoplatform] final val MaxSerializedBytes =
MaxHeaderBytes + MaxInterlinks * ModifierIdLength + MaxMerkleProofBytes + 64

private val MerkleProofCountBytes = 8
private val MerkleIndexBytes = 36
private val MerkleProofNodeBytes = 33
private[ergoplatform] final val MaxMerkleProofDepth =
Extension.FieldKeySize * java.lang.Byte.SIZE
private val MaxMerkleLeafIndex = (1 << MaxMerkleProofDepth) - 1

implicit val hf: HF = Algos.hash
val merkleProofSerializer = new BatchMerkleProofSerializer[Digest32, HF]

private def requireWithinLimit(value: Int, limit: Int, what: String): Unit =
require(value <= limit, s"$what $value exceeds sanity limit $limit")

private[ergoplatform] def hasValidMerkleProofStructure(indices: Seq[Int],
proofCount: Int): Boolean = {
if (indices.isEmpty) {
proofCount == 0
} else if (proofCount < 0 ||
indices.exists(index => index < 0 || index > MaxMerkleLeafIndex) ||
indices.distinct.size != indices.size) {
false
} else {
var current = indices.sorted.toVector
var remainingProofs = proofCount
var depth = 0
var valid = true

while (valid && !(current.size == 1 && current.head == 0 && remainingProofs == 0) &&
depth < MaxMerkleProofDepth) {
val currentIndices = current.toSet
val missingSiblings = current.count(index => !currentIndices.contains(index ^ 1))
if (missingSiblings > remainingProofs) {
valid = false
} else {
remainingProofs -= missingSiblings
current = current.map(_ / 2).distinct
depth += 1
}
}

valid && current.size == 1 && current.head == 0 && remainingProofs == 0
}
}

private def validateMerkleProofPayload(bytes: Array[Byte]): Unit = {
require(bytes.length >= MerkleProofCountBytes,
s"Merkle proof counts require at least $MerkleProofCountBytes bytes")
// BatchMerkleProofSerializer stores both counts as fixed-width big-endian ints.
val counts = ByteBuffer.wrap(bytes)
val indexCount = counts.getInt
val proofCount = counts.getInt
require(indexCount >= 0 && proofCount >= 0,
"Merkle proof counts must be non-negative")

val indexBytes = Math.multiplyExact(indexCount.toLong, MerkleIndexBytes.toLong)
val proofBytes = Math.multiplyExact(proofCount.toLong, MerkleProofNodeBytes.toLong)
val requiredBytes = Math.addExact(
MerkleProofCountBytes.toLong,
Math.addExact(indexBytes, proofBytes)
)
require(requiredBytes == bytes.length.toLong,
s"Merkle proof counts require $requiredBytes bytes, payload has ${bytes.length}")

val indices = Vector.newBuilder[Int]
var index = 0
while (index < indexCount) {
indices += counts.getInt
counts.position(counts.position() + MerkleIndexBytes - Integer.BYTES)
index += 1
}
require(hasValidMerkleProofStructure(indices.result(), proofCount),
"Merkle proof structure exceeds the extension key space")
}

override def serialize(obj: PoPowHeader, w: Writer): Unit = {
val headerBytes = obj.header.bytes
w.putUInt(headerBytes.length.toLong)
Expand All @@ -157,12 +289,17 @@ object PoPowHeaderSerializer extends ErgoSerializer[PoPowHeader] {
}

override def parse(r: Reader): PoPowHeader = {
val headerSize = r.getUInt().toIntExact
val header = HeaderSerializer.parseBytes(r.getBytes(headerSize))
val headerLength = r.getUInt().toIntExact
requireWithinLimit(headerLength, MaxHeaderBytes, "header length")
val header = HeaderSerializer.parseBytes(r.getBytes(headerLength))
val linksQty = r.getUInt().toIntExact
requireWithinLimit(linksQty, MaxInterlinks, "interlink count")
val interlinks = (0 until linksQty).map(_ => bytesToId(r.getBytes(ModifierIdLength)))
val interlinksProofSize = r.getUInt().toIntExact
val interlinksProof = merkleProofSerializer.deserialize(r.getBytes(interlinksProofSize)).get
val interlinksProofLength = r.getUInt().toIntExact
requireWithinLimit(interlinksProofLength, MaxMerkleProofBytes, "Merkle proof length")
val proofBytes = r.getBytes(interlinksProofLength)
validateMerkleProofPayload(proofBytes)
val interlinksProof = merkleProofSerializer.deserialize(proofBytes).get
PoPowHeader(header, interlinks, interlinksProof)
}

Expand Down
Loading
Loading