Implemented general payout curves (#3854)

* Implemented general payout curves including hyperbola curve piece support

WIP

Get GUI compiling

* WIP

* Get backward compatible serialization working, everything is equivalent except for in memory Scala data structures

* Get numeric contracts backward compatible

* Get rest of codebase compiling

* Add test case for old contract info

* Add sanity test vector for old enum contract descriptors

* Fix docs

* Remove comment

* Scalafmt

* Add DLCSerializationVersion, replace isOldSerialization

* Remove ValueIterator.takeNoSkip()

* Fix docs

Co-authored-by: nkohen <nadavk25@gmail.com>
This commit is contained in:
Chris Stewart 2022-01-05 07:07:22 -06:00 committed by GitHub
parent a7af8cac4c
commit 8c5288d758
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1035 additions and 382 deletions

View File

@ -1,13 +1,14 @@
package org.bitcoins.commons.jsonmodels.cli
import org.bitcoins.commons.serializers.Picklers
import org.bitcoins.core.protocol.dlc.models.DLCPayoutCurve
import org.bitcoins.core.protocol.tlv.{
ContractDescriptorTLV,
ContractDescriptorV0TLV,
ContractDescriptorV1TLV,
DLCSerializationVersion,
DigitDecompositionEventDescriptorV0TLV,
OracleAnnouncementTLV,
PayoutFunctionV0TLV,
RoundingIntervalsV0TLV,
TLVPoint
}
@ -26,12 +27,16 @@ object ContractDescriptorParser {
//we read the number of digits from the announcement,
//take in tlv points for the payout curve
//and don't provide access to give a rounding mode as a parameter
val payoutPoints = arr.value.toVector.map { pointJs =>
val payoutPoints: Vector[TLVPoint] = arr.value.toVector.map { pointJs =>
upickle.default
.read[TLVPoint](pointJs)(Picklers.tlvPointReader)
}
val payoutCurve = PayoutFunctionV0TLV(payoutPoints)
val payoutCurve = DLCPayoutCurve
.fromPoints(payoutPoints,
serializationVersion =
DLCSerializationVersion.Post144Pre163)
.toTLV
val numDigits = announcementTLV.eventTLV.eventDescriptor
.asInstanceOf[DigitDecompositionEventDescriptorV0TLV]
.numDigits

View File

@ -570,8 +570,7 @@ object Picklers {
val outcome = map(PicklerKeys.outcomeKey).num.toLong
val payout = jsToSatoshis(map(PicklerKeys.payoutKey))
val extraPrecision = map(PicklerKeys.extraPrecisionKey).num.toInt
val isEndPoint = map(PicklerKeys.isEndpointKey).bool
TLVPoint(outcome, payout, extraPrecision, isEndPoint)
TLVPoint(outcome, payout, extraPrecision)
}
}
@ -580,17 +579,14 @@ object Picklers {
Obj(
PicklerKeys.outcomeKey -> Num(point.outcome.toDouble),
PicklerKeys.payoutKey -> Num(point.value.toLong.toDouble),
PicklerKeys.extraPrecisionKey -> Num(point.extraPrecision.toDouble),
PicklerKeys.isEndpointKey -> Bool(point.isEndpoint)
PicklerKeys.extraPrecisionKey -> Num(point.extraPrecision.toDouble)
)
}
}
implicit val payoutFunctionV0TLVWriter: Writer[PayoutFunctionV0TLV] =
writer[Obj].comap { payoutFunc =>
import payoutFunc._
val pointsJs = points.map { point =>
val pointsJs = payoutFunc.endpoints.map { point =>
writeJs(point)
}
@ -608,8 +604,11 @@ object Picklers {
upickle.default.read[TLVPoint](obj)
}.toVector
PayoutFunctionV0TLV(points)
DLCPayoutCurve
.fromPoints(points,
serializationVersion =
DLCSerializationVersion.Post144Pre163)
.toTLV
}
}

View File

@ -458,20 +458,24 @@ class CreateDLCOfferDialog
throw new RuntimeException(
"Got incompatible contract info and announcement")
case descriptor: ContractDescriptorV1TLV =>
descriptor.payoutFunction.points.init.foreach { point =>
addPointRow(
xOpt = Some(numberFormatter.format(point.outcome)),
yOpt = Some(numberFormatter.format(point.value.toLong)),
isEndPoint = point.isEndpoint)
}
descriptor.payoutFunction.piecewisePolynomialEndpoints.init
.foreach { point =>
addPointRow(
xOpt = Some(numberFormatter.format(point.outcome)),
yOpt = Some(
numberFormatter.format(point.payout.toLongExact)),
isEndPoint = point.isEndpoint)
}
// handle last specially so user can add more rows
val last = descriptor.payoutFunction.points.last
val last =
descriptor.payoutFunction.piecewisePolynomialEndpoints.last
addPointRow(xOpt = Some(numberFormatter.format(last.outcome)),
yOpt =
Some(numberFormatter.format(last.value.toLong)),
isEndPoint = last.isEndpoint,
row = 9999)
addPointRow(
xOpt = Some(numberFormatter.format(last.outcome)),
yOpt =
Some(numberFormatter.format(last.payout.toLongExact)),
isEndPoint = last.isEndpoint,
row = 9999)
nextPointRow -= 1 // do this so the max is the last row
// add rounding intervals
@ -629,8 +633,12 @@ object CreateDLCOfferDialog {
if (xTF.text.value.nonEmpty && yTF.text.value.nonEmpty) {
val x = numberFormatter.parse(xTF.text.value).longValue()
val y = numberFormatter.parse(yTF.text.value).longValue()
Some(
OutcomePayoutPoint(x, Satoshis(y), checkBox.selected.value))
val point = if (checkBox.selected.value) {
PiecewisePolynomialMidpoint(x, y)
} else {
PiecewisePolynomialEndpoint(x, y)
}
Some(point)
} else {
None
}
@ -656,7 +664,9 @@ object CreateDLCOfferDialog {
require(sorted == outcomesValuePoints,
s"Must be sorted by outcome, got $outcomesValuePoints")
val func = DLCPayoutCurve(outcomesValuePoints)
val func = DLCPayoutCurve.polynomialInterpolate(
outcomesValuePoints,
serializationVersion = DLCSerializationVersion.Post144Pre163)
(totalCollateral,
NumericContractDescriptor(
func,

View File

@ -5,9 +5,10 @@ import org.bitcoins.core.protocol.dlc.compute.CETCalculator
import org.bitcoins.core.protocol.dlc.compute.CETCalculator._
import org.bitcoins.core.protocol.dlc.models.{
DLCPayoutCurve,
OutcomePayoutPoint,
PiecewisePolynomialPoint,
RoundingIntervals
}
import org.bitcoins.core.protocol.tlv.DLCSerializationVersion
import org.bitcoins.testkitcore.gen.NumberGenerator
import org.bitcoins.testkitcore.util.BitcoinSUnitTest
import org.scalacheck.Gen
@ -23,20 +24,22 @@ class CETCalculatorTest extends BitcoinSUnitTest {
private val baseGen: Gen[Int] = Gen.choose(2, 256)
it should "correctly split into ranges" in {
val func = DLCPayoutCurve(
val func = DLCPayoutCurve.polynomialInterpolate(
Vector(
OutcomePayoutPoint(0, Satoshis(-1000), isEndpoint = true),
OutcomePayoutPoint(10, Satoshis(-1000), isEndpoint = true),
OutcomePayoutPoint(20, Satoshis(0), isEndpoint = false),
OutcomePayoutPoint(30, Satoshis(3000), isEndpoint = true),
OutcomePayoutPoint(40, Satoshis(4000), isEndpoint = true),
OutcomePayoutPoint(50, Satoshis(4000), isEndpoint = true),
OutcomePayoutPoint(70, Satoshis(0), isEndpoint = false),
OutcomePayoutPoint(80, Satoshis(1000), isEndpoint = true),
OutcomePayoutPoint(90, Satoshis(1000), isEndpoint = true),
OutcomePayoutPoint(100, Satoshis(11000), isEndpoint = false),
OutcomePayoutPoint(110, Satoshis(9000), isEndpoint = true)
))
PiecewisePolynomialPoint(0, -1000, isEndpoint = true),
PiecewisePolynomialPoint(10, -1000, isEndpoint = true),
PiecewisePolynomialPoint(20, 0, isEndpoint = false),
PiecewisePolynomialPoint(30, 3000, isEndpoint = true),
PiecewisePolynomialPoint(40, 4000, isEndpoint = true),
PiecewisePolynomialPoint(50, 4000, isEndpoint = true),
PiecewisePolynomialPoint(70, 0, isEndpoint = false),
PiecewisePolynomialPoint(80, 1000, isEndpoint = true),
PiecewisePolynomialPoint(90, 1000, isEndpoint = true),
PiecewisePolynomialPoint(100, 11000, isEndpoint = false),
PiecewisePolynomialPoint(110, 9000, isEndpoint = true)
),
serializationVersion = DLCSerializationVersion.Post144Pre163
)
val expected = Vector(
ConstantPayoutRange(0, 20), // 0
@ -58,17 +61,19 @@ class CETCalculatorTest extends BitcoinSUnitTest {
}
it should "correctly split into ranges when payout is constantly changing" in {
val func = DLCPayoutCurve(
val func = DLCPayoutCurve.polynomialInterpolate(
Vector(
OutcomePayoutPoint(0, 1000, isEndpoint = true),
OutcomePayoutPoint(1, 0, isEndpoint = true),
OutcomePayoutPoint(2, 1000, isEndpoint = true),
OutcomePayoutPoint(3, 0, isEndpoint = true),
OutcomePayoutPoint(4, 1000, isEndpoint = true),
OutcomePayoutPoint(5, 0, isEndpoint = true),
OutcomePayoutPoint(6, 1000, isEndpoint = true),
OutcomePayoutPoint(7, 0, isEndpoint = true)
))
PiecewisePolynomialPoint(0, 1000, isEndpoint = true),
PiecewisePolynomialPoint(1, 0, isEndpoint = true),
PiecewisePolynomialPoint(2, 1000, isEndpoint = true),
PiecewisePolynomialPoint(3, 0, isEndpoint = true),
PiecewisePolynomialPoint(4, 1000, isEndpoint = true),
PiecewisePolynomialPoint(5, 0, isEndpoint = true),
PiecewisePolynomialPoint(6, 1000, isEndpoint = true),
PiecewisePolynomialPoint(7, 0, isEndpoint = true)
),
serializationVersion = DLCSerializationVersion.Post144Pre163
)
val expected = Vector(VariablePayoutRange(0, 7))
@ -257,20 +262,22 @@ class CETCalculatorTest extends BitcoinSUnitTest {
}
it should "correctly compute all needed CETs" in {
val func = DLCPayoutCurve(
val func = DLCPayoutCurve.polynomialInterpolate(
Vector(
OutcomePayoutPoint(0, Satoshis(-1000), isEndpoint = true),
OutcomePayoutPoint(10, Satoshis(-1000), isEndpoint = true),
OutcomePayoutPoint(20, Satoshis(0), isEndpoint = false),
OutcomePayoutPoint(30, Satoshis(3000), isEndpoint = true),
OutcomePayoutPoint(40, Satoshis(4000), isEndpoint = true),
OutcomePayoutPoint(50, Satoshis(4000), isEndpoint = true),
OutcomePayoutPoint(70, Satoshis(0), isEndpoint = false),
OutcomePayoutPoint(80, Satoshis(1000), isEndpoint = true),
OutcomePayoutPoint(90, Satoshis(1000), isEndpoint = true),
OutcomePayoutPoint(100, Satoshis(11000), isEndpoint = false),
OutcomePayoutPoint(110, Satoshis(9000), isEndpoint = true)
))
PiecewisePolynomialPoint(0, -1000, isEndpoint = true),
PiecewisePolynomialPoint(10, -1000, isEndpoint = true),
PiecewisePolynomialPoint(20, 0, isEndpoint = false),
PiecewisePolynomialPoint(30, 3000, isEndpoint = true),
PiecewisePolynomialPoint(40, 4000, isEndpoint = true),
PiecewisePolynomialPoint(50, 4000, isEndpoint = true),
PiecewisePolynomialPoint(70, 0, isEndpoint = false),
PiecewisePolynomialPoint(80, 1000, isEndpoint = true),
PiecewisePolynomialPoint(90, 1000, isEndpoint = true),
PiecewisePolynomialPoint(100, 11000, isEndpoint = false),
PiecewisePolynomialPoint(110, 9000, isEndpoint = true)
),
serializationVersion = DLCSerializationVersion.Post144Pre163
)
val firstZeroRange = Vector(
Vector(0, 0) -> Satoshis(0),

View File

@ -2,88 +2,134 @@ package org.bitcoins.core.protocol.dlc
import org.bitcoins.core.currency._
import org.bitcoins.core.protocol.dlc.models._
import org.bitcoins.core.protocol.tlv.ContractDescriptorV1TLV
import org.bitcoins.core.protocol.tlv.{
ContractDescriptorV1TLV,
DLCSerializationVersion,
EnumOutcome
}
import org.bitcoins.testkitcore.util.BitcoinSUnitTest
class ContractDescriptorTest extends BitcoinSUnitTest {
behavior of "ContractDescriptor"
it must "construct a basic enum contract descriptor" in {
val expectedHex =
"fda7102903055452554d50000000000000000005424944454e0000000005f5e100035449450000000002faf080"
val vec: Vector[(EnumOutcome, Satoshis)] = Vector(
(EnumOutcome("TRUMP"), Satoshis.zero),
(EnumOutcome("BIDEN"), Bitcoins.one.satoshis),
(EnumOutcome("TIE"), Satoshis(50000000))
)
val contract = EnumContractDescriptor(vec)
assert(contract.hex == expectedHex)
}
it should "fail to create an empty EnumContractDescriptor" in {
assertThrows[IllegalArgumentException](EnumContractDescriptor(Vector.empty))
}
it should "fail for not starting with a endpoint" in {
val func = DLCPayoutCurve(
Vector(
OutcomePayoutPoint(0, Satoshis(0), isEndpoint = false),
OutcomePayoutPoint(3, Satoshis(100), isEndpoint = true)
))
assertThrows[IllegalArgumentException](
NumericContractDescriptor(func, 2, RoundingIntervals.noRounding))
assertThrows[IllegalArgumentException] {
val func = DLCPayoutCurve.polynomialInterpolate(
Vector(
PiecewisePolynomialPoint(0, Satoshis(0), isEndpoint = false),
PiecewisePolynomialPoint(3, Satoshis(100), isEndpoint = true)
),
serializationVersion = DLCSerializationVersion.Post144Pre163
)
NumericContractDescriptor(func, 2, RoundingIntervals.noRounding)
}
}
it should "fail for not ending with a endpoint" in {
val func = DLCPayoutCurve(
Vector(
OutcomePayoutPoint(0, Satoshis(0), isEndpoint = true),
OutcomePayoutPoint(3, Satoshis(100), isEndpoint = false)
))
assertThrows[IllegalArgumentException](
NumericContractDescriptor(func, 2, RoundingIntervals.noRounding))
assertThrows[IllegalArgumentException] {
val func = DLCPayoutCurve.polynomialInterpolate(
Vector(
PiecewisePolynomialPoint(0, Satoshis(0), isEndpoint = true),
PiecewisePolynomialPoint(3, Satoshis(100), isEndpoint = false)
),
serializationVersion = DLCSerializationVersion.Post144Pre163
)
NumericContractDescriptor(func, 2, RoundingIntervals.noRounding)
}
}
it should "fail for starting below the minimum" in {
val func = DLCPayoutCurve(
val func = DLCPayoutCurve.polynomialInterpolate(
Vector(
OutcomePayoutPoint(-1, Satoshis(0), isEndpoint = true),
OutcomePayoutPoint(3, Satoshis(100), isEndpoint = true)
))
PiecewisePolynomialPoint(-1, Satoshis(0), isEndpoint = true),
PiecewisePolynomialPoint(3, Satoshis(100), isEndpoint = true)
),
serializationVersion = DLCSerializationVersion.Post144Pre163
)
assertThrows[IllegalArgumentException](
NumericContractDescriptor(func, 2, RoundingIntervals.noRounding))
}
it should "fail for starting above the minimum" in {
val func = DLCPayoutCurve(
val func = DLCPayoutCurve.polynomialInterpolate(
Vector(
OutcomePayoutPoint(1, Satoshis(0), isEndpoint = true),
OutcomePayoutPoint(3, Satoshis(100), isEndpoint = true)
))
PiecewisePolynomialPoint(1, Satoshis(0), isEndpoint = true),
PiecewisePolynomialPoint(3, Satoshis(100), isEndpoint = true)
),
serializationVersion = DLCSerializationVersion.Post144Pre163
)
assertThrows[IllegalArgumentException](
NumericContractDescriptor(func, 2, RoundingIntervals.noRounding))
}
it should "fail for ending below the maximum" in {
val func = DLCPayoutCurve(
val func = DLCPayoutCurve.polynomialInterpolate(
Vector(
OutcomePayoutPoint(0, Satoshis(0), isEndpoint = true),
OutcomePayoutPoint(2, Satoshis(100), isEndpoint = true)
))
PiecewisePolynomialPoint(0, Satoshis(0), isEndpoint = true),
PiecewisePolynomialPoint(2, Satoshis(100), isEndpoint = true)
),
serializationVersion = DLCSerializationVersion.Post144Pre163
)
assertThrows[IllegalArgumentException](
NumericContractDescriptor(func, 2, RoundingIntervals.noRounding))
}
it should "fail for ending above the maximum" in {
val func = DLCPayoutCurve(
val func = DLCPayoutCurve.polynomialInterpolate(
Vector(
OutcomePayoutPoint(0, Satoshis(0), isEndpoint = true),
OutcomePayoutPoint(4, Satoshis(100), isEndpoint = true)
))
PiecewisePolynomialPoint(0, Satoshis(0), isEndpoint = true),
PiecewisePolynomialPoint(4, Satoshis(100), isEndpoint = true)
),
serializationVersion = DLCSerializationVersion.Post144Pre163
)
assertThrows[IllegalArgumentException](
NumericContractDescriptor(func, 2, RoundingIntervals.noRounding))
}
it should "correctly create a NumericContractDescriptor" in {
val func = DLCPayoutCurve(
it should "parse a numeric contract descriptor pre 144" in {
//we have to be able to parse old numeric contract descriptors
//pre pr 144 on the DLC spec as we have old wallets deployed with this
//https://github.com/discreetlogcontracts/dlcspecs/pull/144
val func = DLCPayoutCurve.polynomialInterpolate(
Vector(
OutcomePayoutPoint(0, Satoshis(0), isEndpoint = true),
OutcomePayoutPoint(3, Satoshis(100), isEndpoint = true)
))
PiecewisePolynomialPoint(outcome = 0,
payout = Satoshis(0),
isEndpoint = true),
PiecewisePolynomialPoint(outcome = 3,
payout = Satoshis(100),
isEndpoint = true)
),
serializationVersion = DLCSerializationVersion.Post144Pre163
)
val descriptor =
NumericContractDescriptor(func, 2, RoundingIntervals.noRounding)
val expected =
NumericContractDescriptor(outcomeValueFunc = func,
numDigits = 2,
roundingIntervals =
RoundingIntervals.noRounding)
assert(descriptor.toTLV == ContractDescriptorV1TLV(
"fda720260002fda7261a0002010000000000000000000000010300000000000000640000fda724020000"))
val oldHex =
"fda720260002fda7261a0002010000000000000000000000010300000000000000640000fda724020000"
val actual = ContractDescriptorV1TLV.fromHex(oldHex)
assert(actual.hex == expected.toTLV.hex)
}
}

View File

@ -2,7 +2,11 @@ package org.bitcoins.core.protocol.dlc
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.protocol.dlc.models._
import org.bitcoins.core.protocol.tlv.{EnumOutcome, OracleAnnouncementV0TLV}
import org.bitcoins.core.protocol.tlv.{
DLCSerializationVersion,
EnumOutcome,
OracleAnnouncementV0TLV
}
import org.bitcoins.core.util.sorted.OrderedAnnouncements
import org.bitcoins.crypto.ECPrivateKey
import org.bitcoins.testkitcore.util.BitcoinSUnitTest
@ -54,11 +58,13 @@ class ContractOraclePairTest extends BitcoinSUnitTest {
it should "not be able to construct an invalid numeric contract oracle pair" in {
val contractDescriptor =
NumericContractDescriptor(
DLCPayoutCurve(
Vector(OutcomePayoutEndpoint(0, 0),
OutcomePayoutEndpoint((1L << 7) - 1, 1))),
DLCPayoutCurve.polynomialInterpolate(
Vector(PiecewisePolynomialEndpoint(0, 0),
PiecewisePolynomialEndpoint((1L << 7) - 1, 1)),
serializationVersion = DLCSerializationVersion.Post144Pre163),
numDigits = 7,
RoundingIntervals.noRounding)
RoundingIntervals.noRounding
)
def numericOracleInfo(numDigits: Int): NumericSingleOracleInfo = {
NumericSingleOracleInfo(

View File

@ -2,15 +2,17 @@ package org.bitcoins.core.protocol.dlc
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.protocol.dlc.models.{
DLCHyperbolaPayoutCurvePiece,
DLCPayoutCurve,
OutcomePayoutCubic,
OutcomePayoutEndpoint,
OutcomePayoutLine,
OutcomePayoutMidpoint,
OutcomePayoutPoint,
OutcomePayoutPolynomial,
OutcomePayoutQuadratic
OutcomePayoutQuadratic,
PiecewisePolynomialEndpoint,
PiecewisePolynomialMidpoint
}
import org.bitcoins.core.protocol.tlv.DLCSerializationVersion
import org.bitcoins.testkitcore.util.BitcoinSUnitTest
import org.scalacheck.Gen
@ -31,21 +33,17 @@ class DLCPayoutCurveTest extends BitcoinSUnitTest {
val pointGen = for {
outcome <- numGen
value <- valueGen
} yield OutcomePayoutEndpoint(outcome, Satoshis(value))
} yield OutcomePayoutPoint(outcome, Satoshis(value))
Gen
.listOfN(n, pointGen)
.suchThat(points =>
points.map(_.outcome).distinct.length == points.length)
.map(_.toVector.sortBy(_.outcome))
.map { points =>
points.head +: points.tail.init.map(_.toMidpoint) :+ points.last
}
}
it should "agree on lines and degree 1 polynomials" in {
forAll(nPoints(2), Gen.listOfN(1000, numGen)) {
case (Vector(point1: OutcomePayoutEndpoint,
point2: OutcomePayoutEndpoint),
case (Vector(point1: OutcomePayoutPoint, point2: OutcomePayoutPoint),
outcomes) =>
val line = OutcomePayoutLine(point1, point2)
val polyDegOne = OutcomePayoutPolynomial(Vector(point1, point2))
@ -78,8 +76,8 @@ class DLCPayoutCurveTest extends BitcoinSUnitTest {
Satoshis(rounded)
}
val point1 = OutcomePayoutEndpoint(x1, expectedPayout(x1))
val point2 = OutcomePayoutEndpoint(x2, expectedPayout(x2))
val point1 = OutcomePayoutPoint(x1, expectedPayout(x1))
val point2 = OutcomePayoutPoint(x2, expectedPayout(x2))
val line = OutcomePayoutLine(point1, point2)
outcomes.foreach { outcome =>
@ -90,9 +88,9 @@ class DLCPayoutCurveTest extends BitcoinSUnitTest {
it should "agree on quadratics and degree 2 polynomials" in {
forAll(nPoints(3), Gen.listOfN(1000, numGen)) {
case (Vector(point1: OutcomePayoutEndpoint,
point2: OutcomePayoutMidpoint,
point3: OutcomePayoutEndpoint),
case (Vector(point1: OutcomePayoutPoint,
point2: OutcomePayoutPoint,
point3: OutcomePayoutPoint),
outcomes) =>
val parabola = OutcomePayoutQuadratic(point1, point2, point3)
val polyDegTwo =
@ -124,9 +122,9 @@ class DLCPayoutCurveTest extends BitcoinSUnitTest {
Satoshis(rounded)
}
val point1 = OutcomePayoutEndpoint(x1, expectedPayout(x1))
val point2 = OutcomePayoutMidpoint(x2, expectedPayout(x2))
val point3 = OutcomePayoutEndpoint(x3, expectedPayout(x3))
val point1 = OutcomePayoutPoint(x1, expectedPayout(x1))
val point2 = OutcomePayoutPoint(x2, expectedPayout(x2))
val point3 = OutcomePayoutPoint(x3, expectedPayout(x3))
val parabola = OutcomePayoutQuadratic(point1, point2, point3)
outcomes.foreach { outcome =>
@ -153,9 +151,9 @@ class DLCPayoutCurveTest extends BitcoinSUnitTest {
Satoshis(rounded)
}
val point1 = OutcomePayoutEndpoint(x1, expectedPayout(x1))
val point2 = OutcomePayoutMidpoint(x2, expectedPayout(x2))
val point3 = OutcomePayoutEndpoint(x3, expectedPayout(x3))
val point1 = OutcomePayoutPoint(x1, expectedPayout(x1))
val point2 = OutcomePayoutPoint(x2, expectedPayout(x2))
val point3 = OutcomePayoutPoint(x3, expectedPayout(x3))
val line = OutcomePayoutLine(point1, point3)
val parabola = OutcomePayoutQuadratic(point1, point2, point3)
@ -167,10 +165,10 @@ class DLCPayoutCurveTest extends BitcoinSUnitTest {
it should "agree on cubics and degree 3 polynomials" in {
forAll(nPoints(4), Gen.listOfN(1000, numGen)) {
case (Vector(point1: OutcomePayoutEndpoint,
point2: OutcomePayoutMidpoint,
point3: OutcomePayoutMidpoint,
point4: OutcomePayoutEndpoint),
case (Vector(point1: OutcomePayoutPoint,
point2: OutcomePayoutPoint,
point3: OutcomePayoutPoint,
point4: OutcomePayoutPoint),
outcomes) =>
val cubic = OutcomePayoutCubic(point1, point2, point3, point4)
val polyDegThree =
@ -208,10 +206,10 @@ class DLCPayoutCurveTest extends BitcoinSUnitTest {
Satoshis(rounded)
}
val point1 = OutcomePayoutEndpoint(x1, expectedPayout(x1))
val point2 = OutcomePayoutMidpoint(x2, expectedPayout(x2))
val point3 = OutcomePayoutMidpoint(x3, expectedPayout(x3))
val point4 = OutcomePayoutEndpoint(x4, expectedPayout(x4))
val point1 = OutcomePayoutPoint(x1, expectedPayout(x1))
val point2 = OutcomePayoutPoint(x2, expectedPayout(x2))
val point3 = OutcomePayoutPoint(x3, expectedPayout(x3))
val point4 = OutcomePayoutPoint(x4, expectedPayout(x4))
val cubic = OutcomePayoutCubic(point1, point2, point3, point4)
outcomes.foreach { outcome =>
@ -239,10 +237,10 @@ class DLCPayoutCurveTest extends BitcoinSUnitTest {
Satoshis(rounded)
}
val point1 = OutcomePayoutEndpoint(x1, expectedPayout(x1))
val point2 = OutcomePayoutMidpoint(x2, expectedPayout(x2))
val point3 = OutcomePayoutMidpoint(x3, expectedPayout(x3))
val point4 = OutcomePayoutEndpoint(x4, expectedPayout(x4))
val point1 = OutcomePayoutPoint(x1, expectedPayout(x1))
val point2 = OutcomePayoutPoint(x2, expectedPayout(x2))
val point3 = OutcomePayoutPoint(x3, expectedPayout(x3))
val point4 = OutcomePayoutPoint(x4, expectedPayout(x4))
val line = OutcomePayoutLine(point1, point4)
val cubic = OutcomePayoutCubic(point1, point2, point3, point4)
@ -271,10 +269,10 @@ class DLCPayoutCurveTest extends BitcoinSUnitTest {
Satoshis(rounded)
}
val point1 = OutcomePayoutEndpoint(x1, expectedPayout(x1))
val point2 = OutcomePayoutMidpoint(x2, expectedPayout(x2))
val point3 = OutcomePayoutMidpoint(x3, expectedPayout(x3))
val point4 = OutcomePayoutEndpoint(x4, expectedPayout(x4))
val point1 = OutcomePayoutPoint(x1, expectedPayout(x1))
val point2 = OutcomePayoutPoint(x2, expectedPayout(x2))
val point3 = OutcomePayoutPoint(x3, expectedPayout(x3))
val point4 = OutcomePayoutPoint(x4, expectedPayout(x4))
val quadratic = OutcomePayoutQuadratic(point1, point2, point4)
val cubic = OutcomePayoutCubic(point1, point2, point3, point4)
@ -284,34 +282,80 @@ class DLCPayoutCurveTest extends BitcoinSUnitTest {
}
}
it should "agree on hyperbolas and y = d/x + translatePayout" in {
forAll(intGen.suchThat(_ > 0),
intGen,
Gen.listOfN(1000, numGen.suchThat(_ > 0))) {
case (d, translatePayout, outcomes) =>
def expectedPayout(outcome: BigDecimal): Satoshis = {
val value = d / outcome + translatePayout
val rounded = value.setScale(0, RoundingMode.FLOOR).toLongExact
Satoshis(rounded)
}
val hyperbola =
DLCHyperbolaPayoutCurvePiece(usePositivePiece = true,
translateOutcome = 0,
translatePayout,
a = 1,
b = 0,
c = 0,
d,
OutcomePayoutPoint(0, 0),
OutcomePayoutPoint(Long.MaxValue, 0))
outcomes.foreach { outcome =>
assert(hyperbola(outcome) == expectedPayout(outcome))
}
}
}
it should "parse points into component functions correctly and compute outputs" in {
val point0 = OutcomePayoutEndpoint(0, Satoshis.zero)
val point1 = OutcomePayoutEndpoint(10, Satoshis(100))
val point0 = PiecewisePolynomialEndpoint(0, Satoshis.zero)
val point1 = PiecewisePolynomialEndpoint(10, Satoshis(100))
val line = DLCPayoutCurve(Vector(point0, point1))
val lineFunc = line.functionComponents
assert(lineFunc == Vector(OutcomePayoutLine(point0, point1)))
val line = DLCPayoutCurve.polynomialInterpolate(
Vector(point0, point1),
serializationVersion = DLCSerializationVersion.Post144Pre163)
val lineFunc = line.pieces
assert(
lineFunc == Vector(OutcomePayoutLine(point0.toOutcomePayoutPoint,
point1.toOutcomePayoutPoint)))
val point2 = OutcomePayoutMidpoint(20, Satoshis.zero)
val point3 = OutcomePayoutEndpoint(30, Satoshis(300))
val point2 = PiecewisePolynomialMidpoint(20, Satoshis.zero)
val point3 = PiecewisePolynomialEndpoint(30, Satoshis(300))
val quad = DLCPayoutCurve(Vector(point1, point2, point3))
val quadFunc = quad.functionComponents
assert(quadFunc == Vector(OutcomePayoutQuadratic(point1, point2, point3)))
val quad =
DLCPayoutCurve.polynomialInterpolate(
Vector(point1, point2, point3),
serializationVersion = DLCSerializationVersion.Post144Pre163)
val quadFunc = quad.pieces
assert(
quadFunc == Vector(
OutcomePayoutQuadratic(point1.toOutcomePayoutPoint,
point2.toOutcomePayoutPoint,
point3.toOutcomePayoutPoint)))
val point4 = OutcomePayoutMidpoint(40, Satoshis(600))
val point5 = OutcomePayoutMidpoint(50, Satoshis(500))
val point6 = OutcomePayoutEndpoint(60, Satoshis(700))
val point4 = PiecewisePolynomialMidpoint(40, Satoshis(600))
val point5 = PiecewisePolynomialMidpoint(50, Satoshis(500))
val point6 = PiecewisePolynomialEndpoint(60, Satoshis(700))
val cubicPoints = Vector(point3, point4, point5, point6)
val cubic = DLCPayoutCurve(cubicPoints)
val cubicFunc = cubic.functionComponents
val cubic = DLCPayoutCurve.polynomialInterpolate(
cubicPoints,
serializationVersion = DLCSerializationVersion.Post144Pre163)
val cubicFunc = cubic.pieces
assert(
cubicFunc == Vector(OutcomePayoutCubic(point3, point4, point5, point6)))
cubicFunc == Vector(
OutcomePayoutCubic(point3.toOutcomePayoutPoint,
point4.toOutcomePayoutPoint,
point5.toOutcomePayoutPoint,
point6.toOutcomePayoutPoint)))
val func = DLCPayoutCurve(
Vector(point0, point1, point2, point3, point4, point5, point6))
val allFuncs = func.functionComponents
val func = DLCPayoutCurve.polynomialInterpolate(
Vector(point0, point1, point2, point3, point4, point5, point6),
serializationVersion = DLCSerializationVersion.Post144Pre163)
val allFuncs = func.pieces
assert(allFuncs == lineFunc ++ quadFunc ++ cubicFunc)
forAll(Gen.choose[Long](0, 60)) { outcome =>

View File

@ -1,5 +1,6 @@
package org.bitcoins.core.protocol.tlv
import org.bitcoins.core.protocol.dlc.models.ContractInfo
import org.bitcoins.testkitcore.gen.TLVGen
import org.bitcoins.testkitcore.util.BitcoinSUnitTest
@ -184,4 +185,13 @@ class TLVTest extends BitcoinSUnitTest {
assert(TLV(sign.bytes) == sign)
}
}
it must "parse a contract info pre 144" in {
//this was a contract info created before we implemented support for
//https://github.com/discreetlogcontracts/dlcspecs/pull/144
val oldHex =
"fdd82efd032500000000000186a0fda720540011fda72648000501000000000000000000000001fd9c400000000000000000000001fda604000000000000c350000001fdafc800000000000186a0000001fe0001ffff00000000000186a00000fda724020000fda712fd02bffdd824fd02b9659e890eef1b223ba45c9993f88c7997859302fd5510ac23f4cac0d4ee8232a77ecbdf50c07f093794370e6a506a836f6b0fb54b45f1fb662e1307166d2e57030574f77305826939fa9124d19bfa8a8b2f00f000586b8c58c79ee8b77969a949fdd822fd025300114762c188048a953803f0edeeeb68c69e6cdc1d371ba8d517003accfe05afc4d6588c3ea326512bc66c26a841adffa68330b8c723da442792e731fb19fda94274a7766bb48e520f118c100bbe62dc3806a8d05a63d92e23683a04b0b8c24148cd166585a6b33b995b3d6c083523a8435b156c05100d88f449f4754310d5574d5e88aad09af1b8ba942cfd305e728044ec6360d847254453ec05b1b518a36660e2238360e02f3a004663a7f3a3534973d8b66a2646c1386779aa820672b6361b88a8696395c0add87840b460dfd8a8c0d520017efc6bf58267d4c9d2a225c5d0e5719068a7dda5d630d7432239b6c9d921d5f3842b584503460ca52612ac2e64337d299513690372e8f4770eb8a28080e8d7c29920ca32af470d65d6f916ee81e3ac15ce02684ba6d2522a9ffea1de7e202b4b699ef7ec4f089dda07f3de5b7d1f853b2c56471999be4efca82674a651c80f047ba3a2b9e6f9999f0cd4062c533d1ae29cab2a5e33cbe98728b7b4271c67f7c5cd6e12e39128b9971e08496cbd84cfa99c77c88867d33e73acef37022ba4422a5221776991d45416db71fb54bc6c104f6a8e50e8905161709215104a7e7b97e866f32cf43233ffd615cab66699832ec607cf59c85a7f56fa957aa5f5d7ec9f46d84d5d4b777122d41ad76c6f4968aeedca243f2030d4f502e58f4181130e9afb75309ac21637bcfd0717528bfb82ffe1b6c9fadee6ba70357210990539184bcc913a0ec65837a736733a2fb6172d601b3900fdd80a11000200074254432f55534400000000001117626974636f696e2d732d70726963652d6578616d706c65"
//https://test.oracle.suredbits.com/contract/numeric/d4d4df2892fb2cfd2e8f030f0e69a568e19668b5d355e7713f69853db09a4c33
assert(ContractInfo.fromHexOpt(oldHex).isDefined)
}
}

View File

@ -4,6 +4,7 @@ import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.protocol.dlc.models._
import org.bitcoins.core.protocol.tlv.{
DLCOutcomeType,
DLCSerializationVersion,
EnumOutcome,
SignedNumericOutcome,
UnsignedNumericOutcome
@ -82,7 +83,7 @@ object CETCalculator {
val (currentFunc, currentFuncIndex) =
if (from + 1 == firstFunc.rightEndpoint.outcome && from + 1 != to) {
(function.functionComponents(firstFuncIndex + 1), firstFuncIndex + 1)
(function.pieces(firstFuncIndex + 1), firstFuncIndex + 1)
} else {
(firstFunc, firstFuncIndex)
}
@ -161,8 +162,7 @@ object CETCalculator {
if (
constantTo + 1 == currentFunc.rightEndpoint.outcome && constantTo + 1 != to
) {
(function.functionComponents(currentFuncIndex + 1),
currentFuncIndex + 1)
(function.pieces(currentFuncIndex + 1), currentFuncIndex + 1)
} else {
(currentFunc, currentFuncIndex)
}
@ -471,21 +471,21 @@ object CETCalculator {
def payoutSample(
func: Long => Long,
numDigits: Int,
numPoints: Long): Vector[OutcomePayoutEndpoint] = {
numPoints: Long): Vector[PiecewisePolynomialEndpoint] = {
val maxVal = (1L << numDigits) - 1
0L.until(maxVal, maxVal / numPoints)
.toVector
.map { outcome =>
val payout = func(outcome)
OutcomePayoutEndpoint(outcome, payout)
PiecewisePolynomialEndpoint(outcome, payout)
}
.:+(OutcomePayoutEndpoint(maxVal, func(maxVal)))
.:+(PiecewisePolynomialEndpoint(maxVal, func(maxVal)))
}
def payoutSampleByInterval(
func: Long => Long,
numDigits: Int,
interval: Int): Vector[OutcomePayoutEndpoint] = {
interval: Int): Vector[PiecewisePolynomialEndpoint] = {
val maxVal = (1L << numDigits) - 1
payoutSample(func, numDigits, maxVal / interval)
}
@ -494,7 +494,9 @@ object CETCalculator {
func: Long => Long,
numDigits: Int,
interval: Int): DLCPayoutCurve = {
DLCPayoutCurve(payoutSampleByInterval(func, numDigits, interval))
DLCPayoutCurve.polynomialInterpolate(
payoutSampleByInterval(func, numDigits, interval),
serializationVersion = DLCSerializationVersion.Post144Pre163)
}
/** Computes all combinations of threshold oracles, preserving order. */

View File

@ -102,31 +102,21 @@ case class NumericContractDescriptor(
private val minValue: Long = 0L
private val maxValue: Long = (Math.pow(2, numDigits) - 1).toLong
require(outcomeValueFunc.points.head.isEndpoint,
"Payout curve must start with an end point")
require(
outcomeValueFunc.points.head.outcome == 0,
s"Payout curve must start with its minimum value, $minValue, got ${outcomeValueFunc.points.head.outcome}. " +
outcomeValueFunc.endpoints.head.outcome == 0,
s"Payout curve must start with its minimum value, $minValue, got ${outcomeValueFunc.endpoints.head.outcome}. " +
s"You must define the payout curve from $minValue - $maxValue"
)
require(outcomeValueFunc.points.last.isEndpoint,
"Payout curve must end with an end point")
require(
outcomeValueFunc.points.last.outcome == maxValue,
s"Payout curve must end with its maximum value, $maxValue, got ${outcomeValueFunc.points.last.outcome}. " +
outcomeValueFunc.endpoints.last.outcome == maxValue,
s"Payout curve must end with its maximum value, $maxValue, got ${outcomeValueFunc.endpoints.last.outcome}. " +
s"You must define the payout curve from $minValue - $maxValue"
)
override def flip(totalCollateral: Satoshis): NumericContractDescriptor = {
val flippedFunc = DLCPayoutCurve(outcomeValueFunc.points.map { point =>
point.copy(payout = totalCollateral.toLong - point.payout)
})
NumericContractDescriptor(
flippedFunc,
outcomeValueFunc.flip(totalCollateral),
numDigits,
roundingIntervals
)

View File

@ -2,6 +2,7 @@ package org.bitcoins.core.protocol.dlc.models
import org.bitcoins.core.currency._
import org.bitcoins.core.protocol.dlc.compute.CETCalculator
import org.bitcoins.core.protocol.tlv.DLCSerializationVersion
sealed trait ContractDescriptorTemplate {
def individualCollateral: CurrencyUnit
@ -106,29 +107,32 @@ sealed trait OptionTemplate extends ContractDescriptorTemplate {
val curve = this match {
case _: CallOption =>
val pointA = OutcomePayoutEndpoint(
val pointA = PiecewisePolynomialEndpoint(
0L,
individualCollateral.satoshis - premium.satoshis)
(individualCollateral - premium).satoshis)
val pointB = OutcomePayoutEndpoint(
val pointB = PiecewisePolynomialEndpoint(
strikePrice,
individualCollateral.satoshis - premium.satoshis)
(individualCollateral - premium).satoshis)
val pointC =
OutcomePayoutEndpoint(maxNum, totalCollateral)
DLCPayoutCurve(Vector(pointA, pointB, pointC))
PiecewisePolynomialEndpoint(maxNum, totalCollateral.satoshis)
DLCPayoutCurve.polynomialInterpolate(
Vector(pointA, pointB, pointC),
serializationVersion = DLCSerializationVersion.Post144Pre163)
case _: PutOption =>
val pointA = OutcomePayoutEndpoint(0L, totalCollateral)
val pointA = PiecewisePolynomialEndpoint(0L, totalCollateral.satoshis)
val pointB = OutcomePayoutEndpoint(
val pointB = PiecewisePolynomialEndpoint(
strikePrice,
individualCollateral.satoshis - premium.satoshis)
(individualCollateral - premium).satoshis)
val pointC =
OutcomePayoutEndpoint(
maxNum,
individualCollateral.satoshis - premium.satoshis)
DLCPayoutCurve(Vector(pointA, pointB, pointC))
PiecewisePolynomialEndpoint(maxNum,
(individualCollateral - premium).satoshis)
DLCPayoutCurve.polynomialInterpolate(
Vector(pointA, pointB, pointC),
serializationVersion = DLCSerializationVersion.Post144Pre163)
}
NumericContractDescriptor(curve, numDigits = numDigits, roundingIntervals)

View File

@ -1,57 +1,45 @@
package org.bitcoins.core.protocol.dlc.models
import org.bitcoins.core.currency.{CurrencyUnit, Satoshis}
import org.bitcoins.core.protocol.tlv.{PayoutFunctionV0TLV, TLVPoint}
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.core.util.{Indexed, NumberUtil}
import scala.math.BigDecimal.RoundingMode
import scala.util.{Failure, Success, Try}
/** A DLC payout curve defined by piecewise interpolating points */
case class DLCPayoutCurve(points: Vector[OutcomePayoutPoint]) {
require(points.init.zip(points.tail).forall { case (p1, p2) =>
p1.outcome < p2.outcome
},
s"Points must be ascending: $points")
case class DLCPayoutCurve(
pieces: Vector[DLCPayoutCurvePiece],
serializationVersion: DLCSerializationVersion)
extends TLVSerializable[PayoutFunctionV0TLV] {
def toTLV: PayoutFunctionV0TLV = {
PayoutFunctionV0TLV(points.map { point =>
TLVPoint(point.outcome,
point.roundedPayout,
point.extraPrecision,
point.isEndpoint)
})
val endpoints: Vector[OutcomePayoutPoint] = {
pieces.map(_.leftEndpoint).:+(pieces.last.rightEndpoint)
}
/** These points (and their indices in this.points) represent the endpoints
* between which interpolation happens.
* In other words these endpoints define the pieces of the piecewise function.
*/
lazy val endpoints: Vector[Indexed[OutcomePayoutPoint]] =
Indexed(points).filter(_.element.isEndpoint)
require(pieces.map(_.rightEndpoint) == endpoints.tail,
s"Endpoints must line up: $this")
/** This Vector contains the function pieces between the endpoints */
lazy val functionComponents: Vector[DLCPayoutCurvePiece] = {
endpoints.init.zip(endpoints.tail).map { // All pairs of adjacent endpoints
case (Indexed(_, index), Indexed(_, nextIndex)) =>
DLCPayoutCurvePiece(points.slice(index, nextIndex + 1))
}
override def toTLV: PayoutFunctionV0TLV = {
val tlvEndpoints = endpoints.map(_.toTLVPoint)
val tlvPieces = pieces.map(_.toTLV)
PayoutFunctionV0TLV(tlvEndpoints, tlvPieces, serializationVersion)
}
private lazy val outcomes = endpoints.map(_.element.outcome)
private lazy val endpointOutcomes = endpoints.map(_.outcome)
/** Returns the function component on which the given oracle outcome is
* defined, along with its index
*/
def componentFor(outcome: Long): Indexed[DLCPayoutCurvePiece] = {
val endpointIndex = NumberUtil.search(outcomes, outcome)
val Indexed(endpoint, _) = endpoints(endpointIndex)
val endpointIndex = NumberUtil.search(endpointOutcomes, outcome)
val endpoint = endpoints(endpointIndex)
if (
endpoint.outcome == outcome && endpointIndex != functionComponents.length
) {
Indexed(functionComponents(endpointIndex), endpointIndex)
if (endpoint.outcome == outcome && endpointIndex != pieces.length) {
Indexed(pieces(endpointIndex), endpointIndex)
} else {
Indexed(functionComponents(endpointIndex - 1), endpointIndex - 1)
Indexed(pieces(endpointIndex - 1), endpointIndex - 1)
}
}
@ -83,29 +71,76 @@ case class DLCPayoutCurve(points: Vector[OutcomePayoutPoint]) {
rounding: RoundingIntervals,
totalCollateral: Satoshis): Satoshis =
getPayout(outcome, rounding, totalCollateral)
}
object DLCPayoutCurve {
def fromTLV(tlv: PayoutFunctionV0TLV): DLCPayoutCurve = {
DLCPayoutCurve(tlv.points.map { point =>
val payoutWithPrecision =
point.value.toLong + (BigDecimal(point.extraPrecision) / (1 << 16))
OutcomePayoutPoint(point.outcome, payoutWithPrecision, point.isEndpoint)
})
def flip(totalCollateral: Satoshis): DLCPayoutCurve = {
DLCPayoutCurve(pieces.map(_.flip(totalCollateral)),
serializationVersion = serializationVersion)
}
}
/** A point on a DLC payout curve to be used for interpolation
*
* outcome: An element of the domain of possible events signed by the oracle
* payout: The payout to the local party corresponding to outcome
* isEndpoint: True if this point defines a boundary between pieces in the curve
*/
sealed trait OutcomePayoutPoint {
object DLCPayoutCurve
extends TLVDeserializable[PayoutFunctionV0TLV, DLCPayoutCurve](
PayoutFunctionV0TLV) {
override def fromTLV(tlv: PayoutFunctionV0TLV): DLCPayoutCurve = {
val pieces =
tlv.endpoints.init.zip(tlv.endpoints.tail).zip(tlv.pieces).map {
case ((leftEndpoint, rightEndpoint), tlvPiece) =>
DLCPayoutCurvePiece.fromTLV(leftEndpoint, tlvPiece, rightEndpoint)
}
DLCPayoutCurve(pieces, tlv.serializationVersion)
}
def polynomialInterpolate(
points: Vector[PiecewisePolynomialPoint],
serializationVersion: DLCSerializationVersion): DLCPayoutCurve = {
require(points.head.isEndpoint && points.last.isEndpoint,
s"First and last points must be endpoints: $points")
val initMidpoints = Vector.empty[PiecewisePolynomialMidpoint]
val initCurvePieces = Vector.empty[DLCPolynomialPayoutCurvePiece]
val (_, _, pieces) =
points.tail.foldLeft((points.head, initMidpoints, initCurvePieces)) {
case ((lastEndpoint, midpointsSoFar, piecesSoFar), point) =>
point match {
case midpoint: PiecewisePolynomialMidpoint =>
(lastEndpoint, midpointsSoFar.:+(midpoint), piecesSoFar)
case endpoint: PiecewisePolynomialEndpoint =>
val all = midpointsSoFar
.+:(lastEndpoint)
.:+(endpoint)
val points = all.map(_.toOutcomePayoutPoint)
(endpoint,
Vector.empty,
piecesSoFar.:+(DLCPolynomialPayoutCurvePiece(points)))
}
}
DLCPayoutCurve(pieces, serializationVersion)
}
def fromPoints(
points: Vector[TLVPoint],
serializationVersion: DLCSerializationVersion): DLCPayoutCurve = {
val pieceEndpoints = points.map { p =>
PiecewisePolynomialEndpoint(p.outcome, p.value)
}
DLCPayoutCurve.polynomialInterpolate(pieceEndpoints, serializationVersion)
}
def fromPointsPre144(points: Vector[OldTLVPoint]): DLCPayoutCurve = {
val newPoints =
points.map(p => TLVPoint(p.outcome, p.value, p.extraPrecision))
fromPoints(newPoints,
serializationVersion = DLCSerializationVersion.PrePR144)
}
}
trait DLCPoint {
def outcome: Long
def payout: BigDecimal
def isEndpoint: Boolean
def roundedPayout: Satoshis = {
Satoshis(payout.setScale(0, RoundingMode.FLOOR).toLongExact)
@ -116,85 +151,96 @@ sealed trait OutcomePayoutPoint {
shifted.setScale(0, RoundingMode.FLOOR).toIntExact
}
def copy(
outcome: Long = this.outcome,
payout: BigDecimal = this.payout): OutcomePayoutPoint = {
this match {
case OutcomePayoutEndpoint(_, _) => OutcomePayoutEndpoint(outcome, payout)
case OutcomePayoutMidpoint(_, _) => OutcomePayoutMidpoint(outcome, payout)
}
def toTLVPoint: TLVPoint = {
TLVPoint(outcome, roundedPayout, extraPrecision)
}
def toOutcomePayoutPoint: OutcomePayoutPoint = {
OutcomePayoutPoint(outcome = outcome, payout = payout)
}
}
/** A point on a DLC payout curve to be used for interpolation
*
* outcome: An element of the domain of possible events signed by the oracle
* payout: The payout to the local party corresponding to outcome
*/
case class OutcomePayoutPoint(outcome: Long, payout: BigDecimal)
extends DLCPoint {
override def toString: String = {
s"OutcomePayoutPoint(outcome=$outcome,payout=$payout)"
}
}
object OutcomePayoutPoint {
def apply(outcome: Long, payout: Satoshis): OutcomePayoutPoint = {
OutcomePayoutPoint(outcome, payout.toLong)
}
def fromTLVPoint(point: TLVPoint): OutcomePayoutPoint = {
OutcomePayoutPoint(point.outcome, point.bigDecimalPayout)
}
}
sealed trait PiecewisePolynomialPoint extends DLCPoint {
/** True if this point defines a boundary between pieces in the curve */
def isEndpoint: Boolean
}
object PiecewisePolynomialPoint {
def apply(
outcome: Long,
payout: BigDecimal,
isEndpoint: Boolean): OutcomePayoutPoint = {
isEndpoint: Boolean): PiecewisePolynomialPoint = {
if (isEndpoint) {
OutcomePayoutEndpoint(outcome, payout)
PiecewisePolynomialEndpoint(outcome, payout)
} else {
OutcomePayoutMidpoint(outcome, payout)
PiecewisePolynomialMidpoint(outcome, payout)
}
}
def apply(
outcome: Long,
payout: Satoshis,
isEndpoint: Boolean): OutcomePayoutPoint = {
OutcomePayoutPoint(outcome, payout.toLong, isEndpoint)
payout: CurrencyUnit,
isEndpoint: Boolean): PiecewisePolynomialPoint = {
PiecewisePolynomialPoint(outcome, payout.toBigDecimal, isEndpoint)
}
}
case class OutcomePayoutEndpoint(outcome: Long, payout: BigDecimal)
extends OutcomePayoutPoint {
override val isEndpoint: Boolean = true
def toMidpoint: OutcomePayoutMidpoint = OutcomePayoutMidpoint(outcome, payout)
case class PiecewisePolynomialEndpoint(outcome: Long, payout: BigDecimal)
extends PiecewisePolynomialPoint {
override def isEndpoint: Boolean = true
}
object OutcomePayoutEndpoint {
object PiecewisePolynomialEndpoint {
def apply(outcome: Long, payout: CurrencyUnit): OutcomePayoutEndpoint = {
OutcomePayoutEndpoint(outcome, payout.satoshis.toLong)
def apply(outcome: Long, payout: Satoshis): PiecewisePolynomialEndpoint = {
PiecewisePolynomialEndpoint(outcome, payout.toBigDecimal)
}
}
case class OutcomePayoutMidpoint(outcome: Long, payout: BigDecimal)
extends OutcomePayoutPoint {
override val isEndpoint: Boolean = false
def toEndpoint: OutcomePayoutEndpoint = OutcomePayoutEndpoint(outcome, payout)
case class PiecewisePolynomialMidpoint(outcome: Long, payout: BigDecimal)
extends PiecewisePolynomialPoint {
override def isEndpoint: Boolean = false
}
object OutcomePayoutMidpoint {
object PiecewisePolynomialMidpoint {
def apply(outcome: Long, payout: Satoshis): OutcomePayoutMidpoint = {
OutcomePayoutMidpoint(outcome, payout.toLong)
def apply(outcome: Long, payout: Satoshis): PiecewisePolynomialMidpoint = {
PiecewisePolynomialMidpoint(outcome, payout.toBigDecimal)
}
}
/** A single piece of a larger piecewise function defined between left and right endpoints */
sealed trait DLCPayoutCurvePiece {
def leftEndpoint: OutcomePayoutEndpoint
def midpoints: Vector[OutcomePayoutMidpoint]
def rightEndpoint: OutcomePayoutEndpoint
sealed trait DLCPayoutCurvePiece extends TLVSerializable[PayoutCurvePieceTLV] {
def leftEndpoint: OutcomePayoutPoint
def rightEndpoint: OutcomePayoutPoint
midpoints.headOption match {
case Some(firstMidpoint) =>
require(leftEndpoint.outcome < firstMidpoint.outcome,
s"Points must be ascending: $this")
require(midpoints.init.zip(midpoints.tail).forall { case (m1, m2) =>
m1.outcome < m2.outcome
},
s"Points must be ascending: $this")
require(rightEndpoint.outcome > midpoints.last.outcome,
s"Points must be ascending: $this")
case None =>
require(leftEndpoint.outcome < rightEndpoint.outcome,
s"Points must be ascending: $this")
}
require(leftEndpoint.outcome < rightEndpoint.outcome,
s"Points must be ascending: $this")
def apply(outcome: Long): Satoshis
@ -219,45 +265,194 @@ sealed trait DLCPayoutCurvePiece {
.setScale(0, RoundingMode.FLOOR)
.toLongExact)
}
def flip(totalCollateral: Satoshis): DLCPayoutCurvePiece
}
object DLCPayoutCurvePiece {
def apply(points: Vector[OutcomePayoutPoint]): DLCPayoutCurvePiece = {
require(points.head.isEndpoint && points.last.isEndpoint,
s"First and last points must be endpoints, $points")
require(points.tail.init.forall(!_.isEndpoint),
s"Endpoint detected in middle, $points")
def fromTLV(
leftEndpoint: TLVPoint,
curvePiece: PayoutCurvePieceTLV,
rightEndpoint: TLVPoint): DLCPayoutCurvePiece = {
curvePiece match {
case polynomial: PolynomialPayoutCurvePieceTLV =>
DLCPolynomialPayoutCurvePiece.fromTLV(leftEndpoint,
polynomial,
rightEndpoint)
case hyperbola: HyperbolaPayoutCurvePieceTLV =>
DLCHyperbolaPayoutCurvePiece.fromTLV(leftEndpoint,
hyperbola,
rightEndpoint)
}
}
}
case class DLCHyperbolaPayoutCurvePiece(
usePositivePiece: Boolean,
translateOutcome: BigDecimal,
translatePayout: BigDecimal,
a: BigDecimal,
b: BigDecimal,
c: BigDecimal,
d: BigDecimal,
leftEndpoint: OutcomePayoutPoint,
rightEndpoint: OutcomePayoutPoint)
extends DLCPayoutCurvePiece
with TLVSerializable[HyperbolaPayoutCurvePieceTLV] {
require(a * d != b * c, s"ad cannot equal bc: $this")
override def apply(outcome: Long): Satoshis = {
val resultT = Try {
val translatedOutcome: BigDecimal = outcome - translateOutcome
val sqrtTermAbsVal: BigDecimal =
BigDecimal(math.sqrt((translatedOutcome.pow(2) - 4 * a * b).toDouble))
val sqrtTerm: BigDecimal =
if (usePositivePiece) sqrtTermAbsVal else -sqrtTermAbsVal
val firstTerm = c * (translatedOutcome + sqrtTerm) / (2 * a)
val secondTerm = 2 * a * d / (translatedOutcome + sqrtTerm)
val value = firstTerm + secondTerm + translatePayout
bigDecimalSats(value)
}
resultT match {
case Success(result) => result
case Failure(err) =>
throw new IllegalArgumentException(s"Illegal input outcome $outcome.",
err)
}
}
override def toTLV: HyperbolaPayoutCurvePieceTLV = {
HyperbolaPayoutCurvePieceTLV(
usePositivePiece,
Signed16PTLVNumber.fromBigDecimal(translateOutcome),
Signed16PTLVNumber.fromBigDecimal(translatePayout),
Signed16PTLVNumber.fromBigDecimal(a),
Signed16PTLVNumber.fromBigDecimal(b),
Signed16PTLVNumber.fromBigDecimal(c),
Signed16PTLVNumber.fromBigDecimal(d)
)
}
override def flip(totalCollateral: Satoshis): DLCHyperbolaPayoutCurvePiece = {
DLCHyperbolaPayoutCurvePiece(
usePositivePiece,
translateOutcome,
totalCollateral.toLong - translatePayout,
a,
b,
-c,
-d,
leftEndpoint.copy(payout = totalCollateral.toLong - leftEndpoint.payout),
rightEndpoint.copy(payout = totalCollateral.toLong - rightEndpoint.payout)
)
}
}
object DLCHyperbolaPayoutCurvePiece {
def fromTLV(
leftEndpoint: TLVPoint,
curvePiece: HyperbolaPayoutCurvePieceTLV,
rightEndpoint: TLVPoint): DLCHyperbolaPayoutCurvePiece = {
DLCHyperbolaPayoutCurvePiece(
curvePiece.usePositivePiece,
curvePiece.translateOutcome.toBigDecimal,
curvePiece.translatePayout.toBigDecimal,
curvePiece.a.toBigDecimal,
curvePiece.b.toBigDecimal,
curvePiece.c.toBigDecimal,
curvePiece.d.toBigDecimal,
OutcomePayoutPoint.fromTLVPoint(leftEndpoint),
OutcomePayoutPoint.fromTLVPoint(rightEndpoint)
)
}
}
/** A single piece of a larger piecewise function defined between left and right endpoints */
sealed trait DLCPolynomialPayoutCurvePiece
extends DLCPayoutCurvePiece
with TLVSerializable[PolynomialPayoutCurvePieceTLV] {
def midpoints: Vector[OutcomePayoutPoint]
def points: Vector[OutcomePayoutPoint] = {
midpoints.+:(leftEndpoint).:+(rightEndpoint)
}
midpoints.headOption.foreach { firstMidpoint =>
require(leftEndpoint.outcome < firstMidpoint.outcome,
s"Points must be ascending: $this")
require(midpoints.init.zip(midpoints.tail).forall { case (m1, m2) =>
m1.outcome < m2.outcome
},
s"Points must be ascending: $this")
require(rightEndpoint.outcome > midpoints.last.outcome,
s"Points must be ascending: $this")
}
override def toTLV: PolynomialPayoutCurvePieceTLV = {
PolynomialPayoutCurvePieceTLV(midpoints.map(_.toTLVPoint))
}
override def flip(
totalCollateral: Satoshis): DLCPolynomialPayoutCurvePiece = {
val flippedPoints = points.map { point =>
point.copy(payout = totalCollateral.toLong - point.payout)
}
DLCPolynomialPayoutCurvePiece(flippedPoints)
}
}
object DLCPolynomialPayoutCurvePiece {
def apply(
points: Vector[OutcomePayoutPoint]): DLCPolynomialPayoutCurvePiece = {
points match {
case Vector(left: OutcomePayoutEndpoint, right: OutcomePayoutEndpoint) =>
case Vector(left: OutcomePayoutPoint, right: OutcomePayoutPoint) =>
if (left.payout == right.payout) {
OutcomePayoutConstant(left, right)
} else {
OutcomePayoutLine(left, right)
}
case Vector(left: OutcomePayoutEndpoint,
mid: OutcomePayoutMidpoint,
right: OutcomePayoutEndpoint) =>
case Vector(left: OutcomePayoutPoint,
mid: OutcomePayoutPoint,
right: OutcomePayoutPoint) =>
OutcomePayoutQuadratic(left, mid, right)
case Vector(left: OutcomePayoutEndpoint,
mid1: OutcomePayoutMidpoint,
mid2: OutcomePayoutMidpoint,
right: OutcomePayoutEndpoint) =>
case Vector(left: OutcomePayoutPoint,
mid1: OutcomePayoutPoint,
mid2: OutcomePayoutPoint,
right: OutcomePayoutPoint) =>
OutcomePayoutCubic(left, mid1, mid2, right)
case _ => OutcomePayoutPolynomial(points)
}
}
def fromTLV(
leftEndpoint: TLVPoint,
curvePiece: PolynomialPayoutCurvePieceTLV,
rightEndpoint: TLVPoint): DLCPolynomialPayoutCurvePiece = {
val tlvPoints = curvePiece.midpoints.+:(leftEndpoint).:+(rightEndpoint)
val points = tlvPoints.map(OutcomePayoutPoint.fromTLVPoint)
DLCPolynomialPayoutCurvePiece(points)
}
}
case class OutcomePayoutConstant(
leftEndpoint: OutcomePayoutEndpoint,
rightEndpoint: OutcomePayoutEndpoint)
extends DLCPayoutCurvePiece {
leftEndpoint: OutcomePayoutPoint,
rightEndpoint: OutcomePayoutPoint)
extends DLCPolynomialPayoutCurvePiece {
require(leftEndpoint.payout == rightEndpoint.payout,
"Constant function must have same values on endpoints")
override lazy val midpoints: Vector[OutcomePayoutMidpoint] = Vector.empty
override lazy val midpoints: Vector[OutcomePayoutPoint] = Vector.empty
override def apply(outcome: Long): Satoshis =
bigDecimalSats(leftEndpoint.payout)
@ -265,10 +460,10 @@ case class OutcomePayoutConstant(
/** A Line between left and right endpoints defining a piece of a larger payout curve */
case class OutcomePayoutLine(
leftEndpoint: OutcomePayoutEndpoint,
rightEndpoint: OutcomePayoutEndpoint)
extends DLCPayoutCurvePiece {
override lazy val midpoints: Vector[OutcomePayoutMidpoint] = Vector.empty
leftEndpoint: OutcomePayoutPoint,
rightEndpoint: OutcomePayoutPoint)
extends DLCPolynomialPayoutCurvePiece {
override lazy val midpoints: Vector[OutcomePayoutPoint] = Vector.empty
lazy val slope: BigDecimal = {
(rightEndpoint.payout - leftEndpoint.payout) / (rightEndpoint.outcome - leftEndpoint.outcome)
@ -286,11 +481,11 @@ case class OutcomePayoutLine(
* A quadratic equation defines a parabola: https://en.wikipedia.org/wiki/Quadratic_function
*/
case class OutcomePayoutQuadratic(
leftEndpoint: OutcomePayoutEndpoint,
midpoint: OutcomePayoutMidpoint,
rightEndpoint: OutcomePayoutEndpoint)
extends DLCPayoutCurvePiece {
override lazy val midpoints: Vector[OutcomePayoutMidpoint] = Vector(midpoint)
leftEndpoint: OutcomePayoutPoint,
midpoint: OutcomePayoutPoint,
rightEndpoint: OutcomePayoutPoint)
extends DLCPolynomialPayoutCurvePiece {
override lazy val midpoints: Vector[OutcomePayoutPoint] = Vector(midpoint)
private lazy val (x01, x02, x12) =
(leftEndpoint.outcome - midpoint.outcome,
@ -318,13 +513,13 @@ case class OutcomePayoutQuadratic(
/** A cubic between left and right endpoints defining a piece of a larger payout curve */
case class OutcomePayoutCubic(
leftEndpoint: OutcomePayoutEndpoint,
leftMidpoint: OutcomePayoutMidpoint,
rightMidpoint: OutcomePayoutMidpoint,
rightEndpoint: OutcomePayoutEndpoint)
extends DLCPayoutCurvePiece {
leftEndpoint: OutcomePayoutPoint,
leftMidpoint: OutcomePayoutPoint,
rightMidpoint: OutcomePayoutPoint,
rightEndpoint: OutcomePayoutPoint)
extends DLCPolynomialPayoutCurvePiece {
override lazy val midpoints: Vector[OutcomePayoutMidpoint] =
override lazy val midpoints: Vector[OutcomePayoutPoint] =
Vector(leftMidpoint, rightMidpoint)
private lazy val (x01, x02, x03, x12, x13, x23) =
@ -363,21 +558,18 @@ case class OutcomePayoutCubic(
}
/** A polynomial interpolating points and defining a piece of a larger payout curve */
case class OutcomePayoutPolynomial(points: Vector[OutcomePayoutPoint])
extends DLCPayoutCurvePiece {
require(points.head.isEndpoint && points.last.isEndpoint,
s"First and last points must be endpoints, $points")
require(points.tail.init.forall(!_.isEndpoint),
s"Endpoint detected in middle, $points")
case class OutcomePayoutPolynomial(
override val points: Vector[OutcomePayoutPoint])
extends DLCPolynomialPayoutCurvePiece {
override lazy val leftEndpoint: OutcomePayoutEndpoint =
points.head.asInstanceOf[OutcomePayoutEndpoint]
override lazy val leftEndpoint: OutcomePayoutPoint =
points.head
override lazy val rightEndpoint: OutcomePayoutEndpoint =
points.last.asInstanceOf[OutcomePayoutEndpoint]
override lazy val rightEndpoint: OutcomePayoutPoint =
points.last
override lazy val midpoints: Vector[OutcomePayoutMidpoint] =
points.tail.init.asInstanceOf[Vector[OutcomePayoutMidpoint]]
override lazy val midpoints: Vector[OutcomePayoutPoint] =
points.tail.init
lazy val coefficients: Vector[BigDecimal] = {
points.map { point =>

View File

@ -0,0 +1,23 @@
package org.bitcoins.core.protocol.tlv
/** We have various binary serializations in our codebase currently.
* This is a product of trying to release a DLC wallet before the
* spec was finalized. Some of the binary level serialization for DLCs
* has changed since we initiallly deployed wallets.
*/
sealed trait DLCSerializationVersion
object DLCSerializationVersion {
/** This format existed in our wallet before we merged support for this PR
* on the DLC spec repo. See the diff below
* @see [[https://github.com/discreetlogcontracts/dlcspecs/pull/144]]
*/
case object PrePR144 extends DLCSerializationVersion
/** This represents binary serialization for the case where we have
* included support for 144, but not included support for 163 yet
* @see [[https://github.com/discreetlogcontracts/dlcspecs/pull/144]]
*/
case object Post144Pre163 extends DLCSerializationVersion
}

View File

@ -43,7 +43,7 @@ object LnMessage extends Factory[LnMessage[TLV]] {
throw new IllegalArgumentException(s"Parsed unknown TLV $unknown")
case _: DLCSetupTLV | _: DLCSetupPieceTLV | _: InitTLV | _: DLCOracleTLV |
_: ErrorTLV | _: PingTLV | _: PongTLV | _: ContractInfoV0TLV |
_: ContractInfoV1TLV =>
_: ContractInfoV1TLV | _: PayoutCurvePieceTLV =>
()
}

View File

@ -4,6 +4,11 @@ import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.number._
import org.bitcoins.core.protocol.dlc.compute.SigningVersion
import org.bitcoins.core.protocol.dlc.compute.SigningVersion.DLCOracleV0SigningVersion
import org.bitcoins.core.protocol.dlc.models.{
DLCPayoutCurve,
OutcomePayoutPoint,
PiecewisePolynomialEndpoint
}
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.tlv.TLV.{
DecodeTLVResult,
@ -20,6 +25,8 @@ import scodec.bits.ByteVector
import java.nio.charset.StandardCharsets
import java.time.Instant
import scala.annotation.tailrec
import scala.math.BigDecimal.RoundingMode
import scala.util.Try
sealed trait TLV extends NetworkElement with TLVUtil {
def tpe: BigSizeUInt
@ -146,7 +153,6 @@ object TLV extends TLVParentFactory[TLV] {
s"Length specified was $length but not enough bytes in ${bytes.drop(prefixSize)}")
val value = bytes.drop(prefixSize).take(length.num.toLong)
DecodeTLVResult(tpe, length, value)
}
@ -169,6 +175,7 @@ object TLV extends TLVParentFactory[TLV] {
DLCAcceptTLV,
DLCSignTLV
) ++ EventDescriptorTLV.allFactories ++
PayoutCurvePieceTLV.allFactories ++
ContractDescriptorTLV.allFactories ++
OracleInfoTLV.allFactories ++
ContractInfoTLV.allFactories ++
@ -965,7 +972,40 @@ object RoundingIntervalsV0TLV extends TLVFactory[RoundingIntervalsV0TLV] {
override val typeName: String = "RoundingIntervalsV0TLV"
}
case class TLVPoint(
case class TLVPoint(outcome: Long, value: Satoshis, extraPrecision: Int)
extends NetworkElement {
lazy val bigDecimalPayout: BigDecimal = {
value.toLong + (BigDecimal(extraPrecision) / (1 << 16))
}
override def bytes: ByteVector = {
BigSizeUInt(outcome).bytes ++
BigSizeUInt(value.toLong).bytes ++
UInt16(extraPrecision).bytes
}
def outcomePayoutPoint: OutcomePayoutPoint = {
OutcomePayoutPoint(outcome, value.toLong)
}
}
object TLVPoint extends Factory[TLVPoint] {
override def fromBytes(bytes: ByteVector): TLVPoint = {
val outcome = BigSizeUInt(bytes)
val value = BigSizeUInt(bytes.drop(outcome.byteSize))
val extraPrecision = UInt16(
bytes.drop(outcome.byteSize + value.byteSize).take(2)).toInt
TLVPoint(outcome = outcome.toLong,
value = Satoshis(value.toLong),
extraPrecision = extraPrecision)
}
}
case class OldTLVPoint(
outcome: Long,
value: Satoshis,
extraPrecision: Int,
@ -986,9 +1026,9 @@ case class TLVPoint(
}
}
object TLVPoint extends Factory[TLVPoint] {
object OldTLVPoint extends Factory[OldTLVPoint] {
override def fromBytes(bytes: ByteVector): TLVPoint = {
override def fromBytes(bytes: ByteVector): OldTLVPoint = {
val isEndpoint = bytes.head match {
case 0 => false
case 1 => true
@ -1001,20 +1041,192 @@ object TLVPoint extends Factory[TLVPoint] {
val value = UInt64(bytes.drop(1 + outcome.byteSize).take(8))
val extraPrecision = UInt16(bytes.drop(9 + outcome.byteSize).take(2)).toInt
TLVPoint(outcome = outcome.toLong,
value = Satoshis(value.toLong),
extraPrecision = extraPrecision,
isEndpoint = isEndpoint)
OldTLVPoint(outcome = outcome.toLong,
value = Satoshis(value.toLong),
extraPrecision = extraPrecision,
isEndpoint = isEndpoint)
}
}
/** @see https://github.com/discreetlogcontracts/dlcspecs/blob/8ee4bbe816c9881c832b1ce320b9f14c72e3506f/NumericOutcome.md#curve-serialization */
case class PayoutFunctionV0TLV(points: Vector[TLVPoint])
sealed trait PayoutCurvePieceTLV extends DLCSetupPieceTLV
object PayoutCurvePieceTLV extends TLVParentFactory[PayoutCurvePieceTLV] {
override val allFactories: Vector[TLVFactory[PayoutCurvePieceTLV]] =
Vector(PolynomialPayoutCurvePieceTLV, HyperbolaPayoutCurvePieceTLV)
override val typeName: String = "PayoutCurvePieceTLV"
}
case class PolynomialPayoutCurvePieceTLV(midpoints: Vector[TLVPoint])
extends PayoutCurvePieceTLV {
override val tpe: BigSizeUInt = PolynomialPayoutCurvePieceTLV.tpe
override val value: ByteVector = {
u16PrefixedList(midpoints)
}
}
object PolynomialPayoutCurvePieceTLV
extends TLVFactory[PolynomialPayoutCurvePieceTLV] {
override val tpe: BigSizeUInt = BigSizeUInt(42792)
override def fromTLVValue(
value: ByteVector): PolynomialPayoutCurvePieceTLV = {
val iter = ValueIterator(value)
val points = iter.takeU16PrefixedList(() => iter.take(TLVPoint))
PolynomialPayoutCurvePieceTLV(points)
}
override val typeName: String = "PolynomialPayoutCurvePieceTLV"
}
case class Signed16PTLVNumber(
sign: Boolean,
withoutPrecision: Long,
extraPrecision: Int)
extends NetworkElement {
lazy val toBigDecimal: BigDecimal = {
val absVal = withoutPrecision + (BigDecimal(extraPrecision) / (1 << 16))
if (sign) absVal else -absVal
}
lazy val signByte: Byte = if (sign) {
1.toByte
} else {
0.toByte
}
override def bytes: ByteVector = {
ByteVector(signByte) ++
BigSizeUInt(withoutPrecision).bytes ++
UInt16(extraPrecision).bytes
}
}
object Signed16PTLVNumber extends Factory[Signed16PTLVNumber] {
override def fromBytes(bytes: ByteVector): Signed16PTLVNumber = {
val sign = bytes.head match {
case 0 => false
case 1 => true
case b: Byte =>
throw new IllegalArgumentException(
s"Did not recognize leading byte: $b")
}
val withoutPrecision = BigSizeUInt(bytes.tail)
val extraPrecision = UInt16(
bytes.drop(1 + withoutPrecision.byteSize).take(2))
Signed16PTLVNumber(sign, withoutPrecision.toLong, extraPrecision.toInt)
}
def fromBigDecimal(number: BigDecimal): Signed16PTLVNumber = {
val sign = number >= 0
val withoutPrecision =
number.abs.setScale(0, RoundingMode.FLOOR).toLongExact
val extraPrecisionBD = (number.abs - withoutPrecision) * (1 << 16)
val extraPrecision =
extraPrecisionBD.setScale(0, RoundingMode.FLOOR).toIntExact
Signed16PTLVNumber(sign, withoutPrecision, extraPrecision)
}
}
case class HyperbolaPayoutCurvePieceTLV(
usePositivePiece: Boolean,
translateOutcome: Signed16PTLVNumber,
translatePayout: Signed16PTLVNumber,
a: Signed16PTLVNumber,
b: Signed16PTLVNumber,
c: Signed16PTLVNumber,
d: Signed16PTLVNumber)
extends PayoutCurvePieceTLV {
override val tpe: BigSizeUInt = HyperbolaPayoutCurvePieceTLV.tpe
override val value: ByteVector = {
boolBytes(usePositivePiece) ++
translateOutcome.bytes ++
translatePayout.bytes ++
a.bytes ++
b.bytes ++
c.bytes ++
d.bytes
}
}
object HyperbolaPayoutCurvePieceTLV
extends TLVFactory[HyperbolaPayoutCurvePieceTLV] {
override val tpe: BigSizeUInt = BigSizeUInt(42794)
override def fromTLVValue(value: ByteVector): HyperbolaPayoutCurvePieceTLV = {
val iter = ValueIterator(value)
val usePositivePiece = iter.takeBoolean()
val translateOutcome = iter.take(Signed16PTLVNumber)
val translatePayout = iter.take(Signed16PTLVNumber)
val a = iter.take(Signed16PTLVNumber)
val b = iter.take(Signed16PTLVNumber)
val c = iter.take(Signed16PTLVNumber)
val d = iter.take(Signed16PTLVNumber)
HyperbolaPayoutCurvePieceTLV(usePositivePiece,
translateOutcome,
translatePayout,
a,
b,
c,
d)
}
override def typeName: String = "HyperbolaPayoutCurvePieceTLV"
}
case class OldPayoutFunctionV0TLV(points: Vector[OldTLVPoint])
extends DLCSetupPieceTLV {
override val tpe: BigSizeUInt = PayoutFunctionV0TLV.tpe
override val value: ByteVector = u16PrefixedList(points)
}
/** @see https://github.com/discreetlogcontracts/dlcspecs/blob/8ee4bbe816c9881c832b1ce320b9f14c72e3506f/NumericOutcome.md#curve-serialization */
case class PayoutFunctionV0TLV(
endpoints: Vector[TLVPoint],
pieces: Vector[PayoutCurvePieceTLV],
serializationVersion: DLCSerializationVersion)
extends DLCSetupPieceTLV {
require(
endpoints.length == pieces.length + 1,
s"Number of endpoints (${endpoints.length}) does not match number of pieces (${pieces.length}).")
override val tpe: BigSizeUInt = PayoutFunctionV0TLV.tpe
override val value: ByteVector = {
u16PrefixedList(points)
u16PrefixedList[(TLVPoint, PayoutCurvePieceTLV)](
endpoints.init.zip(pieces),
{ case (leftEndpoint: TLVPoint, piece: PayoutCurvePieceTLV) =>
leftEndpoint.bytes ++ piece.bytes
}) ++ endpoints.last.bytes
}
def piecewisePolynomialEndpoints: Vector[PiecewisePolynomialEndpoint] = {
endpoints.map(e => PiecewisePolynomialEndpoint(e.outcome, e.value))
}
override val byteSize: Long = {
serializationVersion match {
case DLCSerializationVersion.PrePR144 =>
val old = OldPayoutFunctionV0TLV(endpoints.map(p =>
OldTLVPoint(p.outcome, p.value, p.extraPrecision, true)))
old.byteSize
case DLCSerializationVersion.Post144Pre163 =>
super.byteSize
}
}
}
@ -1022,11 +1234,31 @@ object PayoutFunctionV0TLV extends TLVFactory[PayoutFunctionV0TLV] {
override val tpe: BigSizeUInt = BigSizeUInt(42790)
override def fromTLVValue(value: ByteVector): PayoutFunctionV0TLV = {
val t = Try {
val iter = ValueIterator(value)
val endpointsAndPieces = iter.takeU16PrefixedList { () =>
val leftEndpoint = iter.take(TLVPoint)
val piece = iter.take(PayoutCurvePieceTLV)
(leftEndpoint, piece)
}
val rightEndpoint = iter.take(TLVPoint)
val endpoints = endpointsAndPieces.map(_._1).:+(rightEndpoint)
val pieces = endpointsAndPieces.map(_._2)
PayoutFunctionV0TLV(endpoints,
pieces,
serializationVersion =
DLCSerializationVersion.Post144Pre163)
}
t.getOrElse(oldfromTLVValue(value))
}
private def oldfromTLVValue(value: ByteVector): PayoutFunctionV0TLV = {
val iter = ValueIterator(value)
val points = iter.takeU16PrefixedList(() => iter.take(TLVPoint))
PayoutFunctionV0TLV(points)
val points = iter.takeU16PrefixedList(() => iter.take(OldTLVPoint))
DLCPayoutCurve.fromPointsPre144(points).toTLV
}
override val typeName: String = "PayoutFunctionV0TLV"
@ -1039,11 +1271,25 @@ case class ContractDescriptorV1TLV(
extends ContractDescriptorTLV {
override val tpe: BigSizeUInt = ContractDescriptorV1TLV.tpe
val numDigitsU16: UInt16 = UInt16(numDigits)
override val value: ByteVector = {
UInt16(numDigits).bytes ++
numDigitsU16.bytes ++
payoutFunction.bytes ++
roundingIntervals.bytes
}
override val byteSize: Long = {
payoutFunction.serializationVersion match {
case DLCSerializationVersion.Post144Pre163 => super.byteSize
case DLCSerializationVersion.PrePR144 =>
val payloadSize =
numDigitsU16.byteSize + payoutFunction.byteSize + roundingIntervals.byteSize
val total =
tpe.byteSize + BigSizeUInt(payloadSize).byteSize + payloadSize
total
}
}
}
object ContractDescriptorV1TLV extends TLVFactory[ContractDescriptorV1TLV] {
@ -1239,7 +1485,9 @@ object ContractInfoV0TLV extends TLVFactory[ContractInfoV0TLV] {
val iter = ValueIterator(value)
val totalCollateral = iter.takeSats()
val contractDescriptor = iter.take(ContractDescriptorTLV)
val oracleInfo = iter.take(OracleInfoTLV)
ContractInfoV0TLV(totalCollateral, contractDescriptor, oracleInfo)

View File

@ -24,6 +24,7 @@ case class ValueIterator(value: ByteVector) {
def skip(bytes: NetworkElement): Unit = {
skip(bytes.byteSize)
()
}
def take(numBytes: Int): ByteVector = {

View File

@ -1,6 +1,6 @@
package org.bitcoins.dlc.testgen
import org.bitcoins.core.number.{UInt16, UInt64}
import org.bitcoins.core.number.UInt16
import org.bitcoins.core.protocol.BigSizeUInt
import org.bitcoins.core.protocol.script.EmptyScriptPubKey
import org.bitcoins.core.protocol.tlv._
@ -141,20 +141,84 @@ object DLCParsingTestVector extends TestVectorParser[DLCParsingTestVector] {
def apply(tlv: TLV): DLCParsingTestVector = {
tlv match {
case PayoutFunctionV0TLV(points) =>
case old: OldPayoutFunctionV0TLV =>
sys.error(s"Should have old payout function here=$old")
case PayoutFunctionV0TLV(endpoints, pieces, _) =>
val fields = Vector(
"tpe" -> Element(PayoutFunctionV0TLV.tpe),
"length" -> Element(tlv.length),
"numPoints" -> Element(UInt16(points.length)),
"points" -> MultiElement(points.map { point =>
"numPieces" -> Element(UInt16(pieces.length)),
"endpointsAndPieces" -> MultiElement(
endpoints
.zip(pieces)
.flatMap { case (leftEndpoint, piece) =>
Vector(leftEndpoint, piece)
}
.:+(endpoints.last)
.map {
case point: TLVPoint =>
NamedMultiElement(
"outcome" -> Element(BigSizeUInt(point.outcome)),
"value" -> Element(BigSizeUInt(point.value.toLong)),
"extraPrecision" -> Element(UInt16(point.extraPrecision))
)
case piece => Element(piece)
})
)
DLCTLVTestVector(tlv, "payout_function_v0", fields)
case PolynomialPayoutCurvePieceTLV(midpoints) =>
val fields = Vector(
"tpe" -> Element(PolynomialPayoutCurvePieceTLV.tpe),
"length" -> Element(tlv.length),
"numMidpoints" -> Element(UInt16(midpoints.length)),
"midpoints" -> MultiElement(midpoints.map { point =>
NamedMultiElement(
"isEndpoint" -> Element(ByteVector(point.leadingByte)),
"outcome" -> Element(BigSizeUInt(point.outcome)),
"value" -> Element(UInt64(point.value.toLong))
"value" -> Element(BigSizeUInt(point.value.toLong)),
"extraPrecision" -> Element(UInt16(point.extraPrecision))
)
})
)
DLCTLVTestVector(tlv, "payout_function_v0", fields)
DLCTLVTestVector(tlv, "polynomial_payout_curve_piece", fields)
case HyperbolaPayoutCurvePieceTLV(usePositivePiece,
translateOutcome,
translatePayout,
a,
b,
c,
d) =>
def boolToElement(bool: Boolean): Element = {
Element(ByteVector(if (bool) 1.toByte else 0.toByte))
}
val fields = Vector(
"tpe" -> Element(HyperbolaPayoutCurvePieceTLV.tpe),
"length" -> Element(tlv.length),
"usePositivePiece" -> boolToElement(usePositivePiece),
"translateOutcomeSign" -> boolToElement(translateOutcome.sign),
"translateOutcome" -> Element(
BigSizeUInt(translateOutcome.withoutPrecision)),
"translateOutcomeExtraPrecision" -> Element(
UInt16(translateOutcome.extraPrecision)),
"translatePayoutSign" -> boolToElement(translatePayout.sign),
"translatePayout" -> Element(
BigSizeUInt(translatePayout.withoutPrecision)),
"translatePayoutExtraPrecision" -> Element(
UInt16(translatePayout.extraPrecision)),
"aSign" -> boolToElement(a.sign),
"a" -> Element(BigSizeUInt(a.withoutPrecision)),
"aExtraPrecision" -> Element(UInt16(a.extraPrecision)),
"bSign" -> boolToElement(b.sign),
"b" -> Element(BigSizeUInt(b.withoutPrecision)),
"bExtraPrecision" -> Element(UInt16(b.extraPrecision)),
"cSign" -> boolToElement(c.sign),
"c" -> Element(BigSizeUInt(c.withoutPrecision)),
"cExtraPrecision" -> Element(UInt16(c.extraPrecision)),
"dSign" -> boolToElement(d.sign),
"d" -> Element(BigSizeUInt(d.withoutPrecision)),
"dExtraPrecision" -> Element(UInt16(d.extraPrecision))
)
DLCTLVTestVector(tlv, "hyperbola_payout_curve_piece", fields)
case RoundingIntervalsV0TLV(intervalStarts) =>
val fields = Vector(
"tpe" -> Element(RoundingIntervalsV0TLV.tpe),

View File

@ -28,7 +28,7 @@ import org.bitcoins.core.util.sorted._
[DLCPayoutCurve.scala](https://github.com/bitcoin-s/bitcoin-s/blob/master/core/src/main/scala/org/bitcoins/core/protocol/dlc/DLCPayoutCurve.scala) provides an interface for serializing and evaluating payout curves for DLCs as specified in the [Payout Curve Specification](https://github.com/discreetlogcontracts/dlcspecs/blob/c4fb12d95a4255eabb873611437d05b740bbeccc/PayoutCurve.md). This file supports arbitrary polynomial interpolation.
To approximate a payout curve that is not a piecewise polynomial function, one may either propose a new kind of curve to the specification, or use approximation. For example by feeding `DLCPayoutCurve` a list of `OutcomePayoutEndpoint`s, one receives a linear approximation of their payout curve which takes the sampled points and "connects the dots" with straight lines. Alternatively one can use spline interpolation and sample two midpoints of every spline to get a piecewise cubic interpolation.
To approximate a payout curve that is not a piecewise polynomial function, one may either propose a new kind of curve to the specification, or use approximation. For example by feeding `DLCPayoutCurve` a list of `PiecewisePolynomialEndpoint`s, one receives a linear approximation of their payout curve which takes the sampled points and "connects the dots" with straight lines. Alternatively one can use spline interpolation and sample two midpoints of every spline to get a piecewise cubic interpolation.
```scala mdoc:to-string
// Constructing a forward contract's payout curve (going long) that looks like this:
@ -40,12 +40,12 @@ To approximate a payout curve that is not a piecewise polynomial function, one m
// Assume a 15 binary digit oracle
val maxVal = (1L << 15) - 1
val pts = Vector(
OutcomePayoutEndpoint(0, 0),
OutcomePayoutEndpoint(1000, 0),
OutcomePayoutEndpoint(2000, 1000),
OutcomePayoutEndpoint(maxVal, 1000)
PiecewisePolynomialEndpoint(0, 0),
PiecewisePolynomialEndpoint(1000, 0),
PiecewisePolynomialEndpoint(2000, 1000),
PiecewisePolynomialEndpoint(maxVal, 1000)
)
val curve = DLCPayoutCurve(pts)
val curve = DLCPayoutCurve.polynomialInterpolate(pts,DLCSerializationVersion.Post144Pre163)
// Let's evalute the curve's values at varios points
curve(500)
@ -58,7 +58,7 @@ val roundTo100 = RoundingIntervals(Vector(IntervalStart(0, 100)))
curve(1667, roundTo100)
// Let's take a look at the pieces in this piece-wise polynomial
curve.functionComponents
curve.pieces
// And we can even see which component is used on a given outcome
val Indexed(line1, _) = curve.componentFor(500)

View File

@ -789,7 +789,7 @@ trait DLCTest {
outcomes: Vector[DLCOutcomeType],
outcomeIndex: Long): Vector[Int] = {
val points =
desc.outcomeValueFunc.points
desc.outcomeValueFunc.endpoints
val left = points(1).outcome
val right = points(2).outcome
// Somewhere in the middle third of the interesting values

View File

@ -5,10 +5,10 @@ import org.bitcoins.core.protocol.dlc.models.{
DLCPayoutCurve,
EnumContractDescriptor,
NumericContractDescriptor,
OutcomePayoutEndpoint,
PiecewisePolynomialEndpoint,
RoundingIntervals
}
import org.bitcoins.core.protocol.tlv.EnumOutcome
import org.bitcoins.core.protocol.tlv.{DLCSerializationVersion, EnumOutcome}
import org.bitcoins.core.util.NumberUtil
object DLCTestUtil {
@ -76,13 +76,15 @@ object DLCTestUtil {
val (leftVal, rightVal) =
if (isGoingLong) (Satoshis.zero, totalCollateral.satoshis)
else (totalCollateral.satoshis, Satoshis.zero)
val func = DLCPayoutCurve(
val func = DLCPayoutCurve.polynomialInterpolate(
Vector(
OutcomePayoutEndpoint(0, leftVal),
OutcomePayoutEndpoint(botCollar + 1, leftVal),
OutcomePayoutEndpoint(topCollar, rightVal),
OutcomePayoutEndpoint(overMaxValue - 1, rightVal)
))
PiecewisePolynomialEndpoint(0, leftVal),
PiecewisePolynomialEndpoint(botCollar + 1, leftVal),
PiecewisePolynomialEndpoint(topCollar, rightVal),
PiecewisePolynomialEndpoint(overMaxValue - 1, rightVal)
),
serializationVersion = DLCSerializationVersion.Post144Pre163
)
val roundingIntervalsToUse =
if (numRounds > 0 && roundingIntervals == RoundingIntervals.noRounding) {
val intervalStarts = 0.until(numRounds).toVector.map { num =>