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] Conversion from Long-encoded nBits representation to BigInt and back #962

Open
wants to merge 28 commits into
base: v6.0.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0843902
DecodeNBitsMethod definition
kushti Apr 2, 2024
49dce78
ToNBits def
kushti Apr 2, 2024
1c1049e
merging w. 6.0.0
kushti Apr 3, 2024
37bb86c
toNBits added to SigmaDsl
kushti Apr 4, 2024
6f8981f
NBitsUtils
kushti Apr 4, 2024
d0f1b7d
unused import removed
kushti Apr 5, 2024
fcc7f0f
merging latest 5.0.14 commits
kushti Apr 6, 2024
6e47167
nbits impl
kushti Apr 9, 2024
9e4e098
failing roundtrip test for deserialization roundtrip
kushti Apr 10, 2024
1016323
importing method def from i675
kushti Apr 10, 2024
93748f1
removing decode nbits
kushti Apr 10, 2024
a3cb64d
versioned nbits serialization roundtrip test
kushti Apr 12, 2024
8941a8e
nbits evaluation test
kushti Apr 15, 2024
c8d75cd
Merge branch 'v6.0.0' of github.com:ScorexFoundation/sigmastate-inter…
kushti May 26, 2024
c9d0889
first pow check test
kushti May 26, 2024
299fdd5
merging w. nbits encoding PR
kushti May 26, 2024
a90e7be
pow check test passing
kushti May 26, 2024
37b23cd
hit <= diff
kushti May 29, 2024
aaa2aa9
Merge branch 'v6.0.0' of github.com:ScorexFoundation/sigmastate-inter…
kushti Jul 31, 2024
a20c04f
moving to Global, finalizing costing
kushti Jul 31, 2024
2c8df31
updating collectMethods
kushti Jul 31, 2024
6981c34
fixing JS test
kushti Jul 31, 2024
d0eef4c
merging w. 6.0.0
kushti Sep 30, 2024
6128fbd
fixing ErgoTreeSpec
kushti Oct 1, 2024
ee25e40
merging w. 6.0.0
kushti Oct 1, 2024
e4a611d
fixing MethodCallSerializerSpecification
kushti Oct 4, 2024
c5c37ff
fixing JS reflection
kushti Oct 4, 2024
e87ad02
Merge branch 'v6.0.0' of github.com:ScorexFoundation/sigmastate-inter…
kushti Oct 8, 2024
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
14 changes: 14 additions & 0 deletions core/shared/src/main/scala/sigma/SigmaDsl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,20 @@ trait SigmaDslBuilder {
*/
def groupGenerator: GroupElement

/**
* @return big integer provided as input approximately encoded using NBits,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit confusing comment, says "@return big integer" while the actual return type is Long.

* see (https://bitcoin.stackexchange.com/questions/57184/what-does-the-nbits-value-represent)
* for format details
*/
def encodeNbits(bi: BigInt): Long

/**
* @return big integer decoded from NBits value provided,
* see (https://bitcoin.stackexchange.com/questions/57184/what-does-the-nbits-value-represent)
* for format details
*/
def decodeNbits(l: Long): BigInt

/**
* Transforms serialized bytes of ErgoTree with segregated constants by replacing constants
* at given positions with new values. This operation allow to use serialized scripts as
Expand Down
2 changes: 1 addition & 1 deletion core/shared/src/main/scala/sigma/ast/SType.scala
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ case object SLong extends SPrimType with SEmbeddable with SNumericType with SMon
}
}

/** Type of 256 bit integet values. Implemented using [[java.math.BigInteger]]. */
/** Type of 256 bit integer values. Implemented using [[java.math.BigInteger]]. */
case object SBigInt extends SPrimType with SEmbeddable with SNumericType with SMonoType {
override type WrappedType = BigInt
override val typeCode: TypeCode = 6: Byte
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,12 @@ object ReflectionData {
},
mkMethod(clazz, "decodePoint", Array[Class[_]](cColl)) { (obj, args) =>
obj.asInstanceOf[SigmaDslBuilder].decodePoint(args(0).asInstanceOf[Coll[Byte]])
},
mkMethod(clazz, "encodeNbits", Array[Class[_]](classOf[BigInt])) { (obj, args) =>
obj.asInstanceOf[SigmaDslBuilder].encodeNbits(args(0).asInstanceOf[BigInt])
},
mkMethod(clazz, "decodeNbits", Array[Class[_]](classOf[Long])) { (obj, args) =>
obj.asInstanceOf[SigmaDslBuilder].decodeNbits(args(0).asInstanceOf[Long])
}
)
)
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
}
}
}

}
71 changes: 49 additions & 22 deletions data/shared/src/main/scala/sigma/ast/methods.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ package sigma.ast

