Add invariant and better error message for invalid numeric contract descriptor (#3338)

* Add invariant and better error message for invalid numeric contract descriptor

* Better error message

* Fix test name
This commit is contained in:
benthecarman 2021-06-28 10:58:15 -05:00 committed by GitHub
parent af9bf21058
commit d7b753a869
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 172 additions and 48 deletions

View file

@ -18,25 +18,25 @@ import java.nio.file.{Path, Paths}
object BundleGUI extends WalletGUI with JFXApp3 {
// Catch unhandled exceptions on FX Application thread
Thread
.currentThread()
.setUncaughtExceptionHandler((_: Thread, ex: Throwable) => {
ex.printStackTrace()
lazy val _ = new Alert(AlertType.Error) {
initOwner(owner)
title = "Unhandled exception"
headerText = "Exception: " + ex.getClass + ""
contentText = Option(ex.getMessage).getOrElse("")
}.showAndWait()
})
implicit override lazy val system: ActorSystem = ActorSystem(
s"bitcoin-s-gui-${System.currentTimeMillis()}")
lazy val args = parameters.raw
override def start(): Unit = {
// Catch unhandled exceptions on FX Application thread
Thread
.currentThread()
.setUncaughtExceptionHandler((_: Thread, ex: Throwable) => {
ex.printStackTrace()
lazy val _ = new Alert(AlertType.Error) {
initOwner(owner)
title = "Unhandled exception"
headerText = "Exception: " + ex.getClass + ""
contentText = Option(ex.getMessage).getOrElse("")
}.showAndWait()
})
// Set log location
val baseConfig: Config = AppConfig
.getBaseConfig(DEFAULT_BITCOIN_S_DATADIR)

View file

@ -21,19 +21,6 @@ object GUI extends WalletGUI with JFXApp3 {
implicit override lazy val system: ActorSystem = ActorSystem(
s"bitcoin-s-gui-${System.currentTimeMillis()}")
// Catch unhandled exceptions on FX Application thread
Thread
.currentThread()
.setUncaughtExceptionHandler((_: Thread, ex: Throwable) => {
ex.printStackTrace()
lazy val _ = new Alert(AlertType.Error) {
initOwner(owner)
title = "Unhandled exception"
headerText = "Exception: " + ex.getClass + ""
contentText = Option(ex.getMessage).getOrElse("")
}.showAndWait()
})
override lazy val glassPane: VBox = new VBox {
children = new ProgressIndicator {
progress = ProgressIndicator.IndeterminateProgress
@ -44,6 +31,19 @@ object GUI extends WalletGUI with JFXApp3 {
}
override def start(): Unit = {
// Catch unhandled exceptions on FX Application thread
Thread
.currentThread()
.setUncaughtExceptionHandler((_: Thread, ex: Throwable) => {
ex.printStackTrace()
lazy val _ = new Alert(AlertType.Error) {
initOwner(owner)
title = "Unhandled exception"
headerText = "Exception: " + ex.getClass + ""
contentText = Option(ex.getMessage).getOrElse("")
}.showAndWait()
})
lazy val argsWithIndex: Vector[(String, Int)] =
parameters.raw.zipWithIndex.toVector

View file

@ -13,6 +13,7 @@ import org.bitcoins.gui.dlc.dialog.CreateDLCOfferDialog.getNumericContractInfo
import org.bitcoins.gui.util.GUIUtil._
import scalafx.Includes._
import scalafx.geometry.{Insets, Pos}
import scalafx.scene.control.Alert.AlertType
import scalafx.scene.control._
import scalafx.scene.layout._
import scalafx.stage.Window
@ -250,19 +251,17 @@ class CreateDLCOfferDialog extends Logging {
val previewGraphButton: Button = new Button("Preview Graph") {
onAction = _ => {
getNumericContractInfo(
val (totalCollateral, descriptor) = getNumericContractInfo(
decompOpt,
pointMap.toVector.sortBy(_._1).map(_._2),
roundingMap.toVector.sortBy(_._1).map(_._2)) match {
case Failure(_) => ()
case Success((totalCollateral, descriptor)) =>
DLCPlotUtil.plotCETsWithOriginalCurve(base = 2,
descriptor.numDigits,
descriptor.outcomeValueFunc,
totalCollateral,
descriptor.roundingIntervals)
()
}
roundingMap.toVector.sortBy(_._1).map(_._2))
DLCPlotUtil.plotCETsWithOriginalCurve(base = 2,
descriptor.numDigits,
descriptor.outcomeValueFunc,
totalCollateral,
descriptor.roundingIntervals)
()
}
}
@ -505,14 +504,12 @@ class CreateDLCOfferDialog extends Logging {
ContractInfo(descriptor, oracleInfo).toTLV
case oracleInfo: NumericOracleInfo =>
getNumericContractInfo(
val (totalCol, numeric) = getNumericContractInfo(
decompOpt,
pointMap.toVector.sortBy(_._1).map(_._2),
roundingMap.toVector.sortBy(_._1).map(_._2)) match {
case Failure(exception) => throw exception
case Success((totalCol, numeric)) =>
ContractInfo(totalCol, numeric, oracleInfo).toTLV
}
roundingMap.toVector.sortBy(_._1).map(_._2))
ContractInfo(totalCol, numeric, oracleInfo).toTLV
}
Some(
@ -539,11 +536,12 @@ object CreateDLCOfferDialog {
def getNumericContractInfo(
decompOpt: Option[DigitDecompositionEventDescriptorV0TLV],
pointVec: Vector[(TextField, TextField, CheckBox)],
roundingVec: Vector[(TextField, TextField)]): Try[
(Satoshis, NumericContractDescriptor)] = {
roundingVec: Vector[(TextField, TextField)]): (
Satoshis,
NumericContractDescriptor) = {
decompOpt match {
case Some(decomp) =>
Try {
val contractInfoT = Try {
val numDigits = decomp.numDigits.toInt
val outcomesValuePoints = pointVec.flatMap {
@ -585,8 +583,23 @@ object CreateDLCOfferDialog {
numDigits,
RoundingIntervals(roundingIntervalsStarts)))
}
case None => Failure(new RuntimeException("No announcement"))
contractInfoT match {
case Success(contractInfo) => contractInfo
case Failure(err) =>
// Do some basic processing of the message so it's prettier
val errorMsg = err.getMessage
.replace("requirement failed: ", "")
.replace(". You must define", ".\nYou must define")
new Alert(AlertType.Error) {
initOwner(owner)
title = "Error construction Contract Info"
headerText = errorMsg
dialogPane().stylesheets = GlobalData.currentStyleSheets
}.showAndWait()
throw err
}
case None => throw new RuntimeException("No announcement")
}
}
}

View file

@ -0,0 +1,89 @@
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.testkitcore.util.BitcoinSUnitTest
class ContractDescriptorTest extends BitcoinSUnitTest {
behavior of "ContractDescriptor"
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))
}
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))
}
it should "fail for starting below the minimum" in {
val func = DLCPayoutCurve(
Vector(
OutcomePayoutPoint(-1, Satoshis(0), isEndpoint = true),
OutcomePayoutPoint(3, Satoshis(100), isEndpoint = true)
))
assertThrows[IllegalArgumentException](
NumericContractDescriptor(func, 2, RoundingIntervals.noRounding))
}
it should "fail for starting above the minimum" in {
val func = DLCPayoutCurve(
Vector(
OutcomePayoutPoint(1, Satoshis(0), isEndpoint = true),
OutcomePayoutPoint(3, Satoshis(100), isEndpoint = true)
))
assertThrows[IllegalArgumentException](
NumericContractDescriptor(func, 2, RoundingIntervals.noRounding))
}
it should "fail for ending below the maximum" in {
val func = DLCPayoutCurve(
Vector(
OutcomePayoutPoint(0, Satoshis(0), isEndpoint = true),
OutcomePayoutPoint(2, Satoshis(100), isEndpoint = true)
))
assertThrows[IllegalArgumentException](
NumericContractDescriptor(func, 2, RoundingIntervals.noRounding))
}
it should "fail for ending above the maximum" in {
val func = DLCPayoutCurve(
Vector(
OutcomePayoutPoint(0, Satoshis(0), isEndpoint = true),
OutcomePayoutPoint(4, Satoshis(100), isEndpoint = true)
))
assertThrows[IllegalArgumentException](
NumericContractDescriptor(func, 2, RoundingIntervals.noRounding))
}
it should "correctly create a NumericContractDescriptor" in {
val func = DLCPayoutCurve(
Vector(
OutcomePayoutPoint(0, Satoshis(0), isEndpoint = true),
OutcomePayoutPoint(3, Satoshis(100), isEndpoint = true)
))
val descriptor =
NumericContractDescriptor(func, 2, RoundingIntervals.noRounding)
assert(descriptor.toTLV == ContractDescriptorV1TLV(
"fda720260002fda7261a0002010000000000000000000000010300000000000000640000fda724020000"))
}
}

View file

@ -52,6 +52,8 @@ case class EnumContractDescriptor(
with TLVSerializable[ContractDescriptorV0TLV]
with SeqWrapper[(EnumOutcome, Satoshis)] {
require(outcomeValueMap.nonEmpty, "Cannot give an empty set of outcomes")
override def wrapped: Vector[(EnumOutcome, Satoshis)] = outcomeValueMap
def keys: Vector[EnumOutcome] = outcomeValueMap.map(_._1)
@ -97,6 +99,26 @@ case class NumericContractDescriptor(
extends ContractDescriptor
with TLVSerializable[ContractDescriptorV1TLV] {
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}. " +
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}. " +
s"You must define the payout curve from $minValue - $maxValue"
)
override def flip(totalCollateral: Satoshis): NumericContractDescriptor = {
val flippedFunc = DLCPayoutCurve(outcomeValueFunc.points.map { point =>