Disjoint union dlc (#3839)

* Implemented Disjoint Union DLC related data structures with dlcTest passing

Broke DLCClientTest up into multiple test files

Optimized DLC execution unit tests by checking all outcomes on a single setup DLC

Refactored DLCTest to allow for Disjoint Union DLC construction and validation, added tests

Responded to review

Fixed after cherry-pick

Fixed docs

* Fixed things after rebase

* Rebase

* Fix json serializer

* Finish fixing compile

* Start trying to make APIs better for multi oracle contract infos

* Clean things up in the GUI, try to make failures on disjoint union contracts as explicit as possible

* Use less numDigits as optimization for non secp CI test cases

* Fix compile

* Refactor BroadcastDLCDialog

* Fix test case optimization

* Clean up comment

Co-authored-by: nkohen <nadavk25@gmail.com>
This commit is contained in:
Chris Stewart 2021-11-29 19:26:00 -06:00 committed by GitHub
parent cd3006c020
commit 8765c2f845
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1984 additions and 1298 deletions

View file

@ -383,18 +383,49 @@ object Picklers {
writer[Obj].comap { contractInfo =>
import contractInfo._
Obj("totalCollateral" -> Num(totalCollateral.toLong.toDouble),
"contractDescriptor" -> writeJs(contractDescriptor),
"oracleInfo" -> writeJs(oracleInfo))
Obj(
PicklerKeys.totalCollateralKey -> writeJs(totalCollateral),
PicklerKeys.contractDescriptorKey -> writeJs(contractDescriptor),
PicklerKeys.oracleInfoKey -> writeJs(oracleInfo)
)
}
val contractInfoV1TLVJsonWriter: Writer[ContractInfoV1TLV] = {
writer[Obj].comap { contractInfo =>
val arrayVec: Vector[ujson.Obj] = contractInfo.contractOraclePairs.map {
case (c, o) =>
val contractDescriptorJson = writeJs(c)
val oracleInfoJson = writeJs(o)
ujson.Obj(PicklerKeys.contractDescriptorKey -> contractDescriptorJson,
PicklerKeys.oracleInfoKey -> oracleInfoJson)
}
val arrayJson = ujson.Arr.from(arrayVec)
Obj(
PicklerKeys.totalCollateralKey -> Num(
contractInfo.totalCollateral.toLong.toDouble),
PicklerKeys.pairsKey -> arrayJson
)
}
}
val contractInfoJsonWriter: Writer[ContractInfoTLV] = {
writer[ujson.Value].comap {
case contractInfoV0TLV: ContractInfoV0TLV =>
writeJs(contractInfoV0TLV)(contractInfoV0TLVJsonWriter)
case contractInfoV1TLV: ContractInfoV1TLV =>
writeJs(contractInfoV1TLV)(contractInfoV1TLVJsonWriter)
}
}
implicit val offerTLVWriter: Writer[DLCOfferTLV] =
writer[Obj].comap { offer =>
import offer._
Obj(
"contractFlags" -> Str(contractFlags.toHexString),
"chainHash" -> Str(chainHash.hex),
"contractInfo" -> writeJs(contractInfo)(contractInfoV0TLVJsonWriter),
"contractInfo" -> writeJs(contractInfo)(contractInfoJsonWriter),
"fundingPubKey" -> Str(fundingPubKey.hex),
"payoutSPK" -> Str(payoutSPK.hex),
"payoutSerialId" -> Num(payoutSerialId.toBigInt.toDouble),

View file

@ -240,56 +240,61 @@ class DLCPaneModel(pane: DLCPane)(implicit ec: ExecutionContext)
}
def cancelDLC(status: DLCStatus): Unit = {
val eventId =
status.oracleInfo.singleOracleInfos.head.announcement.eventTLV.eventId
status.contractInfo match {
case _: SingleContractInfo =>
val eventId = status.eventIds.head
val confirmed = status.state match {
case DLCState.Offered | DLCState.Accepted =>
new Alert(AlertType.Confirmation) {
initOwner(owner)
headerText = "Confirm Canceling DLC"
contentText =
s"Are you sure you want to cancel this DLC for $eventId?\n" +
"This cannot be undone."
}.showAndWait() match {
case Some(ButtonType.OK) => true
case None | Some(_) => false
val confirmed = status.state match {
case DLCState.Offered | DLCState.Accepted =>
new Alert(AlertType.Confirmation) {
initOwner(owner)
headerText = "Confirm Canceling DLC"
contentText =
s"Are you sure you want to cancel this DLC for $eventId?\n" +
"This cannot be undone."
}.showAndWait() match {
case Some(ButtonType.OK) => true
case None | Some(_) => false
}
case DLCState.Signed =>
new Alert(AlertType.Confirmation) {
initOwner(owner)
headerText = "Confirm Unsafe Canceling DLC"
contentText =
"Danger! If your counter-party has received your sign message then they will be able to execute the DLC even if you cancel!\n"
s"Are you sure you want to cancel this DLC for $eventId?\n" +
"This cannot be undone.\n"
}.showAndWait() match {
case Some(ButtonType.OK) => true
case None | Some(_) => false
}
case DLCState.Broadcasted | DLCState.Confirmed | DLCState.Claimed |
DLCState.RemoteClaimed | DLCState.Refunded =>
new Alert(AlertType.Error) {
initOwner(owner)
headerText = "Failed to Cancel DLC"
contentText = "Cannot cancel a DLC after it has been signed"
}.showAndWait()
false
}
case DLCState.Signed =>
new Alert(AlertType.Confirmation) {
initOwner(owner)
headerText = "Confirm Unsafe Canceling DLC"
contentText =
"Danger! If your counter-party has received your sign message then they will be able to execute the DLC even if you cancel!\n"
s"Are you sure you want to cancel this DLC for $eventId?\n" +
"This cannot be undone.\n"
}.showAndWait() match {
case Some(ButtonType.OK) => true
case None | Some(_) => false
}
case DLCState.Broadcasted | DLCState.Confirmed | DLCState.Claimed |
DLCState.RemoteClaimed | DLCState.Refunded =>
new Alert(AlertType.Error) {
initOwner(owner)
headerText = "Failed to Cancel DLC"
contentText = "Cannot cancel a DLC after it has been signed"
}.showAndWait()
false
}
if (confirmed) {
taskRunner.run(
caption = "Canceling DLC",
op = {
ConsoleCli.exec(CancelDLC(status.dlcId),
GlobalData.consoleCliConfig) match {
case Success(_) => ()
case Failure(err) => throw err
}
updateDLCs()
Platform.runLater(GUI.model.updateBalance())
if (confirmed) {
taskRunner.run(
caption = "Canceling DLC",
op = {
ConsoleCli.exec(CancelDLC(status.dlcId),
GlobalData.consoleCliConfig) match {
case Success(_) => ()
case Failure(err) => throw err
}
updateDLCs()
Platform.runLater(GUI.model.updateBalance())
}
)
}
)
case _: DisjointUnionContractInfo =>
sys.error(
s"Disjoint contract infos are not supported via the GUI, cannot cancel")
}
}
@ -304,15 +309,24 @@ class DLCPaneModel(pane: DLCPane)(implicit ec: ExecutionContext)
case Success(txId) =>
logger.info(s"Successfully rebroadcast funding tx " + txId)
// Looking for Event Hash in status, but don't see it
val announcementHash =
status.oracleInfo.singleOracleInfos.head.announcement.sha256.hex
Platform.runLater(
FundingTransactionDialog.show(
parentWindow.value,
txId,
GUIUtil.epochToDateString(status.timeouts.contractTimeout),
GlobalData.buildAnnouncementUrl(announcementHash),
true))
status.contractInfo match {
case single: SingleContractInfo =>
val announcementHash =
single.announcements.head.sha256.hex
Platform.runLater(
FundingTransactionDialog.show(
parentWindow.value,
txId,
GUIUtil.epochToDateString(
status.timeouts.contractTimeout),
GlobalData.buildAnnouncementUrl(announcementHash),
true))
case disjointUnionContractInfo: DisjointUnionContractInfo =>
sys.error(
s"Don't know how to show correcit funding transaction dialog for" +
s"disjoint union contracts, contracts=${disjointUnionContractInfo.contracts}")
}
case Failure(err) => throw err
}
}

View file

@ -20,9 +20,15 @@ class DLCTableView(model: DLCPaneModel) {
text = "Event Id"
prefWidth = 230
cellValueFactory = { status =>
val eventIdStr =
status.value.oracleInfo.singleOracleInfos.head.announcement.eventTLV.eventId
new StringProperty(status, "Event Id", eventIdStr)
status.value.contractInfo match {
case _: SingleContractInfo =>
val eventIdStr =
status.value.eventIds.head
new StringProperty(status, "Event Id", eventIdStr)
case _: DisjointUnionContractInfo =>
sys.error(
s"Disjoint contracts are not supported via the GUI, cannot add to table")
}
}
}
@ -163,7 +169,7 @@ class DLCTableView(model: DLCPaneModel) {
onAction = _ => {
val dlc = selectionModel.value.getSelectedItem
val primaryOracle =
dlc.oracleInfo.singleOracleInfos.head.announcement
dlc.announcements.head
val url =
GUIUtil.getAnnouncementUrl(GlobalData.network, primaryOracle)
GUIUtil.openUrl(url)

View file

@ -107,11 +107,8 @@ class AcceptOfferDialog extends CliCommandProducer[AcceptDLCCliCommand] {
}
def showOfferTerms(offer: DLCOfferTLV): Unit = {
val (oracleKey, eventId) = offer.contractInfo.oracleInfo match {
case OracleInfoV0TLV(announcement) =>
(announcement.publicKey.hex, announcement.eventTLV.eventId)
case _: MultiOracleInfoTLV =>
throw new RuntimeException("This is impossible.")
val (oracleKey, eventId) = {
GUIUtil.getOraclePubKeyEventId(offer.contractInfo)
}
gridPane.add(new Label("Event Id"), 0, nextRow)
@ -133,7 +130,7 @@ class AcceptOfferDialog extends CliCommandProducer[AcceptDLCCliCommand] {
nextRow)
gridPane.add(
new TextField() {
text = oracleKey
text = oracleKey.hex
editable = false
},
1,
@ -170,55 +167,61 @@ class AcceptOfferDialog extends CliCommandProducer[AcceptDLCCliCommand] {
nextRow)
nextRow += 1
offer.contractInfo.contractDescriptor match {
case v0: ContractDescriptorV0TLV =>
gridPane.add(new Label("Potential Outcome"), 0, nextRow)
gridPane.add(new Label("Payouts"), 1, nextRow)
nextRow += 1
offer.contractInfo match {
case contractInfoV0TLV: ContractInfoV0TLV =>
contractInfoV0TLV.contractDescriptor match {
case v0: ContractDescriptorV0TLV =>
gridPane.add(new Label("Potential Outcome"), 0, nextRow)
gridPane.add(new Label("Payouts"), 1, nextRow)
nextRow += 1
val descriptor = EnumContractDescriptor
.fromTLV(v0)
.flip(offer.contractInfo.totalCollateral)
descriptor.foreach { case (str, satoshis) =>
gridPane.add(new TextField() {
text = str.outcome
editable = false
},
0,
nextRow)
gridPane.add(
new TextField() {
text = satoshis.toString
editable = false
tooltip = Tooltip(
s"""Amount you will win if the oracle signs for "$str".""")
tooltip.value.setShowDelay(new javafx.util.Duration(100))
},
1,
nextRow
)
nextRow += 1
val descriptor = EnumContractDescriptor
.fromTLV(v0)
.flip(offer.contractInfo.totalCollateral)
descriptor.foreach { case (str, satoshis) =>
gridPane.add(new TextField() {
text = str.outcome
editable = false
},
0,
nextRow)
gridPane.add(
new TextField() {
text = satoshis.toString
editable = false
tooltip = Tooltip(
s"""Amount you will win if the oracle signs for "$str".""")
tooltip.value.setShowDelay(new javafx.util.Duration(100))
},
1,
nextRow
)
nextRow += 1
}
case v1: ContractDescriptorV1TLV =>
val descriptor = NumericContractDescriptor
.fromTLV(v1)
.flip(offer.contractInfo.totalCollateral)
val previewGraphButton: Button = new Button("Preview Graph") {
onAction = _ => {
DLCPlotUtil.plotCETsWithOriginalCurve(
base = 2,
descriptor.numDigits,
descriptor.outcomeValueFunc,
offer.contractInfo.totalCollateral,
RoundingIntervals.fromTLV(v1.roundingIntervals))
()
}
}
gridPane.add(new Label("Payout Curve"), 0, nextRow)
gridPane.add(previewGraphButton, 1, nextRow)
nextRow += 1
}
case v1: ContractDescriptorV1TLV =>
val descriptor = NumericContractDescriptor
.fromTLV(v1)
.flip(offer.contractInfo.totalCollateral)
val previewGraphButton: Button = new Button("Preview Graph") {
onAction = _ => {
DLCPlotUtil.plotCETsWithOriginalCurve(
base = 2,
descriptor.numDigits,
descriptor.outcomeValueFunc,
offer.contractInfo.totalCollateral,
RoundingIntervals.fromTLV(v1.roundingIntervals))
()
}
}
gridPane.add(new Label("Payout Curve"), 0, nextRow)
gridPane.add(previewGraphButton, 1, nextRow)
nextRow += 1
case _: ContractInfoV1TLV =>
sys.error(
s"Disjoint union contracts cannot be accepted on the GUI currently")
}
gridPane.add(

View file

@ -134,12 +134,8 @@ object BroadcastDLCDialog
}
vbox.children.add(gridPane)
val (oracleKey, eventId) = status.contractInfo.oracleInfo.toTLV match {
case OracleInfoV0TLV(announcement) =>
(announcement.publicKey.hex, announcement.eventTLV.eventId)
case _: MultiOracleInfoTLV =>
throw new RuntimeException("This is impossible.")
}
val (oracleKey, eventId) =
GUIUtil.getOraclePubKeyEventId(status.contractInfo.toTLV)
gridPane.add(new Label("Event Id"), 0, nextRow)
gridPane.add(
@ -156,7 +152,7 @@ object BroadcastDLCDialog
gridPane.add(new Label("Oracle Public Key"), 0, nextRow)
gridPane.add(
new TextField() {
text = oracleKey
text = oracleKey.hex
editable = false
},
1,
@ -182,56 +178,62 @@ object BroadcastDLCDialog
nextRow)
nextRow += 1
status.contractInfo.contractDescriptor.toTLV match {
case v0: ContractDescriptorV0TLV =>
gridPane.add(new Label("Potential Outcome"), 0, nextRow)
gridPane.add(new Label("Payouts"), 1, nextRow)
nextRow += 1
status.contractInfo match {
case singleContractInfo: SingleContractInfo =>
singleContractInfo.contractDescriptor.toTLV match {
case v0: ContractDescriptorV0TLV =>
gridPane.add(new Label("Potential Outcome"), 0, nextRow)
gridPane.add(new Label("Payouts"), 1, nextRow)
nextRow += 1
val descriptor = EnumContractDescriptor
.fromTLV(v0)
.flip(status.totalCollateral.satoshis)
val descriptor = EnumContractDescriptor
.fromTLV(v0)
.flip(status.totalCollateral.satoshis)
descriptor.foreach { case (str, satoshis) =>
gridPane.add(new TextField() {
text = str.outcome
editable = false
},
0,
nextRow)
gridPane.add(new TextField() {
text = satoshis.toString
editable = false
},
1,
nextRow)
nextRow += 1
}
case v1: ContractDescriptorV1TLV =>
val previewGraphButton: Button = new Button("Preview Graph") {
onAction = _ => {
val descriptor = NumericContractDescriptor.fromTLV(v1)
val payoutCurve = if (status.isInitiator) {
descriptor.outcomeValueFunc
} else {
descriptor
.flip(status.totalCollateral.satoshis)
.outcomeValueFunc
descriptor.foreach { case (str, satoshis) =>
gridPane.add(new TextField() {
text = str.outcome
editable = false
},
0,
nextRow)
gridPane.add(new TextField() {
text = satoshis.toString
editable = false
},
1,
nextRow)
nextRow += 1
}
case v1: ContractDescriptorV1TLV =>
val previewGraphButton: Button = new Button("Preview Graph") {
onAction = _ => {
val descriptor = NumericContractDescriptor.fromTLV(v1)
val payoutCurve = if (status.isInitiator) {
descriptor.outcomeValueFunc
} else {
descriptor
.flip(status.totalCollateral.satoshis)
.outcomeValueFunc
}
DLCPlotUtil.plotCETs(base = 2,
descriptor.numDigits,
payoutCurve,
status.contractInfo.totalCollateral,
descriptor.roundingIntervals,
None)
()
}
}
DLCPlotUtil.plotCETs(base = 2,
descriptor.numDigits,
payoutCurve,
status.contractInfo.totalCollateral,
descriptor.roundingIntervals,
None)
()
}
gridPane.add(new Label("Payout Function"), 0, nextRow)
gridPane.add(previewGraphButton, 1, nextRow)
nextRow += 1
}
gridPane.add(new Label("Payout Function"), 0, nextRow)
gridPane.add(previewGraphButton, 1, nextRow)
nextRow += 1
case _: DisjointUnionContractInfo =>
sys.error(
s"Disjoint union contract is not supported in the broadcast dialog")
}
gridPane.add(new Label("Fee Rate"), 0, nextRow)

View file

@ -561,6 +561,14 @@ class CreateDLCOfferDialog
UInt32(instant.getEpochSecond)
}
val inputs = fields.values.flatMap { case (str, value) =>
if (str.text.value.nonEmpty && value.text.value.nonEmpty) {
val amount =
numberFormatter.parse(value.text.value).longValue()
Some((str.text.value, amount))
} else None
}
val contractInfo = oracleInfo match {
case oracleInfo: EnumOracleInfo =>
val missingOutcomes = fields.values.filter(_._2.text.value.isEmpty)
@ -570,27 +578,27 @@ class CreateDLCOfferDialog
s"You missed outcomes $missing. Please enter payouts for these situations")
}
val inputs = fields.values.flatMap { case (str, value) =>
if (str.text.value.nonEmpty && value.text.value.nonEmpty) {
val amount =
numberFormatter.parse(value.text.value).longValue()
Some((str.text.value, amount))
} else None
}
val contractMap = inputs.map { case (str, value) =>
EnumOutcome(str) -> Satoshis(value)
}.toVector
val descriptor = EnumContractDescriptor(contractMap)
ContractInfo(descriptor, oracleInfo).toTLV
SingleContractInfo(descriptor, oracleInfo).toTLV
case oracleInfo: NumericOracleInfo =>
val (totalCol, numeric) = getNumericContractInfo(
decompOpt,
pointMap.toVector.sortBy(_._1).map(_._2),
roundingMap.toVector.sortBy(_._1).map(_._2))
val textFields: Vector[(TextField, TextField)] = {
roundingMap.toVector.sortBy(_._1).map(_._2)
}
val (totalCollateral, numericContractDescriptor) =
getNumericContractInfo(
decompOpt,
pointMap.toVector.sortBy(_._1).map(_._2),
textFields
)
ContractInfo(totalCol, numeric, oracleInfo).toTLV
SingleContractInfo(totalCollateral,
numericContractDescriptor,
oracleInfo).toTLV
}
CreateDLCOffer(

View file

@ -149,12 +149,8 @@ object SignDLCDialog
}
vbox.children.add(gridPane)
val (oracleKey, eventId) = status.contractInfo.oracleInfo.toTLV match {
case OracleInfoV0TLV(announcement) =>
(announcement.publicKey.hex, announcement.eventTLV.eventId)
case _: MultiOracleInfoTLV =>
throw new RuntimeException("This is impossible.")
}
val (oracleKey, eventId) =
GUIUtil.getOraclePubKeyEventId(status.contractInfo.toTLV)
gridPane.add(new Label("Event Id"), 0, nextRow)
gridPane.add(
@ -171,7 +167,7 @@ object SignDLCDialog
gridPane.add(new Label("Oracle Public Key"), 0, nextRow)
gridPane.add(
new TextField() {
text = oracleKey
text = oracleKey.hex
editable = false
},
1,
@ -197,54 +193,59 @@ object SignDLCDialog
nextRow)
nextRow += 1
status.contractInfo.contractDescriptor.toTLV match {
case v0: ContractDescriptorV0TLV =>
gridPane.add(new Label("Potential Outcome"), 0, nextRow)
gridPane.add(new Label("Payouts"), 1, nextRow)
nextRow += 1
status.contractInfo match {
case single: SingleContractInfo =>
single.contractDescriptors.head.toTLV match {
case v0: ContractDescriptorV0TLV =>
gridPane.add(new Label("Potential Outcome"), 0, nextRow)
gridPane.add(new Label("Payouts"), 1, nextRow)
nextRow += 1
val descriptor = EnumContractDescriptor.fromTLV(v0)
val descriptor = EnumContractDescriptor.fromTLV(v0)
descriptor.foreach { case (str, satoshis) =>
gridPane.add(new TextField() {
text = str.outcome
editable = false
},
0,
nextRow)
gridPane.add(new TextField() {
text = satoshis.toString
editable = false
},
1,
nextRow)
nextRow += 1
}
case v1: ContractDescriptorV1TLV =>
val previewGraphButton: Button = new Button("Preview Graph") {
onAction = _ => {
val descriptor = NumericContractDescriptor.fromTLV(v1)
val payoutCurve = if (status.isInitiator) {
descriptor.outcomeValueFunc
} else {
descriptor
.flip(status.totalCollateral.satoshis)
.outcomeValueFunc
descriptor.foreach { case (str, satoshis) =>
gridPane.add(new TextField() {
text = str.outcome
editable = false
},
0,
nextRow)
gridPane.add(new TextField() {
text = satoshis.toString
editable = false
},
1,
nextRow)
nextRow += 1
}
case v1: ContractDescriptorV1TLV =>
val previewGraphButton: Button = new Button("Preview Graph") {
onAction = _ => {
val descriptor = NumericContractDescriptor.fromTLV(v1)
val payoutCurve = if (status.isInitiator) {
descriptor.outcomeValueFunc
} else {
descriptor
.flip(status.totalCollateral.satoshis)
.outcomeValueFunc
}
DLCPlotUtil.plotCETs(base = 2,
descriptor.numDigits,
payoutCurve,
status.contractInfo.totalCollateral,
descriptor.roundingIntervals,
None)
()
}
}
DLCPlotUtil.plotCETs(base = 2,
descriptor.numDigits,
payoutCurve,
status.contractInfo.totalCollateral,
descriptor.roundingIntervals,
None)
()
}
gridPane.add(new Label("Payout Function"), 0, nextRow)
gridPane.add(previewGraphButton, 1, nextRow)
nextRow += 1
}
gridPane.add(new Label("Payout Function"), 0, nextRow)
gridPane.add(previewGraphButton, 1, nextRow)
nextRow += 1
case _: DisjointUnionContractInfo =>
sys.error(s"Disjoint union contract info not supported in the GUI")
}
gridPane.add(new Label("Fee Rate"), 0, nextRow)

View file

@ -54,8 +54,23 @@ object ViewDLCDialog {
}
def buildView(status: DLCStatus, model: DLCPaneModel) = {
status.contractInfo match {
case singleContractInfo: SingleContractInfo =>
buildGridPane(status, singleContractInfo, model)
case _: DisjointUnionContractInfo =>
sys.error(s"Disjoint union contracts are not supported")
}
}
private def buildGridPane(
status: DLCStatus,
singleContractInfo: SingleContractInfo,
model: DLCPaneModel) = {
require(status.contractInfo == singleContractInfo,
s"Conflicting contract infos")
val closingTxId: StringProperty = StringProperty(
DLCStatus.getClosingTxId(status).map(_.hex).getOrElse(""))
new GridPane() {
alignment = Pos.Center
padding = Insets(10)
@ -68,11 +83,7 @@ object ViewDLCDialog {
row += 1
add(getLabel("Event Id"), 0, row)
add(
getTextField(
status.oracleInfo.singleOracleInfos.head.announcement.eventTLV.eventId),
columnIndex = 1,
rowIndex = row)
add(getTextField(status.eventIds.head), columnIndex = 1, rowIndex = row)
row += 1
add(getLabel("Initiator"), 0, row)
@ -217,7 +228,7 @@ object ViewDLCDialog {
// TODO : Refund button and discriminator
row += 1
status.contractInfo.contractDescriptor match {
singleContractInfo.contractDescriptor match {
case _: EnumContractDescriptor => ()
case descriptor: NumericContractDescriptor =>
val previewGraphButton: Button = new Button("Preview Graph") {

View file

@ -4,7 +4,13 @@ import javafx.beans.value.ObservableValue
import org.bitcoins.commons.jsonmodels.ExplorerEnv
import org.bitcoins.core.config.BitcoinNetwork
import org.bitcoins.core.protocol.BlockTimeStamp
import org.bitcoins.core.protocol.tlv.OracleAnnouncementTLV
import org.bitcoins.core.protocol.tlv.{
ContractInfoTLV,
ContractInfoV0TLV,
ContractInfoV1TLV,
OracleAnnouncementTLV
}
import org.bitcoins.crypto.SchnorrPublicKey
import org.bitcoins.gui.{GUI, GlobalData}
import scalafx.beans.property.StringProperty
import scalafx.scene.control.{Button, TextField, Tooltip}
@ -194,4 +200,16 @@ object GUIUtil {
val logoTestnet = new Image("/icons/bitcoin-s-testnet.png")
val logoSignet = new Image("/icons/bitcoin-s-signet.png")
val logoRegtest = new Image("/icons/bitcoin-s-regtest.png")
def getOraclePubKeyEventId(
contractInfo: ContractInfoTLV): (SchnorrPublicKey, String) = {
contractInfo match {
case ContractInfoV0TLV(_, _, oracleInfo) =>
(oracleInfo.announcements.head.publicKey,
oracleInfo.announcements.head.eventTLV.eventId)
case ContractInfoV1TLV(_, contractOraclePairs) =>
(contractOraclePairs.head._2.announcements.head.publicKey,
contractOraclePairs.head._2.announcements.head.eventTLV.eventId)
}
}
}

View file

@ -923,7 +923,7 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
EnumOutcome(loseStr)).sigPoint -> ECAdaptorSignature.dummy
)
val contractInfo = ContractInfo(contractDesc, oracleInfo)
val contractInfo = SingleContractInfo(contractDesc, oracleInfo)
val contractInfoTLV = contractInfo.toTLV
val offer = DLCOffer(
@ -944,7 +944,7 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
"create a dlc offer" in {
(mockWalletApi
.createDLCOffer(_: ContractInfoV0TLV,
.createDLCOffer(_: ContractInfoTLV,
_: Satoshis,
_: Option[SatoshisPerVirtualByte],
_: UInt32,

View file

@ -5,9 +5,9 @@ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import org.bitcoins.core.api.dlc.node.DLCNodeApi
import org.bitcoins.core.protocol.dlc.models.{
ContractInfo,
EnumSingleOracleInfo,
NumericSingleOracleInfo
NumericSingleOracleInfo,
SingleContractInfo
}
import org.bitcoins.core.protocol.tlv.{
EnumEventDescriptorV0TLV,
@ -50,9 +50,9 @@ case class DLCRoutes(dlcNode: DLCNodeApi)(implicit system: ActorSystem)
case _: EnumEventDescriptorV0TLV =>
EnumSingleOracleInfo(create.announcementTLV)
}
val contractInfo = ContractInfo(create.totalCollateral,
create.contractDescriptor,
oracleInfo)
val contractInfo = SingleContractInfo(create.totalCollateral,
create.contractDescriptor,
oracleInfo)
Server.httpSuccess(contractInfo.hex)
}
}

View file

@ -48,7 +48,7 @@ class DLCMessageTest extends BitcoinSJvmTest {
assertThrows[IllegalArgumentException](
DLCOffer(
protocolVersionOpt = DLCOfferTLV.currentVersionOpt,
contractInfo = ContractInfo.dummy,
contractInfo = SingleContractInfo.dummy,
pubKeys = DLCPublicKeys(dummyPubKey, dummyAddress),
totalCollateral = Satoshis(-1),
fundingInputs = Vector.empty,
@ -65,7 +65,7 @@ class DLCMessageTest extends BitcoinSJvmTest {
assertThrows[IllegalArgumentException](
DLCOffer(
protocolVersionOpt = DLCOfferTLV.currentVersionOpt,
contractInfo = ContractInfo.dummy,
contractInfo = SingleContractInfo.dummy,
pubKeys = DLCPublicKeys(dummyPubKey, dummyAddress),
totalCollateral = Satoshis(-1),
fundingInputs = Vector.empty,

View file

@ -18,7 +18,7 @@ import scala.concurrent._
trait DLCWalletApi { self: WalletApi =>
def createDLCOffer(
contractInfoTLV: ContractInfoV0TLV,
contractInfoTLV: ContractInfoTLV,
collateral: Satoshis,
feeRateOpt: Option[SatoshisPerVirtualByte],
locktime: UInt32,

View file

@ -1,9 +1,9 @@
package org.bitcoins.core.protocol.dlc.compute
import org.bitcoins.core.protocol.dlc.models.{
ContractInfo,
EnumContractDescriptor,
NumericContractDescriptor
NumericContractDescriptor,
SingleContractInfo
}
import org.bitcoins.core.protocol.tlv.{
EnumOutcome,
@ -121,7 +121,8 @@ object DLCAdaptorPointComputer {
/** Efficiently computes all adaptor points, in order, for a given ContractInfo.
* @see https://medium.com/crypto-garage/optimizing-numeric-outcome-dlc-creation-6d6091ac0e47
*/
def computeAdaptorPoints(contractInfo: ContractInfo): Vector[ECPublicKey] = {
def computeAdaptorPoints(
contractInfo: SingleContractInfo): Vector[ECPublicKey] = {
// The possible messages a single nonce may be used to sign
val possibleOutcomes: Vector[ByteVector] =
contractInfo.contractDescriptor match {

View file

@ -132,7 +132,18 @@ object DLCExecutor {
fundingTx: Transaction,
fundOutputIndex: Int
): ExecutedDLCOutcome = {
val threshold = contractInfo.oracleInfo.threshold
val sigOracles = oracleSigs.map(_.oracle)
val oracleInfoOpt = contractInfo.oracleInfos.find { oracleInfo =>
oracleInfo.threshold <= oracleSigs.length &&
sigOracles.forall(oracleInfo.singleOracleInfos.contains)
}
val oracleInfo = oracleInfoOpt.getOrElse(
throw new IllegalArgumentException(
s"Signatures do not correspond to any possible outcome! $oracleSigs"))
val threshold = oracleInfo.threshold
val sigCombinations = CETCalculator.combinations(oracleSigs, threshold)
var msgOpt: Option[OracleOutcome] = None

View file

@ -9,9 +9,16 @@ import org.bitcoins.core.protocol.dlc.compute.{
CETCalculator,
DLCAdaptorPointComputer
}
import org.bitcoins.core.protocol.dlc.models.ContractOraclePair.{
EnumPair,
NumericPair
}
import org.bitcoins.core.protocol.dlc.models.DLCMessage.DLCAccept
import org.bitcoins.core.protocol.tlv.{
ContractInfoTLV,
ContractInfoV0TLV,
ContractInfoV1TLV,
OracleAnnouncementTLV,
TLVDeserializable,
TLVSerializable,
UnsignedNumericOutcome
@ -21,6 +28,139 @@ import org.bitcoins.crypto.ECPublicKey
import scala.collection.immutable.HashMap
sealed trait ContractInfo extends TLVSerializable[ContractInfoTLV] {
def contracts: Vector[SingleContractInfo]
require(contracts.nonEmpty, s"Cannot have empty contract: $this")
def totalCollateral: Satoshis
def oracleInfos: Vector[OracleInfo]
def contractDescriptors: Vector[ContractDescriptor]
/** Returns the maximum payout the offerer could win from this contract */
def max: Satoshis
/** Computes the CET set and their corresponding payouts using CETCalculator. */
def allOutcomesAndPayouts: Vector[(OracleOutcome, Satoshis)]
/** Corresponds with this DLC's CET set */
lazy val allOutcomes: Vector[OracleOutcome] =
allOutcomesAndPayouts.map(_._1)
/** Maps adpator points to their corresponding OracleOutcomes (which correspond to CETs) */
lazy val sigPointMap: Map[ECPublicKey, OracleOutcome] =
adaptorPoints.zip(allOutcomes).toMap
/** Map OracleOutcomes (which correspond to CETs) to their adpator point and payouts */
lazy val outcomeMap: Map[OracleOutcome, (ECPublicKey, Satoshis, Satoshis)] = {
val builder =
HashMap.newBuilder[OracleOutcome, (ECPublicKey, Satoshis, Satoshis)]
allOutcomesAndPayouts.zip(adaptorPoints).foreach {
case ((outcome, offerPayout), adaptorPoint) =>
val acceptPayout = (totalCollateral - offerPayout).satoshis
builder.+=((outcome, (adaptorPoint, offerPayout, acceptPayout)))
}
builder.result()
}
lazy val adaptorPoints: Vector[ECPublicKey] = {
contracts.flatMap(DLCAdaptorPointComputer.computeAdaptorPoints)
}
lazy val adaptorPointsIndexed: Vector[Indexed[ECPublicKey]] = Indexed(
adaptorPoints)
/** Checks if the given OracleSignatures exactly match the given OracleOutcome.
*
* Warning: This will return false if too many OracleSignatures are given.
*
* TODO: Needs a lot of optimization
*/
def verifySigs(
outcome: OracleOutcome,
sigs: Vector[OracleSignatures]): Boolean = {
if (!sigs.map(_.oracle).forall(outcome.oracles.contains)) {
false
} else {
outcome match {
case EnumOracleOutcome(oracles, enumOutcome) =>
oracles.foldLeft(true) { case (boolSoFar, oracle) =>
lazy val sig = sigs.find(_.oracle == oracle)
boolSoFar && sig.exists(_.verifySignatures(enumOutcome))
}
case NumericOracleOutcome(oraclesAndOutcomes) =>
oraclesAndOutcomes.foldLeft(true) {
case (boolSoFar, (oracle, digits)) =>
lazy val sig = sigs.find(_.oracle == oracle)
boolSoFar && sig.exists(_.verifySignatures(digits))
}
}
}
}
/** Searches all possible outcomes for one which corresponds to the given signatures.
*
* Warning: This will return false if too many OracleSignatures are given.
*/
def findOutcome(sigs: Vector[OracleSignatures]): Option[OracleOutcome] = {
// TODO: Optimize by looking at nonces
// TODO: Optimize using NumericOracleSignatures.computeOutcome
allOutcomes.find(verifySigs(_, sigs))
}
/** Returns the adaptor point and payouts for a given OracleOutcome */
def resultOfOutcome(
outcome: OracleOutcome): (ECPublicKey, Satoshis, Satoshis) = {
outcomeMap(outcome)
}
/** Returns the payouts for the signature as (toOffer, toAccept) */
def getPayouts(sigs: Vector[OracleSignatures]): (Satoshis, Satoshis) = {
val outcome = findOutcome(sigs) match {
case Some(outcome) => outcome
case None =>
throw new IllegalArgumentException(
s"Signatures do not correspond to a possible outcome! $sigs")
}
getPayouts(outcome)
}
/** Returns the payouts for the outcome as (toOffer, toAccept) */
def getPayouts(outcome: OracleOutcome): (Satoshis, Satoshis) = {
val (_, offerOutcome, acceptOutcome) = resultOfOutcome(outcome)
(offerOutcome, acceptOutcome)
}
/** A ContractInfo can be constructed by the offerer, but it will not contain new
* information which alters the DLC's contract which is received in the accept message.
*
* Specifically if the total collateral changes or negotiation fields are relevant.
*
* In these cases, this function should be called to update the ContractInfo.
*/
def updateOnAccept(
newTotalCollateral: Satoshis,
negotiationFields: DLCAccept.NegotiationFields): ContractInfo
}
object ContractInfo
extends TLVDeserializable[ContractInfoTLV, ContractInfo](ContractInfoTLV) {
override def fromTLV(tlv: ContractInfoTLV): ContractInfo = {
tlv match {
case tlv: ContractInfoV0TLV => SingleContractInfo.fromTLV(tlv)
case tlv: ContractInfoV1TLV => DisjointUnionContractInfo.fromTLV(tlv)
}
}
}
/** Fully determines a DLC up to public keys and funding UTXOs to be used.
*
* Contains all contract and oracle information and provides an external
@ -32,31 +172,50 @@ import scala.collection.immutable.HashMap
* done regarding CETs during DLC setup and execution.
* @see https://github.com/discreetlogcontracts/dlcspecs/blob/a8876ed28ed33d5f7d5104f01aa2a8d80d128460/Messaging.md#the-contract_info-type
*/
case class ContractInfo(
totalCollateral: Satoshis,
case class SingleContractInfo(
override val totalCollateral: Satoshis,
contractOraclePair: ContractOraclePair)
extends TLVSerializable[ContractInfoV0TLV] {
extends ContractInfo
with TLVSerializable[ContractInfoV0TLV] {
override def contracts: Vector[SingleContractInfo] = {
Vector(this)
}
def contractDescriptor: ContractDescriptor =
contractOraclePair.contractDescriptor
def oracleInfo: OracleInfo = contractOraclePair.oracleInfo
def announcements: Vector[OracleAnnouncementTLV] = {
contractOraclePair match {
case EnumPair(_, oracleInfo) =>
oracleInfo.singleOracleInfos.map(_.announcement)
case NumericPair(_, oracleInfo) =>
oracleInfo.singleOracleInfos.map(_.announcement)
}
}
override def contractDescriptors: Vector[ContractDescriptor] = Vector(
contractDescriptor)
override def oracleInfos: Vector[OracleInfo] = Vector(oracleInfo)
override def toTLV: ContractInfoV0TLV = {
ContractInfoV0TLV(totalCollateral,
contractDescriptor.toTLV,
oracleInfo.toTLV)
}
/** Returns the maximum payout the offerer could win from this contract */
val max: Satoshis = contractDescriptor match {
/** @inheritdoc */
override val max: Satoshis = contractDescriptor match {
case descriptor: EnumContractDescriptor =>
descriptor.values.maxBy(_.toLong)
case _: NumericContractDescriptor => totalCollateral
}
/** Computes the CET set and their corresponding payouts using CETCalculator. */
lazy val allOutcomesAndPayouts: Vector[(OracleOutcome, Satoshis)] = {
/** @inheritdoc */
override lazy val allOutcomesAndPayouts: Vector[(OracleOutcome, Satoshis)] = {
contractOraclePair match {
case ContractOraclePair.EnumPair(descriptor,
single: EnumSingleOracleInfo) =>
@ -132,174 +291,149 @@ case class ContractInfo(
}
}
/** Corresponds with this DLC's CET set */
lazy val allOutcomes: Vector[OracleOutcome] =
allOutcomesAndPayouts.map(_._1)
/** Maps adpator points to their corresponding OracleOutcomes (which correspond to CETs) */
lazy val sigPointMap: Map[ECPublicKey, OracleOutcome] =
adaptorPoints.zip(allOutcomes).toMap
/** Map OracleOutcomes (which correspond to CETs) to their adpator point and payouts */
lazy val outcomeMap: Map[OracleOutcome, (ECPublicKey, Satoshis, Satoshis)] = {
val builder =
HashMap.newBuilder[OracleOutcome, (ECPublicKey, Satoshis, Satoshis)]
allOutcomesAndPayouts.zip(adaptorPoints).foreach {
case ((outcome, offerPayout), adaptorPoint) =>
val acceptPayout = (totalCollateral - offerPayout).satoshis
builder.+=((outcome, (adaptorPoint, offerPayout, acceptPayout)))
}
builder.result()
}
lazy val adaptorPoints: Vector[ECPublicKey] = {
DLCAdaptorPointComputer.computeAdaptorPoints(this)
}
lazy val adaptorPointsIndexed: Vector[Indexed[ECPublicKey]] = Indexed(
adaptorPoints)
/** Checks if the given OracleSignatures exactly match the given OracleOutcome.
*
* Warning: This will return false if too many OracleSignatures are given.
*
* TODO: Needs a lot of optimization
*/
def verifySigs(
outcome: OracleOutcome,
sigs: Vector[OracleSignatures]): Boolean = {
outcome match {
case EnumOracleOutcome(oracles, enumOutcome) =>
oracleInfo match {
case _: NumericOracleInfo =>
throw new IllegalArgumentException(
s"Cannot handle $enumOutcome with numeric oracle commitments: $oracleInfo")
case _: EnumOracleInfo =>
oracles.foldLeft(true) { case (boolSoFar, oracle) =>
lazy val sig = sigs.find(_.oracle == oracle)
boolSoFar && sig.exists(_.verifySignatures(enumOutcome))
}
}
case NumericOracleOutcome(oraclesAndOutcomes) =>
oracleInfo match {
case _: EnumOracleInfo =>
throw new IllegalArgumentException(
s"Cannot handle numeric outcomes with enum oracle commitments: $oracleInfo")
case _: NumericOracleInfo =>
oraclesAndOutcomes.foldLeft(true) {
case (boolSoFar, (oracle, digits)) =>
lazy val sig = sigs.find(_.oracle == oracle)
boolSoFar && sig.exists(_.verifySignatures(digits))
}
}
}
}
/** Searches all possible outcomes for one which corresponds to the given signatures.
*
* Warning: This will return false if too many OracleSignatures are given.
*/
def findOutcome(sigs: Vector[OracleSignatures]): Option[OracleOutcome] = {
// TODO: Optimize by looking at nonces
// TODO: Optimize using NumericOracleSignatures.computeOutcome
allOutcomes.find(verifySigs(_, sigs))
}
/** Returns the adaptor point and payouts for a given OracleOutcome */
def resultOfOutcome(
outcome: OracleOutcome): (ECPublicKey, Satoshis, Satoshis) = {
outcomeMap(outcome)
}
/** Returns the payouts for the signature as (toOffer, toAccept) */
def getPayouts(sigs: Vector[OracleSignatures]): (Satoshis, Satoshis) = {
val outcome = findOutcome(sigs) match {
case Some(outcome) => outcome
case None =>
throw new IllegalArgumentException(
s"Signatures do not correspond to a possible outcome! $sigs")
}
getPayouts(outcome)
}
/** Returns the payouts for the outcome as (toOffer, toAccept) */
def getPayouts(outcome: OracleOutcome): (Satoshis, Satoshis) = {
val (_, offerOutcome, acceptOutcome) = resultOfOutcome(outcome)
(offerOutcome, acceptOutcome)
}
/** A ContractInfo can be constructed by the offerer, but it will not contain new
* information which alters the DLC's contract which is received in the accept message.
*
* Specifically if the total collateral changes or negotiation fields are relevant.
*
* In these cases, this function should be called to update the ContractInfo.
*/
def updateOnAccept(
/** @inheritdoc */
override def updateOnAccept(
newTotalCollateral: Satoshis,
negotiationFields: DLCAccept.NegotiationFields): ContractInfo = {
if (newTotalCollateral == totalCollateral) {
this
} else {
contractOraclePair match {
case ContractOraclePair.EnumPair(_, _) =>
if (negotiationFields != DLCAccept.NoNegotiationFields) {
throw new IllegalArgumentException(
s"Cannot have rounding intervals for single nonce contract: $negotiationFields")
}
negotiationFields: DLCAccept.NegotiationFields): SingleContractInfo = {
contractOraclePair match {
case ContractOraclePair.EnumPair(_, _) =>
if (negotiationFields != DLCAccept.NoNegotiationFields) {
throw new IllegalArgumentException(
s"Cannot have rounding intervals for single nonce contract: $negotiationFields")
}
if (newTotalCollateral == totalCollateral) {
this
} else {
this.copy(totalCollateral = newTotalCollateral)
}
case ContractOraclePair.NumericPair(descriptor, oracleInfo) =>
val newRoundingIntervals = negotiationFields match {
case DLCAccept.NegotiationFieldsV1(acceptRoundingIntervals) =>
descriptor.roundingIntervals.minRoundingWith(
acceptRoundingIntervals)
case DLCAccept.NoNegotiationFields => descriptor.roundingIntervals
}
case ContractOraclePair.NumericPair(descriptor, oracleInfo) =>
val newRoundingIntervals = negotiationFields match {
case DLCAccept.NegotiationFieldsV1(acceptRoundingIntervals) =>
descriptor.roundingIntervals.minRoundingWith(
acceptRoundingIntervals)
case DLCAccept.NoNegotiationFields => descriptor.roundingIntervals
case _: DLCAccept.NegotiationFieldsV2 =>
throw new IllegalArgumentException(
s"Cannot use disjoint union negotiation fields for a SingleContractInfo, $negotiationFields")
}
if (
newTotalCollateral == totalCollateral && newRoundingIntervals == descriptor.roundingIntervals
) {
this
} else {
val newDescriptor =
descriptor.copy(roundingIntervals = newRoundingIntervals)
val contractOraclePair =
ContractOraclePair.NumericPair(newDescriptor, oracleInfo)
ContractInfo(totalCollateral = newTotalCollateral,
contractOraclePair = contractOraclePair)
}
SingleContractInfo(totalCollateral = newTotalCollateral,
contractOraclePair = contractOraclePair)
}
}
}
}
object ContractInfo
extends TLVDeserializable[ContractInfoV0TLV, ContractInfo](
object SingleContractInfo
extends TLVDeserializable[ContractInfoV0TLV, SingleContractInfo](
ContractInfoV0TLV) {
lazy val dummy: ContractInfo = fromTLV(ContractInfoV0TLV.dummy)
override def fromTLV(tlv: ContractInfoV0TLV): ContractInfo = {
override def fromTLV(tlv: ContractInfoV0TLV): SingleContractInfo = {
val contract = ContractDescriptor.fromTLV(tlv.contractDescriptor)
val oracleInfo = OracleInfo.fromTLV(tlv.oracleInfo)
val contractOraclePair =
ContractOraclePair.fromDescriptorOracle(contract, oracleInfo)
ContractInfo(tlv.totalCollateral, contractOraclePair)
SingleContractInfo(tlv.totalCollateral, contractOraclePair)
}
def apply(
enumDescriptor: EnumContractDescriptor,
enumOracleInfo: EnumOracleInfo): ContractInfo = {
enumOracleInfo: EnumOracleInfo): SingleContractInfo = {
val enumPair = ContractOraclePair.EnumPair(enumDescriptor, enumOracleInfo)
ContractInfo(totalCollateral = enumDescriptor.values.maxBy(_.toLong),
enumPair)
SingleContractInfo(totalCollateral = enumDescriptor.values.maxBy(_.toLong),
enumPair)
}
def apply(
totalCollateral: Satoshis,
contractDescriptor: ContractDescriptor,
oracleInfo: OracleInfo): ContractInfo = {
ContractInfo(
oracleInfo: OracleInfo): SingleContractInfo = {
SingleContractInfo(
totalCollateral,
ContractOraclePair.fromDescriptorOracle(contractDescriptor, oracleInfo))
}
}
case class DisjointUnionContractInfo(contracts: Vector[SingleContractInfo])
extends ContractInfo
with TLVSerializable[ContractInfoV1TLV] {
override val totalCollateral: Satoshis = contracts.head.totalCollateral
require(contracts.forall(_.totalCollateral == totalCollateral),
"All contract total collaterals must be equal.")
override def oracleInfos: Vector[OracleInfo] = contracts.map(_.oracleInfo)
override def contractDescriptors: Vector[ContractDescriptor] =
contracts.map(_.contractDescriptor)
override def toTLV: ContractInfoV1TLV = {
ContractInfoV1TLV(
totalCollateral,
contracts.map(contract =>
(contract.contractDescriptor.toTLV, contract.oracleInfo.toTLV)))
}
/** @inheritdoc */
override val max: Satoshis = contracts.map(_.max).max
/** @inheritdoc */
override lazy val allOutcomesAndPayouts: Vector[(OracleOutcome, Satoshis)] = {
contracts.flatMap(_.allOutcomesAndPayouts)
}
/** @inheritdoc */
override def updateOnAccept(
newTotalCollateral: Satoshis,
negotiationFields: DLCAccept.NegotiationFields): DisjointUnionContractInfo = {
negotiationFields match {
case DLCAccept.NegotiationFieldsV2(nestedNegotiationFields) =>
require(
nestedNegotiationFields.length == contracts.length,
s"Expected ${contracts.length} negotiation fields, got $negotiationFields.")
val newContracts = contracts.zip(nestedNegotiationFields).map {
case (contract, negotiationFields) =>
contract.updateOnAccept(newTotalCollateral, negotiationFields)
}
DisjointUnionContractInfo(newContracts)
case _: DLCAccept.NegotiationFields =>
throw new IllegalArgumentException(
s"Required disjoint union negotiation fields for disjoint union contract info, got $negotiationFields")
}
}
}
object DisjointUnionContractInfo
extends TLVDeserializable[ContractInfoV1TLV, DisjointUnionContractInfo](
ContractInfoV1TLV) {
override def fromTLV(tlv: ContractInfoV1TLV): DisjointUnionContractInfo = {
val contracts = tlv.contractOraclePairs.map {
case (descriptorTLV, oracleTLV) =>
val contract = ContractDescriptor.fromTLV(descriptorTLV)
val oracleInfo = OracleInfo.fromTLV(oracleTLV)
val contractOraclePair =
ContractOraclePair.fromDescriptorOracle(contract, oracleInfo)
SingleContractInfo(tlv.totalCollateral, contractOraclePair)
}
DisjointUnionContractInfo(contracts)
}
}

View file

@ -98,8 +98,10 @@ object DLCMessage {
changeSerialId != fundOutputSerialId,
s"changeSerialId ($changeSerialId) cannot be equal to fundOutputSerialId ($fundOutputSerialId)")
val oracleInfo: OracleInfo = contractInfo.oracleInfo
val contractDescriptor: ContractDescriptor = contractInfo.contractDescriptor
val oracleInfos: Vector[OracleInfo] = contractInfo.oracleInfos
val contractDescriptors: Vector[ContractDescriptor] =
contractInfo.contractDescriptors
lazy val dlcId: Sha256Digest = calcDLCId(fundingInputs.map(_.outPoint))
@ -261,6 +263,18 @@ object DLCMessage {
NegotiationFieldsV1TLV(roundingIntervals.toTLV)
}
case class NegotiationFieldsV2(
nestedNegotiationFields: Vector[NegotiationFields])
extends TLVSerializable[NegotiationFieldsV2TLV]
with NegotiationFields {
require(
nestedNegotiationFields.forall(!_.isInstanceOf[NegotiationFieldsV2]))
override def toTLV: NegotiationFieldsV2TLV = {
NegotiationFieldsV2TLV(nestedNegotiationFields.map(_.toTLV))
}
}
object NegotiationFields {
def fromTLV(tlv: NegotiationFieldsTLV): NegotiationFields = {
@ -268,6 +282,9 @@ object DLCMessage {
case NoNegotiationFieldsTLV => NoNegotiationFields
case NegotiationFieldsV1TLV(roundingIntervalsTLV) =>
NegotiationFieldsV1(RoundingIntervals.fromTLV(roundingIntervalsTLV))
case NegotiationFieldsV2TLV(nestedNegotiationFields) =>
NegotiationFieldsV2(
nestedNegotiationFields.map(NegotiationFields.fromTLV))
}
}
}

View file

@ -8,6 +8,7 @@ import org.bitcoins.core.protocol.dlc.models.DLCMessage.{
DLCOffer,
DLCSign
}
import org.bitcoins.core.protocol.tlv.OracleAnnouncementTLV
import org.bitcoins.core.protocol.transaction.WitnessTransaction
import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.crypto._
@ -24,13 +25,21 @@ sealed trait DLCStatus {
def lastUpdated: Instant
def tempContractId: Sha256Digest
def contractInfo: ContractInfo
def oracleInfo: OracleInfo = contractInfo.oracleInfo
def oracleInfos: Vector[OracleInfo] = contractInfo.oracleInfos
def timeouts: DLCTimeouts
def feeRate: FeeUnit
def totalCollateral: CurrencyUnit
def localCollateral: CurrencyUnit
def remoteCollateral: CurrencyUnit = totalCollateral - localCollateral
lazy val announcements: Vector[OracleAnnouncementTLV] = {
oracleInfos.flatMap(_.singleOracleInfos.map(_.announcement))
}
lazy val eventIds: Vector[String] = {
announcements.map(_.eventTLV.eventId)
}
lazy val statusString: String = state.toString
}

View file

@ -41,7 +41,7 @@ case class SingleOracleDLCTemplate(
override val toContractInfo: ContractInfo = {
val pair: NumericPair =
ContractOraclePair.NumericPair(contractDescriptor, oracleInfo)
ContractInfo(totalCollateral.satoshis, pair)
SingleContractInfo(totalCollateral.satoshis, pair)
}
}
@ -72,7 +72,7 @@ case class MultiOracleDLCTemplate(
override val toContractInfo: ContractInfo = {
val pair: NumericPair =
ContractOraclePair.NumericPair(contractDescriptor, oracleInfo)
ContractInfo(totalCollateral.satoshis, pair)
SingleContractInfo(totalCollateral.satoshis, pair)
}
}

View file

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

View file

@ -162,7 +162,6 @@ object TLV extends TLVParentFactory[TLV] {
RoundingIntervalsV0TLV,
PayoutFunctionV0TLV,
OracleParamsV0TLV,
ContractInfoV0TLV,
FundingInputV0TLV,
CETSignaturesV0TLV,
FundingSignaturesV0TLV,
@ -172,6 +171,7 @@ object TLV extends TLVParentFactory[TLV] {
) ++ EventDescriptorTLV.allFactories ++
ContractDescriptorTLV.allFactories ++
OracleInfoTLV.allFactories ++
ContractInfoTLV.allFactories ++
OracleAnnouncementTLV.allFactories ++
OracleAttestmentTLV.allFactories ++
NegotiationFieldsTLV.allFactories
@ -1062,7 +1062,9 @@ object ContractDescriptorV1TLV extends TLVFactory[ContractDescriptorV1TLV] {
override val typeName: String = "ContractDescriptorV1TLV"
}
sealed trait OracleInfoTLV extends DLCSetupPieceTLV
sealed trait OracleInfoTLV extends DLCSetupPieceTLV {
def announcements: Vector[OracleAnnouncementTLV]
}
object OracleInfoTLV extends TLVParentFactory[OracleInfoTLV] {
@ -1079,6 +1081,10 @@ case class OracleInfoV0TLV(announcement: OracleAnnouncementTLV)
override val value: ByteVector = {
announcement.bytes
}
override val announcements: Vector[OracleAnnouncementTLV] = {
Vector(announcement)
}
}
object OracleInfoV0TLV extends TLVFactory[OracleInfoV0TLV] {
@ -1098,6 +1104,10 @@ object OracleInfoV0TLV extends TLVFactory[OracleInfoV0TLV] {
sealed trait MultiOracleInfoTLV extends OracleInfoTLV {
def threshold: Int
def oracles: OrderedAnnouncements
override val announcements: Vector[OracleAnnouncementTLV] = {
oracles.toVector
}
}
case class OracleInfoV1TLV(threshold: Int, oracles: OrderedAnnouncements)
@ -1168,6 +1178,8 @@ case class OracleInfoV2TLV(
override val value: ByteVector = {
UInt16(threshold).bytes ++ u16PrefixedList(oracles.toVector) ++ params.bytes
}
override val announcements: Vector[OracleAnnouncementTLV] = oracles.toVector
}
object OracleInfoV2TLV extends TLVFactory[OracleInfoV2TLV] {
@ -1187,11 +1199,23 @@ object OracleInfoV2TLV extends TLVFactory[OracleInfoV2TLV] {
override val typeName: String = "OracleInfoV2TLV"
}
sealed trait ContractInfoTLV extends DLCSetupPieceTLV {
def totalCollateral: Satoshis
}
object ContractInfoTLV extends TLVParentFactory[ContractInfoTLV] {
override val allFactories: Vector[TLVFactory[ContractInfoTLV]] =
Vector(ContractInfoV0TLV, ContractInfoV1TLV)
override def typeName: String = "ContractInfoTLV"
}
case class ContractInfoV0TLV(
totalCollateral: Satoshis,
contractDescriptor: ContractDescriptorTLV,
oracleInfo: OracleInfoTLV)
extends DLCSetupPieceTLV {
extends ContractInfoTLV {
override val tpe: BigSizeUInt = ContractInfoV0TLV.tpe
override val value: ByteVector = {
@ -1224,6 +1248,43 @@ object ContractInfoV0TLV extends TLVFactory[ContractInfoV0TLV] {
override val typeName: String = "ContractInfoV0TLV"
}
case class ContractInfoV1TLV(
totalCollateral: Satoshis,
contractOraclePairs: Vector[(ContractDescriptorTLV, OracleInfoTLV)])
extends ContractInfoTLV {
override val tpe: BigSizeUInt = ContractInfoV0TLV.tpe
override val value: ByteVector = {
satBytes(totalCollateral) ++
bigSizePrefixedList[(ContractDescriptorTLV, OracleInfoTLV)](
contractOraclePairs,
{ case (descriptor, oracleInfo) =>
descriptor.bytes ++ oracleInfo.bytes
}
)
}
}
object ContractInfoV1TLV extends TLVFactory[ContractInfoV1TLV] {
override val tpe: BigSizeUInt = BigSizeUInt(55344)
override def fromTLVValue(value: ByteVector): ContractInfoV1TLV = {
val iter = ValueIterator(value)
val totalCollateral = iter.takeSats()
val contracts = iter.takeBigSizePrefixedList { () =>
val descriptor = iter.take(ContractDescriptorTLV)
val oracleInfo = iter.take(OracleInfoTLV)
(descriptor, oracleInfo)
}
ContractInfoV1TLV(totalCollateral, contracts)
}
override val typeName: String = "ContractInfoV1TLV"
}
sealed trait FundingInputTLV extends DLCSetupPieceTLV {
def inputSerialId: UInt64
}
@ -1377,7 +1438,7 @@ case class DLCOfferTLV(
protocolVersionOpt: Option[Int],
contractFlags: Byte,
chainHash: DoubleSha256Digest,
contractInfo: ContractInfoV0TLV,
contractInfo: ContractInfoTLV,
fundingPubKey: ECPublicKey,
payoutSPK: ScriptPubKey,
payoutSerialId: UInt64,
@ -1523,6 +1584,34 @@ object NegotiationFieldsV1TLV extends TLVFactory[NegotiationFieldsV1TLV] {
override val typeName: String = "NegotiationFieldsV1TLV"
}
case class NegotiationFieldsV2TLV(
nestedNegotiationFields: Vector[NegotiationFieldsTLV])
extends NegotiationFieldsTLV {
require(
nestedNegotiationFields.forall(!_.isInstanceOf[NegotiationFieldsV2TLV]))
override val tpe: BigSizeUInt = NegotiationFieldsV2TLV.tpe
override val value: ByteVector = {
bigSizePrefixedList(nestedNegotiationFields)
}
}
object NegotiationFieldsV2TLV extends TLVFactory[NegotiationFieldsV2TLV] {
override val tpe: BigSizeUInt = BigSizeUInt(55346)
override def fromTLVValue(value: ByteVector): NegotiationFieldsV2TLV = {
val iter = ValueIterator(value)
val nestedNegotiationFields =
iter.takeBigSizePrefixedList(() => iter.take(NegotiationFieldsTLV))
NegotiationFieldsV2TLV(nestedNegotiationFields)
}
override val typeName: String = "NegotiationFieldsV2TLV"
}
case class DLCAcceptTLV(
tempContractId: Sha256Digest,
totalCollateralSatoshis: Satoshis,

View file

@ -20,4 +20,10 @@ object PicklerKeys {
//offers
final val protocolVersionKey: String = "protocolVersion"
//contract info
final val totalCollateralKey = "totalCollateral"
final val contractDescriptorKey = "contractDescriptor"
final val oracleInfoKey = "oracleInfo"
final val pairsKey = "pairs"
}

View file

@ -14,4 +14,9 @@ object EnvUtil {
lazy val isCI: Boolean = Properties.envOrNone("CI").contains("true")
def getVersion: String = getClass.getPackage.getImplementationVersion
def isNativeSecp256k1Disabled: Boolean = {
val secpDisabled = System.getenv("DISABLE_SECP256K1")
secpDisabled != null && (secpDisabled.toLowerCase == "true" || secpDisabled == "1")
}
}

View file

@ -0,0 +1,40 @@
package org.bitcoins.dlc
import org.bitcoins.core.protocol.tlv.OracleParamsV0TLV
import org.bitcoins.testkitcore.dlc.DLCTest
import org.bitcoins.testkitcore.util.BitcoinSJvmTest
class DLCAdaptorPointComputerTest extends BitcoinSJvmTest with DLCTest {
behavior of "DLCAdaptorPointComputer"
it should "compute sigpoints correctly" in {
runTestsForParam(Vector(4, 6, 8)) { numDigitsOrOutcomes =>
runTestsForParam(Vector(true, false)) { isNumeric =>
runTestsForParam(Vector((1, 1), (2, 3), (3, 5))) {
case (threshold, numOracles) =>
runTestsForParam(
Vector(None,
Some(
OracleParamsV0TLV(numDigitsOrOutcomes / 2 + 1,
numDigitsOrOutcomes / 2,
maximizeCoverage = true)))) {
oracleParams =>
val contractParams = SingleContractParams(numDigitsOrOutcomes,
isNumeric,
threshold,
numOracles,
oracleParams)
val (client, _, _) = constructDLCClients(contractParams)
val contract = client.offer.contractInfo
val outcomes = contract.allOutcomes
val adaptorPoints = contract.adaptorPoints
val expectedAdaptorPoints = outcomes.map(_.sigPoint)
assert(adaptorPoints == expectedAdaptorPoints)
}
}
}
}
}
}

View file

@ -1,659 +0,0 @@
package org.bitcoins.dlc
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.dlc.build.DLCTxBuilder
import org.bitcoins.core.protocol.dlc.execution.{
DLCOutcome,
ExecutedDLCOutcome,
RefundDLCOutcome,
SetupDLC
}
import org.bitcoins.core.protocol.dlc.models.DLCMessage._
import org.bitcoins.core.protocol.dlc.models._
import org.bitcoins.core.protocol.dlc.verify.DLCSignatureVerifier
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.tlv.{
DLCOutcomeType,
EnumOutcome,
OracleParamsV0TLV,
UnsignedNumericOutcome
}
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.script.crypto.HashType
import org.bitcoins.core.util.{BitcoinScriptUtil, Indexed, NumberUtil}
import org.bitcoins.core.wallet.utxo._
import org.bitcoins.crypto._
import org.bitcoins.testkitcore.dlc.{DLCFeeTestUtil, DLCTest, TestDLCClient}
import org.bitcoins.testkitcore.util.{BitcoinSJvmTest, BytesUtil}
import org.scalatest.Assertion
import scodec.bits.BitVector
import scala.concurrent.Future
import scala.util.Random
class DLCClientTest extends BitcoinSJvmTest with DLCTest {
behavior of "AdaptorDLCClient"
def validateOutcome(
outcome: DLCOutcome,
dlcOffer: TestDLCClient,
dlcAccept: TestDLCClient): Assertion = {
val fundingTx = outcome.fundingTx
assert(noEmptySPKOutputs(fundingTx))
val fundOutputIndex = dlcOffer.dlcTxBuilder.fundOutputIndex
val signers = Vector(dlcOffer.fundingPrivKey, dlcAccept.fundingPrivKey)
val closingSpendingInfo = ScriptSignatureParams(
P2WSHV0InputInfo(
TransactionOutPoint(fundingTx.txId, UInt32(fundOutputIndex)),
fundingTx.outputs(fundOutputIndex).value,
P2WSHWitnessV0(
MultiSignatureScriptPubKey(2,
signers.map(_.publicKey).sortBy(_.hex))),
ConditionalPath.NoCondition
),
fundingTx,
signers,
HashType.sigHashAll
)
outcome match {
case ExecutedDLCOutcome(fundingTx, cet, _, _) =>
DLCFeeTestUtil.validateFees(dlcOffer.dlcTxBuilder,
fundingTx,
cet,
fundingTxSigs = 5)
assert(noEmptySPKOutputs(cet))
assert(BitcoinScriptUtil.verifyScript(cet, Vector(closingSpendingInfo)))
case RefundDLCOutcome(fundingTx, refundTx) =>
DLCFeeTestUtil.validateFees(dlcOffer.dlcTxBuilder,
fundingTx,
refundTx,
fundingTxSigs = 5)
assert(noEmptySPKOutputs(refundTx))
assert(
BitcoinScriptUtil.verifyScript(refundTx, Vector(closingSpendingInfo)))
}
}
def setupDLC(
dlcOffer: TestDLCClient,
dlcAccept: TestDLCClient): Future[(SetupDLC, SetupDLC)] = {
setupDLC(dlcOffer,
dlcAccept,
_.map(_.fundingTx),
_ => Future.successful(()))
}
def constructAndSetupDLC(
numOutcomes: Int,
isMultiDigit: Boolean,
oracleThreshold: Int,
numOracles: Int,
paramsOpt: Option[OracleParamsV0TLV] = None): Future[
(
TestDLCClient,
SetupDLC,
TestDLCClient,
SetupDLC,
Vector[DLCOutcomeType])] = {
val (offerDLC, acceptDLC, outcomes) =
constructDLCClients(numOutcomes,
isMultiDigit,
oracleThreshold,
numOracles,
paramsOpt)
for {
(offerSetup, acceptSetup) <- setupDLC(offerDLC, acceptDLC)
} yield (offerDLC, offerSetup, acceptDLC, acceptSetup, outcomes)
}
def executeForCase(
outcomeIndex: Long,
numOutcomes: Int,
isMultiDigit: Boolean,
oracleThreshold: Int,
numOracles: Int,
paramsOpt: Option[OracleParamsV0TLV] = None): Future[Assertion] = {
constructAndSetupDLC(numOutcomes,
isMultiDigit,
oracleThreshold,
numOracles,
paramsOpt)
.flatMap {
case (dlcOffer, offerSetup, dlcAccept, acceptSetup, outcomes) =>
val oracleSigs = genOracleSignatures(numOutcomes,
isMultiDigit,
dlcOffer,
outcomes,
outcomeIndex,
paramsOpt)
for {
offerOutcome <-
dlcOffer.executeDLC(offerSetup, Future.successful(oracleSigs))
acceptOutcome <-
dlcAccept.executeDLC(acceptSetup, Future.successful(oracleSigs))
} yield {
assert(offerOutcome.fundingTx == acceptOutcome.fundingTx)
validateOutcome(offerOutcome, dlcOffer, dlcAccept)
validateOutcome(acceptOutcome, dlcOffer, dlcAccept)
}
}
}
def executeRefundCase(
numOutcomes: Int,
isMultiNonce: Boolean,
oracleThreshold: Int,
numOracles: Int,
paramsOpt: Option[OracleParamsV0TLV] = None): Future[Assertion] = {
constructAndSetupDLC(numOutcomes,
isMultiNonce,
oracleThreshold,
numOracles,
paramsOpt)
.flatMap { case (dlcOffer, offerSetup, dlcAccept, acceptSetup, _) =>
val offerOutcome = dlcOffer.executeRefundDLC(offerSetup)
val acceptOutcome = dlcAccept.executeRefundDLC(acceptSetup)
validateOutcome(offerOutcome, dlcOffer, dlcAccept)
validateOutcome(acceptOutcome, dlcOffer, dlcAccept)
assert(acceptOutcome.fundingTx == offerOutcome.fundingTx)
assert(acceptOutcome.refundTx == offerOutcome.refundTx)
}
}
val enumOracleSchemesToTest: Vector[(Int, Int)] =
Vector((1, 1), (1, 2), (2, 2), (2, 3), (3, 5), (5, 8))
val numEnumOutcomesToTest: Vector[Int] = Vector(2, 3, 5, 8)
def runSingleNonceTests(
exec: (Long, Int, Boolean, Int, Int, Option[OracleParamsV0TLV]) => Future[
Assertion]): Future[Assertion] = {
runTestsForParam(numEnumOutcomesToTest) { numOutcomes =>
runTestsForParam(0.until(numOutcomes).toVector) { outcomeIndex =>
runTestsForParam(enumOracleSchemesToTest) {
case (threshold, numOracles) =>
exec(outcomeIndex, numOutcomes, false, threshold, numOracles, None)
}
}
}
}
val numericOracleSchemesToTest: Vector[(Int, Int)] =
Vector((1, 1), (2, 2), (2, 3))
val numDigitsToTest: Vector[Int] = Vector(4, 5, 10)
def runMultiNonceTests(
exec: (Long, Int, Boolean, Int, Int, Option[OracleParamsV0TLV]) => Future[
Assertion]): Future[Assertion] = {
runTestsForParam(numDigitsToTest) { numDigits =>
runTestsForParam(numericOracleSchemesToTest) {
case (threshold, numOracles) =>
val randDigits = (0 until numDigits).toVector.map { _ =>
scala.util.Random.nextInt(2)
}
val num =
BitVector
.fromValidBin(randDigits.mkString(""))
.toLong(signed = false)
exec(num, numDigits, true, threshold, numOracles, None)
}
}
}
it should "be able to construct and verify with ScriptInterpreter every tx in a DLC for the normal enum case" in {
runSingleNonceTests(executeForCase)
}
it should "be able to construct and verify with ScriptInterpreter every tx in a DLC for the normal numeric case" in {
runMultiNonceTests(executeForCase)
}
it should "be able to construct and verify with ScriptInterpreter every tx in a DLC for the large numeric case" in {
val numDigits = 17
val randDigits = (0 until numDigits).toVector.map { _ =>
scala.util.Random.nextInt(2)
}
val num =
BitVector.fromValidBin(randDigits.mkString("")).toLong(signed = false)
executeForCase(num,
numDigits,
isMultiDigit = true,
oracleThreshold = 1,
numOracles = 1)
}
it should "be able to construct and verify with ScriptInterpreter every tx in a DLC for a normal multi-oracle numeric case with bounded differences allowed" in {
val threshold = 3
val numOracles = 5
val numDigits = 8
val params = OracleParamsV0TLV(maxErrorExp = 4,
minFailExp = 2,
maximizeCoverage = false)
val randDigits = (0 until numDigits).toVector.map { _ =>
scala.util.Random.nextInt(2)
}
val num =
BitVector
.fromValidBin(randDigits.mkString(""))
.toLong(signed = false)
executeForCase(num,
numDigits,
isMultiDigit = true,
threshold,
numOracles,
Some(params))
}
it should "be able to construct and verify with ScriptInterpreter every tx in a DLC for the refund case" in {
runTestsForParam(Vector(false, true)) { isNumeric =>
val numOutcomesOrDigitsToTest =
if (isNumeric) numDigitsToTest else numEnumOutcomesToTest
runTestsForParam(numOutcomesOrDigitsToTest) { numOutcomesOrDigits =>
runTestsForParam(numericOracleSchemesToTest) {
case (threshold, numOracles) =>
executeRefundCase(numOutcomesOrDigits,
isMultiNonce = isNumeric,
oracleThreshold = threshold,
numOracles = numOracles)
}
}
}
}
it should "all work for a 100 outcome DLC" in {
val numOutcomes = 100
val testFs = (0 until 10).map(_ * 10).map { outcomeIndex =>
for {
_ <- executeForCase(outcomeIndex,
numOutcomes,
isMultiDigit = false,
oracleThreshold = 1,
numOracles = 1)
} yield succeed
}
Future
.sequence(testFs)
.flatMap(_ =>
executeRefundCase(numOutcomes,
isMultiNonce = false,
oracleThreshold = 1,
numOracles = 1))
}
it should "fail on invalid funding signatures" in {
val (offerClient, acceptClient, _) =
constructDLCClients(numOutcomesOrDigits = 3,
isNumeric = false,
oracleThreshold = 1,
numOracles = 1,
paramsOpt = None)
val builder = offerClient.dlcTxBuilder
val offerVerifier = DLCSignatureVerifier(builder, isInitiator = true)
val acceptVerifier = DLCSignatureVerifier(builder, isInitiator = false)
val offerFundingSigs = offerClient.dlcTxSigner.signFundingTx().get
val acceptFundingSigs = acceptClient.dlcTxSigner.signFundingTx().get
val badOfferFundingSigs = BytesUtil.flipBit(offerFundingSigs)
val badAcceptFundingSigs = BytesUtil.flipBit(acceptFundingSigs)
assert(
offerClient.dlcTxSigner
.completeFundingTx(badAcceptFundingSigs)
.failed
.get
.isInstanceOf[RuntimeException])
assert(
acceptClient.dlcTxSigner
.completeFundingTx(badOfferFundingSigs)
.failed
.get
.isInstanceOf[RuntimeException])
assert(offerVerifier.verifyRemoteFundingSigs(acceptFundingSigs))
assert(acceptVerifier.verifyRemoteFundingSigs(offerFundingSigs))
assert(!offerVerifier.verifyRemoteFundingSigs(badAcceptFundingSigs))
assert(!acceptVerifier.verifyRemoteFundingSigs(badOfferFundingSigs))
assert(!offerVerifier.verifyRemoteFundingSigs(offerFundingSigs))
assert(!acceptVerifier.verifyRemoteFundingSigs(acceptFundingSigs))
}
it should "succeed on valid CET signatures" in {
val (offerClient, acceptClient, outcomes) =
constructDLCClients(numOutcomesOrDigits = 2,
isNumeric = false,
oracleThreshold = 1,
numOracles = 1,
paramsOpt = None)
val builder = offerClient.dlcTxBuilder
val offerVerifier = DLCSignatureVerifier(builder, isInitiator = true)
val acceptVerifier = DLCSignatureVerifier(builder, isInitiator = false)
val offerCETSigs = offerClient.dlcTxSigner.createCETSigs()
val acceptCETSigs = acceptClient.dlcTxSigner.createCETSigs()
outcomes.zipWithIndex.foreach { case (outcomeUncast, index) =>
val outcome = EnumOracleOutcome(
Vector(offerClient.offer.oracleInfo.asInstanceOf[EnumSingleOracleInfo]),
outcomeUncast.asInstanceOf[EnumOutcome])
assert(
offerVerifier.verifyCETSig(Indexed(outcome.sigPoint, index),
acceptCETSigs(outcome.sigPoint)))
assert(
acceptVerifier.verifyCETSig(Indexed(outcome.sigPoint, index),
offerCETSigs(outcome.sigPoint)))
}
assert(offerVerifier.verifyRefundSig(acceptCETSigs.refundSig))
assert(offerVerifier.verifyRefundSig(offerCETSigs.refundSig))
assert(acceptVerifier.verifyRefundSig(offerCETSigs.refundSig))
assert(acceptVerifier.verifyRefundSig(acceptCETSigs.refundSig))
}
it should "fail on invalid CET signatures" in {
val (offerClient, acceptClient, outcomes) =
constructDLCClients(numOutcomesOrDigits = 3,
isNumeric = false,
oracleThreshold = 1,
numOracles = 1,
paramsOpt = None)
val builder = offerClient.dlcTxBuilder
val offerVerifier = DLCSignatureVerifier(builder, isInitiator = true)
val acceptVerifier = DLCSignatureVerifier(builder, isInitiator = false)
val offerCETSigs = offerClient.dlcTxSigner.createCETSigs()
val acceptCETSigs = acceptClient.dlcTxSigner.createCETSigs()
val badOfferCETSigs = BytesUtil.flipBit(offerCETSigs)
val badAcceptCETSigs = BytesUtil.flipBit(acceptCETSigs)
outcomes.foreach { outcomeUncast =>
val outcome = outcomeUncast.asInstanceOf[EnumOutcome]
val oracleInfo =
offerClient.offer.oracleInfo.asInstanceOf[EnumSingleOracleInfo]
val oracleOutcome = EnumOracleOutcome(Vector(oracleInfo), outcome)
val oracleSig = genEnumOracleSignature(oracleInfo, outcome.outcome)
assertThrows[RuntimeException] {
offerClient.dlcTxSigner.completeCET(
oracleOutcome,
badAcceptCETSigs(oracleOutcome.sigPoint),
Vector(oracleSig))
}
assertThrows[RuntimeException] {
acceptClient.dlcTxSigner
.completeCET(oracleOutcome,
badOfferCETSigs(oracleOutcome.sigPoint),
Vector(oracleSig))
}
}
assertThrows[RuntimeException] {
offerClient.dlcTxSigner.completeRefundTx(badAcceptCETSigs.refundSig)
}
assertThrows[RuntimeException] {
acceptClient.dlcTxSigner.completeRefundTx(badOfferCETSigs.refundSig)
}
outcomes.zipWithIndex.foreach { case (outcomeUncast, index) =>
val outcome = EnumOracleOutcome(
Vector(offerClient.offer.oracleInfo.asInstanceOf[EnumSingleOracleInfo]),
outcomeUncast.asInstanceOf[EnumOutcome])
val adaptorPoint = Indexed(outcome.sigPoint, index)
assert(
!offerVerifier.verifyCETSig(adaptorPoint,
badAcceptCETSigs(outcome.sigPoint)))
assert(
!acceptVerifier.verifyCETSig(adaptorPoint,
badOfferCETSigs(outcome.sigPoint)))
assert(
!offerVerifier.verifyCETSig(adaptorPoint,
offerCETSigs(outcome.sigPoint)))
assert(
!acceptVerifier.verifyCETSig(adaptorPoint,
acceptCETSigs(outcome.sigPoint)))
}
assert(!offerVerifier.verifyRefundSig(badAcceptCETSigs.refundSig))
assert(!offerVerifier.verifyRefundSig(badOfferCETSigs.refundSig))
assert(!acceptVerifier.verifyRefundSig(badOfferCETSigs.refundSig))
assert(!acceptVerifier.verifyRefundSig(badAcceptCETSigs.refundSig))
}
it should "compute sigpoints correctly" in {
runTestsForParam(Vector(4, 6, 8)) { numDigitsOrOutcomes =>
runTestsForParam(Vector(true, false)) { isNumeric =>
runTestsForParam(Vector((1, 1), (2, 3), (3, 5))) {
case (threshold, numOracles) =>
runTestsForParam(
Vector(None,
Some(
OracleParamsV0TLV(numDigitsOrOutcomes / 2 + 1,
numDigitsOrOutcomes / 2,
maximizeCoverage = true)))) {
oracleParams =>
val (client, _, _) = constructDLCClients(numDigitsOrOutcomes,
isNumeric,
threshold,
numOracles,
oracleParams)
val contract = client.offer.contractInfo
val outcomes = contract.allOutcomes
val adaptorPoints = contract.adaptorPoints
val expectedAdaptorPoints = outcomes.map(_.sigPoint)
assert(adaptorPoints == expectedAdaptorPoints)
}
}
}
}
}
def assertCorrectSigDerivation(
offerSetup: SetupDLC,
dlcOffer: TestDLCClient,
acceptSetup: SetupDLC,
dlcAccept: TestDLCClient,
oracleSigs: Vector[OracleSignatures],
outcome: OracleOutcome): Future[Assertion] = {
val aggR = outcome.aggregateNonce
val aggS = outcome match {
case EnumOracleOutcome(oracles, _) =>
assert(oracles.length == oracleSigs.length)
val sVals = oracleSigs.map {
case EnumOracleSignature(oracle, sig) =>
assert(oracles.contains(oracle))
sig.sig
case _: NumericOracleSignatures =>
fail("Expected EnumOracleSignature")
}
sVals.reduce(_.add(_))
case NumericOracleOutcome(oraclesAndOutcomes) =>
assert(oraclesAndOutcomes.length == oracleSigs.length)
val sVals = oracleSigs.map {
case NumericOracleSignatures(oracle, sigs) =>
val oracleAndOutcomeOpt = oraclesAndOutcomes.find(_._1 == oracle)
assert(oracleAndOutcomeOpt.isDefined)
val outcome = oracleAndOutcomeOpt.get._2
val sVals = sigs.take(outcome.digits.length).map(_.sig)
sVals.reduce(_.add(_))
case _: EnumOracleSignature =>
fail("Expected NumericOracleSignatures")
}
sVals.reduce(_.add(_))
}
val aggSig = SchnorrDigitalSignature(aggR, aggS)
// Must use stored adaptor sigs because adaptor signing nonce is not deterministic (auxRand)
val offerRefundSig = dlcOffer.dlcTxSigner.signRefundTx
val acceptRefundSig = dlcAccept.dlcTxSigner.signRefundTx
val acceptAdaptorSigs = offerSetup.cets.map { case (outcome, info) =>
(outcome, info.remoteSignature)
}
val acceptCETSigs = CETSignatures(acceptAdaptorSigs, acceptRefundSig)
val offerAdaptorSigs = acceptSetup.cets.map { case (outcome, info) =>
(outcome, info.remoteSignature)
}
val offerCETSigs = CETSignatures(offerAdaptorSigs, offerRefundSig)
for {
offerFundingSigs <- Future.fromTry(dlcOffer.dlcTxSigner.signFundingTx())
offerOutcome <-
dlcOffer.executeDLC(offerSetup, Future.successful(oracleSigs))
acceptOutcome <-
dlcAccept.executeDLC(acceptSetup, Future.successful(oracleSigs))
} yield {
val builder = DLCTxBuilder(dlcOffer.offer, dlcAccept.accept)
val contractId = builder.buildFundingTx.txIdBE.bytes
.xor(dlcAccept.accept.tempContractId.bytes)
val offer = dlcOffer.offer
val accept = dlcOffer.accept.withSigs(acceptCETSigs)
val sign = DLCSign(offerCETSigs, offerFundingSigs, contractId)
val (offerOracleSig, offerDLCOutcome) =
DLCStatus
.calculateOutcomeAndSig(isInitiator = true,
offer,
accept,
sign,
acceptOutcome.cet)
.get
val (acceptOracleSig, acceptDLCOutcome) =
DLCStatus
.calculateOutcomeAndSig(isInitiator = false,
offer,
accept,
sign,
offerOutcome.cet)
.get
assert(offerDLCOutcome == outcome)
assert(acceptDLCOutcome == outcome)
assert(offerOracleSig == aggSig)
assert(acceptOracleSig == aggSig)
}
}
it should "be able to derive oracle signature from remote CET signature" in {
val outcomeIndex = 1
runTestsForParam(numEnumOutcomesToTest) { numOutcomes =>
runTestsForParam(enumOracleSchemesToTest) {
case (threshold, numOracles) =>
constructAndSetupDLC(numOutcomes,
isMultiDigit = false,
oracleThreshold = threshold,
numOracles = numOracles).flatMap {
case (dlcOffer, offerSetup, dlcAccept, acceptSetup, outcomes) =>
val (oracleOutcome, sigs) =
genOracleOutcomeAndSignatures(numOutcomes,
isNumeric = false,
dlcOffer,
outcomes,
outcomeIndex,
paramsOpt = None)
assertCorrectSigDerivation(offerSetup = offerSetup,
dlcOffer = dlcOffer,
acceptSetup = acceptSetup,
dlcAccept = dlcAccept,
oracleSigs = sigs,
outcome = oracleOutcome)
}
}
}
}
it should "be able to derive aggregate oracle signature from remote CET signatures" in {
// Larger numbers of digits make tests take too long.
val numDigitsToTest = Vector(5, 9)
runTestsForParam(numDigitsToTest) { numDigits =>
runTestsForParam(Vector((1, 1), (2, 2), (2, 3))) {
case (threshold, numOracles) =>
val oracleParamOptsToTest = if (threshold > 1) {
Vector(None,
Some(
OracleParamsV0TLV(numDigits - 2,
numDigits - 4,
maximizeCoverage = false)))
} else Vector(None)
runTestsForParam(oracleParamOptsToTest) { oracleParamsOpt =>
val max = (1L << numDigits) - 1
val outcomesToTest = 0
.until(9)
.toVector
.map(num => (max / num.toDouble).toLong)
.map(num => NumberUtil.decompose(num, 2, numDigits))
constructAndSetupDLC(numDigits,
isMultiDigit = true,
oracleThreshold = threshold,
numOracles = numOracles,
paramsOpt = oracleParamsOpt).flatMap {
case (dlcOffer, offerSetup, dlcAccept, acceptSetup, outcomes) =>
runTestsForParam(outcomesToTest) { outcomeToTest =>
val possibleOutcomes = outcomes
.collect { case ds: UnsignedNumericOutcome => ds }
.filter(outcome => outcomeToTest.startsWith(outcome.digits))
val outcome =
possibleOutcomes(Random.nextInt(possibleOutcomes.length))
val oracleInfo = dlcOffer.offer.oracleInfo
val oracleIndices =
0.until(oracleInfo.numOracles).toVector
val chosenOracles =
Random
.shuffle(oracleIndices)
.take(oracleInfo.threshold)
.sorted
val oracleOutcome =
genNumericOracleOutcome(chosenOracles,
dlcOffer.offer.contractInfo,
outcome.digits,
oracleParamsOpt)
val oracleSigs = genNumericOracleSignatures(oracleOutcome)
assertCorrectSigDerivation(offerSetup = offerSetup,
dlcOffer = dlcOffer,
acceptSetup = acceptSetup,
dlcAccept = dlcAccept,
oracleSigs = oracleSigs,
outcome = oracleOutcome)
}
}
}
}
}
}
}

View file

@ -0,0 +1,150 @@
package org.bitcoins.dlc
import org.bitcoins.core.protocol.dlc.models.{
EnumOracleOutcome,
EnumSingleOracleInfo
}
import org.bitcoins.core.protocol.dlc.verify.DLCSignatureVerifier
import org.bitcoins.core.protocol.tlv.EnumOutcome
import org.bitcoins.core.util.Indexed
import org.bitcoins.testkitcore.dlc.DLCTest
import org.bitcoins.testkitcore.util.{BitcoinSJvmTest, BytesUtil}
class DLCValidationTest extends BitcoinSJvmTest with DLCTest {
behavior of "DLC Validation"
it should "fail on invalid funding signatures" in {
val contractParms =
EnumContractParams(numOutcomes = 3, oracleThreshold = 1, numOracles = 1)
val (offerClient, acceptClient, _) = constructDLCClients(contractParms)
val builder = offerClient.dlcTxBuilder
val offerVerifier = DLCSignatureVerifier(builder, isInitiator = true)
val acceptVerifier = DLCSignatureVerifier(builder, isInitiator = false)
val offerFundingSigs = offerClient.dlcTxSigner.signFundingTx().get
val acceptFundingSigs = acceptClient.dlcTxSigner.signFundingTx().get
val badOfferFundingSigs = BytesUtil.flipBit(offerFundingSigs)
val badAcceptFundingSigs = BytesUtil.flipBit(acceptFundingSigs)
assert(
offerClient.dlcTxSigner
.completeFundingTx(badAcceptFundingSigs)
.isFailure)
assert(
acceptClient.dlcTxSigner
.completeFundingTx(badOfferFundingSigs)
.isFailure)
assert(offerVerifier.verifyRemoteFundingSigs(acceptFundingSigs))
assert(acceptVerifier.verifyRemoteFundingSigs(offerFundingSigs))
assert(!offerVerifier.verifyRemoteFundingSigs(badAcceptFundingSigs))
assert(!acceptVerifier.verifyRemoteFundingSigs(badOfferFundingSigs))
assert(!offerVerifier.verifyRemoteFundingSigs(offerFundingSigs))
assert(!acceptVerifier.verifyRemoteFundingSigs(acceptFundingSigs))
}
it should "succeed on valid CET signatures" in {
val contractParms =
EnumContractParams(numOutcomes = 3, oracleThreshold = 1, numOracles = 1)
val (offerClient, acceptClient, outcomes) =
constructDLCClients(contractParms)
val builder = offerClient.dlcTxBuilder
val offerVerifier = DLCSignatureVerifier(builder, isInitiator = true)
val acceptVerifier = DLCSignatureVerifier(builder, isInitiator = false)
val offerCETSigs = offerClient.dlcTxSigner.createCETSigs()
val acceptCETSigs = acceptClient.dlcTxSigner.createCETSigs()
outcomes.zipWithIndex.foreach { case (outcomeUncast, index) =>
val outcome = EnumOracleOutcome(Vector(
offerClient.offer.oracleInfos.head
.asInstanceOf[EnumSingleOracleInfo]),
outcomeUncast.asInstanceOf[EnumOutcome])
assert(
offerVerifier.verifyCETSig(Indexed(outcome.sigPoint, index),
acceptCETSigs(outcome.sigPoint)))
assert(
acceptVerifier.verifyCETSig(Indexed(outcome.sigPoint, index),
offerCETSigs(outcome.sigPoint)))
}
assert(offerVerifier.verifyRefundSig(acceptCETSigs.refundSig))
assert(offerVerifier.verifyRefundSig(offerCETSigs.refundSig))
assert(acceptVerifier.verifyRefundSig(offerCETSigs.refundSig))
assert(acceptVerifier.verifyRefundSig(acceptCETSigs.refundSig))
}
it should "fail on invalid CET signatures" in {
val contractParms =
EnumContractParams(numOutcomes = 3, oracleThreshold = 1, numOracles = 1)
val (offerClient, acceptClient, outcomes) =
constructDLCClients(contractParms)
val builder = offerClient.dlcTxBuilder
val offerVerifier = DLCSignatureVerifier(builder, isInitiator = true)
val acceptVerifier = DLCSignatureVerifier(builder, isInitiator = false)
val offerCETSigs = offerClient.dlcTxSigner.createCETSigs()
val acceptCETSigs = acceptClient.dlcTxSigner.createCETSigs()
val badOfferCETSigs = BytesUtil.flipBit(offerCETSigs)
val badAcceptCETSigs = BytesUtil.flipBit(acceptCETSigs)
outcomes.foreach { outcomeUncast =>
val outcome = outcomeUncast.asInstanceOf[EnumOutcome]
val oracleInfo =
offerClient.offer.oracleInfos.head.asInstanceOf[EnumSingleOracleInfo]
val oracleOutcome = EnumOracleOutcome(Vector(oracleInfo), outcome)
val oracleSig = genEnumOracleSignature(oracleInfo, outcome.outcome)
assertThrows[RuntimeException] {
offerClient.dlcTxSigner.completeCET(
oracleOutcome,
badAcceptCETSigs(oracleOutcome.sigPoint),
Vector(oracleSig))
}
assertThrows[RuntimeException] {
acceptClient.dlcTxSigner
.completeCET(oracleOutcome,
badOfferCETSigs(oracleOutcome.sigPoint),
Vector(oracleSig))
}
}
assertThrows[RuntimeException] {
offerClient.dlcTxSigner.completeRefundTx(badAcceptCETSigs.refundSig)
}
assertThrows[RuntimeException] {
acceptClient.dlcTxSigner.completeRefundTx(badOfferCETSigs.refundSig)
}
outcomes.zipWithIndex.foreach { case (outcomeUncast, index) =>
val outcome = EnumOracleOutcome(Vector(
offerClient.offer.oracleInfos.head
.asInstanceOf[EnumSingleOracleInfo]),
outcomeUncast.asInstanceOf[EnumOutcome])
assert(
!offerVerifier.verifyCETSig(Indexed(outcome.sigPoint, index),
badAcceptCETSigs(outcome.sigPoint)))
assert(
!acceptVerifier.verifyCETSig(Indexed(outcome.sigPoint, index),
badOfferCETSigs(outcome.sigPoint)))
assert(
!offerVerifier.verifyCETSig(Indexed(outcome.sigPoint, index),
offerCETSigs(outcome.sigPoint)))
assert(
!acceptVerifier.verifyCETSig(Indexed(outcome.sigPoint, index),
acceptCETSigs(outcome.sigPoint)))
}
assert(!offerVerifier.verifyRefundSig(badAcceptCETSigs.refundSig))
assert(!offerVerifier.verifyRefundSig(badOfferCETSigs.refundSig))
assert(!acceptVerifier.verifyRefundSig(badOfferCETSigs.refundSig))
assert(!acceptVerifier.verifyRefundSig(badAcceptCETSigs.refundSig))
}
}

View file

@ -0,0 +1,71 @@
package org.bitcoins.dlc
import org.bitcoins.core.protocol.tlv.OracleParamsV0TLV
import org.bitcoins.testkitcore.dlc.DLCTest
import org.bitcoins.testkitcore.util.BitcoinSJvmTest
class DisjointUnionDLCTest extends BitcoinSJvmTest with DLCTest {
behavior of "Disjoint Union DLC"
it should "be able to construct and verify with ScriptInterpreter every tx in a double enum contract" in {
val numDisjoint = 2
val numOutcomes = 10
val singleParams = 0.until(numDisjoint).toVector.map { _ =>
EnumContractParams(numOutcomes, oracleThreshold = 1, numOracles = 1)
}
val contractParams = DisjointUnionContractParams(singleParams)
val outcomes = 0.until(numDisjoint).toVector.flatMap { contractIndex =>
0L.until(numOutcomes).toVector.map { outcomeIndex =>
(contractIndex, outcomeIndex)
}
}
executeForCasesInUnion(outcomes, contractParams)
}
it should "be able to construct and verify with ScriptInterpreter every tx in a double numeric contract" in {
val numDisjoint = 2
val numDigits = 10
val singleParams = 0.until(numDisjoint).toVector.map { _ =>
val oracleParams = OracleParamsV0TLV(maxErrorExp = 6,
minFailExp = 2,
maximizeCoverage = true)
NumericContractParams(numDigits,
oracleThreshold = 1,
numOracles = 1,
Some(oracleParams))
}
val contractParams = DisjointUnionContractParams(singleParams)
val outcomes = 0.until(numDisjoint).toVector.flatMap { contractIndex =>
tenRandomNums(numDigits).map { outcomeIndex =>
(contractIndex, outcomeIndex)
}
}
executeForCasesInUnion(outcomes, contractParams)
}
it should "be able to construct and verify with ScriptInterpreter every tx in a mixed enum and numeric contract" in {
val numOutcomes = 10
val numDigits = 10
val enumParams =
EnumContractParams(numOutcomes, oracleThreshold = 1, numOracles = 1)
val oracleParams = OracleParamsV0TLV(maxErrorExp = 6,
minFailExp = 2,
maximizeCoverage = true)
val numericParams =
NumericContractParams(numDigits,
oracleThreshold = 1,
numOracles = 1,
Some(oracleParams))
val contractParams =
DisjointUnionContractParams(Vector(enumParams, numericParams))
val enumOutcomes = 0L.until(numOutcomes).toVector.map((0, _))
val numericOutcomes = tenRandomNums(numDigits).map((1, _))
val outcomes = enumOutcomes ++ numericOutcomes
executeForCasesInUnion(outcomes, contractParams)
}
}

View file

@ -0,0 +1,81 @@
package org.bitcoins.dlc
import org.bitcoins.testkitcore.dlc.DLCTest
import org.bitcoins.testkitcore.util.BitcoinSJvmTest
class EnumDLCTest extends BitcoinSJvmTest with DLCTest {
behavior of "Enum DLC"
val enumOracleSchemesToTest: Vector[(Int, Int)] =
Vector((1, 1), (1, 2), (2, 2), (2, 3), (3, 5), (5, 8))
val numEnumOutcomesToTest: Vector[Int] = 2.until(10).toVector
it should "be able to construct and verify with ScriptInterpreter every tx in a DLC for the normal enum case" in {
runTestsForParam(numEnumOutcomesToTest) { numOutcomes =>
runTestsForParam(enumOracleSchemesToTest) {
case (threshold, numOracles) =>
val contractParams =
EnumContractParams(numOutcomes, threshold, numOracles)
val outcomes = 0L.until(numOutcomes).toVector
executeForCases(outcomes, contractParams)
}
}
}
it should "be able to construct and verify with ScriptInterpreter every tx in a DLC for the refund enum case" in {
runTestsForParam(numEnumOutcomesToTest) { numOutcomes =>
runTestsForParam(enumOracleSchemesToTest) {
case (threshold, numOracles) =>
val contractParams =
EnumContractParams(numOutcomes, threshold, numOracles)
executeRefundCase(contractParams)
}
}
}
it should "all work for a 100 outcome DLC" in {
val numOutcomes = 100
val outcomes = 0L.until(numOutcomes).toVector
val contractParams =
EnumContractParams(numOutcomes, oracleThreshold = 1, numOracles = 1)
for {
_ <- executeForCases(outcomes, contractParams)
_ <- executeRefundCase(contractParams)
} yield succeed
}
it should "be able to derive oracle signature from remote CET signature" in {
val outcomeIndex = 1
runTestsForParam(numEnumOutcomesToTest) { numOutcomes =>
runTestsForParam(enumOracleSchemesToTest) {
case (threshold, numOracles) =>
val contractParams =
EnumContractParams(numOutcomes, threshold, numOracles)
constructAndSetupDLC(contractParams).flatMap {
case (dlcOffer, offerSetup, dlcAccept, acceptSetup, outcomes) =>
val (oracleOutcome, sigs) =
genOracleOutcomeAndSignatures(
numOutcomes,
isNumeric = false,
dlcOffer.offer.contractInfo.contracts.head,
outcomes,
outcomeIndex,
paramsOpt = None)
assertCorrectSigDerivation(offerSetup = offerSetup,
dlcOffer = dlcOffer,
acceptSetup = acceptSetup,
dlcAccept = dlcAccept,
oracleSigs = sigs,
outcome = oracleOutcome)
}
}
}
}
}

View file

@ -0,0 +1,137 @@
package org.bitcoins.dlc
import org.bitcoins.core.protocol.tlv.{
OracleParamsV0TLV,
UnsignedNumericOutcome
}
import org.bitcoins.core.util.{EnvUtil, NumberUtil}
import org.bitcoins.testkitcore.dlc.DLCTest
import org.bitcoins.testkitcore.util.BitcoinSJvmTest
import scala.util.Random
class NumericDLCTest extends BitcoinSJvmTest with DLCTest {
behavior of "Numeric DLC"
val numericOracleSchemesToTest: Vector[(Int, Int)] =
Vector((1, 1), (2, 2), (2, 3))
val numDigitsToTest: Vector[Int] = Vector(4, 5, 10)
it should "be able to construct and verify with ScriptInterpreter every tx in a DLC for the normal numeric case" in {
runTestsForParam(numDigitsToTest) { numDigits =>
runTestsForParam(numericOracleSchemesToTest) {
case (threshold, numOracles) =>
val contractParams =
NumericContractParams(numDigits, threshold, numOracles)
val nums = tenRandomNums(numDigits)
executeForCases(nums, contractParams)
}
}
}
it should "be able to construct and verify with ScriptInterpreter every tx in a DLC for the large numeric case" in {
val numDigits = if (EnvUtil.isNativeSecp256k1Disabled) {
12
} else {
//optimization for CI, tests are much slower when secp256k1 isnt used
17
}
val contractParams =
NumericContractParams(numDigits, oracleThreshold = 1, numOracles = 1)
val nums = tenRandomNums(numDigits)
executeForCases(nums, contractParams)
}
it should "be able to construct and verify with ScriptInterpreter every tx in a DLC for a normal multi-oracle numeric case with bounded differences allowed" in {
val threshold = 3
val numOracles = 5
val numDigits = 8
val params = OracleParamsV0TLV(maxErrorExp = 4,
minFailExp = 2,
maximizeCoverage = false)
val contractParams =
NumericContractParams(numDigits, threshold, numOracles, Some(params))
val nums = tenRandomNums(numDigits)
executeForCases(nums, contractParams)
}
it should "be able to construct and verify with ScriptInterpreter every tx in a DLC for the refund numeric case" in {
runTestsForParam(numDigitsToTest) { numDigits =>
runTestsForParam(numericOracleSchemesToTest) {
case (threshold, numOracles) =>
val contractParams =
NumericContractParams(numDigits, threshold, numOracles)
executeRefundCase(contractParams)
}
}
}
it should "be able to derive aggregate oracle signature from remote CET signatures" in {
// Larger numbers of digits make tests take too long.
val numDigitsToTest = Vector(5, 9)
runTestsForParam(numDigitsToTest) { numDigits =>
runTestsForParam(Vector((1, 1), (2, 2), (2, 3))) {
case (threshold, numOracles) =>
val oracleParamOptsToTest = if (threshold > 1) {
Vector(None,
Some(
OracleParamsV0TLV(numDigits - 2,
numDigits - 4,
maximizeCoverage = false)))
} else Vector(None)
runTestsForParam(oracleParamOptsToTest) { oracleParamsOpt =>
val max = (1L << numDigits) - 1
val outcomesToTest = 0
.until(9)
.toVector
.map(num => (max / num.toDouble).toLong)
.map(num => NumberUtil.decompose(num, 2, numDigits))
val contractParams = NumericContractParams(numDigits,
threshold,
numOracles,
oracleParamsOpt)
constructAndSetupDLC(contractParams).flatMap {
case (dlcOffer, offerSetup, dlcAccept, acceptSetup, outcomes) =>
runTestsForParam(outcomesToTest) { outcomeToTest =>
val possibleOutcomes = outcomes
.collect { case ds: UnsignedNumericOutcome => ds }
.filter(outcome => outcomeToTest.startsWith(outcome.digits))
val outcome =
possibleOutcomes(Random.nextInt(possibleOutcomes.length))
val oracleInfo = dlcOffer.offer.oracleInfos.head
val oracleIndices =
0.until(oracleInfo.numOracles).toVector
val chosenOracles =
Random
.shuffle(oracleIndices)
.take(oracleInfo.threshold)
.sorted
val oracleOutcome =
genNumericOracleOutcome(chosenOracles,
dlcOffer.offer.contractInfo,
outcome.digits,
oracleParamsOpt)
val oracleSigs = genNumericOracleSignatures(oracleOutcome)
assertCorrectSigDerivation(offerSetup = offerSetup,
dlcOffer = dlcOffer,
acceptSetup = acceptSetup,
dlcAccept = dlcAccept,
oracleSigs = oracleSigs,
outcome = oracleOutcome)
}
}
}
}
}
}
}

View file

@ -236,6 +236,22 @@ object DLCParsingTestVector extends TestVectorParser[DLCParsingTestVector] {
"oracleInfo" -> Element(oracleInfo)
)
DLCTLVTestVector(tlv, "contract_info_v0", fields)
case ContractInfoV1TLV(totalCollateral, contracts) =>
val fields = Vector(
"tpe" -> Element(ContractInfoV1TLV.tpe),
"length" -> Element(tlv.length),
"totalCollateral" -> Element(totalCollateral.toUInt64),
"numDisjointEvents" -> Element(BigSizeUInt(contracts.length)),
"contracts" -> MultiElement(contracts.map {
case (descriptor, oracleInfo) =>
NamedMultiElement(
Vector(
"contractDescriptor" -> Element(descriptor),
"oracleInfo" -> Element(oracleInfo)
))
})
)
DLCTLVTestVector(tlv, "contract_info_v1", fields)
case FundingInputV0TLV(inputSerialId,
prevTx,
prevTxVout,
@ -343,6 +359,14 @@ object DLCParsingTestVector extends TestVectorParser[DLCParsingTestVector] {
"rounding_intervals_v0" -> Element(roundingIntervalsV0TLV)
)
DLCTLVTestVector(tlv, "negotiation_fields_v1", fields)
case NegotiationFieldsV2TLV(nestedNegotiationFields) =>
val fields = Vector(
"tpe" -> Element(NegotiationFieldsV2TLV.tpe),
"length" -> Element(tlv.length),
"nested_negotiation_fields" -> MultiElement(
nestedNegotiationFields.map(Element(_)))
)
DLCTLVTestVector(tlv, "negotiation_fields_v2", fields)
case DLCAcceptTLV(tempContractId,
totalCollateralSatoshis,
fundingPubKey,

View file

@ -74,8 +74,7 @@ object DLCTLVGen {
outcomes,
totalInput)
//this doesn't ever try numeric contracts?
ContractInfo(totalInput.satoshis, pair)
SingleContractInfo(totalInput.satoshis, pair)
}
def contractInfoParsingTestVector(
@ -403,7 +402,7 @@ object DLCTLVGen {
cetSigs(
offer.contractInfo.allOutcomes.map(
_.asInstanceOf[EnumOracleOutcome].outcome),
offer.contractInfo.oracleInfo.asInstanceOf[EnumSingleOracleInfo],
offer.contractInfo.oracleInfos.head.asInstanceOf[EnumSingleOracleInfo],
fundingPubKey
)
@ -470,7 +469,7 @@ object DLCTLVGen {
cetSigs(
offer.contractInfo.allOutcomes.map(
_.asInstanceOf[EnumOracleOutcome].outcome),
offer.oracleInfo.asInstanceOf[EnumSingleOracleInfo],
offer.oracleInfos.head.asInstanceOf[EnumSingleOracleInfo],
offer.pubKeys.fundingKey
)
val fundingSignatures = fundingSigs(offer.fundingInputs.map(_.outPoint))

View file

@ -118,7 +118,7 @@ case class DLCPartyParams(
def toOffer(params: DLCParams): DLCOffer = {
DLCOffer(
DLCOfferTLV.currentVersionOpt,
ContractInfo(
SingleContractInfo(
EnumContractDescriptor(params.contractInfo.map(_.toMapEntry)),
params.oracleInfo),
DLCPublicKeys(fundingPrivKey.publicKey, payoutAddress),

View file

@ -255,10 +255,10 @@ class DLCClientIntegrationTest extends BitcoindRpcTest with DLCTest {
)
}
val params =
EnumContractParams(numOutcomes, oracleThreshold = 1, numOracles = 1)
constructEnumDLCClients(
numOutcomes,
oracleThreshold = 1,
numOracles = 1,
params,
localFundingPrivKey,
localPayoutPrivKey,
remoteFundingPrivKey,
@ -362,7 +362,7 @@ class DLCClientIntegrationTest extends BitcoindRpcTest with DLCTest {
constructAndSetupDLC(numOutcomes)
oracleSig = genEnumOracleSignature(
offerDLC.offer.oracleInfo.asInstanceOf[EnumSingleOracleInfo],
offerDLC.offer.oracleInfos.head.asInstanceOf[EnumSingleOracleInfo],
outcomes(outcomeIndex).outcome)
(unilateralDLC, unilateralSetup, otherDLC, otherSetup) = {

View file

@ -1,6 +1,11 @@
package org.bitcoins.dlc.wallet
import org.bitcoins.core.protocol.dlc.models.{DLCState, DLCStatus}
import org.bitcoins.core.protocol.dlc.models.{
DLCState,
DLCStatus,
DisjointUnionContractInfo,
SingleContractInfo
}
import org.bitcoins.testkit.rpc.CachedBitcoindNewest
import org.bitcoins.testkit.wallet.{BitcoinSDualWalletTest, DLCWalletUtil}
import org.bitcoins.testkit.wallet.DLCWalletUtil.InitializedDLCWallet
@ -60,7 +65,15 @@ class DLCExecutionBitcoindBackendTest
contractInfo = broadcastB.contractInfo
contractId = broadcastB.contractId
dlcId = broadcastB.dlcId
(oracleSigs, _) = DLCWalletUtil.getSigs(contractInfo)
(oracleSigs, _) = {
contractInfo match {
case single: SingleContractInfo =>
DLCWalletUtil.getSigs(single)
case disjoint: DisjointUnionContractInfo =>
sys.error(
s"Cannot retrieve sigs for disjoint union contract, got=$disjoint")
}
}
closingTx <- dlcB.executeDLC(contractId, oracleSigs)
//broadcast the closing tx
_ <- dlcB.broadcastTransaction(closingTx)

View file

@ -3,7 +3,11 @@ package org.bitcoins.dlc.wallet
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.dlc.models.DLCMessage.DLCOffer
import org.bitcoins.core.protocol.dlc.models.DLCState
import org.bitcoins.core.protocol.dlc.models.{
DLCState,
DisjointUnionContractInfo,
SingleContractInfo
}
import org.bitcoins.core.protocol.dlc.models.DLCStatus.{
Claimed,
Refunded,
@ -95,7 +99,15 @@ class DLCExecutionTest extends BitcoinSDualWalletTest {
for {
contractId <- getContractId(wallets._1.wallet)
status <- getDLCStatus(wallets._1.wallet)
(sig, _) = getSigs(status.contractInfo)
(sig, _) = {
status.contractInfo match {
case single: SingleContractInfo =>
DLCWalletUtil.getSigs(single)
case disjoint: DisjointUnionContractInfo =>
sys.error(
s"Cannot retrieve sigs for disjoint union contract, got=$disjoint")
}
}
func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sig)
result <- dlcExecutionTest(wallets = wallets,
@ -131,7 +143,15 @@ class DLCExecutionTest extends BitcoinSDualWalletTest {
for {
contractId <- getContractId(wallets._1.wallet)
status <- getDLCStatus(wallets._2.wallet)
(_, sig) = getSigs(status.contractInfo)
(_, sig) = {
status.contractInfo match {
case single: SingleContractInfo =>
DLCWalletUtil.getSigs(single)
case disjoint: DisjointUnionContractInfo =>
sys.error(
s"Cannot retrieve sigs for disjoint union contract, got=$disjoint")
}
}
func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sig)
result <- dlcExecutionTest(wallets = wallets,
@ -171,7 +191,15 @@ class DLCExecutionTest extends BitcoinSDualWalletTest {
for {
contractId <- getContractId(wallet)
status <- getDLCStatus(wallet)
(sig, _) = getSigs(status.contractInfo)
(sig, _) = {
status.contractInfo match {
case single: SingleContractInfo =>
DLCWalletUtil.getSigs(single)
case disjoint: DisjointUnionContractInfo =>
sys.error(
s"Cannot retrieve sigs for disjoint union contract, got=$disjoint")
}
}
tx1 <- wallet.executeDLC(contractId, sig)
tx2 <- wallet.executeDLC(contractId, sig)
@ -185,7 +213,15 @@ class DLCExecutionTest extends BitcoinSDualWalletTest {
contractId <- getContractId(wallets._1.wallet)
status <- getDLCStatus(dlcA)
// use dlcB winning sigs
(_, sig) = getSigs(status.contractInfo)
(_, sig) = {
status.contractInfo match {
case single: SingleContractInfo =>
DLCWalletUtil.getSigs(single)
case disjoint: DisjointUnionContractInfo =>
sys.error(
s"Cannot retrieve sigs for disjoint union contract, got=$disjoint")
}
}
func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sig)
@ -354,7 +390,15 @@ class DLCExecutionTest extends BitcoinSDualWalletTest {
for {
contractId <- getContractId(walletA)
status <- getDLCStatus(walletB)
(_, sig) = getSigs(status.contractInfo)
(_, sig) = {
status.contractInfo match {
case single: SingleContractInfo =>
DLCWalletUtil.getSigs(single)
case disjoint: DisjointUnionContractInfo =>
sys.error(
s"Cannot retrieve sigs for disjoint union contract, got=$disjoint")
}
}
func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sig)
result <- dlcExecutionTest(wallets = wallets,

View file

@ -48,7 +48,7 @@ class DLCMultiOracleExactNumericExecutionTest extends BitcoinSDualWalletTest {
def getSigs(contractInfo: ContractInfo): (
Vector[OracleAttestmentTLV],
Vector[OracleAttestmentTLV]) = {
contractInfo.contractDescriptor match {
contractInfo.contractDescriptors.head match {
case _: NumericContractDescriptor => ()
case _: EnumContractDescriptor =>
throw new IllegalArgumentException("Unexpected Contract Info")

View file

@ -55,7 +55,7 @@ class DLCMultiOracleNumericExecutionTest
def getSigs(contractInfo: ContractInfo): (
Vector[OracleAttestmentTLV],
Vector[OracleAttestmentTLV]) = {
contractInfo.contractDescriptor match {
contractInfo.contractDescriptors.head match {
case _: NumericContractDescriptor => ()
case _: EnumContractDescriptor =>
throw new IllegalArgumentException("Unexpected Contract Info")

View file

@ -21,13 +21,13 @@ class DLCNumericExecutionTest extends BitcoinSDualWalletTest {
def getSigs(contractInfo: ContractInfo): (
OracleAttestmentTLV,
OracleAttestmentTLV) = {
contractInfo.contractDescriptor match {
contractInfo.contractDescriptors.head match {
case _: NumericContractDescriptor => ()
case _: EnumContractDescriptor =>
throw new IllegalArgumentException("Unexpected Contract Info")
}
val oracleInfo = DLCWalletUtil.multiNonceContractInfo.oracleInfo
val oracleInfo = DLCWalletUtil.multiNonceContractInfo.oracleInfos.head
.asInstanceOf[NumericSingleOracleInfo]
val initiatorWinVec =

View file

@ -1,15 +1,11 @@
package org.bitcoins.dlc.wallet
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.protocol.BlockStamp.BlockHash
import org.bitcoins.core.protocol.dlc.models.{
ContractInfo,
DLCState,
EnumContractDescriptor,
NumericContractDescriptor
DisjointUnionContractInfo,
SingleContractInfo
}
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.crypto.CryptoUtil
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.testkit.wallet.DLCWalletUtil._
import org.bitcoins.testkit.wallet.{DLCWalletUtil, DualWalletTestCachedBitcoind}
@ -35,7 +31,15 @@ class RescanDLCTest extends DualWalletTestCachedBitcoind {
for {
contractId <- getContractId(wallet)
status <- getDLCStatus(wallet)
(sig, _) = getSigs(status.contractInfo)
(sig, _) = {
status.contractInfo match {
case single: SingleContractInfo =>
DLCWalletUtil.getSigs(single)
case disjoint: DisjointUnionContractInfo =>
sys.error(
s"Cannot retrieve sigs for disjoint union contract, got=$disjoint")
}
}
func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sig)
result <- dlcExecutionTest(wallets = (walletA, walletB),
@ -66,7 +70,15 @@ class RescanDLCTest extends DualWalletTestCachedBitcoind {
for {
contractId <- getContractId(wallet)
status <- getDLCStatus(wallet)
(sig, _) = getSigs(status.contractInfo)
(sig, _) = {
status.contractInfo match {
case single: SingleContractInfo =>
DLCWalletUtil.getSigs(single)
case disjoint: DisjointUnionContractInfo =>
sys.error(
s"Cannot retrieve sigs for disjoint union contract, got=$disjoint")
}
}
func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sig)
result <- dlcExecutionTest(wallets = (walletA, walletB),
@ -87,49 +99,4 @@ class RescanDLCTest extends DualWalletTestCachedBitcoind {
postStatus <- getDLCStatus(wallet)
} yield assert(postStatus.state == DLCState.RemoteClaimed)
}
private def getSigs(contractInfo: ContractInfo): (
OracleAttestmentTLV,
OracleAttestmentTLV) = {
val desc: EnumContractDescriptor = contractInfo.contractDescriptor match {
case desc: EnumContractDescriptor => desc
case _: NumericContractDescriptor =>
throw new IllegalArgumentException("Unexpected Contract Info")
}
// Get a hash that the initiator wins for
val initiatorWinStr =
desc
.maxBy(_._2.toLong)
._1
.outcome
val initiatorWinSig = DLCWalletUtil.oraclePrivKey
.schnorrSignWithNonce(CryptoUtil
.sha256DLCAttestation(initiatorWinStr)
.bytes,
DLCWalletUtil.kValue)
// Get a hash that the recipient wins for
val recipientWinStr =
desc.find(_._2 == Satoshis.zero).get._1.outcome
val recipientWinSig = DLCWalletUtil.oraclePrivKey
.schnorrSignWithNonce(CryptoUtil
.sha256DLCAttestation(recipientWinStr)
.bytes,
DLCWalletUtil.kValue)
val publicKey = DLCWalletUtil.oraclePrivKey.schnorrPublicKey
val eventId = DLCWalletUtil.sampleOracleInfo.announcement.eventTLV match {
case v0: OracleEventV0TLV => v0.eventId
}
(OracleAttestmentV0TLV(eventId,
publicKey,
Vector(initiatorWinSig),
Vector(initiatorWinStr)),
OracleAttestmentV0TLV(eventId,
publicKey,
Vector(recipientWinSig),
Vector(recipientWinStr)))
}
}

View file

@ -46,7 +46,7 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
_ = {
assert(find1.isDefined)
assert(dlcA1Opt.get.state == DLCState.Offered)
assert(offer.oracleInfo == offerData.oracleInfo)
assert(offer.oracleInfos == offerData.oracleInfos)
assert(offer.contractInfo == offerData.contractInfo)
assert(offer.totalCollateral == offerData.totalCollateral)
assert(offer.feeRate == offerData.feeRate)
@ -152,7 +152,7 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
ContractOraclePair.EnumPair(EnumContractDescriptor(outcomes),
sampleOracleInfo)
val contractInfo: ContractInfo = ContractInfo(totalCol, oraclePair)
val contractInfo: ContractInfo = SingleContractInfo(totalCol, oraclePair)
val offerData =
sampleDLCOffer.copy(contractInfo = contractInfo,
@ -209,7 +209,7 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
ContractOraclePair.EnumPair(EnumContractDescriptor(outcomes),
sampleOracleInfo)
val contractInfo: ContractInfo = ContractInfo(totalCol, oraclePair)
val contractInfo: ContractInfo = SingleContractInfo(totalCol, oraclePair)
val offerData =
sampleDLCOffer.copy(contractInfo = contractInfo,
@ -270,7 +270,7 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
_ = {
assert(dlcA1Opt.isDefined)
assert(dlcA1Opt.get.state == DLCState.Offered)
assert(offer.oracleInfo == offerData.oracleInfo)
assert(offer.oracleInfos == offerData.oracleInfos)
assert(offer.contractInfo == offerData.contractInfo)
assert(offer.totalCollateral == offerData.totalCollateral)
assert(offer.feeRate == offerData.feeRate)
@ -495,7 +495,8 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
val offerData: DLCOffer = DLCWalletUtil.sampleDLCOffer
val announcementTLVs =
offerData.contractInfo.oracleInfo.singleOracleInfos.map(_.announcement)
offerData.contractInfo.oracleInfos.head.singleOracleInfos
.map(_.announcement)
assert(announcementTLVs.size == 1)
val announcementTLV = announcementTLVs.head
@ -710,7 +711,7 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
val offerData = DLCOffer(
DLCOfferTLV.currentVersionOpt,
ContractInfo(contractDescriptor, oracleInfo),
SingleContractInfo(contractDescriptor, oracleInfo),
dummyDLCKeys,
Satoshis(5000),
Vector(dummyFundingInputs.head),
@ -736,7 +737,7 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
offerData.timeouts.contractTimeout.toUInt32
)
_ = {
assert(offer.oracleInfo == offerData.oracleInfo)
assert(offer.oracleInfos == offerData.oracleInfos)
assert(offer.contractInfo == offerData.contractInfo)
assert(offer.totalCollateral == offerData.totalCollateral)
assert(offer.feeRate == offerData.feeRate)

View file

@ -291,7 +291,7 @@ abstract class DLCWallet
refundLocktime: UInt32): Future[DLCOffer] = {
logger.info("Creating DLC Offer")
val announcements =
contractInfo.oracleInfo.singleOracleInfos.map(_.announcement)
contractInfo.oracleInfos.head.singleOracleInfos.map(_.announcement)
//hack for now to get around https://github.com/bitcoin-s/bitcoin-s/issues/3127
//filter announcements that we already have in the db
@ -385,7 +385,8 @@ abstract class DLCWallet
timeouts = timeouts
)
oracleParamsOpt = OracleInfo.getOracleParamsOpt(contractInfo.oracleInfo)
oracleParamsOpt = OracleInfo.getOracleParamsOpt(
contractInfo.oracleInfos.head)
dlcDb = DLCDb(
dlcId = dlcId,
@ -408,9 +409,9 @@ abstract class DLCWallet
contractDataDb = DLCContractDataDb(
dlcId = dlcId,
oracleThreshold = contractInfo.oracleInfo.threshold,
oracleThreshold = contractInfo.oracleInfos.head.threshold,
oracleParamsTLVOpt = oracleParamsOpt,
contractDescriptorTLV = contractInfo.contractDescriptor.toTLV,
contractDescriptorTLV = contractInfo.contractDescriptors.head.toTLV,
contractMaturity = timeouts.contractMaturity,
contractTimeout = timeouts.contractTimeout,
totalCollateral = contractInfo.totalCollateral
@ -454,7 +455,8 @@ abstract class DLCWallet
.map(account => (dlcDb, account.get))
case None =>
val announcements =
offer.contractInfo.oracleInfo.singleOracleInfos.map(_.announcement)
offer.contractInfo.oracleInfos.head.singleOracleInfos
.map(_.announcement)
//filter announcements that we already have in the db
val groupedAnnouncementsF: Future[AnnouncementGrouping] = {
@ -492,12 +494,13 @@ abstract class DLCWallet
contractDataDb = {
val oracleParamsOpt =
OracleInfo.getOracleParamsOpt(contractInfo.oracleInfo)
OracleInfo.getOracleParamsOpt(contractInfo.oracleInfos.head)
DLCContractDataDb(
dlcId = dlcId,
oracleThreshold = contractInfo.oracleInfo.threshold,
oracleThreshold = contractInfo.oracleInfos.head.threshold,
oracleParamsTLVOpt = oracleParamsOpt,
contractDescriptorTLV = contractInfo.contractDescriptor.toTLV,
contractDescriptorTLV =
contractInfo.contractDescriptors.head.toTLV,
contractMaturity = offer.timeouts.contractMaturity,
contractTimeout = offer.timeouts.contractTimeout,
totalCollateral = contractInfo.totalCollateral

View file

@ -153,7 +153,9 @@ private[bitcoins] trait DLCDataManagement { self: DLCWallet =>
EnumMultiOracleInfo(contractDataDb.oracleThreshold,
announcementTLVs)
}
ContractInfo(contractDataDb.totalCollateral.satoshis, enum, oracleInfo)
SingleContractInfo(contractDataDb.totalCollateral.satoshis,
enum,
oracleInfo)
case numeric: NumericContractDescriptor =>
val oracleInfo =
if (announcementTLVs.size == 1) {
@ -169,9 +171,9 @@ private[bitcoins] trait DLCDataManagement { self: DLCWallet =>
announcementTLVs)
}
}
ContractInfo(contractDataDb.totalCollateral.satoshis,
numeric,
oracleInfo)
SingleContractInfo(contractDataDb.totalCollateral.satoshis,
numeric,
oracleInfo)
}
}

View file

@ -151,7 +151,7 @@ val oracleInfo = NumericMultiOracleInfo(
maximizeCoverage = false
)
val contractInfo = ContractInfo(totalCollateral, ContractOraclePair.NumericPair(descriptor, oracleInfo))
val contractInfo = SingleContractInfo(totalCollateral, ContractOraclePair.NumericPair(descriptor, oracleInfo))
contractInfo.max
contractInfo.allOutcomes.length

View file

@ -5,25 +5,41 @@ import org.bitcoins.core.currency.{CurrencyUnit, CurrencyUnits, Satoshis}
import org.bitcoins.core.number.{UInt16, UInt32, UInt64}
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.BlockStamp.BlockTime
import org.bitcoins.core.protocol.dlc.build.DLCTxBuilder
import org.bitcoins.core.protocol.dlc.compute.CETCalculator
import org.bitcoins.core.protocol.dlc.execution.{CETInfo, SetupDLC}
import org.bitcoins.core.protocol.dlc.execution._
import org.bitcoins.core.protocol.dlc.models.DLCMessage.DLCSign
import org.bitcoins.core.protocol.dlc.models._
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.tlv.{NumericDLCOutcomeType, _}
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.script.crypto.HashType
import org.bitcoins.core.util.sorted.OrderedAnnouncements
import org.bitcoins.core.util.{FutureUtil, NumberUtil}
import org.bitcoins.core.util.{BitcoinScriptUtil, FutureUtil, NumberUtil}
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo._
import org.bitcoins.crypto._
import org.scalatest.Assertions.{assert, fail, succeed}
import org.scalatest.{Assertion, Assertions}
import scodec.bits.BitVector
import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.util.Random
trait DLCTest {
def tenRandomNums(numDigits: Int): Vector[Long] = {
0.until(10).toVector.map { _ =>
val randDigits = (0 until numDigits).toVector.map { _ =>
scala.util.Random.nextInt(2)
}
BitVector
.fromValidBin(randDigits.mkString(""))
.toLong(signed = false)
}
}
val oraclePrivKeys: Vector[ECPrivateKey] =
(0 until 50).toVector.map(_ => ECPrivateKey.freshPrivateKey)
@ -241,6 +257,46 @@ trait DLCTest {
val feeRate: SatoshisPerVirtualByte = SatoshisPerVirtualByte(Satoshis(10))
sealed trait ContractParams
sealed trait SingleContractParams extends ContractParams
object SingleContractParams {
def apply(
numDigitsOrOutcomes: Int,
isNumeric: Boolean,
threshold: Int,
numOracles: Int,
paramsOpt: Option[OracleParamsV0TLV]): SingleContractParams = {
if (isNumeric) {
EnumContractParams(numDigitsOrOutcomes, threshold, numOracles)
} else {
NumericContractParams(numDigitsOrOutcomes,
threshold,
numOracles,
paramsOpt)
}
}
}
case class EnumContractParams(
numOutcomes: Int,
oracleThreshold: Int,
numOracles: Int)
extends SingleContractParams
case class NumericContractParams(
numDigits: Int,
oracleThreshold: Int,
numOracles: Int,
paramsOpt: Option[OracleParamsV0TLV] = None)
extends SingleContractParams
case class DisjointUnionContractParams(
singleParams: Vector[SingleContractParams])
extends ContractParams
def constructDLCClientsFromInfos(
offerInfo: ContractInfo,
acceptInfo: ContractInfo,
@ -310,10 +366,40 @@ trait DLCTest {
(offerDLC, acceptDLC)
}
def constructEnumContractInfos(
params: EnumContractParams,
oracleShift: Int = 0): (SingleContractInfo, SingleContractInfo) = {
val outcomeStrs = DLCTestUtil.genOutcomes(params.numOutcomes)
val outcomes = outcomeStrs.map(EnumOutcome.apply)
val announcements =
oraclePrivKeys
.slice(oracleShift, oracleShift + params.numOracles)
.zip(
preCommittedRsPerOracle
.slice(oracleShift, oracleShift + params.numOracles)
.map(_.head))
.map { case (privKey, rVal) =>
OracleAnnouncementV0TLV.dummyForEventsAndKeys(privKey, rVal, outcomes)
}
val oracleInfo = if (params.numOracles == 1) {
EnumSingleOracleInfo(announcements.head)
} else {
val ordered = OrderedAnnouncements(announcements)
EnumMultiOracleInfo(params.oracleThreshold, ordered)
}
val (outcomesDesc, otherOutcomesDesc) =
DLCTestUtil.genContractDescriptors(outcomeStrs, totalInput)
val offerInfo = SingleContractInfo(outcomesDesc, oracleInfo)
val acceptInfo = SingleContractInfo(otherOutcomesDesc, oracleInfo)
(offerInfo, acceptInfo)
}
def constructEnumDLCClients(
numOutcomes: Int,
oracleThreshold: Int,
numOracles: Int,
contractParams: EnumContractParams,
offerFundingPrivKey: ECPrivateKey = this.offerFundingPrivKey,
offerPayoutPrivKey: ECPrivateKey = this.offerPayoutPrivKey,
acceptFundingPrivKey: ECPrivateKey = this.acceptFundingPrivKey,
@ -329,28 +415,7 @@ trait DLCTest {
TestDLCClient,
TestDLCClient,
Vector[EnumOutcome]) = {
val outcomeStrs = DLCTestUtil.genOutcomes(numOutcomes)
val outcomes = outcomeStrs.map(EnumOutcome.apply)
val announcements =
oraclePrivKeys.take(numOracles).zip(preCommittedRs.take(numOracles)).map {
case (privKey, rVal) =>
OracleAnnouncementV0TLV.dummyForEventsAndKeys(privKey, rVal, outcomes)
}
val orderedAnnouncements = OrderedAnnouncements(announcements)
val oracleInfo = if (numOracles == 1) {
EnumSingleOracleInfo(announcements.head)
} else {
EnumMultiOracleInfo(oracleThreshold, orderedAnnouncements)
}
val (outcomesDesc, otherOutcomesDesc) =
DLCTestUtil.genContractDescriptors(outcomeStrs, totalInput)
val offerInfo = ContractInfo(outcomesDesc, oracleInfo)
val acceptInfo = ContractInfo(otherOutcomesDesc, oracleInfo)
val (offerInfo, acceptInfo) = constructEnumContractInfos(contractParams)
val (offerDLC, acceptDLC) = constructDLCClientsFromInfos(
offerInfo,
@ -367,14 +432,54 @@ trait DLCTest {
timeouts
)
val outcomes = offerInfo.contractDescriptor
.asInstanceOf[EnumContractDescriptor]
.outcomeValueMap
.map(_._1)
(offerDLC, acceptDLC, outcomes)
}
def constructNumericContractInfos(
params: NumericContractParams,
oracleShift: Int = 0): (SingleContractInfo, SingleContractInfo) = {
val (offerDesc, acceptDesc) =
DLCTestUtil.genMultiDigitContractInfo(params.numDigits,
totalInput,
numRounds = 4)
val announcements =
oraclePrivKeys
.slice(oracleShift, oracleShift + params.numOracles)
.zip(preCommittedRsPerOracle
.slice(oracleShift, oracleShift + params.numOracles))
.map { case (privKey, rVals) =>
OracleAnnouncementV0TLV.dummyForKeys(privKey,
rVals.take(params.numDigits))
}
val oracleInfo = if (params.numOracles == 1) {
NumericSingleOracleInfo(announcements.head)
} else {
val ordered = OrderedAnnouncements(announcements)
params.paramsOpt match {
case None =>
NumericExactMultiOracleInfo(params.oracleThreshold, ordered)
case Some(boundParams) =>
NumericMultiOracleInfo(params.oracleThreshold, ordered, boundParams)
}
}
val numericPairOffer = ContractOraclePair.NumericPair(offerDesc, oracleInfo)
val numericPairAccept =
ContractOraclePair.NumericPair(acceptDesc, oracleInfo)
val offerInfo = SingleContractInfo(totalInput.satoshis, numericPairOffer)
val acceptInfo = SingleContractInfo(totalInput.satoshis, numericPairAccept)
(offerInfo, acceptInfo)
}
def constructNumericDLCClients(
numDigits: Int,
oracleThreshold: Int,
numOracles: Int,
paramsOpt: Option[OracleParamsV0TLV],
contractParams: NumericContractParams,
offerFundingPrivKey: ECPrivateKey = this.offerFundingPrivKey,
offerPayoutPrivKey: ECPrivateKey = this.offerPayoutPrivKey,
acceptFundingPrivKey: ECPrivateKey = this.acceptFundingPrivKey,
@ -390,37 +495,7 @@ trait DLCTest {
TestDLCClient,
TestDLCClient,
Vector[UnsignedNumericOutcome]) = {
val (offerDesc, acceptDesc) =
DLCTestUtil.genMultiDigitContractInfo(numDigits,
totalInput,
numRounds = 4)
val announcements =
oraclePrivKeys
.take(numOracles)
.zip(preCommittedRsPerOracle.take(numOracles))
.map { case (privKey, rVals) =>
OracleAnnouncementV0TLV.dummyForKeys(privKey, rVals.take(numDigits))
}
val orderedAnnouncements = OrderedAnnouncements(announcements)
val oracleInfo = if (numOracles == 1) {
NumericSingleOracleInfo(announcements.head)
} else {
paramsOpt match {
case None =>
NumericExactMultiOracleInfo(oracleThreshold, orderedAnnouncements)
case Some(params) =>
NumericMultiOracleInfo(oracleThreshold, orderedAnnouncements, params)
}
}
val numericPairOffer = ContractOraclePair.NumericPair(offerDesc, oracleInfo)
val numericPairAccept =
ContractOraclePair.NumericPair(acceptDesc, oracleInfo)
val offerInfo = ContractInfo(totalInput.satoshis, numericPairOffer)
val acceptInfo = ContractInfo(totalInput.satoshis, numericPairAccept)
val (offerInfo, acceptInfo) = constructNumericContractInfos(contractParams)
val outcomes =
offerInfo.allOutcomes.map(_.asInstanceOf[NumericOracleOutcome].outcome)
@ -442,12 +517,8 @@ trait DLCTest {
(offerDLC, acceptDLC, outcomes)
}
def constructDLCClients(
numOutcomesOrDigits: Int,
isNumeric: Boolean,
oracleThreshold: Int,
numOracles: Int,
paramsOpt: Option[OracleParamsV0TLV],
def constructDisjointUnionDLCClients(
contractParams: DisjointUnionContractParams,
offerFundingPrivKey: ECPrivateKey = this.offerFundingPrivKey,
offerPayoutPrivKey: ECPrivateKey = this.offerPayoutPrivKey,
acceptFundingPrivKey: ECPrivateKey = this.acceptFundingPrivKey,
@ -463,39 +534,108 @@ trait DLCTest {
TestDLCClient,
TestDLCClient,
Vector[DLCOutcomeType]) = {
if (isNumeric) {
constructNumericDLCClients(
numOutcomesOrDigits,
oracleThreshold,
numOracles,
paramsOpt,
offerFundingPrivKey,
offerPayoutPrivKey,
acceptFundingPrivKey,
acceptPayoutPrivKey,
offerFundingUtxos,
offerFundingInputs,
acceptFundingUtxos,
acceptFundingInputs,
feeRate,
timeouts
)
} else {
constructEnumDLCClients(
numOutcomesOrDigits,
oracleThreshold,
numOracles,
offerFundingPrivKey,
offerPayoutPrivKey,
acceptFundingPrivKey,
acceptPayoutPrivKey,
offerFundingUtxos,
offerFundingInputs,
acceptFundingUtxos,
acceptFundingInputs,
feeRate,
timeouts
)
var oraclesSoFar = 0
val singleInfosAndOutcomes = contractParams.singleParams.map {
case enumParams: EnumContractParams =>
val (offerInfo, acceptInfo) =
constructEnumContractInfos(enumParams, oraclesSoFar)
oraclesSoFar += enumParams.numOracles
val outcomes =
offerInfo.allOutcomes.map(_.asInstanceOf[EnumOracleOutcome].outcome)
(offerInfo, acceptInfo, outcomes)
case numericParams: NumericContractParams =>
val (offerInfo, acceptInfo) =
constructNumericContractInfos(numericParams, oraclesSoFar)
oraclesSoFar += numericParams.numOracles
val outcomes = offerInfo.allOutcomes.map(
_.asInstanceOf[NumericOracleOutcome].outcome)
(offerInfo, acceptInfo, outcomes)
}
val offerInfos = singleInfosAndOutcomes.map(_._1)
val acceptInfos = singleInfosAndOutcomes.map(_._2)
val outcomes = singleInfosAndOutcomes.flatMap(_._3)
val offerInfo = DisjointUnionContractInfo(offerInfos)
val acceptInfo = DisjointUnionContractInfo(acceptInfos)
val (offerDLC, acceptDLC) = constructDLCClientsFromInfos(
offerInfo,
acceptInfo,
offerFundingPrivKey,
offerPayoutPrivKey,
acceptFundingPrivKey,
acceptPayoutPrivKey,
offerFundingUtxos,
offerFundingInputs,
acceptFundingUtxos,
acceptFundingInputs,
feeRate,
timeouts
)
(offerDLC, acceptDLC, outcomes)
}
def constructDLCClients(
contractParams: ContractParams,
offerFundingPrivKey: ECPrivateKey = this.offerFundingPrivKey,
offerPayoutPrivKey: ECPrivateKey = this.offerPayoutPrivKey,
acceptFundingPrivKey: ECPrivateKey = this.acceptFundingPrivKey,
acceptPayoutPrivKey: ECPrivateKey = this.acceptPayoutPrivKey,
offerFundingUtxos: Vector[SpendingInfoWithSerialId] =
this.offerFundingUtxos,
offerFundingInputs: Vector[DLCFundingInput] = this.offerFundingInputs,
acceptFundingUtxos: Vector[SpendingInfoWithSerialId] =
this.acceptFundingUtxos,
acceptFundingInputs: Vector[DLCFundingInput] = this.acceptFundingInputs,
feeRate: SatoshisPerVirtualByte = this.feeRate,
timeouts: DLCTimeouts = this.timeouts)(implicit ec: ExecutionContext): (
TestDLCClient,
TestDLCClient,
Vector[DLCOutcomeType]) = {
contractParams match {
case enumParams: EnumContractParams =>
constructEnumDLCClients(
enumParams,
offerFundingPrivKey,
offerPayoutPrivKey,
acceptFundingPrivKey,
acceptPayoutPrivKey,
offerFundingUtxos,
offerFundingInputs,
acceptFundingUtxos,
acceptFundingInputs,
feeRate,
timeouts
)
case numericParams: NumericContractParams =>
constructNumericDLCClients(
numericParams,
offerFundingPrivKey,
offerPayoutPrivKey,
acceptFundingPrivKey,
acceptPayoutPrivKey,
offerFundingUtxos,
offerFundingInputs,
acceptFundingUtxos,
acceptFundingInputs,
feeRate,
timeouts
)
case disjointUnionParams: DisjointUnionContractParams =>
constructDisjointUnionDLCClients(
disjointUnionParams,
offerFundingPrivKey,
offerPayoutPrivKey,
acceptFundingPrivKey,
acceptPayoutPrivKey,
offerFundingUtxos,
offerFundingInputs,
acceptFundingUtxos,
acceptFundingInputs,
feeRate,
timeouts
)
}
}
@ -548,6 +688,30 @@ trait DLCTest {
}
}
def setupDLC(dlcOffer: TestDLCClient, dlcAccept: TestDLCClient)(implicit
ec: ExecutionContext): Future[(SetupDLC, SetupDLC)] = {
setupDLC(dlcOffer,
dlcAccept,
_.map(_.fundingTx),
_ => Future.successful(()))
}
def constructAndSetupDLC(contractParams: ContractParams)(implicit
ec: ExecutionContext): Future[
(
TestDLCClient,
SetupDLC,
TestDLCClient,
SetupDLC,
Vector[DLCOutcomeType])] = {
val (offerDLC, acceptDLC, outcomes) = constructDLCClients(contractParams)
for {
(offerSetup, acceptSetup) <- setupDLC(offerDLC, acceptDLC)
} yield (offerDLC, offerSetup, acceptDLC, acceptSetup, outcomes)
}
/** Computes an EnumOracleSignature for the given outcome and oracle */
def genEnumOracleSignature(
oracleInfo: EnumSingleOracleInfo,
@ -564,13 +728,13 @@ trait DLCTest {
def genEnumOracleOutcome(
chosenOracles: Vector[Int],
dlcOffer: TestDLCClient,
contractInfo: SingleContractInfo,
outcomes: Vector[DLCOutcomeType],
outcomeIndex: Long): EnumOracleOutcome = {
outcomes(outcomeIndex.toInt) match {
case outcome: EnumOutcome =>
val oracles = chosenOracles
.map(dlcOffer.offer.oracleInfo.singleOracleInfos.apply)
.map(contractInfo.oracleInfos.head.singleOracleInfos.apply)
.map(_.asInstanceOf[EnumSingleOracleInfo])
EnumOracleOutcome(oracles, outcome)
case _: NumericDLCOutcomeType =>
@ -589,17 +753,17 @@ trait DLCTest {
genEnumOracleSignature(oracle,
outcome.outcome.outcome,
oraclePrivKeys(index),
preCommittedKs(index))
preCommittedKsPerOracle(index).head)
}
}
def genEnumOracleSignatures(
chosenOracles: Vector[Int],
dlcOffer: TestDLCClient,
contractInfo: SingleContractInfo,
outcomes: Vector[DLCOutcomeType],
outcomeIndex: Long): Vector[EnumOracleSignature] = {
val outcome =
genEnumOracleOutcome(chosenOracles, dlcOffer, outcomes, outcomeIndex)
genEnumOracleOutcome(chosenOracles, contractInfo, outcomes, outcomeIndex)
genEnumOracleSignatures(outcome)
}
@ -646,7 +810,7 @@ trait DLCTest {
contractInfo: ContractInfo,
digits: Vector[Int],
paramsOpt: Option[OracleParamsV0TLV]): NumericOracleOutcome = {
contractInfo.contractOraclePair match {
contractInfo.contracts.head.contractOraclePair match {
case e: ContractOraclePair.EnumPair =>
Assertions.fail(s"Expected Numeric Contract, got enum=$e")
case ContractOraclePair.NumericPair(_, oracleInfo) =>
@ -686,20 +850,17 @@ trait DLCTest {
def genNumericOracleOutcome(
numDigits: Int,
chosenOracles: Vector[Int],
dlcOffer: TestDLCClient,
contractInfo: SingleContractInfo,
outcomes: Vector[DLCOutcomeType],
outcomeIndex: Long,
paramsOpt: Option[OracleParamsV0TLV]): NumericOracleOutcome = {
dlcOffer.offer.contractInfo.contractOraclePair match {
contractInfo.contractOraclePair match {
case e: ContractOraclePair.EnumPair =>
Assertions.fail(s"Expected Numeric Contract, got enum=$e")
case ContractOraclePair.NumericPair(descriptor, _) =>
val digits =
computeNumericOutcome(numDigits, descriptor, outcomes, outcomeIndex)
genNumericOracleOutcome(chosenOracles,
dlcOffer.offer.contractInfo,
digits,
paramsOpt)
genNumericOracleOutcome(chosenOracles, contractInfo, digits, paramsOpt)
}
}
@ -723,13 +884,13 @@ trait DLCTest {
def genNumericOracleSignatures(
numDigits: Int,
chosenOracles: Vector[Int],
dlcOffer: TestDLCClient,
contractInfo: SingleContractInfo,
outcomes: Vector[DLCOutcomeType],
outcomeIndex: Long,
paramsOpt: Option[OracleParamsV0TLV]): Vector[NumericOracleSignatures] = {
val outcome = genNumericOracleOutcome(numDigits,
chosenOracles,
dlcOffer,
contractInfo,
outcomes,
outcomeIndex,
paramsOpt)
@ -739,11 +900,11 @@ trait DLCTest {
def genOracleOutcome(
numOutcomesOrDigits: Int,
isNumeric: Boolean,
dlcOffer: TestDLCClient,
contractInfo: SingleContractInfo,
outcomes: Vector[DLCOutcomeType],
outcomeIndex: Long,
paramsOpt: Option[OracleParamsV0TLV]): OracleOutcome = {
val oracleInfo = dlcOffer.offer.oracleInfo
val oracleInfo = contractInfo.oracleInfos.head
val oracleIndices =
0.until(oracleInfo.numOracles).toVector
@ -751,11 +912,11 @@ trait DLCTest {
Random.shuffle(oracleIndices).take(oracleInfo.threshold).sorted
if (!isNumeric) {
genEnumOracleOutcome(chosenOracles, dlcOffer, outcomes, outcomeIndex)
genEnumOracleOutcome(chosenOracles, contractInfo, outcomes, outcomeIndex)
} else {
genNumericOracleOutcome(numOutcomesOrDigits,
chosenOracles,
dlcOffer,
contractInfo,
outcomes,
outcomeIndex,
paramsOpt)
@ -765,7 +926,7 @@ trait DLCTest {
def genOracleOutcomeAndSignatures(
numOutcomesOrDigits: Int,
isNumeric: Boolean,
dlcOffer: TestDLCClient,
contractInfo: SingleContractInfo,
outcomes: Vector[DLCOutcomeType],
outcomeIndex: Long,
paramsOpt: Option[OracleParamsV0TLV]): (
@ -773,7 +934,7 @@ trait DLCTest {
Vector[OracleSignatures]) = {
val outcome = genOracleOutcome(numOutcomesOrDigits,
isNumeric,
dlcOffer,
contractInfo,
outcomes,
outcomeIndex,
paramsOpt)
@ -792,19 +953,282 @@ trait DLCTest {
def genOracleSignatures(
numOutcomesOrDigits: Int,
isNumeric: Boolean,
dlcOffer: TestDLCClient,
contractInfo: SingleContractInfo,
outcomes: Vector[DLCOutcomeType],
outcomeIndex: Long,
paramsOpt: Option[OracleParamsV0TLV]): Vector[OracleSignatures] = {
val (_, sigs) = genOracleOutcomeAndSignatures(numOutcomesOrDigits,
isNumeric,
dlcOffer,
contractInfo,
outcomes,
outcomeIndex,
paramsOpt)
sigs
}
def validateOutcome(
outcome: DLCOutcome,
dlcOffer: TestDLCClient,
dlcAccept: TestDLCClient): Assertion = {
val fundingTx = outcome.fundingTx
assert(noEmptySPKOutputs(fundingTx))
val fundOutputIndex = dlcOffer.dlcTxBuilder.fundOutputIndex
val signers = Vector(dlcOffer.fundingPrivKey, dlcAccept.fundingPrivKey)
val closingSpendingInfo = ScriptSignatureParams(
P2WSHV0InputInfo(
TransactionOutPoint(fundingTx.txId, UInt32(fundOutputIndex)),
fundingTx.outputs(fundOutputIndex).value,
P2WSHWitnessV0(
MultiSignatureScriptPubKey(2,
signers.map(_.publicKey).sortBy(_.hex))),
ConditionalPath.NoCondition
),
fundingTx,
signers,
HashType.sigHashAll
)
outcome match {
case ExecutedDLCOutcome(fundingTx, cet, _, _) =>
DLCFeeTestUtil.validateFees(dlcOffer.dlcTxBuilder,
fundingTx,
cet,
fundingTxSigs = 5)
assert(noEmptySPKOutputs(cet))
assert(BitcoinScriptUtil.verifyScript(cet, Vector(closingSpendingInfo)))
case RefundDLCOutcome(fundingTx, refundTx) =>
DLCFeeTestUtil.validateFees(dlcOffer.dlcTxBuilder,
fundingTx,
refundTx,
fundingTxSigs = 5)
assert(noEmptySPKOutputs(refundTx))
assert(
BitcoinScriptUtil.verifyScript(refundTx, Vector(closingSpendingInfo)))
}
}
def executeForCase(outcomeIndex: Long, contractParams: ContractParams)(
implicit ec: ExecutionContext): Future[Assertion] = {
executeForCase(contractIndex = 0, outcomeIndex, contractParams)
}
def executeForCase(
contractIndex: Int,
outcomeIndex: Long,
contractParams: ContractParams)(implicit
ec: ExecutionContext): Future[Assertion] = {
constructAndSetupDLC(contractParams)
.flatMap {
case (dlcOffer, offerSetup, dlcAccept, acceptSetup, outcomes) =>
executeForOutcome(outcomeIndex,
dlcOffer,
offerSetup,
dlcAccept,
acceptSetup,
outcomes,
contractIndex)
}
}
def executeForCases(
outcomeIndices: Vector[Long],
contractParams: ContractParams)(implicit
ec: ExecutionContext): Future[Assertion] = {
executeForCasesInUnion(outcomeIndices.map((0, _)), contractParams)
}
def executeForCasesInUnion(
outcomeIndices: Vector[(Int, Long)],
contractParams: ContractParams)(implicit
ec: ExecutionContext): Future[Assertion] = {
constructAndSetupDLC(contractParams)
.flatMap {
case (dlcOffer, offerSetup, dlcAccept, acceptSetup, outcomes) =>
val testFs = outcomeIndices.map {
case (contractIndex, outcomeIndex) =>
executeForOutcome(outcomeIndex,
dlcOffer,
offerSetup,
dlcAccept,
acceptSetup,
outcomes,
contractIndex)
}
Future.sequence(testFs).map(_ => succeed)
}
}
def executeForOutcome(
outcomeIndex: Long,
dlcOffer: TestDLCClient,
offerSetup: SetupDLC,
dlcAccept: TestDLCClient,
acceptSetup: SetupDLC,
outcomes: Vector[DLCOutcomeType],
contractIndex: Int = 0)(implicit
ec: ExecutionContext): Future[Assertion] = {
val contractInfo = dlcOffer.offer.contractInfo
val contractSizes = contractInfo.contracts.map { contract =>
contract.allOutcomes.length
}
val indexOfOutcomeStart = contractSizes.take(contractIndex).sum
val singleContractInfo = contractInfo.contracts(contractIndex)
val possibleOutcomesForContract =
outcomes.slice(
indexOfOutcomeStart,
indexOfOutcomeStart + singleContractInfo.allOutcomes.length)
val contractDesc = singleContractInfo.contractDescriptor
val (numOutcomes, isMultiDigit, paramsOpt) = contractDesc match {
case EnumContractDescriptor(outcomeValueMap) =>
(outcomeValueMap.length, false, None)
case NumericContractDescriptor(_, numDigits, _) =>
val paramsOpt = contractInfo.oracleInfos.head match {
case NumericMultiOracleInfo(_,
_,
maxErrorExp,
minFailExp,
maximizeCoverage) =>
Some(OracleParamsV0TLV(maxErrorExp, minFailExp, maximizeCoverage))
case _: OracleInfo => None
}
(numDigits, true, paramsOpt)
}
val oracleSigs = genOracleSignatures(numOutcomes,
isMultiDigit,
singleContractInfo,
possibleOutcomesForContract,
outcomeIndex,
paramsOpt)
for {
offerOutcome <-
dlcOffer.executeDLC(offerSetup, Future.successful(oracleSigs))
acceptOutcome <-
dlcAccept.executeDLC(acceptSetup, Future.successful(oracleSigs))
} yield {
assert(offerOutcome.fundingTx == acceptOutcome.fundingTx)
validateOutcome(offerOutcome, dlcOffer, dlcAccept)
validateOutcome(acceptOutcome, dlcOffer, dlcAccept)
}
}
def executeRefundCase(contractParams: ContractParams)(implicit
ec: ExecutionContext): Future[Assertion] = {
constructAndSetupDLC(contractParams)
.map { case (dlcOffer, offerSetup, dlcAccept, acceptSetup, _) =>
val offerOutcome = dlcOffer.executeRefundDLC(offerSetup)
val acceptOutcome = dlcAccept.executeRefundDLC(acceptSetup)
validateOutcome(offerOutcome, dlcOffer, dlcAccept)
validateOutcome(acceptOutcome, dlcOffer, dlcAccept)
assert(acceptOutcome.fundingTx == offerOutcome.fundingTx)
assert(acceptOutcome.refundTx == offerOutcome.refundTx)
}
}
def assertCorrectSigDerivation(
offerSetup: SetupDLC,
dlcOffer: TestDLCClient,
acceptSetup: SetupDLC,
dlcAccept: TestDLCClient,
oracleSigs: Vector[OracleSignatures],
outcome: OracleOutcome)(implicit
ec: ExecutionContext): Future[Assertion] = {
val aggR = outcome.aggregateNonce
val aggS = outcome match {
case EnumOracleOutcome(oracles, _) =>
assert(oracles.length == oracleSigs.length)
val sVals = oracleSigs.map {
case EnumOracleSignature(oracle, sig) =>
assert(oracles.contains(oracle))
sig.sig
case _: NumericOracleSignatures =>
fail("Expected EnumOracleSignature")
}
sVals.reduce(_.add(_))
case NumericOracleOutcome(oraclesAndOutcomes) =>
assert(oraclesAndOutcomes.length == oracleSigs.length)
val sVals = oracleSigs.map {
case NumericOracleSignatures(oracle, sigs) =>
val oracleAndOutcomeOpt = oraclesAndOutcomes.find(_._1 == oracle)
assert(oracleAndOutcomeOpt.isDefined)
val outcome = oracleAndOutcomeOpt.get._2
val sVals = sigs.take(outcome.digits.length).map(_.sig)
sVals.reduce(_.add(_))
case _: EnumOracleSignature =>
fail("Expected NumericOracleSignatures")
}
sVals.reduce(_.add(_))
}
val aggSig = SchnorrDigitalSignature(aggR, aggS)
// Must use stored adaptor sigs because adaptor signing nonce is not deterministic (auxRand)
val offerRefundSig = dlcOffer.dlcTxSigner.signRefundTx
val acceptRefundSig = dlcAccept.dlcTxSigner.signRefundTx
val acceptAdaptorSigs = offerSetup.cets.map { case (outcome, info) =>
(outcome, info.remoteSignature)
}
val acceptCETSigs = CETSignatures(acceptAdaptorSigs, acceptRefundSig)
val offerAdaptorSigs = acceptSetup.cets.map { case (outcome, info) =>
(outcome, info.remoteSignature)
}
val offerCETSigs = CETSignatures(offerAdaptorSigs, offerRefundSig)
for {
offerFundingSigs <- Future.fromTry(dlcOffer.dlcTxSigner.signFundingTx())
offerOutcome <-
dlcOffer.executeDLC(offerSetup, Future.successful(oracleSigs))
acceptOutcome <-
dlcAccept.executeDLC(acceptSetup, Future.successful(oracleSigs))
} yield {
val builder = DLCTxBuilder(dlcOffer.offer, dlcAccept.accept)
val contractId = builder.buildFundingTx.txIdBE.bytes
.xor(dlcAccept.accept.tempContractId.bytes)
val offer = dlcOffer.offer
val accept = dlcOffer.accept.withSigs(acceptCETSigs)
val sign = DLCSign(offerCETSigs, offerFundingSigs, contractId)
val (offerOracleSig, offerDLCOutcome) =
DLCStatus
.calculateOutcomeAndSig(isInitiator = true,
offer,
accept,
sign,
acceptOutcome.cet)
.get
val (acceptOracleSig, acceptDLCOutcome) =
DLCStatus
.calculateOutcomeAndSig(isInitiator = false,
offer,
accept,
sign,
offerOutcome.cet)
.get
assert(offerDLCOutcome == outcome)
assert(acceptDLCOutcome == outcome)
assert(offerOracleSig == aggSig)
assert(acceptOracleSig == aggSig)
}
}
/** Synchronously runs the test function on each paramsToTest in turn. */
def runTestsForParam[T](paramsToTest: Vector[T])(
test: T => Future[Assertion])(implicit

View file

@ -155,11 +155,21 @@ object TestDLCClient {
)
val remoteOutcomes: ContractInfo = {
val descriptor =
outcomes.contractDescriptor.flip((input + remoteInput).satoshis)
val pair =
ContractOraclePair.fromDescriptorOracle(descriptor, outcomes.oracleInfo)
outcomes.copy(contractOraclePair = pair)
val descriptors =
outcomes.contractDescriptors.map(_.flip((input + remoteInput).satoshis))
val contracts = descriptors.zip(outcomes.oracleInfos).map {
case (descriptor, oracleInfo) =>
val pair =
ContractOraclePair.fromDescriptorOracle(descriptor, oracleInfo)
SingleContractInfo(outcomes.totalCollateral, pair)
}
outcomes match {
case _: SingleContractInfo => contracts.head
case _: DisjointUnionContractInfo =>
DisjointUnionContractInfo(contracts)
}
}
val changeAddress = BitcoinAddress.fromScriptPubKey(changeSPK, network)
@ -222,6 +232,13 @@ object TestDLCClient {
timeouts = timeouts
)
val negotiationFields = offerOutcomes match {
case _: SingleContractInfo => DLCAccept.NoNegotiationFields
case DisjointUnionContractInfo(contracts) =>
DLCAccept.NegotiationFieldsV2(
contracts.map(_ => DLCAccept.NoNegotiationFields))
}
val accept = DLCMessage.DLCAcceptWithoutSigs(
totalCollateral = acceptInput.satoshis,
pubKeys = acceptPubKeys,
@ -229,7 +246,7 @@ object TestDLCClient {
changeAddress = acceptChangeAddress,
payoutSerialId = acceptPayoutSerialId,
changeSerialId = acceptChangeSerialId,
negotiationFields = DLCAccept.NoNegotiationFields,
negotiationFields = negotiationFields,
tempContractId = offer.tempContractId
)

View file

@ -428,8 +428,9 @@ trait TLVGen {
offer <- dlcOfferTLV
(oracleInfo, oraclePrivKey, oracleRValue) <- oracleInfoV0TLVWithKeys
} yield {
(offer.copy(contractInfo =
offer.contractInfo.copy(oracleInfo = oracleInfo)),
(offer.copy(contractInfo = offer.contractInfo
.asInstanceOf[ContractInfoV0TLV]
.copy(oracleInfo = oracleInfo)),
oraclePrivKey,
oracleRValue)
}

View file

@ -4,9 +4,10 @@ import org.bitcoins.commons.config.AppConfig
import org.bitcoins.core.api.chain.ChainQueryApi
import org.bitcoins.core.api.node.NodeApi
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.protocol.dlc.models.{ContractInfo, ContractOraclePair}
import org.bitcoins.core.protocol.dlc.models.ContractOraclePair
import org.bitcoins.dlc.wallet.{DLCAppConfig, DLCWallet}
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.core.protocol.dlc.models.SingleContractInfo
import org.bitcoins.server.BitcoinSAppConfig
import org.bitcoins.testkit.BitcoinSTestAppConfig
import org.bitcoins.testkit.wallet.DLCWalletUtil.InitializedDLCWallet
@ -159,7 +160,7 @@ trait BitcoinSDualWalletTest extends BitcoinSWalletTest {
bip39PasswordOpt = getBIP39PasswordOpt(),
extraConfig = Some(segwitWalletConf))(config2, system)
amt = expectedDefaultAmt / Satoshis(2)
contractInfo = ContractInfo(amt.satoshis, contractOraclePair)
contractInfo = SingleContractInfo(amt.satoshis, contractOraclePair)
(dlcWalletA, dlcWalletB) <-
DLCWalletUtil.initDLC(walletA, walletB, contractInfo)
} yield (dlcWalletA, dlcWalletB)
@ -182,7 +183,7 @@ trait BitcoinSDualWalletTest extends BitcoinSWalletTest {
bip39PasswordOpt = getBIP39PasswordOpt(),
extraConfig = Some(segwitWalletConf))(config2, system)
amt = expectedDefaultAmt / Satoshis(2)
contractInfo = ContractInfo(amt.satoshis, contractOraclePair)
contractInfo = SingleContractInfo(amt.satoshis, contractOraclePair)
(dlcWalletA, dlcWalletB) <-
DLCWalletUtil.initDLC(walletA, walletB, contractInfo)
} yield (dlcWalletA, dlcWalletB)

View file

@ -71,7 +71,7 @@ object DLCWalletUtil extends Logging {
ContractOraclePair.EnumPair(sampleContractDescriptor, sampleOracleInfo)
lazy val sampleContractInfo: ContractInfo =
ContractInfo(half, sampleContractOraclePair)
SingleContractInfo(half, sampleContractOraclePair)
lazy val sampleOracleWinSig: SchnorrDigitalSignature =
oraclePrivKey.schnorrSignWithNonce(winHash.bytes, kValue)
@ -95,7 +95,7 @@ object DLCWalletUtil extends Logging {
}
lazy val multiNonceContractInfo: ContractInfo =
ContractInfo(total, multiNonceContractOraclePair)
SingleContractInfo(total, multiNonceContractOraclePair)
lazy val dummyContractMaturity: BlockTimeStamp = BlockTimeStamp(0)
lazy val dummyContractTimeout: BlockTimeStamp = BlockTimeStamp(1)
@ -356,7 +356,7 @@ object DLCWalletUtil extends Logging {
}
}
def getSigs(contractInfo: ContractInfo): (
def getSigs(contractInfo: SingleContractInfo): (
OracleAttestmentTLV,
OracleAttestmentTLV) = {
val desc: EnumContractDescriptor = contractInfo.contractDescriptor match {

View file

@ -2,7 +2,10 @@ package org.bitcoins.testkit.wallet
import org.bitcoins.commons.config.AppConfig
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.protocol.dlc.models.{ContractInfo, ContractOraclePair}
import org.bitcoins.core.protocol.dlc.models.{
ContractOraclePair,
SingleContractInfo
}
import org.bitcoins.dlc.wallet.DLCAppConfig
import org.bitcoins.server.BitcoinSAppConfig
import org.bitcoins.testkit.BitcoinSTestAppConfig
@ -87,7 +90,7 @@ trait DualWalletTestCachedBitcoind
walletA <- walletAF
walletB <- walletBF
amt = expectedDefaultAmt / Satoshis(2)
contractInfo = ContractInfo(amt.satoshis, contractOraclePair)
contractInfo = SingleContractInfo(amt.satoshis, contractOraclePair)
(dlcWalletA, dlcWalletB) <-
DLCWalletUtil.initDLC(walletA, walletB, contractInfo)
bitcoind <- bitcoindF