import org.ergoplatform._
import org.ergoplatform.validation._
import sigma._
import sigma.{VersionContext, _}
import sigma.ast.SCollection.{SBooleanArray, SBoxArray, SByteArray, SByteArray2, SHeaderArray}
import sigma.ast.SMethod.{MethodCallIrBuilder, MethodCostFunc, javaMethodOf}
import sigma.ast.SType.TypeCode
import sigma.ast.syntax.{SValue, ValueOps}
import sigma.data.OverloadHack.Overloaded1
import sigma.data.{DataValueComparer, KeyValueColl, Nullable, RType, SigmaConstants}
import sigma.data.{CBigInt, DataValueComparer, KeyValueColl, Nullable, RType, SigmaConstants}
import sigma.eval.{CostDetails, ErgoTreeEvaluator, TracedCost}
import sigma.reflection.RClass
import sigma.serialization.CoreByteWriter.ArgInfo
import sigma.util.NBitsUtils
import sigma.utils.SparseArrayContainer

import scala.annotation.unused
Expand Down Expand Up @@ -309,13 +310,6 @@ case object SBigIntMethods extends SNumericTypeMethods {
/** Type for which this container defines methods. */
override def ownerType: SMonoType = SBigInt

final val ToNBitsCostInfo = OperationCostInfo(
FixedCost(JitCost(5)), NamedDesc("NBitsMethodCall"))

//id = 8 to make it after toBits
val ToNBits = SMethod(this, "nbits", SFunc(this.ownerType, SLong), 8, ToNBitsCostInfo.costKind)
.withInfo(ModQ, "Encode this big integer value as NBits")

/** The following `modQ` methods are not fully implemented in v4.x and this descriptors.
* This descritors are remain here in the code and are waiting for full implementation
* is upcoming soft-forks at which point the cost parameters should be calculated and
Expand All @@ -333,7 +327,7 @@ case object SBigIntMethods extends SNumericTypeMethods {

protected override def getMethods(): Seq[SMethod] = {
if (VersionContext.current.isV6SoftForkActivated) {
super.getMethods() ++ Seq(ToNBits)
super.getMethods()
// ModQMethod,
// PlusModQMethod,
// MinusModQMethod,
Expand All @@ -343,14 +337,6 @@ case object SBigIntMethods extends SNumericTypeMethods {
super.getMethods()
}
}

/**
*
*/
def nbits_eval(mc: MethodCall, bi: sigma.BigInt)(implicit E: ErgoTreeEvaluator): Long = {
???
}

}

/** Methods of type `String`. */
Expand Down Expand Up @@ -1519,9 +1505,50 @@ case object SGlobalMethods extends MonoTypeMethods {
Xor.xorWithCosting(ls, rs)
}

protected override def getMethods() = super.getMethods() ++ Seq(
groupGeneratorMethod,
xorMethod
)
private lazy val EnDecodeNBitsCost = FixedCost(JitCost(5)) // the same cost for nbits encoding and decoding
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cost is too low. And encoding is cheaper than decoding (because of array creation).
There is a profiler in LSV5 (see the comments and afterAll method).
I suggest at least 2 for encoding and 40 for decoding.


lazy val encodeNBitsMethod: SMethod = SMethod(
this, "encodeNbits", SFunc(Array(SGlobal, SBigInt), SLong), 3, EnDecodeNBitsCost)
.withIRInfo(MethodCallIrBuilder)
.withInfo(MethodCall, "Encode big integer number as nbits", ArgInfo("bigInt", "Big integer"))

lazy val decodeNBitsMethod: SMethod = SMethod(
this, "decodeNbits", SFunc(Array(SGlobal, SLong), SBigInt), 4, EnDecodeNBitsCost)
.withIRInfo(MethodCallIrBuilder)
.withInfo(MethodCall, "Decode nbits-encoded big integer number", ArgInfo("nbits", "NBits-encoded argument"))

/**
* encodeNBits evaluation with costing
*/
def encodeNbits_eval(mc: MethodCall, G: SigmaDslBuilder, bigInt: BigInt)(implicit E: ErgoTreeEvaluator): Long = {
E.addFixedCost(EnDecodeNBitsCost, encodeNBitsMethod.opDesc) {
NBitsUtils.encodeCompactBits(bigInt.asInstanceOf[CBigInt].wrappedValue)
}
}

/**
* decodeNBits evaluation with costing
*/
def decodeNbits_eval(mc: MethodCall, G: SigmaDslBuilder, l: Long)(implicit E: ErgoTreeEvaluator): BigInt = {
E.addFixedCost(EnDecodeNBitsCost, decodeNBitsMethod.opDesc) {
CBigInt(NBitsUtils.decodeCompactBits(l).bigInteger)
}
}

protected override def getMethods() = {
if (VersionContext.current.isV6SoftForkActivated) {
super.getMethods() ++ Seq(
groupGeneratorMethod,
xorMethod,
encodeNBitsMethod,
decodeNBitsMethod
)
} else {
super.getMethods() ++ Seq(
groupGeneratorMethod,
xorMethod
)
}
}
}

9 changes: 9 additions & 0 deletions data/shared/src/main/scala/sigma/data/CSigmaDslBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import sigma.crypto.{CryptoConstants, EcPointType, Ecp}
import sigma.eval.Extensions.EvalCollOps
import sigma.serialization.{GroupElementSerializer, SigmaSerializer}
import sigma.util.Extensions.BigIntegerOps
import sigma.util.NBitsUtils
import sigma.validation.SigmaValidationSettings
import sigma.{AvlTree, BigInt, Box, Coll, CollBuilder, GroupElement, SigmaDslBuilder, SigmaProp, VersionContext}

Expand Down Expand Up @@ -175,6 +176,14 @@ class CSigmaDslBuilder extends SigmaDslBuilder { dsl =>

override def groupGenerator: GroupElement = _generatorElement

def encodeNbits(bi: BigInt): Long = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

override missing

NBitsUtils.encodeCompactBits(bi.asInstanceOf[CBigInt].wrappedValue)
}

def decodeNbits(l: Long): BigInt = {
CBigInt(NBitsUtils.decodeCompactBits(l).bigInteger)
}

/**
* @return the identity of the Dlog group used in ErgoTree
*/
Expand Down
4 changes: 2 additions & 2 deletions data/shared/src/main/scala/sigma/eval/ErgoTreeEvaluator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ abstract class ErgoTreeEvaluator {
* @param opDesc the operation descriptor to associate the cost with (when costTracingEnabled)
* @param block operation executed under the given cost
*/
def addFixedCost(costKind: FixedCost, opDesc: OperationDesc)(block: => Unit): Unit
def addFixedCost[R](costKind: FixedCost, opDesc: OperationDesc)(block: => R): R

def addFixedCost(costInfo: OperationCostInfo[FixedCost])(block: => Unit): Unit
def addFixedCost[R](costInfo: OperationCostInfo[FixedCost])(block: => R): R

/** Adds the given cost to the `coster`. If tracing is enabled, creates a new cost item
* with the given operation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ class CErgoTreeEvaluator(
}

/** @hotspot don't beautify the code */
override def addFixedCost(costKind: FixedCost, opDesc: OperationDesc)(block: => Unit): Unit = {
override def addFixedCost[R](costKind: FixedCost, opDesc: OperationDesc)(block: => R): R = {
var costItem: FixedCostItem = null
if (settings.costTracingEnabled) {
costItem = FixedCostItem(opDesc, costKind)
Expand All @@ -305,16 +305,17 @@ class CErgoTreeEvaluator(
}
val start = System.nanoTime()
coster.add(costKind.cost)
val _ = block
val res = block
val end = System.nanoTime()
profiler.addCostItem(costItem, end - start)
res
} else {
coster.add(costKind.cost)
block
}
}

override def addFixedCost(costInfo: OperationCostInfo[FixedCost])(block: => Unit): Unit = {
override def addFixedCost[R](costInfo: OperationCostInfo[FixedCost])(block: => R): R = {
addFixedCost(costInfo.costKind, costInfo.opDesc)(block)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,34 @@ class MethodCallSerializerSpecification extends SerializationSpecification {
roundTripTest(expr)
}

property("MethodCall deserialization round trip for BigInt.nbits") {
property("MethodCall deserialization round trip for Global.encodeNBits") {
def code = {
val bi = BigIntConstant(5)
val expr = MethodCall(bi,
SBigIntMethods.ToNBits,
Vector(),
val expr = MethodCall(Global,
SGlobalMethods.encodeNBitsMethod,
Vector(bi),
Map()
)
roundTripTest(expr)
}

// should be ok
VersionContext.withVersions(VersionContext.V6SoftForkVersion, 1) {
code
}

an[ValidationException] should be thrownBy (
VersionContext.withVersions((VersionContext.V6SoftForkVersion - 1).toByte, 1) {
code
})
}

property("MethodCall deserialization round trip for Global.decodeNBits") {
def code = {
val l = LongConstant(5)
val expr = MethodCall(Global,
SGlobalMethods.decodeNBitsMethod,
Vector(l),
Map()
)
roundTripTest(expr)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ package sigmastate.eval

import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import sigma.ast.{BigIntConstant, ErgoTree, Global, JitCost, MethodCall, SBigIntMethods, SGlobalMethods}
import sigma.crypto.SecP256K1Group
import sigma.data.{CSigmaDslBuilder => SigmaDsl, TrivialProp}
import sigma.data.{CBigInt, TrivialProp}
import sigma.eval.SigmaDsl
import sigma.util.Extensions.SigmaBooleanOps
import sigma.util.NBitsUtils

import java.math.BigInteger
import sigma.{ContractsTestkit, SigmaProp}
import sigmastate.interpreter.{CErgoTreeEvaluator, CostAccumulator}
import sigmastate.interpreter.CErgoTreeEvaluator.DefaultProfiler

import scala.language.implicitConversions

Expand Down Expand Up @@ -63,4 +68,27 @@ class BasicOpsTests extends AnyFunSuite with ContractsTestkit with Matchers {
box.creationInfo._1 shouldBe a [Integer]
}

/**
* Checks BigInt.nbits evaluation for SigmaDSL as well as AST interpreter (MethodCall) layers
*/
test("nbits evaluation") {
SigmaDsl.encodeNbits(CBigInt(BigInteger.valueOf(0))) should be
(NBitsUtils.encodeCompactBits(0))

val es = CErgoTreeEvaluator.DefaultEvalSettings
val accumulator = new CostAccumulator(
initialCost = JitCost(0),
costLimit = Some(JitCost.fromBlockCost(es.scriptCostLimitInEvaluator)))
val evaluator = new CErgoTreeEvaluator(
context = null,
constants = ErgoTree.EmptyConstants,
coster = accumulator, DefaultProfiler, es)

val res = MethodCall(Global, SGlobalMethods.encodeNBitsMethod, IndexedSeq(BigIntConstant(BigInteger.valueOf(0))), Map.empty)
.evalTo[Long](Map.empty)(evaluator)

res should be (NBitsUtils.encodeCompactBits(0))

}

}
Loading
Loading