Introduces Adaptor Signature based Discreet Log Contracts to Bitcoin-S

Fixed things after rebase

Fix adaptor dlc compile issue (#2188)

No more Maps in ContractInfo (#2215)

Co-authored-by: Ben Carman <benthecarman@live.com>

Remove Unneeded Escaped param for DLC Cli (#2213)

Fixed backward stack usage in dlc things (#2218)

Made test for DLCMessage Json serialization symmetry (#2225)

2020 10 31 Fix indeterminism with pairing dlc funding puts with funding transactions (#2227)

* WIP

* Fix indeterminism with matching DLCFundingInputDb with their funding transactions

* Remove unused imports

* Replace all other instances of .zip() in DLCWallet

Properly track DLC transactions in wallet (#2201)

* Properly track DLC transactions in wallet

* Small cleanups

DLCStatus Table in GUI (#2203)

* DLC Table View & Inspect DLC

* Move to new file, clean up asInstanceOfs

* Update DLCs on View

* Rebase fixes

Generalize dlc builders (#2206)

Co-authored-by: nkohen <nadavk25@gmail.com>
Co-authored-by: Ben Carman <benthecarman@live.com>

DLC Wallet & GUI touch ups (#2305)

* DLC Wallet & GUI touch ups

* Rename back to DLCStatus

* Only create dummy oracle on test networks

* Bring up Oracle TLVs

* Use announcement instead of oracle info

* Fix names, compile issue, assert we can safely delete sigs

* Fix TLV parsing for non-standard strings

* Fix RoutesSpec

Give raw tx, tx data, and link to broadcast for txs (#2340)

* Give raw tx, tx data, and link to broadcast for txs

* Change format
This commit is contained in:
Ben Carman 2020-07-29 17:07:00 -05:00 committed by nkohen
parent ddd352d844
commit 935145d46b
115 changed files with 27207 additions and 100 deletions

View file

@ -22,4 +22,4 @@ jobs:
~/.bitcoin-s/binaries
key: ${{ runner.os }}-cache
- name: run tests
run: sbt ++2.12.12 downloadBitcoind coverage keyManagerTest/test keyManager/coverageReport keyManager/coverageAggregate keyManager/coveralls feeProviderTest/test walletTest/test wallet/coverageReport wallet/coverageAggregate wallet/coveralls dlcOracleTest/test dlcOracle/coverageReport dlcOracle/coverageAggregate dlcOracle/coveralls
run: sbt ++2.12.12 downloadBitcoind coverage keyManagerTest/test keyManager/coverageReport keyManager/coverageAggregate keyManager/coveralls feeProviderTest/test walletTest/test wallet/coverageReport wallet/coverageAggregate wallet/coveralls dlcOracleTest/test dlcOracle/coverageReport dlcOracle/coverageAggregate dlcOracle/coveralls dlcTest/test dlcWalletTest/test

View file

@ -22,4 +22,4 @@ jobs:
~/.bitcoin-s/binaries
key: ${{ runner.os }}-cache
- name: run tests
run: sbt ++2.13.4 downloadBitcoind coverage keyManagerTest/test keyManager/coverageReport keyManager/coverageAggregate keyManager/coveralls feeProviderTest/test walletTest/test wallet/coverageReport wallet/coverageAggregate wallet/coveralls dlcOracleTest/test dlcOracle/coverageReport dlcOracle/coverageAggregate dlcOracle/coveralls
run: sbt ++2.13.4 downloadBitcoind coverage keyManagerTest/test keyManager/coverageReport keyManager/coverageAggregate keyManager/coveralls feeProviderTest/test walletTest/test wallet/coverageReport wallet/coverageAggregate wallet/coveralls dlcOracleTest/test dlcOracle/coverageReport dlcOracle/coverageAggregate dlcOracle/coveralls dlcTest/test dlcWalletTest/test

View file

@ -22,4 +22,4 @@ jobs:
~/.bitcoin-s/binaries
key: ${{ runner.os }}-cache
- name: run tests
run: sbt ++2.13.4 downloadBitcoind coverage walletTest/test wallet/coverageReport wallet/coverageAggregate wallet/coveralls nodeTest/test node/coverageReport node/coverageAggregate node/coveralls dlcOracleTest/test dlcOracle/coverageReport dlcOracle/coveralls
run: sbt ++2.13.4 downloadBitcoind coverage walletTest/test wallet/coverageReport wallet/coverageAggregate wallet/coveralls nodeTest/test node/coverageReport node/coverageAggregate node/coveralls dlcOracleTest/test dlcOracle/coverageReport dlcOracle/coveralls dlcTest/test dlcWalletTest/test

View file

@ -65,4 +65,30 @@ class DLCMessageTest extends BitcoinSAsyncTest {
)
)
}
it must "be able to go back and forth between TLV and deserialized" in {
forAll(TLVGen.dlcOfferTLVAcceptTLVSignTLV) {
case (offerTLV, acceptTLV, signTLV) =>
val offer = DLCOffer.fromTLV(offerTLV)
val accept = DLCAccept.fromTLV(acceptTLV, offer)
val sign = DLCSign.fromTLV(signTLV, offer)
assert(offer.toTLV == offerTLV)
assert(accept.toTLV == acceptTLV)
assert(sign.toTLV == signTLV)
}
}
it must "be able to go back and forth between LN Message and deserialized" in {
forAll(LnMessageGen.dlcOfferMessageAcceptMessageSignMessage) {
case (offerMsg, acceptMsg, signMsg) =>
val offer = DLCOffer.fromMessage(offerMsg)
val accept = DLCAccept.fromMessage(acceptMsg, offer)
val sign = DLCSign.fromMessage(signMsg, offer)
assert(offer.toMessage == offerMsg)
assert(accept.toMessage == acceptMsg)
assert(sign.toMessage == signMsg)
}
}
}

View file

@ -0,0 +1,3 @@
name := "bitcoin-s-sbclient-test"
publish / skip := true

View file

@ -0,0 +1,133 @@
package org.bitcoins.sbclient
import akka.Done
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.{ContentTypes, HttpEntity}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import org.bitcoins.core.protocol.ln.currency.{LnCurrencyUnit, MilliSatoshis}
import org.bitcoins.core.protocol.ln.{LnInvoice, PaymentPreimage}
import org.bitcoins.crypto._
import org.bitcoins.rpc.util.RpcUtil
import org.bitcoins.testkit.eclair.MockEclairClient
import scodec.bits.ByteVector
import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.util.matching.Regex
class MockServer()(implicit ec: ExecutionContext) {
val mockEclair: MockEclairClient = new MockEclairClient()
def preImage(invoice: LnInvoice): PaymentPreimage = {
mockEclair.preImage(invoice.lnTags.paymentHash)
}
def encryptData(data: String, price: LnCurrencyUnit)(implicit
ec: ExecutionContext): Future[(LnInvoice, String)] = {
val invoiceF =
mockEclair.createInvoice(
description = s"Mock Invoice",
price.toMSat
)
invoiceF.map { invoice =>
val preimage = preImage(invoice)
val dataBytes: ByteVector = ByteVector.encodeUtf8(data) match {
case Left(err) =>
throw new RuntimeException(s"Could not encode data to UTF-8!", err)
case Right(bytes) => bytes
}
val key = AesKey.fromValidBytes(preimage.bytes)
val encrypted: AesEncryptedData = AesCrypt.encrypt(dataBytes, key)
(invoice, encrypted.toBase64)
}
}
val exchangeRegex: Regex =
RegexUtil.noCaseOrRegex(Exchange.all.map(_.toLongString).:+("bitmex"))
val tradingPairRegex: Regex = RegexUtil.noCaseOrRegex(TradingPair.all)
val requestTypeRegex: Regex =
RegexUtil.noCaseOrRegex(RequestType.all :+ "PublicKey")
def route: Route = {
path(exchangeRegex / tradingPairRegex / requestTypeRegex) {
case (exchangeStr, pairStr, requestTypeStr) =>
if (requestTypeStr.toLowerCase == "publickey") {
val hash = CryptoUtil.sha256(s"$exchangeStr|$pairStr|pubkey")
val pubKey = ECPrivateKey(hash.bytes).schnorrPublicKey
complete {
HttpEntity(ContentTypes.`text/plain(UTF-8)`, pubKey.hex)
}
} else {
val data = Vector(exchangeStr, pairStr, requestTypeStr).mkString("|")
val encryptedF =
encryptData(data, MilliSatoshis(10000).toLnCurrencyUnit)
val responseF = encryptedF.map {
case (invoice, encrypted) =>
InvoiceAndDataResponse(invoice, encrypted).toJsonString
}
complete {
responseF.map { response =>
HttpEntity(ContentTypes.`application/json`, response)
}
}
}
} ~ path("ping") {
get {
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>pong</h1>"))
}
}
}
val port: Int = RpcUtil.randomPort
val endpoint: String = s"http://localhost:$port"
private val serverBindingP = Promise[Future[Http.ServerBinding]]()
private val serverBindingF: Future[Http.ServerBinding] =
serverBindingP.future.flatten
private val stoppedP = Promise[Future[Done]]()
private val stoppedF: Future[Done] = stoppedP.future.flatten
serverBindingF.map { binding =>
scala.sys.addShutdownHook {
if (!stoppedF.isCompleted) {
val _ = binding.unbind()
}
}
}
def start()(implicit system: ActorSystem): Future[Http.ServerBinding] = {
if (serverBindingP.isCompleted) {
Future.failed(new RuntimeException("Mock Server already started!"))
} else {
val bindingF = Http().bindAndHandle(handler = route,
interface = "localhost",
port = port)
val _ = serverBindingP.success(bindingF)
bindingF
}
}
def stop(): Future[Done] = {
if (stoppedP.isCompleted || !serverBindingP.isCompleted) {
Future.failed(new RuntimeException(
"Cannot stop server if it has not been started or has already been stopped"))
} else {
val resultF = serverBindingF.flatMap(_.unbind())
stoppedP.success(resultF)
resultF
}
}
}

View file

@ -0,0 +1,36 @@
package org.bitcoins.sbclient
import scala.util.matching.Regex
object RegexUtil {
/** Given a finite collection of case objects or strings, generates a Regex
* that matches its elements, insensitive to case.
*
* Example:
* case object A
* case object BC
* case object DEF
*
* Util.noCaseOrRegex(Vector(A, BC, DEF)) == "(?i)(?:DEF|BC|A)".r
*/
def noCaseOrRegex[T](all: Seq[T]): Regex = {
val orStr = all
.sortBy(_.toString.length)(Ordering.Int.reverse)
.foldLeft("") {
case (rx, t) =>
rx + t + "|"
}
.init
noCaseRegex(orStr)
}
/** Turns a String into a case insensitive Regex */
def noCaseRegex(str: String): Regex = {
s"""(?i)(?:$str)""".r
}
/** Matches any String of any length */
val anyString: Regex = ".*".r
}

View file

@ -0,0 +1,124 @@
package org.bitcoins.sbclient
import org.bitcoins.commons.jsonmodels.eclair.{
IncomingPaymentStatus,
OutgoingPaymentStatus
}
import org.bitcoins.crypto.{CryptoUtil, ECPrivateKey}
import org.bitcoins.core.protocol.ln.currency.MilliSatoshis
import org.bitcoins.testkit.eclair.MockEclairClient
import org.bitcoins.testkit.util.BitcoinSAsyncTest
import org.scalacheck.Gen
import scodec.bits.ByteVector
import scala.concurrent.Await
import scala.concurrent.duration.DurationInt
class SbClientTest extends BitcoinSAsyncTest {
behavior of "SbClient"
val str = "Never gonna give you up, never gonna let you down"
val amt: MilliSatoshis = MilliSatoshis(10000)
val server = new MockServer()
val client = new MockEclairClient()
override def beforeAll(): Unit = {
super.beforeAll()
client.otherClient = Some(server.mockEclair)
server.mockEclair.otherClient = Some(client)
val _ = Await.result(server.start(), 5.seconds)
}
override def afterAll(): Unit = {
Await.result(server.stop(), 5.seconds)
super.afterAll()
}
it should "successfully decrypt data from server" in {
server.encryptData(str, amt.toLnCurrencyUnit).map {
case (invoice, encrypted) =>
val preImage = server.preImage(invoice)
val decrypted = SbClient.decryptData(encrypted, preImage)
assert(decrypted == str)
}
}
it should "successfully make a payment" in {
for {
invoice <- server.mockEclair.createInvoice(str, amt)
preImage <- SbClient.makePayment(client, invoice)
serverNodeId <- server.mockEclair.nodeId()
} yield {
assert(server.preImage(invoice) == preImage)
val incoming =
server.mockEclair.incomingPayment(invoice.lnTags.paymentHash)
val outgoing = client.outgoingPayment(invoice.lnTags.paymentHash)
assert(incoming.paymentRequest == outgoing.paymentRequest.get)
assert(incoming.paymentPreimage == preImage)
assert(incoming.status.isInstanceOf[IncomingPaymentStatus.Received])
assert(outgoing.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
assert(
outgoing.status
.asInstanceOf[OutgoingPaymentStatus.Succeeded]
.paymentPreimage == preImage)
assert(outgoing.amount == amt)
assert(outgoing.recipientNodeId == serverNodeId)
}
}
def exchangeAndPairGen: Gen[(Exchange, TradingPair)] =
Gen.oneOf(Exchange.all).flatMap { exchange =>
Gen.oneOf(exchange.pairs).map { pair =>
(exchange, pair)
}
}
it should "successfully request and decrypt an R value" in {
forAllAsync(exchangeAndPairGen) {
case (exchange, pair) =>
SbClient
.requestAndPay(exchange,
pair,
RequestType.RValue,
client,
server.endpoint)
.map { decrypted =>
assert(
decrypted == s"${exchange.toLongString}|${pair.toLowerString}|rvalue")
}
}
}
it should "successfully request and decrypt the last signature" in {
forAllAsync(exchangeAndPairGen) {
case (exchange, pair) =>
SbClient
.requestAndPay(exchange,
pair,
RequestType.LastSig,
client,
server.endpoint)
.map { decrypted =>
assert(
decrypted == s"${exchange.toLongString}|${pair.toLowerString}|lastsig")
}
}
}
it should "successfully request and receive a public key" in {
forAllAsync(exchangeAndPairGen) {
case (exchange, pair) =>
SbClient
.getPublicKey(exchange, pair, server.endpoint)
.map { response =>
val hash = CryptoUtil.sha256(
s"${exchange.toLongString}|${pair.toLowerString}|pubkey")
val expected = ECPrivateKey(hash.bytes).schnorrPublicKey
assert(response == expected)
}
}
}
}

View file

@ -0,0 +1,3 @@
name := "bitcoin-s-dlc-suredbits-client"
libraryDependencies ++= Deps.dlcSuredbitsClient

View file

@ -0,0 +1,44 @@
package org.bitcoins.sbclient
sealed abstract class Asset {
def toLowerString: String = this.toString.toLowerCase
def toUpperString: String = this.toString.toUpperCase
}
object Asset {
case object BTC extends Asset
case object ETH extends Asset
case object USD extends Asset
case object USDT extends Asset
case object LTC extends Asset
case object BCH extends Asset
case object XRP extends Asset
case object EOS extends Asset
case object EUR extends Asset
case object DOGE extends Asset
case object STR extends Asset
case class OtherAsset(name: String) extends Asset {
override def toString: String = name
}
def fromLowerString(str: String): Asset = {
val name = if (str.toLowerCase == "xbt") "btc" else str
val assetOpt = all.find(asset => asset.toLowerString == name)
assetOpt.getOrElse(OtherAsset(name.toUpperCase))
}
def fromUpperString(str: String): Asset = {
val name = if (str.toUpperCase == "XBT") "BTC" else str
val assetOpt = all.find(asset => asset.toUpperString == name)
assetOpt.getOrElse(OtherAsset(name))
}
def fromString(str: String): Asset = {
fromUpperString(str.toUpperCase)
}
def all: Vector[Asset] =
Vector(BTC, ETH, USD, USDT, LTC, BCH, XRP, EOS, EUR, DOGE, STR)
}

View file

@ -0,0 +1,129 @@
package org.bitcoins.sbclient
sealed trait Exchange {
def pairs: Vector[TradingPair]
/**
* The unique string representation of of this exchange.
* Used to differentiate between spot and futures exchanges,
* e.g. Kraken vs KrakenFut etc
*/
def toLongString: String
/** The unique string representation of this exchange where
* bitmex is not bitmexfut.
*/
def toGeneralString: String = {
if (this == Exchange.Bitmex) {
"bitmex"
} else {
this.toLongString
}
}
}
sealed trait SpotExchange extends Exchange
sealed trait FuturesExchange extends Exchange
object Exchange {
/** Spot exchanges located in US, this is useful for log filtering */
val allUSSpot: Vector[SpotExchange] = Vector(Coinbase, Gemini, Kraken)
/** International spot exchanges, this is useful for log filtering */
val allInternationalSpot: Vector[SpotExchange] =
Vector(Bitfinex, Binance, Bitstamp)
val allSpot: Vector[SpotExchange] =
allUSSpot ++ allInternationalSpot
val allFut: Vector[FuturesExchange] = Vector(KrakenFut, Bitmex)
val all: Vector[Exchange] = allSpot ++ allFut
// Bitmex is special because we want to accept both bitmexfut and bitmex
val acceptedHistoricalNames: Vector[String] =
all.map(_.toLongString).:+("bitmex")
case object Bitfinex extends SpotExchange {
override def toString: String = "bitfinex"
override def toLongString: String = "bitfinex"
override def pairs: Vector[TradingPair] = BitfinexTradingPair.all
}
case object Binance extends SpotExchange {
override def toString: String = "binance"
override def toLongString: String = "binance"
override def pairs: Vector[TradingPair] = BinanceTradingPair.all
}
case object Coinbase extends SpotExchange {
override def toString: String = "coinbase"
override def toLongString: String = "coinbase"
override def pairs: Vector[TradingPair] = CoinbaseTradingPair.all
}
case object Bitstamp extends SpotExchange {
override def toString: String = "bitstamp"
override def toLongString: String = "bitstamp"
override def pairs: Vector[TradingPair] = BitstampTradingPair.all
}
case object Bitmex extends FuturesExchange {
override def toString: String = "bitmex"
override def toLongString: String = "bitmexfut"
override def pairs: Vector[TradingPair] = BitmexTradingPair.all
}
case object Gemini extends SpotExchange {
override def toString: String = "gemini"
override def toLongString: String = "gemini"
override def pairs: Vector[TradingPair] = GeminiTradingPair.all
}
case object Kraken extends SpotExchange {
override def toString: String = "kraken"
override def toLongString: String = "kraken"
override def pairs: Vector[TradingPair] = KrakenTradingPair.all
}
case object KrakenFut extends FuturesExchange {
override def toString: String = "kraken"
override def toLongString: String = "krakenfut"
override def pairs: Vector[TradingPair] = KrakenFutTradingPair.all
}
}
object SpotExchange {
def fromString(exchange: String): Option[SpotExchange] = {
exchange.toLowerCase match {
case "bitfinex" => Some(Exchange.Bitfinex)
case "binance" => Some(Exchange.Binance)
case "coinbase" => Some(Exchange.Coinbase)
case "bitstamp" => Some(Exchange.Bitstamp)
case "gemini" => Some(Exchange.Gemini)
case "kraken" => Some(Exchange.Kraken)
case _: String => None
}
}
override def toString: String = "spotexchange"
}
object FuturesExchange {
def fromString(exchange: String): Option[FuturesExchange] = {
exchange.toLowerCase match {
case "bitmex" => Some(Exchange.Bitmex)
case "bitmexfut" => Some(Exchange.Bitmex)
case "kraken" => Some(Exchange.KrakenFut)
case "krakenfut" => Some(Exchange.KrakenFut)
case _: String => None
}
}
override def toString: String = "futuresexchange"
}

View file

@ -0,0 +1,10 @@
package org.bitcoins.sbclient
import org.bitcoins.core.protocol.ln.LnInvoice
case class InvoiceAndDataResponse(invoice: LnInvoice, encryptedData: String) {
def toJsonString: String = {
s"""{"invoice":"${invoice.toString}", "encryptedData":"$encryptedData"}"""
}
}

View file

@ -0,0 +1,18 @@
package org.bitcoins.sbclient
sealed trait RequestType {
def requestString: String
}
object RequestType {
case object RValue extends RequestType {
override def requestString: String = "rvalue"
}
case object LastSig extends RequestType {
override def requestString: String = "lastsig"
}
val all: Vector[RequestType] = Vector(RValue, LastSig)
}

View file

@ -0,0 +1,149 @@
package org.bitcoins.sbclient
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.{HttpRequest, Uri}
import akka.stream.Materializer
import akka.util.ByteString
import org.bitcoins.commons.jsonmodels.eclair.OutgoingPaymentStatus
import org.bitcoins.crypto.{
AesCrypt,
AesEncryptedData,
AesKey,
ECPublicKey,
SchnorrDigitalSignature,
SchnorrPublicKey
}
import org.bitcoins.core.protocol.ln.{LnInvoice, PaymentPreimage}
import org.bitcoins.eclair.rpc.api.EclairApi
import play.api.libs.json.{JsError, JsSuccess, JsValue, Json, Reads}
import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future}
import scala.concurrent.duration.{DurationInt, FiniteDuration}
object SbClient {
def rawRestCall(uri: Uri)(implicit system: ActorSystem): Future[String] = {
implicit val m: Materializer = Materializer.createMaterializer(system)
implicit val ec: ExecutionContextExecutor = m.executionContext
Http()
.singleRequest(HttpRequest(uri = uri))
.flatMap(response =>
response.entity.dataBytes
.runFold(ByteString.empty)(_ ++ _)
.map(payload => payload.decodeString(ByteString.UTF_8)))
}
def getPublicKey(
exchange: Exchange,
pair: TradingPair,
endpoint: String = "https://test.api.suredbits.com/dlc/v0")(implicit
system: ActorSystem): Future[SchnorrPublicKey] = {
val uri =
s"$endpoint/${exchange.toLongString}/${pair.toLowerString}/PublicKey"
import system.dispatcher
rawRestCall(uri).map(SchnorrPublicKey.fromHex)
}
def restCall(uri: Uri)(implicit system: ActorSystem): Future[JsValue] = {
rawRestCall(uri).map(Json.parse)(system.dispatcher)
}
import org.bitcoins.commons.serializers.JsonReaders.lnInvoiceReads
implicit val invoiceAndDataResponseReads: Reads[InvoiceAndDataResponse] =
Json.reads[InvoiceAndDataResponse]
def request(
exchange: Exchange,
tradingPair: TradingPair,
requestType: RequestType,
endpoint: String = "https://test.api.suredbits.com/dlc/v0")(implicit
system: ActorSystem): Future[InvoiceAndDataResponse] = {
implicit val ec: ExecutionContextExecutor = system.dispatcher
val uri =
s"$endpoint/${exchange.toLongString}/${tradingPair.toLowerString}/${requestType.requestString}"
restCall(uri)
.map(_.validate[InvoiceAndDataResponse])
.flatMap {
case JsSuccess(response, _) => Future.successful(response)
case JsError(error) =>
Future.failed(
new RuntimeException(
s"Unexpected error when parsing response: $error"))
}
}
def makePayment(
eclairApi: EclairApi,
invoice: LnInvoice,
timeout: FiniteDuration = 5.seconds)(implicit
ec: ExecutionContext): Future[PaymentPreimage] = {
for {
payment <- eclairApi.payAndMonitorInvoice(invoice,
externalId = None,
interval = timeout / 10,
maxAttempts = 10)
} yield payment.status match {
case OutgoingPaymentStatus.Succeeded(preImage, _, _, _) => preImage
case OutgoingPaymentStatus.Failed(errs) =>
val errMsgs = errs.map(_.failureMessage).mkString(",\n")
throw new RuntimeException(s"Payment failed: $errMsgs")
case OutgoingPaymentStatus.Pending =>
throw new IllegalStateException(
"This should not be possible because invoice monitoring should only return on Succeeded or Failed.")
}
}
def decryptData(data: String, preImage: PaymentPreimage): String = {
AesCrypt.decrypt(AesEncryptedData.fromValidBase64(data),
AesKey.fromValidBytes(preImage.bytes)) match {
case Left(err) => throw err
case Right(decrypted) =>
decrypted.decodeUtf8 match {
case Right(decodedStr) => decodedStr
case Left(err) => throw err
}
}
}
def requestAndPay(
exchange: Exchange,
tradingPair: TradingPair,
requestType: RequestType,
eclairApi: EclairApi,
endpoint: String = "https://test.api.suredbits.com/dlc/v0")(implicit
system: ActorSystem): Future[String] = {
implicit val ec: ExecutionContextExecutor = system.dispatcher
for {
InvoiceAndDataResponse(invoice, encryptedData) <-
request(exchange, tradingPair, requestType, endpoint)
preImage <- makePayment(eclairApi, invoice)
} yield decryptData(encryptedData, preImage)
}
def requestRValueAndPay(
exchange: Exchange,
tradingPair: TradingPair,
eclairApi: EclairApi)(implicit
system: ActorSystem): Future[ECPublicKey] = {
val dataF =
requestAndPay(exchange, tradingPair, RequestType.RValue, eclairApi)
dataF.map(ECPublicKey.fromHex)(system.dispatcher)
}
def requestLastSigAndPay(
exchange: Exchange,
tradingPair: TradingPair,
eclairApi: EclairApi)(implicit
system: ActorSystem): Future[SchnorrDigitalSignature] = {
val dataF =
requestAndPay(exchange, tradingPair, RequestType.LastSig, eclairApi)
dataF.map(SchnorrDigitalSignature.fromHex)(system.dispatcher)
}
}

View file

@ -0,0 +1,538 @@
package org.bitcoins.sbclient
import org.bitcoins.sbclient.Asset._
import org.bitcoins.sbclient.TradingPair.UnsupportedTradingPair
sealed abstract class TradingPair(left: Asset, right: Asset)
extends Serializable {
def toLowerString: String = this.toString.toLowerCase
def toUpperString: String = this.toString.toUpperCase
def getLeft: Asset = left
def getRight: Asset = right
def isSupportedBy(exchange: Exchange): Boolean = {
exchange match {
case Exchange.Bitfinex => this.isInstanceOf[BitfinexTradingPair]
case Exchange.Binance => this.isInstanceOf[BinanceTradingPair]
case Exchange.Coinbase => this.isInstanceOf[CoinbaseTradingPair]
case Exchange.Bitstamp => this.isInstanceOf[BitstampTradingPair]
case Exchange.Gemini => this.isInstanceOf[GeminiTradingPair]
case Exchange.Kraken => this.isInstanceOf[KrakenTradingPair]
case Exchange.KrakenFut => this.isInstanceOf[KrakenFutTradingPair]
case Exchange.Bitmex => this.isInstanceOf[BitmexTradingPair]
}
}
}
sealed trait BitfinexTradingPair extends TradingPair
sealed trait BinanceTradingPair extends TradingPair
sealed trait CoinbaseTradingPair extends TradingPair
sealed trait BitstampTradingPair extends TradingPair
sealed trait GeminiTradingPair extends TradingPair
sealed trait KrakenTradingPair extends TradingPair
sealed trait KrakenFutTradingPair extends TradingPair
sealed trait BitmexTradingPair extends TradingPair
object TradingPair {
private val aliases = Vector("XBT", "XDG", "XLM", "BCHABC", "BAB")
private def mapAlias(alias: String): String = {
alias match {
case "XBT" => "BTC"
case "XDG" => "DOGE"
case "XLM" => "STR"
case "BCHABC" | "BAB" => "BCH"
case notAlias: String => notAlias
}
}
private def handleAliasOfSize(size: Int, pair: String): String = {
val safeToCheck = pair.length >= size
lazy val leftIsAlias = aliases.contains(pair.take(size).toUpperCase)
lazy val rightIsAlias = aliases.contains(pair.takeRight(size).toUpperCase)
if (safeToCheck && leftIsAlias) {
mapAlias(pair.take(size).toUpperCase) + pair.drop(size)
} else if (safeToCheck && rightIsAlias) {
pair.dropRight(size) + mapAlias(pair.takeRight(size).toUpperCase)
} else {
pair
}
}
private def handleAlias(pair: String): String = {
val handled3 = handleAliasOfSize(size = 3, pair = pair)
handleAliasOfSize(size = 6, pair = handled3)
}
/** Reads a [[TradingPair trading pair]] from a string
* returns None if it DNE
*/
def fromStringOpt(pair: String): Option[TradingPair] = {
val translatedPair = handleAlias(pair)
val pairOpt =
all.find(tradingPair =>
tradingPair.toUpperString == translatedPair.toUpperCase)
pairOpt
}
/** Reads a [[TradingPair trading pair]] from a string
* returns a [[UnsupportedTradingPair unsupported traiding pair]]
* that contains the given string if it does not exist.
*/
def fromString(pair: String): TradingPair = {
val translatedPair = handleAlias(pair)
val pairOpt = fromStringOpt(pair)
lazy val unsupported =
UnsupportedTradingPair.fromString(translatedPair)
pairOpt.getOrElse(unsupported)
}
case object BTCUSD
extends TradingPair(BTC, USD)
with BitfinexTradingPair
with CoinbaseTradingPair
with BitstampTradingPair
with GeminiTradingPair
with KrakenTradingPair
with KrakenFutTradingPair
with BitmexTradingPair
case object ETHUSD
extends TradingPair(ETH, USD)
with BitfinexTradingPair
with CoinbaseTradingPair
with BitstampTradingPair
with GeminiTradingPair
with KrakenTradingPair
with KrakenFutTradingPair
with BitmexTradingPair
case object ETHBTC
extends TradingPair(ETH, BTC)
with BitfinexTradingPair
with BinanceTradingPair
with CoinbaseTradingPair
with BitstampTradingPair
with GeminiTradingPair
with KrakenTradingPair
with BitmexTradingPair
case object LTCUSD
extends TradingPair(LTC, USD)
with BitfinexTradingPair
with CoinbaseTradingPair
with BitstampTradingPair
with GeminiTradingPair
with KrakenTradingPair
with KrakenFutTradingPair
case object LTCBTC
extends TradingPair(LTC, BTC)
with BitfinexTradingPair
with BinanceTradingPair
with CoinbaseTradingPair
with BitstampTradingPair
with GeminiTradingPair
with KrakenTradingPair
with BitmexTradingPair
case object LTCETH
extends TradingPair(LTC, ETH)
with BinanceTradingPair
with GeminiTradingPair
case object BCHUSD
extends TradingPair(BCH, USD)
with BitfinexTradingPair
with CoinbaseTradingPair
with BitstampTradingPair
with GeminiTradingPair
with KrakenTradingPair
with KrakenFutTradingPair
case object BCHBTC
extends TradingPair(BCH, BTC)
with BitfinexTradingPair
with BinanceTradingPair
with CoinbaseTradingPair
with BitstampTradingPair
with GeminiTradingPair
with KrakenTradingPair
with BitmexTradingPair
case object BCHETH extends TradingPair(BCH, ETH) with GeminiTradingPair
case object LTCBCH extends TradingPair(LTC, BCH) with GeminiTradingPair
case object XRPUSD
extends TradingPair(XRP, USD)
with BitfinexTradingPair
with BitstampTradingPair
with CoinbaseTradingPair
with KrakenTradingPair
with KrakenFutTradingPair
case object XRPBTC
extends TradingPair(XRP, BTC)
with BinanceTradingPair
with BitfinexTradingPair
with BitstampTradingPair
with CoinbaseTradingPair
with KrakenTradingPair
with BitmexTradingPair
with KrakenFutTradingPair
case object XRPETH extends TradingPair(XRP, ETH) with BinanceTradingPair
case object EOSUSD
extends TradingPair(EOS, USD)
with BitfinexTradingPair
with CoinbaseTradingPair
with KrakenTradingPair
case object EOSBTC
extends TradingPair(EOS, BTC)
with BitfinexTradingPair
with BinanceTradingPair
with CoinbaseTradingPair
with KrakenTradingPair
with BitmexTradingPair
case object EOSETH
extends TradingPair(EOS, ETH)
with BitfinexTradingPair
with BinanceTradingPair
with KrakenTradingPair
case object BTCUSDT extends TradingPair(BTC, USDT) with BinanceTradingPair
case object ETHUSDT extends TradingPair(ETH, USDT) with BinanceTradingPair
case object LTCUSDT extends TradingPair(LTC, USDT) with BinanceTradingPair
case object BCHUSDT extends TradingPair(BCH, USDT) with BinanceTradingPair
case object XRPUSDT extends TradingPair(XRP, USDT) with BinanceTradingPair
case object EOSUSDT extends TradingPair(EOS, USDT) with BinanceTradingPair
case object EURUSD extends TradingPair(EUR, USD) with BitstampTradingPair
case object BTCEUR
extends TradingPair(BTC, EUR)
with BitfinexTradingPair
with BitstampTradingPair
with CoinbaseTradingPair
with KrakenTradingPair
case object ETHEUR
extends TradingPair(ETH, EUR)
with BitfinexTradingPair
with BitstampTradingPair
with CoinbaseTradingPair
with KrakenTradingPair
case object LTCEUR
extends TradingPair(LTC, EUR)
with BitstampTradingPair
with CoinbaseTradingPair
with KrakenTradingPair
case object BCHEUR
extends TradingPair(BCH, EUR)
with BitstampTradingPair
with CoinbaseTradingPair
with KrakenTradingPair
case object XRPEUR
extends TradingPair(XRP, EUR)
with BitstampTradingPair
with CoinbaseTradingPair
with KrakenTradingPair
case object EOSEUR
extends TradingPair(EOS, EUR)
with BitfinexTradingPair
with CoinbaseTradingPair
with KrakenTradingPair
case class UnsupportedTradingPair(left: Asset, right: Asset)
extends TradingPair(left, right)
with BitfinexTradingPair
with BinanceTradingPair
with CoinbaseTradingPair
with BitstampTradingPair
with GeminiTradingPair
with KrakenTradingPair
with KrakenFutTradingPair
with BitmexTradingPair {
override def toLowerString: String = {
s"$left$right".toLowerCase
}
override def toUpperString: String = {
s"$left$right".toUpperCase
}
}
object UnsupportedTradingPair {
def fromString(pair: String): UnsupportedTradingPair = {
val lowerPair = pair.toLowerCase
val cut = Math.min(3, pair.length)
UnsupportedTradingPair(
Asset.fromLowerString(lowerPair.substring(0, cut)),
Asset.fromLowerString(lowerPair.substring(cut))
)
}
}
val all: Vector[TradingPair] =
Vector(
BTCUSD,
ETHUSD,
ETHBTC,
BTCUSDT,
ETHUSDT,
LTCUSD,
LTCBTC,
LTCETH,
LTCUSDT,
BCHUSD,
BCHBTC,
BCHETH,
LTCBCH,
BCHUSDT,
XRPUSD,
XRPBTC,
XRPETH,
XRPUSDT,
EOSUSD,
EOSBTC,
EOSETH,
EOSUSDT,
EURUSD,
BTCEUR,
ETHEUR,
BCHEUR,
LTCEUR,
XRPEUR,
EOSEUR
)
def allForExchange(exchange: Exchange): Vector[TradingPair] = {
exchange match {
case Exchange.Binance => BinanceTradingPair.all
case Exchange.Bitfinex => BitfinexTradingPair.all
case Exchange.Bitstamp => BitstampTradingPair.all
case Exchange.Coinbase => CoinbaseTradingPair.all
case Exchange.Gemini => GeminiTradingPair.all
case Exchange.Kraken => KrakenTradingPair.all
case Exchange.Bitmex => BitmexTradingPair.all
case Exchange.KrakenFut => KrakenFutTradingPair.all
}
}
}
/** This trait is to avoid a bunch of copying code in MyExchangeTradingPair companion object */
trait TradingPairObject[PairType >: UnsupportedTradingPair <: TradingPair] {
def all: Vector[PairType]
def fromString(pair: String): PairType = {
val tradingPair: TradingPair = TradingPair.fromString(pair)
fromTradingPair(tradingPair)
}
def fromTradingPair(pair: TradingPair): PairType = {
all
.find(_ == pair)
.getOrElse(UnsupportedTradingPair(pair.getLeft, pair.getRight))
}
}
object BitfinexTradingPair extends TradingPairObject[BitfinexTradingPair] {
def all: Vector[BitfinexTradingPair] =
Vector(
TradingPair.BTCUSD,
TradingPair.ETHUSD,
TradingPair.ETHBTC,
TradingPair.LTCUSD,
TradingPair.LTCBTC,
TradingPair.BCHUSD,
TradingPair.BCHBTC,
TradingPair.XRPUSD,
TradingPair.XRPBTC,
TradingPair.EOSUSD,
TradingPair.EOSBTC,
TradingPair.EOSETH,
TradingPair.BTCEUR,
TradingPair.ETHEUR,
TradingPair.EOSEUR
)
}
object BinanceTradingPair extends TradingPairObject[BinanceTradingPair] {
def all: Vector[BinanceTradingPair] =
Vector(
TradingPair.BTCUSDT,
TradingPair.ETHUSDT,
TradingPair.ETHBTC,
TradingPair.LTCUSDT,
TradingPair.LTCBTC,
TradingPair.LTCETH,
TradingPair.BCHUSDT,
TradingPair.BCHBTC,
TradingPair.XRPUSDT,
TradingPair.XRPBTC,
TradingPair.XRPETH,
TradingPair.EOSUSDT,
TradingPair.EOSBTC,
TradingPair.EOSETH
)
}
object CoinbaseTradingPair extends TradingPairObject[CoinbaseTradingPair] {
def fromCoinbaseString(pair: String): CoinbaseTradingPair = {
val noHyphenPair = pair.filter(_ != '-')
CoinbaseTradingPair.fromString(noHyphenPair)
}
def all: Vector[CoinbaseTradingPair] =
Vector(
TradingPair.BTCUSD,
TradingPair.ETHUSD,
TradingPair.ETHBTC,
TradingPair.LTCUSD,
TradingPair.LTCBTC,
TradingPair.BCHUSD,
TradingPair.BCHBTC,
TradingPair.XRPUSD,
TradingPair.XRPBTC,
TradingPair.EOSUSD,
TradingPair.EOSBTC,
TradingPair.BTCEUR,
TradingPair.ETHEUR,
TradingPair.LTCEUR,
TradingPair.BCHEUR,
TradingPair.XRPEUR,
TradingPair.EOSEUR
)
}
object BitstampTradingPair extends TradingPairObject[BitstampTradingPair] {
def fromBitstampChannelName(channel: String): BitstampTradingPair = {
val pieces = channel.split("_")
// Bitstamp omits a currency pair for btcusd
// (e.g. live_trades = live_trades_btcusd)
if (pieces.length == 2) {
TradingPair.BTCUSD
} else {
fromString(pieces.last)
}
}
def all: Vector[BitstampTradingPair] =
Vector(
TradingPair.BTCUSD,
TradingPair.ETHUSD,
TradingPair.ETHBTC,
TradingPair.LTCUSD,
TradingPair.LTCBTC,
TradingPair.BCHUSD,
TradingPair.BCHBTC,
TradingPair.XRPUSD,
TradingPair.XRPBTC,
TradingPair.EURUSD,
TradingPair.BTCEUR,
TradingPair.ETHEUR,
TradingPair.LTCEUR,
TradingPair.BCHEUR,
TradingPair.XRPEUR
)
}
object GeminiTradingPair extends TradingPairObject[GeminiTradingPair] {
def all: Vector[GeminiTradingPair] =
Vector(
TradingPair.BTCUSD,
TradingPair.ETHUSD,
TradingPair.ETHBTC,
TradingPair.LTCUSD,
TradingPair.LTCBTC,
TradingPair.LTCETH,
TradingPair.BCHUSD,
TradingPair.BCHBTC,
TradingPair.BCHETH,
TradingPair.LTCBCH
)
}
object KrakenTradingPair extends TradingPairObject[KrakenTradingPair] {
def fromKrakenString(pair: String): KrakenTradingPair = {
val pairNoSeparators = pair.filter(_ != '/')
KrakenTradingPair.fromString(pairNoSeparators)
}
def all: Vector[KrakenTradingPair] =
Vector(
TradingPair.BTCUSD,
TradingPair.ETHUSD,
TradingPair.ETHBTC,
TradingPair.LTCUSD,
TradingPair.LTCBTC,
TradingPair.BCHUSD,
TradingPair.BCHBTC,
TradingPair.XRPUSD,
TradingPair.XRPBTC,
TradingPair.EOSUSD,
TradingPair.EOSBTC,
TradingPair.EOSETH,
TradingPair.BTCEUR,
TradingPair.ETHEUR,
TradingPair.LTCEUR,
TradingPair.BCHEUR,
TradingPair.XRPEUR,
TradingPair.EOSEUR
)
}
object KrakenFutTradingPair extends TradingPairObject[KrakenFutTradingPair] {
def all: Vector[KrakenFutTradingPair] =
Vector(
TradingPair.BTCUSD,
TradingPair.ETHUSD,
TradingPair.LTCUSD,
TradingPair.BCHUSD,
TradingPair.XRPUSD,
TradingPair.XRPBTC
)
}
object BitmexTradingPair extends TradingPairObject[BitmexTradingPair] {
def all: Vector[BitmexTradingPair] =
Vector(
TradingPair.BTCUSD,
TradingPair.ETHUSD,
TradingPair.ETHBTC,
TradingPair.LTCBTC,
TradingPair.BCHBTC,
TradingPair.XRPBTC,
TradingPair.EOSBTC
)
}

View file

@ -1,7 +1,8 @@
package org.bitcoins.gui
import org.bitcoins.cli.Config
import org.bitcoins.core.config.BitcoinNetwork
import org.bitcoins.core.config._
import org.bitcoins.crypto.DoubleSha256DigestBE
import org.bitcoins.gui.settings.Themes
import scalafx.beans.property.{DoubleProperty, StringProperty}
@ -34,4 +35,24 @@ object GlobalData {
case Some(rpcPort) =>
Config(debug = debug, rpcPort = rpcPort)
}
lazy val broadcastUrl: String = GlobalData.network match {
case MainNet =>
"https://blockstream.info/api/tx"
case TestNet3 =>
"https://blockstream.info/testnet/api/tx"
case net @ (RegTest | SigNet) => s"Broadcast from your own node on $net"
}
/** Builds a url for the blockstream explorer to view the tx */
def buildTxUrl(txid: DoubleSha256DigestBE): String = {
network match {
case MainNet =>
s"https://blockstream.info/tx/${txid.hex}"
case TestNet3 =>
s"https://blockstream.info/testnet/tx/${txid.hex}"
case net @ (RegTest | SigNet) =>
s"View transaction on your own node on $net"
}
}
}

View file

@ -7,6 +7,7 @@ import org.bitcoins.commons.jsonmodels.dlc.DLCStatus
import org.bitcoins.core.config.MainNet
import org.bitcoins.core.number.{Int32, UInt16, UInt32}
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.crypto.{CryptoUtil, ECPrivateKey, Sha256DigestBE}
import org.bitcoins.gui.dlc.dialog._
import org.bitcoins.gui.{GlobalData, TaskRunner}
@ -15,11 +16,32 @@ import scalafx.collections.ObservableBuffer
import scalafx.scene.control.TextArea
import scalafx.stage.Window
import scala.util.{Failure, Success}
import scala.util.{Failure, Success, Try}
class DLCPaneModel(resultArea: TextArea, oracleInfoArea: TextArea) {
var taskRunner: TaskRunner = _
lazy val txPrintFunc: String => String = str => {
// See if it was an error or not
Try(Transaction.fromHex(str)) match {
case Failure(_) =>
// if it was print the error
str
case Success(tx) =>
s"""|TxId: ${tx.txIdBE.hex}
|
|url: ${GlobalData.buildTxUrl(tx.txIdBE)}
|
|If the tx doesn't show up after a few minutes at this url you may need to manually
|broadcast the tx with the full hex below
|
|Link to broadcast: ${GlobalData.broadcastUrl}
|
|Transaction: ${tx.hex}
""".stripMargin
}
}
// Sadly, it is a Java "pattern" to pass null into
// constructors to signal that you want some default
val parentWindow: ObjectProperty[Window] =
@ -60,7 +82,8 @@ class DLCPaneModel(resultArea: TextArea, oracleInfoArea: TextArea) {
def printDLCDialogResult[T <: CliCommand](
caption: String,
dialog: DLCDialog[T]): Unit = {
dialog: DLCDialog[T],
postProcessStr: String => String = str => str): Unit = {
val result = dialog.showAndWait(parentWindow.value)
result match {
@ -69,7 +92,8 @@ class DLCPaneModel(resultArea: TextArea, oracleInfoArea: TextArea) {
caption = caption,
op = {
ConsoleCli.exec(command, GlobalData.consoleCliConfig) match {
case Success(commandReturn) => resultArea.text = commandReturn
case Success(commandReturn) =>
resultArea.text = postProcessStr(commandReturn)
case Failure(err) =>
err.printStackTrace()
resultArea.text = s"Error executing command:\n${err.getMessage}"
@ -218,7 +242,9 @@ class DLCPaneModel(resultArea: TextArea, oracleInfoArea: TextArea) {
}
def onGetFunding(): Unit = {
printDLCDialogResult("GetDLCFundingTx", new GetFundingDLCDialog)
printDLCDialogResult("GetDLCFundingTx",
new GetFundingDLCDialog,
txPrintFunc)
}
def onExecute(): Unit = {

View file

@ -1,10 +1,11 @@
package org.bitcoins.server
import java.time.{ZoneId, ZonedDateTime}
import akka.http.scaladsl.model.ContentTypes._
import akka.http.scaladsl.server.ValidationRejection
import akka.http.scaladsl.testkit.ScalatestRouteTest
import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage._
import org.bitcoins.commons.jsonmodels.dlc._
import org.bitcoins.core.Core
import org.bitcoins.core.api.chain.ChainApi
import org.bitcoins.core.api.wallet.db._
@ -20,18 +21,22 @@ import org.bitcoins.core.protocol.BlockStamp.{
BlockTime,
InvalidBlockStamp
}
import org.bitcoins.core.protocol.script.EmptyScriptWitness
import org.bitcoins.core.protocol.script.{EmptyScriptWitness, P2WPKHWitnessV0}
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp, P2PKHAddress}
import org.bitcoins.core.protocol.{
Bech32Address,
BitcoinAddress,
BlockStamp,
P2PKHAddress
}
import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature
import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.core.wallet.fee.{FeeUnit, SatoshisPerVirtualByte}
import org.bitcoins.core.wallet.utxo._
import org.bitcoins.crypto.{
DoubleSha256DigestBE,
ECPublicKey,
Sha256Hash160Digest
}
import org.bitcoins.crypto._
import org.bitcoins.dlc.wallet.models._
import org.bitcoins.node.Node
import org.bitcoins.server.BitcoinSAppConfig.implicitToWalletConf
import org.bitcoins.testkit.BitcoinSTestAppConfig
@ -44,12 +49,15 @@ import ujson._
import scala.collection.mutable
import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.duration.DurationInt
class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
implicit val conf: BitcoinSAppConfig =
BitcoinSTestAppConfig.getSpvTestConfig()
implicit val timeout: RouteTestTimeout = RouteTestTimeout(5.seconds)
// the genesis address
val testAddressStr = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
val testAddress = BitcoinAddress.fromString(testAddressStr)
@ -732,6 +740,330 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
}
}
val contractInfoDigests =
Vector("ffbbcde836cee437a2fa4ef7db1ea3d79ca71c0c821d2a197dda51bc6534f562",
"e770f42c578084a4a096ce1085f7fe508f8d908d2c5e6e304b2c3eab9bc973ea")
val contractInfo = SingleNonceContractInfo.fromStringVec(
Vector(
(contractInfoDigests.head, Satoshis(5)),
(contractInfoDigests.last, Satoshis(4))
))
val contractInfoTLV = contractInfo.toTLV
val contractMaturity = 1580323752
val contractTimeout = 1581323752
val dummyKey = ECPrivateKey.freshPrivateKey
val dummyPubKey = dummyKey.publicKey
val dummySig = dummyKey.sign(Sha256DigestBE.empty)
val dummyPartialSig = PartialSignature(dummyPubKey, dummySig)
val dummyScriptWitness: P2WPKHWitnessV0 = {
P2WPKHWitnessV0(dummyPartialSig.pubKey, dummyPartialSig.signature)
}
val dummyOracleSig = SchnorrDigitalSignature(
"65ace55b5d073cc7a1c783fa8c254692c421270fa988247e3c87627ffe804ed06c20bf779da91f82da3311b1d9e0a3a513409a15c66f25201280751177dad24c")
lazy val winStr: String = "WIN"
lazy val loseStr: String = "LOSE"
lazy val dummyOutcomeSigs: Vector[(EnumOutcome, ECAdaptorSignature)] =
Vector(EnumOutcome(winStr) -> ECAdaptorSignature.dummy,
EnumOutcome(loseStr) -> ECAdaptorSignature.dummy)
val dummyAddress = "bc1quq29mutxkgxmjfdr7ayj3zd9ad0ld5mrhh89l2"
val dummyDLCKeys =
DLCPublicKeys(dummyPubKey, BitcoinAddress(dummyAddress))
val paramHash = Sha256DigestBE(
"de462f212d95ca4cf5db54eee08f14be0ee934e9ecfc6e9b7014ecfa51ba7b66")
val contractId = ByteVector.fromValidHex(
"4c6eb53573aae186dbb1a93274cc00c795473d7cfe2cb69e7d185ee28a39b919")
val wtx: WitnessTransaction = WitnessTransaction(
"02000000000101a2619b5d58b209439c937e563018efcf174063ca011e4f177a5b14e5ba76211c0100000017160014614e9b96cbc7477eda98f0936385ded6b636f74efeffffff024e3f57c4000000001600147cf00288c2c1b3c5cdf275db532a1c15c514bb2fae1112000000000016001440efb02597b9e9d9bc968f12cec3347e2e264c570247304402205768c2ac8178539fd44721e2a7541bedd6b55654f095143514624203c133f7e8022060d51f33fc2b5c1f51f26c7f703de21be6246dbb5fb7e1c6919aae6d442610c6012102b99a63f166ef53ca67a5c55ae969e80c33456e07189f8457e3438f000be42c19307d1900")
val fundingInput: DLCFundingInputP2WPKHV0 =
DLCFundingInputP2WPKHV0(wtx, UInt32.zero, UInt32.zero)
val announcementTLV = OracleAnnouncementV0TLV(
"fdd8249426cbca0e5366f6688fd837a83d3fe34d103f8d88a3bdc40d648e47e43c6f70a7e65fb85d5de46779604b541ebe74d06b3c316446a6f97fcd23d6de8e1d7b9451f74577f8cab8361962ce642a8da4b1f48f8813ed243203cb50ebba45c789abf0fdd8223000015b0fb6b85a9badee0a826349822db7412f79c71efdd903eac94a10ee10d6e4425fe3da00fdd80604000101620161")
val oracleInfo = SingleNonceOracleInfo(announcementTLV.publicKey,
announcementTLV.eventTLV.nonces.head)
val offer = DLCOffer(
OracleAndContractInfo(oracleInfo, contractInfo),
dummyDLCKeys,
Satoshis(2500),
Vector(fundingInput, fundingInput),
Bech32Address.fromString(dummyAddress),
SatoshisPerVirtualByte.one,
DLCTimeouts(BlockStamp(contractMaturity), BlockStamp(contractTimeout))
)
"create a dlc offer" in {
(mockWalletApi
.createDLCOffer(_: OracleInfo,
_: ContractInfoTLV,
_: Satoshis,
_: Option[FeeUnit],
_: UInt32,
_: UInt32))
.expects(
oracleInfo,
contractInfoTLV,
Satoshis(2500),
Some(SatoshisPerVirtualByte(Satoshis.one)),
UInt32(contractMaturity),
UInt32(contractTimeout)
)
.returning(Future.successful(offer))
val route = walletRoutes.handleCommand(
ServerCommand(
"createdlcoffer",
Arr(
Str(announcementTLV.hex),
Str(contractInfoTLV.hex),
Num(2500),
Num(1),
Num(contractMaturity),
Num(contractTimeout)
)
))
Post() ~> route ~> check {
assert(contentType == `application/json`)
assert(responseAs[String] == s"""{"result":"${LnMessage(
offer.toTLV).hex}","error":null}""")
}
}
val accept = DLCAccept(
Satoshis(1000),
dummyDLCKeys,
Vector(fundingInput),
Bech32Address
.fromString(dummyAddress),
CETSignatures(dummyOutcomeSigs, dummyPartialSig),
Sha256Digest.empty
)
"accept a dlc offer" in {
(mockWalletApi
.acceptDLCOffer(_: DLCOfferTLV))
.expects(offer.toTLV)
.returning(Future.successful(accept))
val route = walletRoutes.handleCommand(
ServerCommand("acceptdlcoffer", Arr(Str(LnMessage(offer.toTLV).hex))))
Post() ~> route ~> check {
assert(contentType == `application/json`)
assert(responseAs[String] == s"""{"result":"${LnMessage(
accept.toTLV).hex}","error":null}""")
}
}
val sign = DLCSign(
CETSignatures(dummyOutcomeSigs, dummyPartialSig),
FundingSignatures(Vector((EmptyTransactionOutPoint, dummyScriptWitness))),
paramHash.bytes
)
"sign a dlc" in {
(mockWalletApi
.signDLC(_: DLCAcceptTLV))
.expects(accept.toTLV)
.returning(Future.successful(sign))
val route = walletRoutes.handleCommand(
ServerCommand("signdlc", Arr(Str(LnMessage(accept.toTLV).hex))))
Post() ~> route ~> check {
assert(contentType == `application/json`)
assert(responseAs[String] == s"""{"result":"${LnMessage(
sign.toTLV).hex}","error":null}""")
}
}
"add dlc sigs" in {
(mockWalletApi
.addDLCSigs(_: DLCSignTLV))
.expects(sign.toTLV)
.returning(Future.successful(DLCDb(
paramHash = paramHash,
tempContractId = Sha256Digest.empty,
contractIdOpt = Some(contractId),
state = DLCState.Signed,
isInitiator = false,
account = HDAccount(HDCoin(HDPurpose(89), HDCoinType.Testnet), 0),
keyIndex = 0,
oracleSigsOpt = None,
fundingOutPointOpt = None,
fundingTxIdOpt = None,
closingTxIdOpt = None,
outcomeOpt = None
)))
val route = walletRoutes.handleCommand(
ServerCommand("adddlcsigs", Arr(Str(LnMessage(sign.toTLV).hex))))
Post() ~> route ~> check {
assert(contentType == `application/json`)
assert(responseAs[
String] == s"""{"result":"Successfully added sigs to DLC ${contractId.toHex}","error":null}""")
}
}
"get dlc funding tx" in {
(mockWalletApi
.getDLCFundingTx(_: ByteVector))
.expects(contractId)
.returning(Future.successful(EmptyTransaction))
val route = walletRoutes.handleCommand(
ServerCommand("getdlcfundingtx", Arr(Str(contractId.toHex))))
Post() ~> route ~> check {
assert(contentType == `application/json`)
assert(
responseAs[
String] == s"""{"result":"${EmptyTransaction.hex}","error":null}""")
}
}
"broadcast dlc funding tx" in {
(mockWalletApi
.broadcastDLCFundingTx(_: ByteVector))
.expects(contractId)
.returning(Future.successful(EmptyTransaction))
val route = walletRoutes.handleCommand(
ServerCommand("broadcastdlcfundingtx", Arr(Str(contractId.toHex))))
Post() ~> route ~> check {
assert(contentType == `application/json`)
assert(
responseAs[
String] == s"""{"result":"${EmptyTransaction.hex}","error":null}""")
}
}
"execute a dlc" in {
(mockWalletApi
.executeDLC(_: ByteVector, _: Vector[SchnorrDigitalSignature]))
.expects(contractId, Vector(dummyOracleSig))
.returning(Future.successful(EmptyTransaction))
val route = walletRoutes.handleCommand(
ServerCommand(
"executedlc",
Arr(Str(contractId.toHex), Arr(Str(dummyOracleSig.hex)), Bool(true))))
Post() ~> route ~> check {
assert(contentType == `application/json`)
assert(
responseAs[
String] == s"""{"result":"${EmptyTransaction.hex}","error":null}""")
}
}
"execute a dlc with multiple sigs" in {
(mockWalletApi
.executeDLC(_: ByteVector, _: Vector[SchnorrDigitalSignature]))
.expects(contractId, Vector(dummyOracleSig, dummyOracleSig))
.returning(Future.successful(EmptyTransaction))
val route = walletRoutes.handleCommand(
ServerCommand("executedlc",
Arr(Str(contractId.toHex),
Arr(Str(dummyOracleSig.hex), Str(dummyOracleSig.hex)),
Bool(true))))
Post() ~> route ~> check {
assert(contentType == `application/json`)
assert(
responseAs[
String] == s"""{"result":"${EmptyTransaction.hex}","error":null}""")
}
}
"execute a dlc with broadcast" in {
(mockWalletApi
.executeDLC(_: ByteVector, _: Vector[SchnorrDigitalSignature]))
.expects(contractId, Vector(dummyOracleSig))
.returning(Future.successful(EmptyTransaction))
(mockWalletApi.broadcastTransaction _)
.expects(EmptyTransaction)
.returning(FutureUtil.unit)
.anyNumberOfTimes()
val route = walletRoutes.handleCommand(
ServerCommand("executedlc",
Arr(Str(contractId.toHex),
Arr(Str(dummyOracleSig.hex)),
Bool(false))))
Post() ~> route ~> check {
assert(contentType == `application/json`)
assert(
responseAs[String] == s"""{"result":"${EmptyTransaction.txIdBE.hex}","error":null}""")
}
}
"execute a dlc refund" in {
(mockWalletApi
.executeDLCRefund(_: ByteVector))
.expects(contractId)
.returning(Future.successful(EmptyTransaction))
val route = walletRoutes.handleCommand(
ServerCommand("executedlcrefund",
Arr(Str(contractId.toHex), Bool(true))))
Post() ~> route ~> check {
assert(contentType == `application/json`)
assert(
responseAs[
String] == s"""{"result":"${EmptyTransaction.hex}","error":null}""")
}
}
"execute a dlc refund with broadcast" in {
(mockWalletApi
.executeDLCRefund(_: ByteVector))
.expects(contractId)
.returning(Future.successful(EmptyTransaction))
(mockWalletApi.broadcastTransaction _)
.expects(EmptyTransaction)
.returning(FutureUtil.unit)
.anyNumberOfTimes()
val route = walletRoutes.handleCommand(
ServerCommand("executedlcrefund",
Arr(Str(contractId.toHex), Bool(false))))
Post() ~> route ~> check {
assert(contentType == `application/json`)
assert(
responseAs[String] == s"""{"result":"${EmptyTransaction.txIdBE.hex}","error":null}""")
}
}
"send to an address" in {
// positive cases
@ -748,7 +1080,7 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
val route = walletRoutes.handleCommand(
ServerCommand("sendtoaddress",
Arr(Str(testAddressStr), Num(100), Num(4), Bool(true))))
Arr(Str(testAddressStr), Num(100), Num(4), Bool(false))))
Post() ~> route ~> check {
assert(contentType == `application/json`)

View file

@ -1,9 +1,9 @@
package org.bitcoins.wallet
import org.bitcoins.core.api.wallet.AnyHDWalletApi
import org.bitcoins.core.api.wallet.db.AccountDb
import org.bitcoins.core.hd.AddressType
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.dlc.wallet.AnyDLCHDWalletApi
import scala.concurrent.Future
@ -11,7 +11,7 @@ import scala.concurrent.Future
* ScalaMock cannot stub traits with protected methods,
* so we need to stub them manually.
*/
abstract class MockWalletApi extends AnyHDWalletApi {
abstract class MockWalletApi extends AnyDLCHDWalletApi {
override def getNewChangeAddress(account: AccountDb): Future[BitcoinAddress] =
stub

View file

@ -6,6 +6,7 @@ import com.typesafe.config.{Config, ConfigFactory}
import org.bitcoins.chain.config.ChainAppConfig
import org.bitcoins.core.util.StartStopAsync
import org.bitcoins.db.AppConfig
import org.bitcoins.dlc.wallet.DLCAppConfig
import org.bitcoins.keymanager.config.KeyManagerAppConfig
import org.bitcoins.node.config.NodeAppConfig
import org.bitcoins.wallet.config.WalletAppConfig
@ -29,6 +30,7 @@ case class BitcoinSAppConfig(
lazy val walletConf: WalletAppConfig = WalletAppConfig(directory, confs: _*)
lazy val nodeConf: NodeAppConfig = NodeAppConfig(directory, confs: _*)
lazy val chainConf: ChainAppConfig = ChainAppConfig(directory, confs: _*)
lazy val dlcConf: DLCAppConfig = DLCAppConfig(directory, confs: _*)
lazy val kmConf: KeyManagerAppConfig =
KeyManagerAppConfig(directory, confs: _*)
@ -42,7 +44,8 @@ case class BitcoinSAppConfig(
walletConf.start(),
nodeConf.start(),
chainConf.start(),
bitcoindRpcConf.start())
bitcoindRpcConf.start(),
dlcConf.start())
Future.sequence(futures).map(_ => ())
}

View file

@ -12,6 +12,7 @@ import org.bitcoins.core.api.feeprovider.FeeRateApi
import org.bitcoins.core.api.node.NodeApi
import org.bitcoins.core.util.{FutureUtil, NetworkUtil}
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.dlc.wallet._
import org.bitcoins.feeprovider.FeeProviderName._
import org.bitcoins.feeprovider.MempoolSpaceTarget.HourFeeTarget
import org.bitcoins.feeprovider._
@ -36,6 +37,7 @@ class BitcoinSServerMain(override val args: Array[String])
implicit val walletConf: WalletAppConfig = conf.walletConf
implicit val nodeConf: NodeAppConfig = conf.nodeConf
implicit val chainConf: ChainAppConfig = conf.chainConf
implicit val dlcConf: DLCAppConfig = conf.dlcConf
implicit val bitcoindRpcConf: BitcoindRpcAppConfig = conf.bitcoindRpcConf
def startBitcoinSBackend(): Future[Unit] = {
@ -69,7 +71,7 @@ class BitcoinSServerMain(override val args: Array[String])
chainApi <- chainApiF
_ = logger.info("Initialized chain api")
feeProvider = getFeeProviderOrElse(MempoolSpaceProvider(HourFeeTarget))
wallet <- walletConf.createHDWallet(node, chainApi, feeProvider)
wallet <- dlcConf.createDLCWallet(node, chainApi, feeProvider)
callbacks <- createCallbacks(wallet)
_ = nodeConf.addCallbacks(callbacks)
} yield {
@ -129,10 +131,10 @@ class BitcoinSServerMain(override val args: Array[String])
_ <- bitcoindRpcConf.start()
_ = logger.info("Creating wallet")
feeProvider = getFeeProviderOrElse(bitcoind)
tmpWallet <- walletConf.createHDWallet(nodeApi = bitcoind,
chainQueryApi = bitcoind,
feeRateApi = feeProvider)
wallet = BitcoindRpcBackendUtil.createWalletWithBitcoindCallbacks(
tmpWallet <- dlcConf.createDLCWallet(nodeApi = bitcoind,
chainQueryApi = bitcoind,
feeRateApi = feeProvider)
wallet = BitcoindRpcBackendUtil.createDLCWalletWithBitcoindCallbacks(
bitcoind,
tmpWallet)
_ = logger.info("Starting wallet")
@ -265,7 +267,7 @@ class BitcoinSServerMain(override val args: Array[String])
private def startHttpServer(
nodeApi: NodeApi,
chainApi: ChainApi,
wallet: Wallet,
wallet: DLCWallet,
rpcPortOpt: Option[Int])(implicit
system: ActorSystem,
conf: BitcoinSAppConfig): Future[Http.ServerBinding] = {

View file

@ -9,6 +9,7 @@ import org.bitcoins.core.protocol.blockchain.Block
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.util.{BitcoinSLogger, FutureUtil}
import org.bitcoins.crypto.DoubleSha256Digest
import org.bitcoins.dlc.wallet.DLCWallet
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.wallet.Wallet
import org.bitcoins.zmq.ZMQSubscriber
@ -107,6 +108,32 @@ object BitcoindRpcBackendUtil extends BitcoinSLogger {
pairedWallet
}
def createDLCWalletWithBitcoindCallbacks(
bitcoind: BitcoindRpcClient,
wallet: DLCWallet)(implicit ec: ExecutionContext): DLCWallet = {
// Kill the old wallet
wallet.stopWalletThread()
// We need to create a promise so we can inject the wallet with the callback
// after we have created it into SyncUtil.getNodeApiWalletCallback
// so we don't lose the internal state of the wallet
val walletCallbackP = Promise[Wallet]()
val pairedWallet = DLCWallet(
keyManager = wallet.keyManager,
nodeApi =
BitcoindRpcBackendUtil.getNodeApiWalletCallback(bitcoind,
walletCallbackP.future),
chainQueryApi = bitcoind,
feeRateApi = wallet.feeRateApi,
creationTime = wallet.keyManager.creationTime
)(wallet.walletConfig, wallet.dlcConfig, wallet.ec)
walletCallbackP.success(pairedWallet)
pairedWallet
}
def createZMQWalletCallbacks(wallet: Wallet)(implicit
bitcoindRpcConf: BitcoindRpcAppConfig): ZMQSubscriber = {
require(bitcoindRpcConf.zmqPortOpt.isDefined,

View file

@ -1,10 +1,13 @@
package org.bitcoins.server
import java.io.File
import java.nio.file.Path
import java.time.Instant
import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.LockUnspentOutputParameter
import org.bitcoins.core.api.wallet.CoinSelectionAlgo
import org.bitcoins.core.currency.{Bitcoins, Satoshis}
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.BlockStamp.BlockHeight
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutPoint}
@ -12,7 +15,8 @@ import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo.AddressLabelTag
import org.bitcoins.crypto.AesPassword
import org.bitcoins.crypto._
import scodec.bits.ByteVector
import ujson._
import upickle.default._
@ -477,6 +481,244 @@ object SendToAddress extends ServerJsonModels {
}
case class GetDLC(paramHash: Sha256DigestBE)
object GetDLC extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[GetDLC] = {
jsArr.arr.toList match {
case paramHashJs :: Nil =>
Try {
val paramHash = Sha256DigestBE(paramHashJs.str)
GetDLC(paramHash)
}
case Nil =>
Failure(new IllegalArgumentException("Missing paramHash argument"))
case other =>
Failure(
new IllegalArgumentException(
s"Bad number of arguments: ${other.length}. Expected: 1"))
}
}
}
case class CreateDLCOffer(
announcement: OracleAnnouncementTLV,
contractInfoTLV: ContractInfoTLV,
collateral: Satoshis,
feeRateOpt: Option[SatoshisPerVirtualByte],
locktime: UInt32,
refundLocktime: UInt32)
object CreateDLCOffer extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[CreateDLCOffer] = {
jsArr.arr.toList match {
case announcementJs :: contractInfoJs :: collateralJs :: feeRateOptJs :: locktimeJs :: refundLTJs :: Nil =>
Try {
val announcement = jsToOracleAnnouncementTLV(announcementJs)
val contractInfoTLV = jsToContractInfoTLV(contractInfoJs)
val collateral = jsToSatoshis(collateralJs)
val feeRate = jsToSatoshisPerVirtualByteOpt(feeRateOptJs)
val locktime = jsToUInt32(locktimeJs)
val refundLT = jsToUInt32(refundLTJs)
CreateDLCOffer(announcement,
contractInfoTLV,
collateral,
feeRate,
locktime,
refundLT)
}
case other =>
Failure(
new IllegalArgumentException(
s"Bad number of arguments: ${other.length}. Expected: 6"))
}
}
}
case class AcceptDLCOffer(offer: LnMessage[DLCOfferTLV])
object AcceptDLCOffer extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[AcceptDLCOffer] = {
jsArr.arr.toList match {
case offerJs :: Nil =>
Try {
val offer = LnMessageFactory(DLCOfferTLV).fromHex(offerJs.str)
AcceptDLCOffer(offer)
}
case Nil =>
Failure(new IllegalArgumentException("Missing offer argument"))
case other =>
Failure(
new IllegalArgumentException(
s"Bad number of arguments: ${other.length}. Expected: 1"))
}
}
}
case class SignDLC(accept: LnMessage[DLCAcceptTLV])
object SignDLC extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[SignDLC] = {
jsArr.arr.toList match {
case acceptJs :: Nil =>
Try {
val accept = LnMessageFactory(DLCAcceptTLV).fromHex(acceptJs.str)
SignDLC(accept)
}
case Nil =>
Failure(new IllegalArgumentException("Missing accept argument"))
case other =>
Failure(
new IllegalArgumentException(
s"Bad number of arguments: ${other.length}. Expected: 1"))
}
}
}
case class AddDLCSigs(sigs: LnMessage[DLCSignTLV])
object AddDLCSigs extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[AddDLCSigs] = {
jsArr.arr.toList match {
case sigsJs :: Nil =>
Try {
val sigs = LnMessageFactory(DLCSignTLV).fromHex(sigsJs.str)
AddDLCSigs(sigs)
}
case Nil =>
Failure(new IllegalArgumentException("Missing sigs argument"))
case other =>
Failure(
new IllegalArgumentException(
s"Bad number of arguments: ${other.length}. Expected: 1"))
}
}
}
case class DLCDataFromFile(path: Path)
object DLCDataFromFile extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[DLCDataFromFile] = {
jsArr.arr.toList match {
case pathJs :: Nil =>
Try {
val path = new File(pathJs.str).toPath
DLCDataFromFile(path)
}
case Nil =>
Failure(new IllegalArgumentException("Missing path argument"))
case other =>
Failure(
new IllegalArgumentException(
s"Bad number of arguments: ${other.length}. Expected: 1"))
}
}
}
case class GetDLCFundingTx(contractId: ByteVector)
object GetDLCFundingTx extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[GetDLCFundingTx] = {
jsArr.arr.toList match {
case contractIdJs :: Nil =>
Try {
val contractId = ByteVector.fromValidHex(contractIdJs.str)
GetDLCFundingTx(contractId)
}
case Nil =>
Failure(new IllegalArgumentException("Missing contractId argument"))
case other =>
Failure(
new IllegalArgumentException(
s"Bad number of arguments: ${other.length}. Expected: 1"))
}
}
}
case class BroadcastDLCFundingTx(contractId: ByteVector)
object BroadcastDLCFundingTx extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[BroadcastDLCFundingTx] = {
jsArr.arr.toList match {
case contractIdJs :: Nil =>
Try {
val contractId = ByteVector.fromValidHex(contractIdJs.str)
BroadcastDLCFundingTx(contractId)
}
case Nil =>
Failure(new IllegalArgumentException("Missing contractId argument"))
case other =>
Failure(
new IllegalArgumentException(
s"Bad number of arguments: ${other.length}. Expected: 1"))
}
}
}
case class ExecuteDLC(
contractId: ByteVector,
oracleSigs: Vector[SchnorrDigitalSignature],
noBroadcast: Boolean)
extends Broadcastable
object ExecuteDLC extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[ExecuteDLC] = {
jsArr.arr.toList match {
case contractIdJs :: oracleSigsJs :: noBroadcastJs :: Nil =>
Try {
val contractId = ByteVector.fromValidHex(contractIdJs.str)
val oracleSigs = jsToSchnorrDigitalSignatureVec(oracleSigsJs)
val noBroadcast = noBroadcastJs.bool
ExecuteDLC(contractId, oracleSigs, noBroadcast)
}
case Nil =>
Failure(
new IllegalArgumentException(
"Missing contractId, oracleSig, and noBroadcast arguments"))
case other =>
Failure(
new IllegalArgumentException(
s"Bad number of arguments: ${other.length}. Expected: 3"))
}
}
}
case class ExecuteDLCRefund(contractId: ByteVector, noBroadcast: Boolean)
extends Broadcastable
object ExecuteDLCRefund extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[ExecuteDLCRefund] = {
jsArr.arr.toList match {
case contractIdJs :: noBroadcastJs :: Nil =>
Try {
val contractId = ByteVector.fromValidHex(contractIdJs.str)
val noBroadcast = noBroadcastJs.bool
ExecuteDLCRefund(contractId, noBroadcast)
}
case Nil =>
Failure(new IllegalArgumentException("Missing contractId argument"))
case other =>
Failure(
new IllegalArgumentException(
s"Bad number of arguments: ${other.length}. Expected: 2"))
}
}
}
case class SendFromOutpoints(
outPoints: Vector[TransactionOutPoint],
address: BitcoinAddress,
@ -814,6 +1056,54 @@ object GetEvent extends ServerJsonModels {
trait ServerJsonModels {
def jsToOracleAnnouncementTLV(js: Value): OracleAnnouncementTLV =
js match {
case str: Str =>
OracleAnnouncementTLV(str.value)
case _: Value =>
throw Value.InvalidData(
js,
"Expected an OracleAnnouncementTLV as a hex string")
}
def jsToContractInfoTLV(js: Value): ContractInfoTLV =
js match {
case str: Str =>
ContractInfoTLV(str.value)
case _: Value =>
throw Value.InvalidData(js, "Expected a ContractInfo as a hex string")
}
def jsToSatoshisPerVirtualByteOpt(js: Value): Option[SatoshisPerVirtualByte] =
nullToOpt(js).map {
case str: Str =>
SatoshisPerVirtualByte(Satoshis(str.value))
case num: Num =>
SatoshisPerVirtualByte(Satoshis(num.value.toLong))
case _: Value =>
throw Value.InvalidData(js, "Expected a fee rate in sats/vbyte")
}
def jsToUInt32(js: Value): UInt32 =
js match {
case str: Str =>
UInt32(BigInt(str.value))
case num: Num =>
UInt32(num.value.toLong)
case _: Value =>
throw Value.InvalidData(js, "Expected a UInt32")
}
def jsToSatoshis(js: Value): Satoshis =
js match {
case str: Str =>
Satoshis(BigInt(str.value))
case num: Num =>
Satoshis(num.value.toLong)
case _: Value =>
throw Value.InvalidData(js, "Expected value in Satoshis")
}
def jsToBitcoinAddress(js: Value): BitcoinAddress = {
try {
BitcoinAddress.fromString(js.str)
@ -859,4 +1149,20 @@ trait ServerJsonModels {
case Arr(arr) if arr.size == 1 => Some(arr.head)
case _: Value => Some(value)
}
def jsToSchnorrDigitalSignature(js: Value): SchnorrDigitalSignature =
js match {
case str: Str =>
SchnorrDigitalSignature(str.value)
case _: Value =>
throw Value.InvalidData(
js,
"Expected a SchnorrDigitalSignature as a hex string")
}
def jsToSchnorrDigitalSignatureVec(
js: Value): Vector[SchnorrDigitalSignature] = {
js.arr.foldLeft(Vector.empty[SchnorrDigitalSignature])((vec, sig) =>
vec :+ jsToSchnorrDigitalSignature(sig))
}
}

View file

@ -3,21 +3,25 @@ package org.bitcoins.server
import akka.actor.ActorSystem
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.stream.Materializer
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.OracleInfo
import org.bitcoins.commons.serializers.Picklers._
import org.bitcoins.core.api.wallet.AnyHDWalletApi
import org.bitcoins.core.api.wallet.db.SpendingInfoDb
import org.bitcoins.core.currency._
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.wallet.utxo.{AddressLabelTagType, TxoState}
import org.bitcoins.crypto.NetworkElement
import org.bitcoins.dlc.wallet.AnyDLCHDWalletApi
import org.bitcoins.keymanager.WalletStorage
import org.bitcoins.wallet.config.WalletAppConfig
import ujson._
import java.nio.file.Files
import scala.concurrent.Future
import scala.util.{Failure, Success}
case class WalletRoutes(wallet: AnyHDWalletApi)(implicit
case class WalletRoutes(wallet: AnyDLCHDWalletApi)(implicit
system: ActorSystem,
walletConf: WalletAppConfig)
extends ServerRoute {
@ -30,6 +34,9 @@ case class WalletRoutes(wallet: AnyHDWalletApi)(implicit
)
}
implicit val materializer: Materializer =
Materializer.createMaterializer(system)
private def handleBroadcastable(
tx: Transaction,
noBroadcast: Boolean): Future[NetworkElement] = {
@ -208,6 +215,210 @@ case class WalletRoutes(wallet: AnyHDWalletApi)(implicit
}
}
case ServerCommand("getdlcs", _) =>
complete {
wallet.listDLCs().map { dlcs =>
Server.httpSuccess(dlcs.map(_.toJson))
}
}
case ServerCommand("getdlc", arr) =>
GetDLC.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(GetDLC(paramHash)) =>
complete {
wallet.findDLC(paramHash).map {
case None => Server.httpSuccess(ujson.Null)
case Some(dlc) =>
Server.httpSuccess(dlc.toJson)
}
}
}
case ServerCommand("createdlcoffer", arr) =>
CreateDLCOffer.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(
CreateDLCOffer(announcement,
contractInfo,
collateral,
feeRateOpt,
locktime,
refundLT)) =>
complete {
if (!announcement.validateSignature) {
throw new RuntimeException(
s"Received Oracle announcement with invalid signature! ${announcement.hex}")
}
val oracleInfo = OracleInfo.fromOracleAnnouncement(announcement)
wallet
.createDLCOffer(oracleInfo,
contractInfo,
collateral,
feeRateOpt,
locktime,
refundLT)
.map { offer =>
Server.httpSuccess(offer.toMessage.hex)
}
}
}
case ServerCommand("acceptdlcoffer", arr) =>
AcceptDLCOffer.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(AcceptDLCOffer(offer)) =>
complete {
wallet
.acceptDLCOffer(offer.tlv)
.map { accept =>
Server.httpSuccess(accept.toMessage.hex)
}
}
}
case ServerCommand("acceptdlcofferfromfile", arr) =>
DLCDataFromFile.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(DLCDataFromFile(path)) =>
complete {
val hex = Files.readAllLines(path).get(0)
val offerMessage = LnMessageFactory(DLCOfferTLV).fromHex(hex)
wallet
.acceptDLCOffer(offerMessage.tlv)
.map { accept =>
Server.httpSuccess(accept.toMessage.hex)
}
}
}
case ServerCommand("signdlc", arr) =>
SignDLC.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(SignDLC(accept)) =>
complete {
wallet
.signDLC(accept.tlv)
.map { sign =>
Server.httpSuccess(sign.toMessage.hex)
}
}
}
case ServerCommand("signdlcfromfile", arr) =>
DLCDataFromFile.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(DLCDataFromFile(path)) =>
complete {
val hex = Files.readAllLines(path).get(0)
val acceptMessage = LnMessageFactory(DLCAcceptTLV).fromHex(hex)
wallet
.signDLC(acceptMessage.tlv)
.map { sign =>
Server.httpSuccess(sign.toMessage.hex)
}
}
}
case ServerCommand("adddlcsigs", arr) =>
AddDLCSigs.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(AddDLCSigs(sigs)) =>
complete {
wallet.addDLCSigs(sigs.tlv).map { db =>
Server.httpSuccess(
s"Successfully added sigs to DLC ${db.contractIdOpt.get.toHex}")
}
}
}
case ServerCommand("adddlcsigsfromfile", arr) =>
DLCDataFromFile.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(DLCDataFromFile(path)) =>
complete {
val hex = Files.readAllLines(path).get(0)
val signMessage = LnMessageFactory(DLCSignTLV).fromHex(hex)
wallet.addDLCSigs(signMessage.tlv).map { db =>
Server.httpSuccess(
s"Successfully added sigs to DLC ${db.contractIdOpt.get.toHex}")
}
}
}
case ServerCommand("getdlcfundingtx", arr) =>
GetDLCFundingTx.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(GetDLCFundingTx(contractId)) =>
complete {
wallet.getDLCFundingTx(contractId).map { tx =>
Server.httpSuccess(tx.hex)
}
}
}
case ServerCommand("broadcastdlcfundingtx", arr) =>
BroadcastDLCFundingTx.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(BroadcastDLCFundingTx(contractId)) =>
complete {
wallet.broadcastDLCFundingTx(contractId).map { tx =>
Server.httpSuccess(tx.hex)
}
}
}
case ServerCommand("executedlc", arr) =>
ExecuteDLC.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(ExecuteDLC(contractId, oracleSigs, noBroadcast)) =>
complete {
for {
tx <- wallet.executeDLC(contractId, oracleSigs)
retStr <- handleBroadcastable(tx, noBroadcast)
} yield {
Server.httpSuccess(retStr.hex)
}
}
}
case ServerCommand("executedlcrefund", arr) =>
ExecuteDLCRefund.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(ExecuteDLCRefund(contractId, noBroadcast)) =>
complete {
for {
tx <- wallet.executeDLCRefund(contractId)
retStr <- handleBroadcastable(tx, noBroadcast)
} yield {
Server.httpSuccess(retStr.hex)
}
}
}
case ServerCommand("sendtoaddress", arr) =>
// TODO create custom directive for this?
SendToAddress.fromJsArr(arr) match {
@ -223,9 +434,9 @@ case class WalletRoutes(wallet: AnyHDWalletApi)(implicit
tx <- wallet.sendToAddress(address,
bitcoins,
satoshisPerVirtualByteOpt)
_ <- handleBroadcastable(tx, noBroadcast)
retStr <- handleBroadcastable(tx, noBroadcast)
} yield {
Server.httpSuccess(tx.txIdBE)
Server.httpSuccess(retStr.hex)
}
}
}
@ -457,5 +668,15 @@ case class WalletRoutes(wallet: AnyHDWalletApi)(implicit
Server.httpSuccess(ujson.Null)
}
}
case ServerCommand("decoderawtransaction", arr) =>
DecodeRawTransaction.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(DecodeRawTransaction(tx)) =>
complete {
val jsonStr = wallet.decodeRawTransaction(tx)
Server.httpSuccess(jsonStr)
}
}
}
}

View file

@ -61,6 +61,14 @@ lazy val `bitcoin-s` = project
feeProviderTest,
dlcOracle,
dlcOracleTest,
dlc,
dlcTest,
dlcWallet,
dlcWalletTest,
dlcOracle,
dlcOracleTest,
dlcSuredbitsClient,
dlcSuredbitsClientTest,
bitcoindRpc,
bitcoindRpcTest,
bench,
@ -259,7 +267,8 @@ lazy val appServer = project
wallet,
bitcoindRpc,
feeProvider,
zmq
zmq,
dlcWallet
)
lazy val appServerTest = project
@ -275,7 +284,8 @@ lazy val cli = project
.in(file("app/cli"))
.settings(CommonSettings.prodSettings: _*)
.dependsOn(
appCommons
appCommons,
dlc
)
lazy val cliTest = project
@ -298,6 +308,19 @@ lazy val gui = project
cli
)
lazy val dlcSuredbitsClient = project
.in(file("app/dlc-suredbits-client"))
.settings(CommonSettings.prodSettings: _*)
.dependsOn(eclairRpc, wallet)
lazy val dlcSuredbitsClientTest = project
.in(file("app/dlc-suredbits-client-test"))
.settings(CommonSettings.testSettings: _*)
.dependsOn(
dlcSuredbitsClient,
testkit
)
lazy val chainDbSettings = dbFlywaySettings("chaindb")
lazy val chain = project
@ -446,7 +469,9 @@ lazy val testkit = project
node,
wallet,
zmq,
dlcOracle
dlcOracle,
dlc,
dlcWallet
)
lazy val docs = project
@ -487,7 +512,7 @@ lazy val wallet = project
name := "bitcoin-s-wallet",
libraryDependencies ++= Deps.wallet(scalaVersion.value)
)
.dependsOn(core, appCommons, dbCommons, keyManager)
.dependsOn(core, appCommons, dbCommons, dlc, keyManager)
.enablePlugins(FlywayPlugin)
lazy val walletTest = project
@ -522,6 +547,47 @@ lazy val dlcOracleTest = project
)
.dependsOn(core % testAndCompile, dlcOracle, testkit)
lazy val dlc = project
.in(file("dlc"))
.settings(CommonSettings.prodSettings: _*)
.settings(
name := "bitcoin-s-dlc",
// version number needed for MicroJson
libraryDependencies ++= Deps.dlc
)
.dependsOn(core, appCommons)
lazy val dlcTest = project
.in(file("dlc-test"))
.settings(CommonSettings.testSettings: _*)
.settings(
name := "bitcoin-s-dlc-test",
libraryDependencies ++= Deps.dlcTest
)
.dependsOn(
core % testAndCompile,
testkit,
dlc
)
lazy val dlcWallet = project
.in(file("dlc-wallet"))
.settings(CommonSettings.prodSettings: _*)
.settings(
name := "bitcoin-s-dlc-wallet",
libraryDependencies ++= Deps.dlcWallet
)
.dependsOn(wallet, dlc)
lazy val dlcWalletTest = project
.in(file("dlc-wallet-test"))
.settings(CommonSettings.testSettings: _*)
.settings(
name := "bitcoin-s-dlc-wallet-test",
libraryDependencies ++= Deps.dlcWalletTest
)
.dependsOn(core % testAndCompile, dlcWallet, testkit)
/** Given a database name, returns the appropriate
* Flyway settings we apply to a project (chain, node, wallet)
*/

View file

@ -0,0 +1,11 @@
package org.bitcoins.core.util
trait MapWrapper[K, +T] extends Map[K, T] {
protected def wrapped: Map[K, T]
override def +[B1 >: T](kv: (K, B1)): Map[K, B1] = wrapped.+(kv)
override def get(key: K): Option[T] = wrapped.get(key)
override def iterator: Iterator[(K, T)] = wrapped.iterator
override def updated[V1 >: T](key: K, value: V1): Map[K, V1] =
wrapped.updated(key, value)
override def -(key: K): Map[K, T] = wrapped.-(key)
}

View file

@ -39,6 +39,8 @@ trait WalletApi extends StartStopAsync[WalletApi] {
val feeRateApi: FeeRateApi
val creationTime: Instant
def decodeRawTransaction(tx: Transaction): String
def broadcastTransaction(transaction: Transaction): Future[Unit] =
nodeApi.broadcastTransaction(transaction)

View file

@ -296,7 +296,7 @@ object TxUtil extends BitcoinSLogger {
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.DurationInt
Await.result(TxUtil.addDummySigs(tx, inputInfos), 5.seconds)
Await.result(TxUtil.addDummySigs(tx, inputInfos), 10.seconds)
}
val actualFee = creditingAmount - spentAmount

View file

@ -673,7 +673,7 @@ case class InputPSBTMap(elements: Vector[InputPSBTRecord])
tx.outputs(txIn.previousOutput.vout.toInt)
} else {
throw new UnsupportedOperationException(
"Not enough information in the InputPSBTMap to get a valid InputInfo")
s"Not enough information in the InputPSBTMap to get a valid InputInfo: $elements")
}
val redeemScriptOpt = finalizedScriptSigOpt match {

View file

@ -5,6 +5,7 @@ import org.bitcoins.chain.config.ChainAppConfig
import org.bitcoins.chain.db.ChainDbManagement
import org.bitcoins.db.DatabaseDriver._
import org.bitcoins.dlc.oracle.config.DLCOracleAppConfig
import org.bitcoins.dlc.wallet.{DLCAppConfig, DLCDbManagement}
import org.bitcoins.node.config.NodeAppConfig
import org.bitcoins.node.db.NodeDbManagement
import org.bitcoins.testkit.BitcoinSTestAppConfig.ProjectType
@ -29,6 +30,13 @@ class DbManagementTest extends BitcoinSAsyncTest with EmbeddedPg {
override def appConfig: ChainAppConfig = chainAppConfig
}
def createDLCDbManagement(dlcAppConfig: DLCAppConfig): DLCDbManagement =
new DLCDbManagement with JdbcProfileComponent[DLCAppConfig] {
override val ec: ExecutionContext = system.dispatcher
override def appConfig: DLCAppConfig = dlcAppConfig
}
def createWalletDbManagement(
walletAppConfig: WalletAppConfig): WalletDbManagement =
new WalletDbManagement with JdbcProfileComponent[WalletAppConfig] {
@ -67,6 +75,29 @@ class DbManagementTest extends BitcoinSAsyncTest with EmbeddedPg {
}
it must "run migrations for dlc db" in {
val dlcAppConfig =
DLCAppConfig(BitcoinSTestAppConfig.tmpDir(), dbConfig(ProjectType.DLC))
val dlcDbManagement = createDLCDbManagement(dlcAppConfig)
val result = dlcDbManagement.migrate()
dlcAppConfig.driver match {
case SQLite =>
val expected = 2
assert(result == expected)
val flywayInfo = dlcAppConfig.info()
assert(flywayInfo.applied().length == expected)
assert(flywayInfo.pending().length == 0)
case PostgreSQL =>
val expected = 2
assert(result == expected)
val flywayInfo = dlcAppConfig.info()
//+1 for << Flyway Schema Creation >>
assert(flywayInfo.applied().length == expected + 1)
assert(flywayInfo.pending().length == 0)
}
}
it must "run migrations for wallet db" in {
val walletAppConfig = WalletAppConfig(BitcoinSTestAppConfig.tmpDir(),
dbConfig(ProjectType.Wallet))

View file

@ -111,6 +111,22 @@ bitcoin-s {
rpcport = 9999
}
dlc = ${bitcoin-s.sqlite}
dlc {
# this config key is read by Slick
db {
name = dlcdb.sqlite
url = "jdbc:sqlite:"${bitcoin-s.dlc.db.path}${bitcoin-s.dlc.db.name}
}
# PostgreSQL example:
# db {
# url = "jdbc:postgresql://localhost:5432/dlc"
# driver = org.postgresql.Driver
# username = postgres
# password = ""
# }
}
oracle = ${bitcoin-s.sqlite}
oracle {
# this config key is read by Slick

View file

@ -0,0 +1,586 @@
package org.bitcoins.dlc
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.SingleNonceOracleInfo
import org.bitcoins.commons.jsonmodels.dlc._
import org.bitcoins.core.config.RegTest
import org.bitcoins.core.currency.{
Bitcoins,
CurrencyUnit,
CurrencyUnits,
Satoshis
}
import org.bitcoins.core.number.{UInt16, UInt32}
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.BlockStamp.BlockHeight
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.transaction.{
Transaction,
TransactionConstants,
TransactionOutPoint
}
import org.bitcoins.core.script.crypto.HashType
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo._
import org.bitcoins.crypto._
import org.bitcoins.dlc.builder.DLCTxBuilder
import org.bitcoins.dlc.execution.{
DLCOutcome,
ExecutedDLCOutcome,
RefundDLCOutcome,
SetupDLC
}
import org.bitcoins.dlc.testgen.{DLCTestUtil, TestDLCClient}
import org.bitcoins.rpc.BitcoindException
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.testkit.util.BitcoindRpcTest
import org.scalatest.Assertion
import scala.concurrent.duration.DurationInt
import scala.concurrent.{Future, Promise}
import scala.util.{Failure, Success}
class DLCClientIntegrationTest extends BitcoindRpcTest {
private val clientsF = BitcoindRpcTestUtil.createNodePairV18(clientAccum)
private val clientF = clientsF.map(_._1)
private val addressForMiningF = clientF.flatMap(_.getNewAddress)
def publishTransaction(tx: Transaction): Future[Transaction] = {
for {
client <- clientF
txid <- client.sendRawTransaction(tx)
_ = assert(tx.txIdBE == txid)
addressForMining <- addressForMiningF
_ <- client.generateToAddress(blocks = 6, addressForMining)
} yield tx
}
def waitUntilBlock(blockHeight: Int): Future[Unit] = {
for {
client <- clientF
addressForMining <- addressForMiningF
_ <- BitcoindRpcTestUtil.waitUntilBlock(blockHeight,
client,
addressForMining)
} yield ()
}
behavior of "AdaptorDLCClient"
val oraclePrivKey: ECPrivateKey = ECPrivateKey.freshPrivateKey
val oraclePubKey: SchnorrPublicKey = oraclePrivKey.schnorrPublicKey
val preCommittedK: ECPrivateKey = ECPrivateKey.freshPrivateKey
val preCommittedR: SchnorrNonce = preCommittedK.schnorrNonce
val localInput: CurrencyUnit = CurrencyUnits.oneBTC
val remoteInput: CurrencyUnit = CurrencyUnits.oneBTC
val totalInput: CurrencyUnit = localInput + remoteInput
val inputPrivKeyLocal: ECPrivateKey = ECPrivateKey.freshPrivateKey
val inputPubKeyLocal: ECPublicKey = inputPrivKeyLocal.publicKey
val inputPrivKeyLocal2A: ECPrivateKey = ECPrivateKey.freshPrivateKey
val inputPrivKeyLocal2B: ECPrivateKey = ECPrivateKey.freshPrivateKey
val inputPubKeyLocal2A: ECPublicKey = inputPrivKeyLocal2A.publicKey
val inputPubKeyLocal2B: ECPublicKey = inputPrivKeyLocal2B.publicKey
val inputPrivKeyRemote: ECPrivateKey = ECPrivateKey.freshPrivateKey
val inputPubKeyRemote: ECPublicKey = inputPrivKeyRemote.publicKey
val inputPrivKeyRemote2A: ECPrivateKey = ECPrivateKey.freshPrivateKey
val inputPrivKeyRemote2B: ECPrivateKey = ECPrivateKey.freshPrivateKey
val inputPubKeyRemote2A: ECPublicKey = inputPrivKeyRemote2A.publicKey
val inputPubKeyRemote2B: ECPublicKey = inputPrivKeyRemote2B.publicKey
val localAddress: BitcoinAddress =
BitcoinAddress.fromScriptPubKey(P2WPKHWitnessSPKV0(inputPubKeyLocal),
RegTest)
val localNestedSPK: IfConditionalScriptPubKey =
NonStandardIfConditionalScriptPubKey(P2PKScriptPubKey(inputPubKeyLocal2A),
P2PKScriptPubKey(inputPubKeyLocal2B))
val localAddress2: BitcoinAddress =
BitcoinAddress.fromScriptPubKey(P2WSHWitnessSPKV0(localNestedSPK), RegTest)
val remoteAddress: BitcoinAddress =
BitcoinAddress.fromScriptPubKey(P2WPKHWitnessSPKV0(inputPubKeyRemote),
RegTest)
val remoteNestedSPK: MultiSignatureScriptPubKey =
MultiSignatureScriptPubKey(2,
Vector(inputPubKeyRemote2A, inputPubKeyRemote2B))
val remoteAddress2: BitcoinAddress =
BitcoinAddress.fromScriptPubKey(
P2SHScriptPubKey(P2WSHWitnessSPKV0(remoteNestedSPK)),
RegTest)
val localChangeSPK: P2WPKHWitnessSPKV0 = P2WPKHWitnessSPKV0(
ECPublicKey.freshPublicKey)
val remoteChangeSPK: P2WPKHWitnessSPKV0 = P2WPKHWitnessSPKV0(
ECPublicKey.freshPublicKey)
def constructDLC(numOutcomes: Int): Future[
(TestDLCClient, TestDLCClient, Vector[String])] = {
def fundingInput(input: CurrencyUnit): Bitcoins = {
Bitcoins((input + Satoshis(200)).satoshis)
}
val fundedInputsTxidF = for {
client <- clientF
transactionWithoutFunds <-
client
.createRawTransaction(
Vector.empty,
Map(
localAddress -> fundingInput(localInput),
localAddress2 -> fundingInput(localInput),
remoteAddress -> fundingInput(remoteInput),
remoteAddress2 -> fundingInput(remoteInput)
)
)
transactionResult <- client.fundRawTransaction(transactionWithoutFunds)
transaction = transactionResult.hex
signedTxResult <- client.signRawTransactionWithWallet(transaction)
localOutputIndex =
signedTxResult.hex.outputs.zipWithIndex
.find {
case (output, _) =>
output.scriptPubKey match {
case p2wpkh: P2WPKHWitnessSPKV0 =>
p2wpkh.pubKeyHash == P2WPKHWitnessSPKV0(
inputPubKeyLocal).pubKeyHash
case _ => false
}
}
.map(_._2)
localOutputIndex2 =
signedTxResult.hex.outputs.zipWithIndex
.find {
case (output, _) =>
output.scriptPubKey match {
case p2wsh: P2WSHWitnessSPKV0 =>
p2wsh.scriptHash == P2WSHWitnessSPKV0(
localNestedSPK).scriptHash
case _ => false
}
}
.map(_._2)
remoteOutputIndex =
signedTxResult.hex.outputs.zipWithIndex
.find {
case (output, _) =>
output.scriptPubKey match {
case p2wpkh: P2WPKHWitnessSPKV0 =>
p2wpkh.pubKeyHash == P2WPKHWitnessSPKV0(
inputPubKeyRemote).pubKeyHash
case _ => false
}
}
.map(_._2)
remoteOutputIndex2 =
signedTxResult.hex.outputs.zipWithIndex
.find {
case (output, _) =>
output.scriptPubKey match {
case p2sh: P2SHScriptPubKey =>
p2sh.scriptHash == P2SHScriptPubKey(
P2WSHWitnessSPKV0(remoteNestedSPK)).scriptHash
case _ => false
}
}
.map(_._2)
tx <- publishTransaction(signedTxResult.hex)
} yield {
assert(localOutputIndex.isDefined)
assert(localOutputIndex2.isDefined)
assert(remoteOutputIndex.isDefined)
assert(remoteOutputIndex2.isDefined)
(tx,
localOutputIndex.get,
localOutputIndex2.get,
remoteOutputIndex.get,
remoteOutputIndex2.get,
signedTxResult.hex)
}
val localFundingUtxosF = fundedInputsTxidF.map {
case (prevTx, localOutputIndex, localOutputIndex2, _, _, tx) =>
Vector(
ScriptSignatureParams(
inputInfo = P2WPKHV0InputInfo(
outPoint =
TransactionOutPoint(prevTx.txIdBE, UInt32(localOutputIndex)),
amount = tx.outputs(localOutputIndex).value,
pubKey = inputPubKeyLocal
),
prevTransaction = prevTx,
signer = inputPrivKeyLocal,
hashType = HashType.sigHashAll
),
ScriptSignatureParams(
P2WSHV0InputInfo(
outPoint =
TransactionOutPoint(prevTx.txIdBE, UInt32(localOutputIndex2)),
amount = tx.outputs(localOutputIndex2).value,
scriptWitness = P2WSHWitnessV0(localNestedSPK),
ConditionalPath.nonNestedTrue
),
prevTransaction = prevTx,
signer = inputPrivKeyLocal2A,
hashType = HashType.sigHashAll
)
)
}
val remoteFundingUtxosF = fundedInputsTxidF.map {
case (prevTx, _, _, remoteOutputIndex, remoteOutputIndex2, tx) =>
Vector(
ScriptSignatureParams(
P2WPKHV0InputInfo(
outPoint =
TransactionOutPoint(prevTx.txIdBE, UInt32(remoteOutputIndex)),
amount = tx.outputs(remoteOutputIndex).value,
pubKey = inputPubKeyRemote
),
prevTx,
inputPrivKeyRemote,
HashType.sigHashAll
),
ScriptSignatureParams(
P2SHNestedSegwitV0InputInfo(
outPoint =
TransactionOutPoint(prevTx.txIdBE, UInt32(remoteOutputIndex2)),
amount = tx.outputs(remoteOutputIndex2).value,
scriptWitness = P2WSHWitnessV0(remoteNestedSPK),
ConditionalPath.NoCondition
),
prevTransaction = prevTx,
signers = Vector(inputPrivKeyRemote2A, inputPrivKeyRemote2B),
hashType = HashType.sigHashAll
)
)
}
val feeRateF = clientF
.flatMap(_.getNetworkInfo.map(_.relayfee))
.map(btc => SatoshisPerVirtualByte(btc.satoshis))
for {
localFundingUtxos <- localFundingUtxosF
remoteFundingUtxos <- remoteFundingUtxosF
feeRate <- feeRateF
client <- clientF
currentHeight <- client.getBlockCount
} yield {
val tomorrowInBlocks = BlockHeight(currentHeight + 144)
val twoDaysInBlocks = BlockHeight(currentHeight + 288)
val localFundingPrivKey = ECPrivateKey.freshPrivateKey
val localPayoutPrivKey = ECPrivateKey.freshPrivateKey
val remoteFundingPrivKey = ECPrivateKey.freshPrivateKey
val remotePayoutPrivKey = ECPrivateKey.freshPrivateKey
val localFundingInputs = localFundingUtxos.map { utxo =>
DLCFundingInput(
utxo.prevTransaction,
utxo.outPoint.vout,
TransactionConstants.sequence,
UInt16(utxo.maxWitnessLen),
InputInfo
.getRedeemScript(utxo.inputInfo)
.map(_.asInstanceOf[WitnessScriptPubKey])
)
}
val remoteFundingInputs = remoteFundingUtxos.map { utxo =>
DLCFundingInput(
utxo.prevTransaction,
utxo.outPoint.vout,
TransactionConstants.sequence,
UInt16(utxo.maxWitnessLen),
InputInfo
.getRedeemScript(utxo.inputInfo)
.map(_.asInstanceOf[WitnessScriptPubKey])
)
}
val outcomeStrs = DLCTestUtil.genOutcomes(numOutcomes)
val (outcomes, otherOutcomes) =
DLCTestUtil.genContractInfos(outcomeStrs, totalInput)
val acceptDLC = TestDLCClient(
outcomes = outcomes,
oracleInfo = SingleNonceOracleInfo(oraclePubKey, preCommittedR),
isInitiator = false,
fundingPrivKey = localFundingPrivKey,
payoutPrivKey = localPayoutPrivKey,
remotePubKeys = DLCPublicKeys.fromPrivKeys(remoteFundingPrivKey,
remotePayoutPrivKey,
RegTest),
input = localInput,
remoteInput = remoteInput,
fundingUtxos = localFundingUtxos,
remoteFundingInputs = remoteFundingInputs,
timeouts = DLCTimeouts(tomorrowInBlocks, twoDaysInBlocks),
feeRate = feeRate,
changeSPK = localChangeSPK,
remoteChangeSPK = remoteChangeSPK,
network = RegTest
)
val offerDLC = TestDLCClient(
outcomes = otherOutcomes,
oracleInfo = SingleNonceOracleInfo(oraclePubKey, preCommittedR),
isInitiator = true,
fundingPrivKey = remoteFundingPrivKey,
payoutPrivKey = remotePayoutPrivKey,
remotePubKeys = DLCPublicKeys.fromPrivKeys(localFundingPrivKey,
localPayoutPrivKey,
RegTest),
input = remoteInput,
remoteInput = localInput,
fundingUtxos = remoteFundingUtxos,
remoteFundingInputs = localFundingInputs,
timeouts = DLCTimeouts(tomorrowInBlocks, twoDaysInBlocks),
feeRate = feeRate,
changeSPK = remoteChangeSPK,
remoteChangeSPK = localChangeSPK,
network = RegTest
)
(acceptDLC, offerDLC, outcomeStrs)
}
}
def noEmptySPKOutputs(tx: Transaction): Boolean = {
tx.outputs.forall(_.scriptPubKey != EmptyScriptPubKey)
}
def validateOutcome(
outcome: DLCOutcome,
builder: DLCTxBuilder): Future[Assertion] = {
val fundingTx = outcome.fundingTx
val closingTx = outcome match {
case ExecutedDLCOutcome(_, cet, _) => cet
case RefundDLCOutcome(_, refundTx) => refundTx
}
for {
client <- clientF
regtestFundingTx <- client.getRawTransaction(fundingTx.txIdBE)
regtestClosingTx <- client.getRawTransaction(closingTx.txIdBE)
} yield {
DLCFeeTestUtil.validateFees(builder,
fundingTx,
closingTx,
fundingTxSigs = 5)
assert(noEmptySPKOutputs(fundingTx))
assert(regtestFundingTx.hex == fundingTx)
assert(regtestFundingTx.confirmations.isDefined)
assert(regtestFundingTx.confirmations.get >= 6)
assert(noEmptySPKOutputs(closingTx))
assert(regtestClosingTx.hex == closingTx)
assert(regtestClosingTx.confirmations.isDefined)
assert(regtestClosingTx.confirmations.get >= 6)
}
}
def setupDLC(
dlcAccept: TestDLCClient,
dlcOffer: TestDLCClient): Future[(SetupDLC, SetupDLC)] = {
val offerSigReceiveP =
Promise[CETSignatures]()
val sendAcceptSigs = { sigs: CETSignatures =>
val _ = offerSigReceiveP.success(sigs)
FutureUtil.unit
}
val acceptSigReceiveP =
Promise[(CETSignatures, FundingSignatures)]()
val sendOfferSigs = {
(cetSigs: CETSignatures, fundingSigs: FundingSignatures) =>
val _ = acceptSigReceiveP.success(cetSigs, fundingSigs)
FutureUtil.unit
}
val acceptSetupF = dlcAccept.setupDLCAccept(sendSigs = sendAcceptSigs,
getSigs =
acceptSigReceiveP.future)
val fundingTxP = Promise[Transaction]()
val watchForFundingTx = new Runnable {
override def run(): Unit = {
if (!fundingTxP.isCompleted) {
clientF.foreach { client =>
val fundingTxResultF =
client.getRawTransaction(dlcOffer.fundingTxIdBE)
fundingTxResultF.onComplete {
case Success(fundingTxResult) =>
if (
fundingTxResult.confirmations.isEmpty || fundingTxResult.confirmations.get < 3
) {
()
} else {
fundingTxP.trySuccess(fundingTxResult.hex)
}
case Failure(_) => ()
}
}
}
}
}
val cancelOnFundingFound =
system.scheduler.scheduleWithFixedDelay(
initialDelay = 100.milliseconds,
delay = 1.second)(runnable = watchForFundingTx)
fundingTxP.future.foreach(_ => cancelOnFundingFound.cancel())
val offerSetupF = dlcOffer.setupDLCOffer(getSigs = offerSigReceiveP.future,
sendSigs = sendOfferSigs,
getFundingTx = fundingTxP.future)
for {
acceptSetup <- acceptSetupF
_ <- publishTransaction(acceptSetup.fundingTx)
offerSetup <- offerSetupF
} yield {
assert(acceptSetup.fundingTx == offerSetup.fundingTx)
assert(acceptSetup.refundTx == offerSetup.refundTx)
acceptSetup.cets.foreach {
case (msg, cetInfo) =>
assert(cetInfo.tx == offerSetup.cets(msg).tx)
}
(acceptSetup, offerSetup)
}
}
def constructAndSetupDLC(numOutcomes: Int): Future[
(TestDLCClient, SetupDLC, TestDLCClient, SetupDLC, Vector[String])] = {
for {
(acceptDLC, offerDLC, outcomes) <- constructDLC(numOutcomes)
(acceptSetup, offerSetup) <- setupDLC(acceptDLC, offerDLC)
} yield (acceptDLC, acceptSetup, offerDLC, offerSetup, outcomes)
}
def executeForCase(
outcomeIndex: Int,
numOutcomes: Int,
local: Boolean): Future[Assertion] = {
for {
(acceptDLC, acceptSetup, offerDLC, offerSetup, outcomes) <-
constructAndSetupDLC(numOutcomes)
oracleSig = oraclePrivKey.schnorrSignWithNonce(
CryptoUtil.sha256(outcomes(outcomeIndex)).bytes,
preCommittedK)
(unilateralDLC, unilateralSetup, otherDLC, otherSetup) = {
if (local) {
(offerDLC, offerSetup, acceptDLC, acceptSetup)
} else {
(acceptDLC, acceptSetup, offerDLC, offerSetup)
}
}
unilateralOutcome <- unilateralDLC.executeDLC(
unilateralSetup,
Future.successful(Vector(oracleSig)))
otherOutcome <-
otherDLC.executeDLC(otherSetup, Future.successful(Vector(oracleSig)))
_ <- recoverToSucceededIf[BitcoindException](
publishTransaction(unilateralOutcome.cet))
_ <- waitUntilBlock(
unilateralDLC.timeouts.contractMaturity.toUInt32.toInt - 1)
_ <- recoverToSucceededIf[BitcoindException](
publishTransaction(unilateralOutcome.cet))
_ <- waitUntilBlock(
unilateralDLC.timeouts.contractMaturity.toUInt32.toInt)
_ <- publishTransaction(unilateralOutcome.cet)
_ <- validateOutcome(unilateralOutcome, offerDLC.dlcTxBuilder)
} yield {
assert(unilateralOutcome.fundingTx == otherOutcome.fundingTx)
assert(unilateralOutcome.cet.txIdBE == otherOutcome.cet.txIdBE)
}
}
def executeForRefundCase(numOutcomes: Int): Future[Assertion] = {
for {
(acceptDLC, acceptSetup, offerDLC, offerSetup, _) <- constructAndSetupDLC(
numOutcomes)
acceptOutcome = acceptDLC.executeRefundDLC(acceptSetup)
offerOutcome = offerDLC.executeRefundDLC(offerSetup)
_ = assert(offerOutcome.refundTx == acceptOutcome.refundTx)
refundTx = offerOutcome.refundTx
_ = assert(acceptDLC.timeouts == offerDLC.timeouts)
timeout = offerDLC.timeouts.contractTimeout.toUInt32.toInt
_ <- recoverToSucceededIf[BitcoindException](publishTransaction(refundTx))
_ <- waitUntilBlock(timeout - 1)
_ <- recoverToSucceededIf[BitcoindException](publishTransaction(refundTx))
_ <- waitUntilBlock(timeout)
_ <- publishTransaction(refundTx)
_ <- validateOutcome(offerOutcome, offerDLC.dlcTxBuilder)
_ <- validateOutcome(acceptOutcome, acceptDLC.dlcTxBuilder)
} yield {
assert(acceptOutcome.fundingTx == offerOutcome.fundingTx)
}
}
val numOutcomesToTest: Vector[Int] = Vector(2, 8, 100)
def indicesToTest(numOutcomes: Int): Vector[Int] = {
if (numOutcomes == 2) {
Vector(0, 1)
} else {
Vector(0, numOutcomes / 2, numOutcomes - 1)
}
}
def runTests(
exec: (Int, Int, Boolean) => Future[Assertion],
local: Boolean): Future[Assertion] = {
val testFs = numOutcomesToTest.flatMap { numOutcomes =>
indicesToTest(numOutcomes).map { outcomeIndex => () =>
exec(outcomeIndex, numOutcomes, local)
}
}
testFs.foldLeft(Future.successful(succeed)) {
case (resultF, testExec) =>
resultF.flatMap { _ =>
testExec()
}
}
}
it should "be able to publish all DLC txs to Regtest for the normal local case" in {
runTests(executeForCase, local = true)
}
it should "be able to publish all DLC txs to Regtest for the normal remote case" in {
runTests(executeForCase, local = false)
}
it should "be able to publish all DLC txs to Regtest for the Refund case" in {
val testFs = numOutcomesToTest.map { numOutcomes => () =>
for {
_ <- executeForRefundCase(numOutcomes)
} yield succeed
}
testFs.foldLeft(Future.successful(succeed)) {
case (resultF, testExec) =>
resultF.flatMap { _ =>
testExec()
}
}
}
}

View file

@ -0,0 +1,722 @@
package org.bitcoins.dlc
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.{
DLCSign,
MultiNonceContractInfo,
MultiNonceOracleInfo,
SingleNonceOracleInfo
}
import org.bitcoins.commons.jsonmodels.dlc._
import org.bitcoins.core.config.RegTest
import org.bitcoins.core.currency.{CurrencyUnit, CurrencyUnits, Satoshis}
import org.bitcoins.core.number.{UInt16, UInt32}
import org.bitcoins.core.protocol.BlockStamp.BlockTime
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.tlv.{
DLCOutcomeType,
EnumOutcome,
UnsignedNumericOutcome
}
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.script.crypto.HashType
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.bitcoins.dlc.builder.DLCTxBuilder
import org.bitcoins.dlc.execution._
import org.bitcoins.dlc.testgen.{DLCTestUtil, TestDLCClient}
import org.bitcoins.dlc.verify.DLCSignatureVerifier
import org.bitcoins.testkit.util.BitcoinSAsyncTest
import org.scalatest.Assertion
import scala.concurrent.{Future, Promise}
class DLCClientTest extends BitcoinSAsyncTest {
behavior of "AdaptorDLCClient"
val oraclePrivKey: ECPrivateKey = ECPrivateKey.freshPrivateKey
val oraclePubKey: SchnorrPublicKey = oraclePrivKey.schnorrPublicKey
val preCommittedKs: Vector[ECPrivateKey] =
(0 until 100).toVector.map(_ => ECPrivateKey.freshPrivateKey)
val preCommittedRs: Vector[SchnorrNonce] = preCommittedKs.map(_.schnorrNonce)
val preCommittedK: ECPrivateKey = preCommittedKs.head
val preCommittedR: SchnorrNonce = preCommittedRs.head
val localInput: CurrencyUnit = CurrencyUnits.oneBTC
val remoteInput: CurrencyUnit = CurrencyUnits.oneBTC
val totalInput: CurrencyUnit = localInput + remoteInput
val inputPrivKeyLocal: ECPrivateKey = ECPrivateKey.freshPrivateKey
val inputPubKeyLocal: ECPublicKey = inputPrivKeyLocal.publicKey
val inputPrivKeyLocal2A: ECPrivateKey = ECPrivateKey.freshPrivateKey
val inputPrivKeyLocal2B: ECPrivateKey = ECPrivateKey.freshPrivateKey
val inputPubKeyLocal2A: ECPublicKey = inputPrivKeyLocal2A.publicKey
val inputPubKeyLocal2B: ECPublicKey = inputPrivKeyLocal2B.publicKey
val inputPrivKeyRemote: ECPrivateKey = ECPrivateKey.freshPrivateKey
val inputPubKeyRemote: ECPublicKey = inputPrivKeyRemote.publicKey
val inputPrivKeyRemote2A: ECPrivateKey = ECPrivateKey.freshPrivateKey
val inputPrivKeyRemote2B: ECPrivateKey = ECPrivateKey.freshPrivateKey
val inputPubKeyRemote2A: ECPublicKey = inputPrivKeyRemote2A.publicKey
val inputPubKeyRemote2B: ECPublicKey = inputPrivKeyRemote2B.publicKey
val blockTimeToday: BlockTime = BlockTime(
UInt32(System.currentTimeMillis() / 1000))
val localFundingTx: Transaction = BaseTransaction(
TransactionConstants.validLockVersion,
Vector.empty,
Vector(
TransactionOutput(localInput,
P2WPKHWitnessSPKV0(inputPrivKeyLocal.publicKey))),
UInt32.zero
)
val localNestedSPK: IfConditionalScriptPubKey =
NonStandardIfConditionalScriptPubKey(P2PKScriptPubKey(inputPubKeyLocal2A),
P2PKScriptPubKey(inputPubKeyLocal2B))
val localFundingTx2: Transaction = BaseTransaction(
TransactionConstants.validLockVersion,
Vector.empty,
Vector(TransactionOutput(localInput, P2WSHWitnessSPKV0(localNestedSPK))),
UInt32.zero
)
val localFundingUtxos = Vector(
ScriptSignatureParams(
P2WPKHV0InputInfo(outPoint =
TransactionOutPoint(localFundingTx.txId, UInt32.zero),
amount = localInput,
pubKey = inputPubKeyLocal),
prevTransaction = localFundingTx,
signer = inputPrivKeyLocal,
hashType = HashType.sigHashAll
),
ScriptSignatureParams(
P2WSHV0InputInfo(
outPoint = TransactionOutPoint(localFundingTx2.txId, UInt32.zero),
amount = localInput,
scriptWitness = P2WSHWitnessV0(localNestedSPK),
ConditionalPath.nonNestedTrue
),
prevTransaction = localFundingTx2,
signer = inputPrivKeyLocal2A,
hashType = HashType.sigHashAll
)
)
val localFundingInputs: Vector[DLCFundingInput] =
Vector(
DLCFundingInputP2WPKHV0(localFundingTx,
UInt32.zero,
TransactionConstants.sequence),
DLCFundingInputP2WSHV0(localFundingTx2,
UInt32.zero,
TransactionConstants.sequence,
maxWitnessLen =
UInt16(localFundingUtxos.last.maxWitnessLen))
)
val remoteFundingTx: Transaction = BaseTransaction(
TransactionConstants.validLockVersion,
Vector.empty,
Vector(
TransactionOutput(remoteInput,
P2WPKHWitnessSPKV0(inputPrivKeyRemote.publicKey))),
UInt32.zero
)
val remoteNestedSPK: MultiSignatureScriptPubKey =
MultiSignatureScriptPubKey(2,
Vector(inputPubKeyRemote2A, inputPubKeyRemote2B))
val remoteFundingTx2: Transaction = BaseTransaction(
TransactionConstants.validLockVersion,
Vector.empty,
Vector(
TransactionOutput(remoteInput,
P2SHScriptPubKey(P2WSHWitnessSPKV0(remoteNestedSPK)))),
UInt32.zero
)
val remoteFundingUtxos = Vector(
ScriptSignatureParams(
P2WPKHV0InputInfo(outPoint = TransactionOutPoint(remoteFundingTx.txId,
UInt32.zero),
amount = remoteInput,
pubKey = inputPubKeyRemote),
prevTransaction = remoteFundingTx,
signer = inputPrivKeyRemote,
hashType = HashType.sigHashAll
),
ScriptSignatureParams(
P2SHNestedSegwitV0InputInfo(
outPoint = TransactionOutPoint(remoteFundingTx2.txId, UInt32.zero),
amount = remoteInput,
scriptWitness = P2WSHWitnessV0(remoteNestedSPK),
ConditionalPath.NoCondition
),
prevTransaction = remoteFundingTx2,
signers = Vector(inputPrivKeyRemote2A, inputPrivKeyRemote2B),
hashType = HashType.sigHashAll
)
)
val remoteFundingInputs: Vector[DLCFundingInput] =
Vector(
DLCFundingInputP2WPKHV0(remoteFundingTx,
UInt32.zero,
TransactionConstants.sequence),
DLCFundingInputP2SHSegwit(
prevTx = remoteFundingTx2,
prevTxVout = UInt32.zero,
sequence = TransactionConstants.sequence,
maxWitnessLen = UInt16(remoteFundingUtxos.last.maxWitnessLen),
redeemScript = P2WSHWitnessSPKV0(remoteNestedSPK)
)
)
val localChangeSPK: P2WPKHWitnessSPKV0 = P2WPKHWitnessSPKV0(
ECPublicKey.freshPublicKey)
val remoteChangeSPK: P2WPKHWitnessSPKV0 = P2WPKHWitnessSPKV0(
ECPublicKey.freshPublicKey)
val offerFundingPrivKey: ECPrivateKey = ECPrivateKey.freshPrivateKey
val offerPayoutPrivKey: ECPrivateKey = ECPrivateKey.freshPrivateKey
val acceptFundingPrivKey: ECPrivateKey = ECPrivateKey.freshPrivateKey
val acceptPayoutPrivKey: ECPrivateKey = ECPrivateKey.freshPrivateKey
val timeouts: DLCTimeouts =
DLCTimeouts(blockTimeToday,
BlockTime(UInt32(blockTimeToday.time.toLong + 1)))
val feeRate: SatoshisPerVirtualByte = SatoshisPerVirtualByte(Satoshis(10))
def constructDLCClients(numOutcomes: Int, isMultiNonce: Boolean): (
TestDLCClient,
TestDLCClient,
Vector[DLCOutcomeType]) = {
val (outcomes, remoteOutcomes, strsOrDigits) = if (!isMultiNonce) {
val outcomeStrs = DLCTestUtil.genOutcomes(numOutcomes)
val (local, remote) =
DLCTestUtil.genContractInfos(outcomeStrs, totalInput)
(local, remote, outcomeStrs.map(EnumOutcome.apply))
} else {
val (local, remote) =
DLCTestUtil.genMultiDigitContractInfo(numOutcomes, totalInput)
(local, remote, local.allOutcomes)
}
val oracleInfo = if (!isMultiNonce) {
SingleNonceOracleInfo(oraclePubKey, preCommittedR)
} else {
MultiNonceOracleInfo(oraclePubKey, preCommittedRs.take(numOutcomes))
}
// Offer is local
val dlcOffer: TestDLCClient = TestDLCClient(
outcomes = outcomes,
oracleInfo = oracleInfo,
isInitiator = true,
fundingPrivKey = offerFundingPrivKey,
payoutPrivKey = offerPayoutPrivKey,
remotePubKeys = DLCPublicKeys.fromPrivKeys(acceptFundingPrivKey,
acceptPayoutPrivKey,
RegTest),
input = localInput,
remoteInput = remoteInput,
fundingUtxos = localFundingUtxos,
remoteFundingInputs = remoteFundingInputs,
timeouts = timeouts,
feeRate = feeRate,
changeSPK = localChangeSPK,
remoteChangeSPK = remoteChangeSPK,
network = RegTest
)
// Accept is remote
val dlcAccept: TestDLCClient = TestDLCClient(
outcomes = remoteOutcomes,
oracleInfo = oracleInfo,
isInitiator = false,
fundingPrivKey = acceptFundingPrivKey,
payoutPrivKey = acceptPayoutPrivKey,
remotePubKeys = DLCPublicKeys.fromPrivKeys(offerFundingPrivKey,
offerPayoutPrivKey,
RegTest),
input = remoteInput,
remoteInput = localInput,
fundingUtxos = remoteFundingUtxos,
remoteFundingInputs = localFundingInputs,
timeouts = timeouts,
feeRate = feeRate,
changeSPK = remoteChangeSPK,
remoteChangeSPK = localChangeSPK,
network = RegTest
)
(dlcOffer, dlcAccept, strsOrDigits)
}
def noEmptySPKOutputs(tx: Transaction): Boolean = {
tx.outputs.forall(_.scriptPubKey != EmptyScriptPubKey)
}
def validateOutcome(
outcome: DLCOutcome,
dlcOffer: TestDLCClient,
dlcAccept: TestDLCClient): Assertion = {
val fundingTx = outcome.fundingTx
assert(noEmptySPKOutputs(fundingTx))
val signers = Vector(dlcOffer.fundingPrivKey, dlcAccept.fundingPrivKey)
val closingSpendingInfo = ScriptSignatureParams(
P2WSHV0InputInfo(
TransactionOutPoint(fundingTx.txId, UInt32.zero),
fundingTx.outputs.head.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(numOutcomes: Int, isMultiDigit: Boolean): Future[
(
SetupDLC,
TestDLCClient,
SetupDLC,
TestDLCClient,
Vector[DLCOutcomeType])] = {
val (dlcOffer, dlcAccept, outcomeStrs) =
constructDLCClients(numOutcomes, isMultiDigit)
val offerSigReceiveP =
Promise[CETSignatures]()
val sendAcceptSigs = { sigs: CETSignatures =>
val _ = offerSigReceiveP.success(sigs)
FutureUtil.unit
}
val acceptSigReceiveP =
Promise[(CETSignatures, FundingSignatures)]()
val sendOfferSigs = {
(cetSigs: CETSignatures, fundingSigs: FundingSignatures) =>
val _ = acceptSigReceiveP.success(cetSigs, fundingSigs)
FutureUtil.unit
}
val acceptSetupF = dlcAccept.setupDLCAccept(sendSigs = sendAcceptSigs,
getSigs =
acceptSigReceiveP.future)
val offerSetupF = dlcOffer.setupDLCOffer(getSigs = offerSigReceiveP.future,
sendSigs = sendOfferSigs,
getFundingTx =
acceptSetupF.map(_.fundingTx))
for {
acceptSetup <- acceptSetupF
offerSetup <- offerSetupF
} yield {
assert(acceptSetup.fundingTx == offerSetup.fundingTx)
acceptSetup.cets.foreach {
case (outcome, CETInfo(cet, _)) =>
assert(cet == offerSetup.cets(outcome).tx)
}
assert(acceptSetup.refundTx == offerSetup.refundTx)
(acceptSetup, dlcAccept, offerSetup, dlcOffer, outcomeStrs)
}
}
def executeForCase(
outcomeIndex: Long,
numOutcomes: Int,
isMultiDigit: Boolean): Future[Assertion] = {
setupDLC(numOutcomes, isMultiDigit).flatMap {
case (acceptSetup, dlcAccept, offerSetup, dlcOffer, outcomes) =>
val oracleSigs = if (!isMultiDigit) {
outcomes(outcomeIndex.toInt) match {
case EnumOutcome(outcome) =>
val sig = oraclePrivKey.schnorrSignWithNonce(
CryptoUtil.sha256(outcome).bytes,
preCommittedK)
Vector(sig)
case UnsignedNumericOutcome(_) => fail("Expected EnumOutcome")
}
} else {
val points =
dlcOffer.dlcTxBuilder.oracleAndContractInfo.offerContractInfo
.asInstanceOf[MultiNonceContractInfo]
.outcomeValueFunc
.points
val left = points(1).outcome.toLongExact
val right = points(2).outcome.toLongExact
// Somewhere in the middle third of the interesting values
val outcomeNum =
(2 * left + right) / 3 + (outcomeIndex % (right - left) / 3)
val fullDigits =
NumberUtil.decompose(outcomeNum, base = 10, numOutcomes)
val digits =
CETCalculator.searchForNumericOutcome(fullDigits, outcomes) match {
case Some(UnsignedNumericOutcome(digits)) => digits
case None => fail(s"Couldn't find outcome for $outcomeIndex")
}
digits.zip(preCommittedKs.take(digits.length)).map {
case (digit, kValue) =>
oraclePrivKey.schnorrSignWithNonce(
CryptoUtil.sha256(digit.toString).bytes,
kValue)
}
}
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): Future[Assertion] = {
setupDLC(numOutcomes, isMultiNonce).flatMap {
case (acceptSetup, dlcAccept, offerSetup, dlcOffer, _) =>
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 runTestsForParam[T](paramsToTest: Vector[T])(
test: T => Future[Assertion]): Future[Assertion] = {
paramsToTest.foldLeft(Future.successful(succeed)) {
case (fut, param) =>
fut.flatMap { _ =>
test(param)
}
}
}
val numEnumOutcomesToTest: Vector[Int] = Vector(2, 3, 5, 8)
def runSingleNonceTests(
exec: (Long, Int, Boolean) => Future[Assertion]): Future[Assertion] = {
runTestsForParam(numEnumOutcomesToTest) { numOutcomes =>
runTestsForParam(0.until(numOutcomes).toVector) { outcomeIndex =>
exec(outcomeIndex, numOutcomes, false)
}
}
}
val numDigitsToTest: Vector[Int] = Vector(2, 3, 5)
def runMultiNonceTests(
exec: (Long, Int, Boolean) => Future[Assertion]): Future[Assertion] = {
runTestsForParam(numDigitsToTest) { numDigits =>
val randDigits = (0 until numDigits).toVector.map { _ =>
scala.util.Random.nextInt(10)
}
val num = randDigits.mkString("").toLong
exec(num, numDigits, true)
}
}
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 = 8
val randDigits = (0 until numDigits).toVector.map { _ =>
scala.util.Random.nextInt(10)
}
val num = randDigits.mkString("").toLong
executeForCase(num, numDigits, isMultiDigit = true)
}
it should "be able to construct and verify with ScriptInterpreter every tx in a DLC for the refund case" in {
val testFs = numEnumOutcomesToTest.map { numOutcomes =>
executeRefundCase(numOutcomes, isMultiNonce = false).flatMap { _ =>
executeRefundCase(numOutcomes, isMultiNonce = true)
}
}
Future.sequence(testFs).map(_ => succeed)
}
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)
} yield succeed
}
Future
.sequence(testFs)
.flatMap(_ => executeRefundCase(numOutcomes, isMultiNonce = false))
}
it should "fail on invalid funding signatures" in {
val (offerClient, acceptClient, _) =
constructDLCClients(numOutcomes = 3, isMultiNonce = false)
val builder = offerClient.dlcTxBuilder
val offerVerifier = DLCSignatureVerifier(builder, isInitiator = true)
val acceptVerifier = DLCSignatureVerifier(builder, isInitiator = false)
for {
offerFundingSigs <- offerClient.dlcTxSigner.createFundingTxSigs()
acceptFundingSigs <- acceptClient.dlcTxSigner.createFundingTxSigs()
badOfferFundingSigs = DLCTestUtil.flipBit(offerFundingSigs)
badAcceptFundingSigs = DLCTestUtil.flipBit(acceptFundingSigs)
_ <- recoverToSucceededIf[RuntimeException] {
offerClient.dlcTxSigner.signFundingTx(badAcceptFundingSigs)
}
_ <- recoverToSucceededIf[RuntimeException] {
acceptClient.dlcTxSigner.signFundingTx(badOfferFundingSigs)
}
} yield {
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 "fail on invalid CET signatures" in {
val (offerClient, acceptClient, outcomes) =
constructDLCClients(numOutcomes = 3, isMultiNonce = false)
val builder = offerClient.dlcTxBuilder
val offerVerifier = DLCSignatureVerifier(builder, isInitiator = true)
val acceptVerifier = DLCSignatureVerifier(builder, isInitiator = false)
for {
offerCETSigs <- offerClient.dlcTxSigner.createCETSigs()
acceptCETSigs <- acceptClient.dlcTxSigner.createCETSigs()
badOfferCETSigs = DLCTestUtil.flipBit(offerCETSigs)
badAcceptCETSigs = DLCTestUtil.flipBit(acceptCETSigs)
cetFailures = outcomes.map { outcome =>
val oracleSig =
oraclePrivKey.schnorrSignWithNonce(
CryptoUtil.sha256(outcome.asInstanceOf[EnumOutcome].outcome).bytes,
preCommittedK)
for {
_ <- recoverToSucceededIf[RuntimeException] {
offerClient.dlcTxSigner.signCET(outcome,
badAcceptCETSigs(outcome),
Vector(oracleSig))
}
_ <- recoverToSucceededIf[RuntimeException] {
acceptClient.dlcTxSigner
.signCET(outcome, badOfferCETSigs(outcome), Vector(oracleSig))
}
} yield succeed
}
_ <- Future.sequence(cetFailures)
_ <- recoverToExceptionIf[RuntimeException] {
offerClient.dlcTxSigner.signRefundTx(badAcceptCETSigs.refundSig)
}
_ <- recoverToExceptionIf[RuntimeException] {
acceptClient.dlcTxSigner.signRefundTx(badOfferCETSigs.refundSig)
}
} yield {
outcomes.foreach { outcome =>
assert(offerVerifier.verifyCETSig(outcome, acceptCETSigs(outcome)))
assert(acceptVerifier.verifyCETSig(outcome, offerCETSigs(outcome)))
}
assert(offerVerifier.verifyRefundSig(acceptCETSigs.refundSig))
assert(offerVerifier.verifyRefundSig(offerCETSigs.refundSig))
assert(acceptVerifier.verifyRefundSig(offerCETSigs.refundSig))
assert(acceptVerifier.verifyRefundSig(acceptCETSigs.refundSig))
outcomes.foreach { outcome =>
assert(!offerVerifier.verifyCETSig(outcome, badAcceptCETSigs(outcome)))
assert(!acceptVerifier.verifyCETSig(outcome, badOfferCETSigs(outcome)))
assert(!offerVerifier.verifyCETSig(outcome, offerCETSigs(outcome)))
assert(!acceptVerifier.verifyCETSig(outcome, acceptCETSigs(outcome)))
}
assert(!offerVerifier.verifyRefundSig(badAcceptCETSigs.refundSig))
assert(!offerVerifier.verifyRefundSig(badOfferCETSigs.refundSig))
assert(!acceptVerifier.verifyRefundSig(badOfferCETSigs.refundSig))
assert(!acceptVerifier.verifyRefundSig(badAcceptCETSigs.refundSig))
}
}
def assertCorrectSigDerivation(
offerSetup: SetupDLC,
dlcOffer: TestDLCClient,
acceptSetup: SetupDLC,
dlcAccept: TestDLCClient,
oracleSigs: Vector[SchnorrDigitalSignature],
outcome: DLCOutcomeType): Future[Assertion] = {
val (aggR, aggS) = oracleSigs
.map(sig => (sig.rx.publicKey, sig.sig))
.reduce[(ECPublicKey, FieldElement)] {
case ((pk1, s1), (pk2, s2)) =>
(pk1.add(pk2), s1.add(s2))
}
val aggSig = SchnorrDigitalSignature(aggR.schnorrNonce, aggS)
for {
acceptCETSigs <- dlcAccept.dlcTxSigner.createCETSigs()
offerCETSigs <- dlcOffer.dlcTxSigner.createCETSigs()
offerFundingSigs <- dlcOffer.dlcTxSigner.createFundingTxSigs()
offerOutcome <-
dlcOffer.executeDLC(offerSetup, Future.successful(oracleSigs))
acceptOutcome <-
dlcAccept.executeDLC(acceptSetup, Future.successful(oracleSigs))
builder = DLCTxBuilder(dlcOffer.offer, dlcAccept.accept)
contractId <- builder.buildFundingTx.map(
_.txIdBE.bytes.xor(dlcAccept.accept.tempContractId.bytes))
} yield {
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)
val (acceptOracleSig, acceptDLCOutcome) =
DLCStatus.calculateOutcomeAndSig(isInitiator = false,
offer,
accept,
sign,
offerOutcome.cet)
assert(offerOracleSig == aggSig)
assert(offerDLCOutcome == outcome)
assert(acceptOracleSig == aggSig)
assert(acceptDLCOutcome == outcome)
}
}
it should "be able to derive oracle signature from remote CET signature" in {
val outcomeIndex = 1
runTestsForParam(numEnumOutcomesToTest) { numOutcomes =>
setupDLC(numOutcomes, isMultiDigit = false).flatMap {
case (acceptSetup, dlcAccept, offerSetup, dlcOffer, outcomes) =>
val outcome = outcomes(outcomeIndex).asInstanceOf[EnumOutcome]
val oracleSig =
oraclePrivKey.schnorrSignWithNonce(CryptoUtil
.sha256(outcome.outcome)
.bytes,
preCommittedK)
assertCorrectSigDerivation(offerSetup = offerSetup,
dlcOffer = dlcOffer,
acceptSetup = acceptSetup,
dlcAccept = dlcAccept,
oracleSigs = Vector(oracleSig),
outcome = outcome)
}
}
}
it should "be able to derive aggregate oracle signature from remote CET signatures" in {
// Larger numbers of digits make tests take too long.
// TODO: In the future when bases other than 10 can be used try more digits with base 2
val numDigitsToTest = Vector(2, 3)
runTestsForParam(numDigitsToTest) { numDigits =>
val outcomesToTest = 0
.until(9)
.toVector
.map(num => Vector(num) ++ Vector.fill(numDigits - 1)(0))
setupDLC(numDigits, isMultiDigit = true).flatMap {
case (acceptSetup, dlcAccept, offerSetup, dlcOffer, outcomes) =>
runTestsForParam(outcomesToTest) { outcomeToTest =>
val outcome = CETCalculator
.searchForNumericOutcome(outcomeToTest, outcomes)
.get
val oracleSigs = outcome.digits
.zip(preCommittedKs.take(numDigits))
.map {
case (digit, kVal) =>
oraclePrivKey.schnorrSignWithNonce(
CryptoUtil.sha256(digit.toString).bytes,
kVal)
}
assertCorrectSigDerivation(offerSetup = offerSetup,
dlcOffer = dlcOffer,
acceptSetup = acceptSetup,
dlcAccept = dlcAccept,
oracleSigs = oracleSigs,
outcome = outcome)
}
}
}
}
}

View file

@ -0,0 +1,116 @@
package org.bitcoins.dlc
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.dlc.builder.DLCTxBuilder
import org.scalatest.{Assertion, Assertions}
object DLCFeeTestUtil extends Assertions {
def validateFees(
builder: DLCTxBuilder,
fundingTx: Transaction,
closingTx: Transaction,
fundingTxSigs: Int = 2,
closingTxSigs: Int = 2): Assertion = {
val feeRate = builder.feeRate
val finalizer = builder.fundingTxBuilder.fundingTxFinalizer
val expectedFundingFee =
finalizer.offerFundingFee + finalizer.acceptFundingFee
val expectedClosingFee =
finalizer.offerFutureFee + finalizer.acceptFutureFee
val fundingInput =
(builder.offerFundingInputs ++ builder.acceptFundingInputs)
.map(_.output.value)
.sum
val fundingOutput = fundingTx.outputs.map(_.value).sum
val actualFundingFee = fundingInput - fundingOutput
val closingInput = fundingTx.outputs.head.value
val closingOutput = closingTx.outputs.map(_.value).sum
val actualClosingFee = closingInput - closingOutput
/** Actual Fee Rate = Actual Fee / Ceil(Actual Weight / 4.0)
* Expected Fee Rate = Expected Fee / Ceil(Expected Weight / 4.0)
*
* Expected Fee = Actual Fee (yay!)
*
* Expected Weight - #sigs - 4 <= Actual Weight <= Expected Weight
* The right comparison is true because Expected Weight is designed to be an upper bound.
* The left comparison is true because the possible savings are from one weight being saved per
* signature, and 1 vbyte = 4 weight being saved if rounding works out well between both parites.
*
* Because of these comparisons, we can derive
*
* Lower Bound Fee Rate = Actual Fee / (Ceil((Actual Weight + #sigs)/4.0) + 1)
* Upper Bound Fee Rate = Actual Fee / Ceil(Actual Weight/4.0)
*
* So that these two fee rates correspond to vbyte amounts 1 apart and represent the
* actual fee rate but allowing for signature size variation after which we should match
* the expected fee rate. This function asserts:
* Lower Bound Fee Rate <= Expected Fee Rate <= Upper Bound Fee Rate
*/
def feeRateBounds(
tx: Transaction,
actualFee: CurrencyUnit,
numSignatures: Int,
missingOutputBytes: Long = 0): (Double, Double) = {
val vbytesLower =
Math.ceil(tx.weight / 4.0) + missingOutputBytes
val vbytesUpper =
Math.ceil((tx.weight + numSignatures) / 4.0) + missingOutputBytes + 1
// Upper VBytes => Lower fee rate
val lowerBound = actualFee.satoshis.toLong / vbytesUpper
// Lower VBytes => Upper fee rate
val upperBound = actualFee.satoshis.toLong / vbytesLower
(lowerBound, upperBound)
}
val (actualFundingFeeRateLower, actualFundingFeeRateUpper) =
feeRateBounds(fundingTx, actualFundingFee, fundingTxSigs)
val (actualClosingFeeRateLower, actualClosingFeeRateUpper) =
feeRateBounds(closingTx, actualClosingFee, closingTxSigs)
assert(actualFundingFee == expectedFundingFee)
assert(actualClosingFee == expectedClosingFee)
val offerOutputBytes =
9 + builder.offerFinalAddress.scriptPubKey.asmBytes.length
val acceptOutputBytes =
9 + builder.acceptFinalAddress.scriptPubKey.asmBytes.length
val (actualClosingFeeRateWithoutOfferLower,
actualClosingFeeRateWithoutOfferUpper) =
feeRateBounds(closingTx,
actualClosingFee,
closingTxSigs,
offerOutputBytes)
val (actualClosingFeeRateWithoutAcceptLower,
actualClosingFeeRateWithoutAcceptUpper) =
feeRateBounds(closingTx,
actualClosingFee,
closingTxSigs,
acceptOutputBytes)
def feeRateBetweenBounds(
lowerBound: Double,
upperBound: Double): Boolean = {
feeRate.toLong >= lowerBound && feeRate.toLong <= upperBound
}
assert(
feeRateBetweenBounds(actualFundingFeeRateLower,
actualFundingFeeRateUpper))
assert(
feeRateBetweenBounds(actualClosingFeeRateLower,
actualClosingFeeRateUpper) ||
feeRateBetweenBounds(actualClosingFeeRateWithoutOfferLower,
actualClosingFeeRateWithoutOfferUpper) ||
feeRateBetweenBounds(actualClosingFeeRateWithoutAcceptLower,
actualClosingFeeRateWithoutAcceptUpper))
}
}

View file

@ -0,0 +1,76 @@
package org.bitcoins.dlc
import org.bitcoins.core.protocol.tlv.EnumOutcome
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.crypto._
import org.bitcoins.dlc.execution.{CETInfo, SetupDLC}
import org.bitcoins.testkit.util.BitcoinSAsyncTest
class SetupDLCTest extends BitcoinSAsyncTest {
behavior of "SetupAdaptorDLC"
val dummyTxId: DoubleSha256DigestBE = DoubleSha256DigestBE.empty
val dummyAdaptorSig: ECAdaptorSignature = ECAdaptorSignature(
ECPublicKey.freshPublicKey,
ECPrivateKey.freshPrivateKey.fieldElement,
ECPublicKey.freshPublicKey,
ECPrivateKey.freshPrivateKey.fieldElement,
ECPrivateKey.freshPrivateKey.fieldElement
)
val validFundingTx: Transaction = Transaction(
"02000000000102f6ec63ebf8589895147803ea2724f1cc9fbcc75077136c15af57210ab8b4342b0000000000ffffffffbfeda85fa8d2335694b3602f0e0918fa7e9d144051217330c8a0328b4d0caabd0000000000ffffffff03c8c2eb0b0000000022002060b1a927da5c8570b1280d6dd0306d6458edc1c4abb7cc3ce8f75d157e78825e1ee0f50500000000160014558979e57695d211a354b87369ae819ae720e2761ee0f50500000000160014dc93c91963fb7b42860f92caf11217ef6c28599702483045022100b9189d799cdc4845b273ff4a471fcaa36e2419609bc3e749d78668bb8c2cb1350220403cdcddf10d06b8cb36e2943d1c8d195cbc0b023d1cdaa29281dfa250a744a9012103f5413f7d813a4e1bff925fa8b1e6ce154b309d6df4433cc1c6ded5e04f4d2d6202473044022001d25acb7ad50c352604a36d20fa25426d98d9d917200b017c052954ddf97e43022005b6d6c70d029206851c5af7ac3013fda87d5536003b0eee1c29cac6903cfbbd0121039ef319718ac70f4a3e86fb4eab23adc1c41dda9109f6c700b9b06690ddb3138b00000000")
val validCET: Transaction = Transaction(
"0200000000010112eb723473aa9ec2a91d82072b054feb4660389dc33ec407ff5836ff1ade73490000000000feffffff029014680300000000160014f161d1f494c1617a385f4480687b49826b5287ec70ad830800000000160014ea0f8ed8de8f6190bc67a2cf19f75bea97d0d582014752210258d139dc2f0507bd2b01794ff04530fd614a86dded69fd944da1942bcf748a7e2103eaf8df8e339381a19dfa5e37a4ce3c04ad3dc62f8d57774d92154e6d26ef06a452ae7c37265f"
)
val validCETInfo: CETInfo = CETInfo(validCET, dummyAdaptorSig)
val validRefundTx: Transaction = Transaction(
"0200000000010112eb723473aa9ec2a91d82072b054feb4660389dc33ec407ff5836ff1ade73490000000000feffffff02ace0f50500000000160014f161d1f494c1617a385f4480687b49826b5287ecabe0f50500000000160014ea0f8ed8de8f6190bc67a2cf19f75bea97d0d582040047304402206e204681682139ca91abca8a090c05d335c3077bcaa801d73ade4d30cf14befc0220114da42320f563f8df7ba29bfb26549b58b4dfc40d8af3a8e352b2da180fa9f001483045022100a9aa4a45d89d936762041cc2793700c3c6228326648a66228c4e265c9938337c0220024637e716c702176bde6029342ac42118a1af774ca6fb7a8225a31b15b1c839014752210258d139dc2f0507bd2b01794ff04530fd614a86dded69fd944da1942bcf748a7e2103eaf8df8e339381a19dfa5e37a4ce3c04ad3dc62f8d57774d92154e6d26ef06a452ae7d37265f")
// These 2 can be the same, we only need them to have 1 input so we can do the correct checks
val invalidCET: Transaction = Transaction(
"02000000000101bdaa2ea7eea92a88f357bd8af92003286c5acb9e9b51006d2394b47afa1c7a040000000000ffffffff018a831e000000000017a914b80e1c53b48628277bd2cb63d9d111c8fbcecdda870400483045022100ee40ca5537b5a9e9aeb04e659a8e7ec8c2d4a33dd8f8e620c98561a9e87f2db802200c1a9e35464412b0c4c28125e0279807b1b0df649a10dbfce41fa52722d71ede01483045022100cea1701d3b7fc9ca7f83cfed3fbd43853d243249a0ece0ec7fbefe51d3c526df02202fed76ce12e54793961ef3611cefcd684c40ba4b640bb3a01abee7c1dd0fab6a01475221031454ce0a0354aadf6bd13f4e27c1287d33534dd02e249d4455341d528b7ea7192103ab051b06e850b33196ddd80b43084883a29c854c0c5a924b273cbe4b1c3a228452ae00000000"
)
val invalidRefundTx: Transaction = invalidCET
def setupDLC(
fundingTx: Transaction = validFundingTx,
cet0: CETInfo = validCETInfo,
cet1: CETInfo = validCETInfo,
refundTx: Transaction = validRefundTx): SetupDLC = {
SetupDLC(
fundingTx = fundingTx,
cets = Map(EnumOutcome("WIN") -> cet0, EnumOutcome("LOSE") -> cet1),
refundTx = refundTx
)
}
it must "not allow an invalid number of inputs for CETs" in {
// Funding tx has more than 1 input
assertThrows[IllegalArgumentException](
setupDLC(cet0 = validCETInfo.copy(tx = validFundingTx)))
assertThrows[IllegalArgumentException](
setupDLC(cet1 = validCETInfo.copy(tx = validFundingTx)))
}
it must "not allow an invalid input for CETs" in {
assertThrows[IllegalArgumentException](
setupDLC(cet0 = validCETInfo.copy(tx = invalidCET)))
assertThrows[IllegalArgumentException](
setupDLC(cet1 = validCETInfo.copy(tx = invalidCET)))
}
it must "not allow an invalid number of inputs for the refundTx" in {
// Funding tx has more than 1 input
assertThrows[IllegalArgumentException](setupDLC(refundTx = validFundingTx))
}
it must "not allow an invalid input for the refundTx" in {
assertThrows[IllegalArgumentException](setupDLC(refundTx = invalidRefundTx))
}
}

View file

@ -0,0 +1,56 @@
package org.bitcoins.dlc.testgen
import org.bitcoins.testkit.core.gen.FeeUnitGen
import org.bitcoins.testkit.util.BitcoinSUnitTest
import org.scalacheck.Gen
class DLCFeeTestVectorTest extends BitcoinSUnitTest {
implicit override val generatorDrivenConfig: PropertyCheckConfiguration =
generatorDrivenConfigNewCode
behavior of "DLCFeeTestVector"
it should "have serialization symmetry" in {
val inputGen = for {
redeemScriptLen <- Gen.oneOf(0, 22, 34)
maxWitnessLen <- Gen.oneOf(Gen.choose(1, 300), Gen.oneOf(107, 108))
} yield FundingFeeInfo(redeemScriptLen, maxWitnessLen)
val gen = for {
numOfferInputs <- Gen.choose(1, 10)
offerInputs <- Gen.listOfN(numOfferInputs, inputGen)
offerPayoutSPKLen <- Gen.oneOf(22, 25, 34, 35, 71, 173)
offerChangeSPKLen <- Gen.oneOf(22, 34)
numAcceptInputs <- Gen.choose(1, 10)
acceptInputs <- Gen.listOfN(numAcceptInputs, inputGen)
acceptPayoutSPKLen <- Gen.oneOf(22, 25, 34, 35, 71, 173)
acceptChangeSPKLen <- Gen.oneOf(22, 34)
feeRate <- FeeUnitGen.satsPerVirtualByte
} yield {
DLCFeeTestVector(offerInputs.toVector,
offerPayoutSPKLen,
offerChangeSPKLen,
acceptInputs.toVector,
acceptPayoutSPKLen,
acceptChangeSPKLen,
feeRate)
}
forAll(gen) { feeTest =>
val feeTestResult = DLCFeeTestVector.fromJson(feeTest.toJson)
assert(feeTestResult.isSuccess)
assert(feeTestResult.get == feeTest)
}
}
it should "pass dlc_fee_test" in {
val vecResult = DLCFeeTestVectorGen.readFromDefaultTestFile()
assert(vecResult.isSuccess)
vecResult.get.foldLeft(succeed) {
case (_, testVec) =>
assert(DLCFeeTestVector(testVec.inputs) == testVec)
}
}
}

View file

@ -0,0 +1,48 @@
package org.bitcoins.dlc.testgen
import org.bitcoins.core.protocol.tlv.{LnMessage, TLV}
import org.bitcoins.crypto.NetworkElement
import org.bitcoins.testkit.util.BitcoinSUnitTest
import org.scalacheck.Gen
class DLCParsingTestVectorTest extends BitcoinSUnitTest {
implicit override val generatorDrivenConfig: PropertyCheckConfiguration =
generatorDrivenConfigNewCode
behavior of "DLCParsingTestVector"
it should "have serialization symmetry" in {
val gen = Gen.oneOf(
DLCTLVGen.contractInfoParsingTestVector(),
DLCTLVGen.oracleInfoParsingTestVector(),
DLCTLVGen.fundingInputParsingTestVector(),
DLCTLVGen.cetSigsParsingTestVector(),
DLCTLVGen.fundingSigsParsingTestVector(),
DLCTLVGen.dlcOfferParsingTestVector(),
DLCTLVGen.dlcAcceptParsingTestVector(),
DLCTLVGen.dlcSignParsingTestVector()
)
forAll(gen) { parsingTest =>
val parsingTestResult = DLCParsingTestVector.fromJson(parsingTest.toJson)
assert(parsingTestResult.isSuccess)
assert(parsingTestResult.get == parsingTest)
}
}
it should "pass dlc_message_test" in {
val vecResult = DLCParsingTestVectorGen.readFromDefaultTestFile()
assert(vecResult.isSuccess)
vecResult.get.foldLeft(succeed) {
case (_, testVec) =>
val tlv = testVec.input match {
case tlv: TLV => tlv
case msg: LnMessage[TLV] => msg.tlv
case _: NetworkElement => fail(s"Could not parse input $testVec")
}
assert(DLCParsingTestVector(tlv) == testVec)
}
}
}

View file

@ -0,0 +1,37 @@
package org.bitcoins.dlc.testgen
import org.bitcoins.testkit.util.BitcoinSAsyncTest
import org.scalacheck.Gen
import scala.concurrent.Future
class DLCTestVectorTest extends BitcoinSAsyncTest {
behavior of "DLCTestVectors"
it should "have serialization symmetry" in {
val gen = TestVectorUtil.testInputGen.map(DLCTxGen.successTestVector(_))
forAllAsync(gen) { testVectorF =>
testVectorF.map { testVector =>
assert(DLCTestVector.fromJson(testVector.toJson).contains(testVector))
}
}
}
it should "pass dlc_test" in {
val vecResult = DLCTestVectorGen.readFromDefaultTestFile()
assert(vecResult.isSuccess)
vecResult.get.foldLeft(Future.successful(succeed)) {
case (assertF, testVec) =>
assertF.flatMap { _ =>
testVec match {
case testVec: SuccessTestVector =>
DLCTxGen
.successTestVector(testVec.testInputs)
.map(regenerated => assert(regenerated == testVec))
}
}
}
}
}

View file

@ -0,0 +1,35 @@
package org.bitcoins.dlc.testgen
import org.bitcoins.testkit.util.BitcoinSAsyncTest
import scala.concurrent.Future
class DLCTxTestVectorTest extends BitcoinSAsyncTest {
behavior of "DLCTxTestVector"
it should "have serialization symmetry" in {
val gen = TestVectorUtil.testInputGen.map(DLCTxGen.dlcTxTestVector(_))
forAllAsync(gen) { testVecF =>
testVecF.map { testVec =>
val testVecResult = DLCTxTestVector.fromJson(testVec.toJson)
assert(testVecResult.isSuccess)
assert(testVecResult.get == testVec)
}
}
}
it should "pass dlc_tx_test" in {
val vecResult = DLCTxTestVectorGen.readFromDefaultTestFile()
assert(vecResult.isSuccess)
vecResult.get.foldLeft(Future.successful(succeed)) {
case (assertF, testVec) =>
assertF.flatMap { _ =>
DLCTxGen
.dlcTxTestVector(testVec.inputs)
.map(regenerated => assert(regenerated == testVec))
}
}
}
}

View file

@ -0,0 +1,40 @@
package org.bitcoins.dlc.testgen
import org.bitcoins.testkit.core.gen.CryptoGenerators
import org.bitcoins.testkit.util.BitcoinSUnitTest
class SchnorrSigPointTestVectorTest extends BitcoinSUnitTest {
implicit override val generatorDrivenConfig: PropertyCheckConfiguration =
generatorDrivenConfigNewCode
behavior of "SchnorrSigPointTestVector"
it should "have serialization symmetry" in {
val gen = for {
privKey <- CryptoGenerators.privateKey
privNonce <- CryptoGenerators.privateKey
hash <- CryptoGenerators.sha256Digest
} yield SchnorrSigPointTestVector(privKey, privNonce, hash)
forAll(gen) { schnorrSigPointTest =>
val schnorrSigPointTestResult =
SchnorrSigPointTestVector.fromJson(schnorrSigPointTest.toJson)
assert(schnorrSigPointTestResult.isSuccess)
assert(schnorrSigPointTestResult.get == schnorrSigPointTest)
}
}
it should "pass dlc_schnorr_test" in {
val vecResult = SchnorrSigPointTestVectorGen.readFromDefaultTestFile()
assert(vecResult.isSuccess)
vecResult.get.foldLeft(succeed) {
case (_, testVec) =>
assert(SchnorrSigPointTestVector(testVec.inputs) == testVec)
assert(
testVec.pubKey.computeSigPoint(testVec.msgHash.bytes,
testVec.pubNonce) == testVec.sigPoint)
}
}
}

View file

@ -0,0 +1,32 @@
package org.bitcoins.dlc.testgen
import org.scalacheck.Gen
object TestVectorUtil {
val p2wpkhInputGen: Gen[FundingInputTx] =
Gen.const(()).map(_ => DLCTxGen.fundingInputTx())
val p2wshInputGen: Gen[FundingInputTx] =
Gen.const(()).map(_ => DLCTxGen.multiSigFundingInputTx())
val p2shInputGen: Gen[FundingInputTx] =
Gen.const(()).map(_ => DLCTxGen.multiSigFundingInputTx(p2shNested = true))
val inputGen: Gen[FundingInputTx] =
Gen.oneOf(p2wpkhInputGen, p2wshInputGen, p2shInputGen)
val inputsGen: Gen[List[FundingInputTx]] =
Gen.oneOf(1, 2, 3, 8).flatMap(Gen.listOfN(_, inputGen))
val testInputGen: Gen[ValidTestInputs] =
Gen.choose(2, 100).flatMap { numOutcomes =>
inputsGen.flatMap { offerInputs =>
inputsGen.flatMap { acceptInputs =>
DLCTxGen.validTestInputsForInputs(offerInputs.toVector,
acceptInputs.toVector,
numOutcomes)
}
}
}
}

View file

@ -0,0 +1,202 @@
package org.bitcoins.dlc.wallet
import org.bitcoins.core.api.wallet.db.TransactionDbHelper
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.script.EmptyScriptPubKey
import org.bitcoins.core.protocol.tlv.{EnumOutcome, UnsignedNumericOutcome}
import org.bitcoins.core.protocol.transaction.{
TransactionOutPoint,
TransactionOutput
}
import org.bitcoins.crypto.{ECAdaptorSignature, Sha256DigestBE}
import org.bitcoins.db.CRUD
import org.bitcoins.dlc.wallet.models._
import org.bitcoins.testkit.fixtures.DLCDAOFixture
import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, DLCWalletUtil}
import org.scalatest.Assertion
import scala.concurrent.Future
class DLCDAOTest extends BitcoinSWalletTest with DLCDAOFixture {
behavior of "DLCDAO"
val dlcDb: DLCDb = DLCWalletUtil.sampleDLCDb
val paramHash: Sha256DigestBE = dlcDb.paramHash
def verifyDatabaseInsertion[ElementType, KeyType](
element: ElementType,
key: KeyType,
dao: CRUD[ElementType, KeyType],
dlcDAO: DLCDAO): Future[Assertion] = {
for {
_ <- dlcDAO.create(dlcDb)
_ <- dao.upsert(element) //upsert in case we are testing the dlcDAO
read <- dao.read(key)
} yield {
assert(read.contains(element))
}
}
it should "correctly insert a DLC into the database" in { daos =>
val dlcDAO = daos.dlcDAO
verifyDatabaseInsertion(dlcDb, paramHash, dlcDAO, dlcDAO)
}
it should "correctly insert a DLCOffer into the database" in { daos =>
val dlcDAO = daos.dlcDAO
val offerDAO = daos.dlcOfferDAO
val offerDb =
DLCOfferDbHelper.fromDLCOffer(DLCWalletUtil.sampleDLCOffer)
verifyDatabaseInsertion(offerDb, paramHash, offerDAO, dlcDAO)
}
it should "correctly insert a DLCAccept into the database" in { daos =>
val dlcDAO = daos.dlcDAO
val acceptDAO = daos.dlcAcceptDAO
val acceptDb =
DLCAcceptDbHelper.fromDLCAccept(paramHash, DLCWalletUtil.sampleDLCAccept)
verifyDatabaseInsertion(acceptDb, paramHash, acceptDAO, dlcDAO)
}
it should "correctly insert funding inputs into the database" in { daos =>
val dlcDAO = daos.dlcDAO
val inputsDAO = daos.dlcInputsDAO
val input = DLCFundingInputDb(
paramHash = paramHash,
isInitiator = true,
outPoint = TransactionOutPoint(testBlockHash, UInt32.zero),
output = TransactionOutput(Satoshis.one, EmptyScriptPubKey),
redeemScriptOpt = None,
witnessScriptOpt = Some(DLCWalletUtil.dummyScriptWitness)
)
verifyDatabaseInsertion(input, input.outPoint, inputsDAO, dlcDAO)
}
it should "correctly find funding inputs by eventId and isInitiator" in {
daos =>
val inputsDAO = daos.dlcInputsDAO
val dlcDAO = daos.dlcDAO
val inputs = Vector(
DLCFundingInputDb(
paramHash = paramHash,
isInitiator = true,
outPoint = TransactionOutPoint(testBlockHash, UInt32.zero),
output = TransactionOutput(Satoshis.one, EmptyScriptPubKey),
redeemScriptOpt = None,
witnessScriptOpt = Some(DLCWalletUtil.dummyScriptWitness)
),
DLCFundingInputDb(
paramHash = paramHash,
isInitiator = false,
outPoint = TransactionOutPoint(testBlockHash, UInt32.one),
output = TransactionOutput(Satoshis.one, EmptyScriptPubKey),
redeemScriptOpt = None,
witnessScriptOpt = Some(DLCWalletUtil.dummyScriptWitness)
),
DLCFundingInputDb(
paramHash = paramHash,
isInitiator = true,
outPoint = TransactionOutPoint(testBlockHash, UInt32(3)),
output = TransactionOutput(Satoshis.one, EmptyScriptPubKey),
redeemScriptOpt = None,
witnessScriptOpt = Some(DLCWalletUtil.dummyScriptWitness)
)
)
for {
_ <- dlcDAO.create(dlcDb)
_ <- inputsDAO.createAll(inputs)
readInput <- inputsDAO.findByParamHash(paramHash, isInitiator = true)
} yield assert(readInput.size == 2)
}
it should "correctly insert enum outcome CET signatures into the database" in {
daos =>
val dlcDAO = daos.dlcDAO
val sigsDAO = daos.dlcSigsDAO
val sig = DLCCETSignatureDb(
paramHash = paramHash,
isInitiator = true,
outcome = EnumOutcome(DLCWalletUtil.winStr),
signature = ECAdaptorSignature.dummy
)
verifyDatabaseInsertion(sig,
(sig.paramHash, sig.outcome),
sigsDAO,
dlcDAO)
}
it should "correctly insert unsigned numeric outcome CET signatures into the database" in {
daos =>
val dlcDAO = daos.dlcDAO
val sigsDAO = daos.dlcSigsDAO
val outcomes = 0.to(100).toVector
val sig = DLCCETSignatureDb(
paramHash = paramHash,
isInitiator = true,
outcome = UnsignedNumericOutcome(outcomes),
signature = ECAdaptorSignature.dummy
)
verifyDatabaseInsertion(sig,
(sig.paramHash, sig.outcome),
sigsDAO,
dlcDAO)
}
it should "correctly find CET signatures by eventId" in { daos =>
val dlcDAO = daos.dlcDAO
val sigsDAO = daos.dlcSigsDAO
val sigs = Vector(
DLCCETSignatureDb(
paramHash = paramHash,
isInitiator = true,
outcome = EnumOutcome(DLCWalletUtil.winStr),
signature = ECAdaptorSignature.dummy
),
DLCCETSignatureDb(
paramHash = paramHash,
isInitiator = false,
outcome = EnumOutcome(DLCWalletUtil.loseStr),
signature = ECAdaptorSignature.dummy
)
)
for {
_ <- dlcDAO.create(dlcDb)
_ <- sigsDAO.createAll(sigs)
readInput <- sigsDAO.findByParamHash(paramHash)
} yield {
assert(readInput.size == 2)
// Do it this way so ordering doesn't matter
assert(readInput.contains(sigs.head))
assert(readInput.contains(sigs.last))
}
}
it should "correctly insert txs into the database" in { daos =>
val remoteTxDAO = daos.dlcRemoteTxDAO
val tx = TransactionDbHelper.fromTransaction(DLCWalletUtil.dummyPrevTx)
verifyDatabaseInsertion(tx, tx.txIdBE, remoteTxDAO, daos.dlcDAO)
}
}

View file

@ -0,0 +1,300 @@
package org.bitcoins.dlc.wallet
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.{
ContractInfo,
MultiNonceContractInfo,
SingleNonceContractInfo
}
import org.bitcoins.commons.jsonmodels.dlc.DLCState
import org.bitcoins.commons.jsonmodels.dlc.DLCStatus.{
Claimed,
Refunded,
RemoteClaimed
}
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.script.interpreter.ScriptInterpreter
import org.bitcoins.crypto.{CryptoUtil, SchnorrDigitalSignature}
import org.bitcoins.testkit.wallet.DLCWalletUtil._
import org.bitcoins.testkit.wallet.{BitcoinSDualWalletTest, DLCWalletUtil}
import org.scalatest.FutureOutcome
class DLCExecutionTest extends BitcoinSDualWalletTest {
type FixtureParam = (InitializedDLCWallet, InitializedDLCWallet)
override def withFixture(test: OneArgAsyncTest): FutureOutcome = {
withDualDLCWallets(test, multiNonce = false)
}
behavior of "DLCWallet"
def getSigs(contractInfo: ContractInfo): (
SchnorrDigitalSignature,
SchnorrDigitalSignature) = {
val info: SingleNonceContractInfo = contractInfo match {
case info: SingleNonceContractInfo => info
case _: MultiNonceContractInfo =>
throw new IllegalArgumentException("Unexpected Contract Info")
}
// Get a hash that the initiator wins for
val initiatorWinStr =
info
.maxBy(_._2.toLong)
._1
.outcome
val initiatorWinSig = DLCWalletUtil.oraclePrivKey
.schnorrSignWithNonce(CryptoUtil.sha256(initiatorWinStr).bytes,
DLCWalletUtil.kValue)
// Get a hash that the recipient wins for
val recipientWinStr =
info.find(_._2 == Satoshis.zero).get._1.outcome
val recipientWinSig = DLCWalletUtil.oraclePrivKey
.schnorrSignWithNonce(CryptoUtil.sha256(recipientWinStr).bytes,
DLCWalletUtil.kValue)
(initiatorWinSig, recipientWinSig)
}
it must "get the correct funding transaction" in { wallets =>
val dlcA = wallets._1.wallet
val dlcB = wallets._2.wallet
for {
contractId <- getContractId(dlcA)
offerDb <- getInitialOffer(dlcA)
paramHash = offerDb.paramHash
offerOpt <- dlcA.dlcOfferDAO.findByParamHash(paramHash)
acceptOpt <- dlcB.dlcAcceptDAO.findByParamHash(paramHash)
inputsA <- dlcA.dlcInputsDAO.findByParamHash(paramHash)
inputsB <- dlcB.dlcInputsDAO.findByParamHash(paramHash)
fundingTx <- dlcB.getDLCFundingTx(contractId)
} yield {
assert(offerOpt.isDefined)
assert(acceptOpt.isDefined)
val offer = offerOpt.get
val accept = acceptOpt.get
val comparableInputsA = inputsA
.sortBy(_.outPoint.hex)
.map(_.copy(redeemScriptOpt = None, witnessScriptOpt = None))
val comparableInputsB =
inputsB
.sortBy(_.outPoint.hex)
.map(
_.copy(redeemScriptOpt = None, witnessScriptOpt = None)
) // initiator will not have funding sigs
assert(comparableInputsA == comparableInputsB)
val fundingTxOutpoints = fundingTx.inputs.map(_.previousOutput)
val outpointsA = inputsA.map(_.outPoint)
val outpointsB = inputsB.map(_.outPoint)
assert(fundingTxOutpoints.diff(outpointsA ++ outpointsB).isEmpty)
assert(fundingTx.outputs.size == 3)
assert(
fundingTx.outputs.exists(
_.scriptPubKey == offer.changeAddress.scriptPubKey))
assert(
fundingTx.outputs.exists(
_.scriptPubKey == accept.changeAddress.scriptPubKey))
assert(ScriptInterpreter.checkTransaction(fundingTx))
val fundingTxPrevOutputRefs = inputsA.map(_.toOutputReference) ++ inputsB
.map(_.toOutputReference)
val fundingTxVerify = fundingTx.inputs.zipWithIndex.forall {
case (input, index) =>
val output = fundingTxPrevOutputRefs
.find(_.outPoint == input.previousOutput)
.get
.output
verifyInput(fundingTx, index, output)
}
assert(fundingTxVerify)
}
}
it must "do a unilateral close as the initiator" in { wallets =>
for {
contractId <- getContractId(wallets._1.wallet)
offer <- getInitialOffer(wallets._1.wallet)
(sig, _) = getSigs(offer.contractInfo)
func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sig)
result <- dlcExecutionTest(wallets = wallets,
asInitiator = true,
func = func,
expectedOutputs = 1)
_ = assert(result)
dlcDbAOpt <- wallets._1.wallet.dlcDAO.findByContractId(contractId)
dlcDbBOpt <- wallets._2.wallet.dlcDAO.findByContractId(contractId)
paramHash = dlcDbAOpt.get.paramHash
statusAOpt <- wallets._1.wallet.findDLC(paramHash)
statusBOpt <- wallets._2.wallet.findDLC(paramHash)
_ = {
(statusAOpt, statusBOpt) match {
case (Some(statusA: Claimed), Some(statusB: RemoteClaimed)) =>
assert(statusA.oracleSigs == Vector(statusB.oracleSig))
case (_, _) => fail()
}
}
} yield {
(dlcDbAOpt, dlcDbBOpt) match {
case (Some(dlcA), Some(dlcB)) =>
assert(dlcA.state == DLCState.Claimed)
assert(dlcB.state == DLCState.RemoteClaimed)
case (_, _) => fail()
}
}
}
it must "do a unilateral close as the recipient" in { wallets =>
for {
contractId <- getContractId(wallets._1.wallet)
offer <- getInitialOffer(wallets._2.wallet)
(_, sig) = getSigs(offer.contractInfo)
func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sig)
result <- dlcExecutionTest(wallets = wallets,
asInitiator = false,
func = func,
expectedOutputs = 1)
_ = assert(result)
dlcDbAOpt <- wallets._1.wallet.dlcDAO.findByContractId(contractId)
dlcDbBOpt <- wallets._2.wallet.dlcDAO.findByContractId(contractId)
paramHash = dlcDbAOpt.get.paramHash
statusAOpt <- wallets._1.wallet.findDLC(paramHash)
statusBOpt <- wallets._2.wallet.findDLC(paramHash)
_ = {
(statusAOpt, statusBOpt) match {
case (Some(statusA: RemoteClaimed), Some(statusB: Claimed)) =>
assert(Vector(statusA.oracleSig) == statusB.oracleSigs)
case (_, _) => fail()
}
}
} yield {
(dlcDbAOpt, dlcDbBOpt) match {
case (Some(dlcA), Some(dlcB)) =>
assert(dlcA.state == DLCState.RemoteClaimed)
assert(dlcB.state == DLCState.Claimed)
case (_, _) => fail()
}
}
}
it must "fail to do losing unilateral close" in { wallets =>
val dlcA = wallets._1.wallet
val executeDLCForceCloseF = for {
contractId <- getContractId(wallets._1.wallet)
offer <- getInitialOffer(dlcA)
(_, sig) = getSigs(offer.contractInfo)
tx <- dlcA.executeDLC(contractId, sig)
} yield tx
recoverToSucceededIf[UnsupportedOperationException](executeDLCForceCloseF)
}
it must "do a refund on a dlc as the initiator" in { wallets =>
for {
contractId <- getContractId(wallets._1.wallet)
func = (wallet: DLCWallet) => wallet.executeDLCRefund(contractId)
result <- dlcExecutionTest(wallets = wallets,
asInitiator = true,
func = func,
expectedOutputs = 2)
_ = assert(result)
dlcDbAOpt <- wallets._1.wallet.dlcDAO.findByContractId(contractId)
dlcDbBOpt <- wallets._2.wallet.dlcDAO.findByContractId(contractId)
paramHash = dlcDbAOpt.get.paramHash
statusAOpt <- wallets._1.wallet.findDLC(paramHash)
statusBOpt <- wallets._2.wallet.findDLC(paramHash)
_ = {
(statusAOpt, statusBOpt) match {
case (Some(statusA: Refunded), Some(statusB: Refunded)) =>
assert(statusA.closingTxId == statusB.closingTxId)
case (_, _) => fail()
}
}
} yield {
(dlcDbAOpt, dlcDbBOpt) match {
case (Some(dlcA), Some(dlcB)) =>
assert(dlcA.state == DLCState.Refunded)
assert(dlcB.state == DLCState.Refunded)
case (_, _) => fail()
}
}
}
it must "do a refund on a dlc as the recipient" in { wallets =>
for {
contractId <- getContractId(wallets._1.wallet)
func = (wallet: DLCWallet) => wallet.executeDLCRefund(contractId)
result <- dlcExecutionTest(wallets = wallets,
asInitiator = false,
func = func,
expectedOutputs = 2)
_ = assert(result)
dlcDbAOpt <- wallets._1.wallet.dlcDAO.findByContractId(contractId)
dlcDbBOpt <- wallets._2.wallet.dlcDAO.findByContractId(contractId)
paramHash = dlcDbAOpt.get.paramHash
statusAOpt <- wallets._1.wallet.findDLC(paramHash)
statusBOpt <- wallets._2.wallet.findDLC(paramHash)
_ = {
(statusAOpt, statusBOpt) match {
case (Some(statusA: Refunded), Some(statusB: Refunded)) =>
assert(statusA.closingTxId == statusB.closingTxId)
case (_, _) => fail()
}
}
} yield {
(dlcDbAOpt, dlcDbBOpt) match {
case (Some(dlcA), Some(dlcB)) =>
assert(dlcA.state == DLCState.Refunded)
assert(dlcB.state == DLCState.Refunded)
case (_, _) => fail()
}
}
}
it must "fail to execute with an empty vec of sigs" in { wallets =>
val dlcA = wallets._1.wallet
val executeDLCForceCloseF = for {
contractId <- getContractId(wallets._1.wallet)
tx <- dlcA.executeDLC(contractId, Vector.empty)
} yield tx
recoverToSucceededIf[IllegalArgumentException](executeDLCForceCloseF)
}
}

View file

@ -0,0 +1,165 @@
package org.bitcoins.dlc.wallet
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.{
ContractInfo,
MultiNonceContractInfo,
SingleNonceContractInfo
}
import org.bitcoins.commons.jsonmodels.dlc.DLCState
import org.bitcoins.commons.jsonmodels.dlc.DLCStatus.{Claimed, RemoteClaimed}
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.protocol.tlv.{EnumOutcome, UnsignedNumericOutcome}
import org.bitcoins.crypto._
import org.bitcoins.testkit.wallet.DLCWalletUtil._
import org.bitcoins.testkit.wallet.{BitcoinSDualWalletTest, DLCWalletUtil}
import org.scalatest.FutureOutcome
class DLCMultiNonceExecutionTest extends BitcoinSDualWalletTest {
type FixtureParam = (InitializedDLCWallet, InitializedDLCWallet)
override def withFixture(test: OneArgAsyncTest): FutureOutcome = {
withDualDLCWallets(test, multiNonce = true)
}
behavior of "DLCWallet"
def getSigs(contractInfo: ContractInfo): (
Vector[SchnorrDigitalSignature],
Vector[SchnorrDigitalSignature]) = {
val multiNonceContractInfo: MultiNonceContractInfo = contractInfo match {
case info: MultiNonceContractInfo => info
case _: SingleNonceContractInfo =>
throw new IllegalArgumentException("Unexpected Contract Info")
}
val initiatorWinVec =
multiNonceContractInfo.outcomeVec
.maxBy(_._2.toLong)
._1
val kValues = DLCWalletUtil.kValues.take(initiatorWinVec.size)
val initiatorWinSigs = initiatorWinVec.zip(kValues).map {
case (num, kValue) =>
DLCWalletUtil.oraclePrivKey
.schnorrSignWithNonce(CryptoUtil.sha256(num.toString).bytes, kValue)
}
val recipientWinVec =
multiNonceContractInfo.outcomeVec.find(_._2 == Satoshis.zero).get._1
val kValues2 = DLCWalletUtil.kValues.take(recipientWinVec.size)
val recipientWinSigs = recipientWinVec.zip(kValues2).map {
case (num, kValue) =>
DLCWalletUtil.oraclePrivKey
.schnorrSignWithNonce(CryptoUtil.sha256(num.toString).bytes, kValue)
}
(initiatorWinSigs, recipientWinSigs)
}
it must "execute as the initiator" in { wallets =>
for {
contractId <- getContractId(wallets._1.wallet)
offer <- getInitialOffer(wallets._1.wallet)
(sigs, _) = getSigs(offer.contractInfo)
func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sigs)
result <- dlcExecutionTest(wallets = wallets,
asInitiator = true,
func = func,
expectedOutputs = 1)
_ = assert(result)
dlcDbAOpt <- wallets._1.wallet.dlcDAO.findByContractId(contractId)
dlcDbBOpt <- wallets._2.wallet.dlcDAO.findByContractId(contractId)
paramHash = dlcDbAOpt.get.paramHash
statusAOpt <- wallets._1.wallet.findDLC(paramHash)
statusBOpt <- wallets._2.wallet.findDLC(paramHash)
_ = {
(statusAOpt, statusBOpt) match {
case (Some(statusA: Claimed), Some(statusB: RemoteClaimed)) =>
verifyingMatchingOracleSigs(statusA, statusB)
case (_, _) => fail()
}
}
} yield {
(dlcDbAOpt, dlcDbBOpt) match {
case (Some(dlcA), Some(dlcB)) =>
assert(dlcA.state == DLCState.Claimed)
assert(dlcB.state == DLCState.RemoteClaimed)
case (_, _) => fail()
}
}
}
it must "execute as the recipient" in { wallets =>
for {
contractId <- getContractId(wallets._1.wallet)
offer <- getInitialOffer(wallets._2.wallet)
(_, sigs) = getSigs(offer.contractInfo)
func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sigs)
result <- dlcExecutionTest(wallets = wallets,
asInitiator = false,
func = func,
expectedOutputs = 1)
_ = assert(result)
dlcDbAOpt <- wallets._1.wallet.dlcDAO.findByContractId(contractId)
dlcDbBOpt <- wallets._2.wallet.dlcDAO.findByContractId(contractId)
paramHash = dlcDbAOpt.get.paramHash
statusAOpt <- wallets._1.wallet.findDLC(paramHash)
statusBOpt <- wallets._2.wallet.findDLC(paramHash)
_ = {
(statusAOpt, statusBOpt) match {
case (Some(statusA: RemoteClaimed), Some(statusB: Claimed)) =>
verifyingMatchingOracleSigs(statusB, statusA)
case (_, _) => fail()
}
}
} yield {
(dlcDbAOpt, dlcDbBOpt) match {
case (Some(dlcA), Some(dlcB)) =>
assert(dlcA.state == DLCState.RemoteClaimed)
assert(dlcB.state == DLCState.Claimed)
case (_, _) => fail()
}
}
}
private def verifyingMatchingOracleSigs(
statusA: Claimed,
statusB: RemoteClaimed): Boolean = {
val outcome = statusB.outcome
val numSigs = outcome match {
case EnumOutcome(outcome) =>
throw new RuntimeException(s"Unexpected outcome type, got $outcome")
case UnsignedNumericOutcome(digits) => digits.size
}
val aggR = statusA.oracleSigs
.take(numSigs)
.map(_.rx.publicKey)
.reduce(_.add(_))
.schnorrNonce
val aggS = statusA.oracleSigs
.take(numSigs)
.map(_.sig)
.reduce(_.add(_))
val aggregateSignature =
SchnorrDigitalSignature(aggR, aggS)
aggregateSignature == statusB.oracleSig
}
}

View file

@ -0,0 +1,455 @@
package org.bitcoins.dlc.wallet
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage._
import org.bitcoins.commons.jsonmodels.dlc._
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.crypto._
import org.bitcoins.testkit.wallet.DLCWalletUtil._
import org.bitcoins.testkit.wallet.FundWalletUtil.FundedDLCWallet
import org.bitcoins.testkit.wallet.{BitcoinSDualWalletTest, DLCWalletUtil}
import org.scalatest.{Assertion, FutureOutcome}
import scala.concurrent.Future
class WalletDLCSetupTest extends BitcoinSDualWalletTest {
type FixtureParam = (FundedDLCWallet, FundedDLCWallet)
override def withFixture(test: OneArgAsyncTest): FutureOutcome = {
withDualFundedDLCWallets(test)
}
behavior of "DLCWallet"
def testNegotiate(
fundedDLCWallets: (FundedDLCWallet, FundedDLCWallet),
offerData: DLCOffer): Future[Assertion] = {
val walletA = fundedDLCWallets._1.wallet
val walletB = fundedDLCWallets._2.wallet
for {
offer <- walletA.createDLCOffer(
offerData.oracleInfo,
offerData.contractInfo,
offerData.totalCollateral,
Some(offerData.feeRate),
offerData.timeouts.contractMaturity.toUInt32,
offerData.timeouts.contractTimeout.toUInt32
)
paramHash = offer.paramHash
dlcA1Opt <- walletA.dlcDAO.read(paramHash)
find1 <- walletA.findDLC(paramHash)
_ = {
assert(dlcA1Opt.isDefined)
assert(find1.isDefined)
assert(dlcA1Opt.get.state == DLCState.Offered)
assert(offer.oracleInfo == offerData.oracleInfo)
assert(offer.contractInfo == offerData.contractInfo)
assert(offer.totalCollateral == offerData.totalCollateral)
assert(offer.feeRate == offerData.feeRate)
assert(offer.timeouts == offerData.timeouts)
assert(offer.fundingInputs.nonEmpty)
assert(offer.changeAddress.value.nonEmpty)
}
accept <- walletB.acceptDLCOffer(offer)
dlcB1Opt <- walletB.dlcDAO.read(paramHash)
_ = {
assert(dlcB1Opt.isDefined)
assert(dlcB1Opt.get.state == DLCState.Accepted)
assert(accept.fundingInputs.nonEmpty)
assert(
accept.fundingInputs
.map(_.output.value)
.sum >= accept.totalCollateral)
assert(
accept.totalCollateral == offer.contractInfo.max - offer.totalCollateral)
assert(accept.changeAddress.value.nonEmpty)
}
sign <- walletA.signDLC(accept)
dlcA2Opt <- walletA.dlcDAO.read(paramHash)
_ = {
assert(dlcA2Opt.isDefined)
assert(dlcA2Opt.get.state == DLCState.Signed)
assert(sign.fundingSigs.length == offerData.fundingInputs.size)
}
dlcDb <- walletB.addDLCSigs(sign)
_ = assert(dlcDb.state == DLCState.Signed)
outcomeSigs <- walletB.dlcSigsDAO.findByParamHash(offer.paramHash)
refundSigsA <-
walletA.dlcRefundSigDAO
.findByParamHash(paramHash)
.map(_.map(_.refundSig))
refundSigsB <-
walletB.dlcRefundSigDAO
.findByParamHash(paramHash)
.map(_.map(_.refundSig))
walletAChange <- walletA.addressDAO.read(offer.changeAddress)
walletAFinal <- walletA.addressDAO.read(offer.pubKeys.payoutAddress)
walletBChange <- walletB.addressDAO.read(accept.changeAddress)
walletBFinal <- walletB.addressDAO.read(accept.pubKeys.payoutAddress)
} yield {
assert(dlcDb.contractIdOpt.get == sign.contractId)
assert(refundSigsA.size == 2)
assert(refundSigsA.forall(refundSigsB.contains))
assert(sign.cetSigs.outcomeSigs.forall(sig =>
outcomeSigs.exists(dbSig => (dbSig.outcome, dbSig.signature) == sig)))
// Test that the Addresses are in the wallet's database
assert(walletAChange.isDefined)
assert(walletAFinal.isDefined)
assert(walletBChange.isDefined)
assert(walletBFinal.isDefined)
}
}
it must "correctly negotiate a dlc" in {
fundedDLCWallets: (FundedDLCWallet, FundedDLCWallet) =>
testNegotiate(fundedDLCWallets, DLCWalletUtil.sampleDLCOffer)
}
it must "correctly negotiate a dlc with a multi-nonce oracle info" in {
fundedDLCWallets: (FundedDLCWallet, FundedDLCWallet) =>
testNegotiate(fundedDLCWallets, DLCWalletUtil.sampleMultiNonceDLCOffer)
}
it must "correctly negotiate a dlc using TLVs" in {
fundedDLCWallets: (FundedDLCWallet, FundedDLCWallet) =>
val walletA = fundedDLCWallets._1.wallet
val walletB = fundedDLCWallets._2.wallet
val offerData = DLCWalletUtil.sampleDLCOffer
for {
offer <- walletA.createDLCOffer(
offerData.oracleInfo,
offerData.contractInfo.toTLV,
offerData.totalCollateral,
Some(offerData.feeRate),
offerData.timeouts.contractMaturity.toUInt32,
offerData.timeouts.contractTimeout.toUInt32
)
paramHash = offer.paramHash
dlcA1Opt <- walletA.dlcDAO.read(paramHash)
_ = {
assert(dlcA1Opt.isDefined)
assert(dlcA1Opt.get.state == DLCState.Offered)
assert(offer.oracleInfo == offerData.oracleInfo)
assert(offer.contractInfo == offerData.contractInfo)
assert(offer.totalCollateral == offerData.totalCollateral)
assert(offer.feeRate == offerData.feeRate)
assert(offer.timeouts == offerData.timeouts)
assert(offer.fundingInputs.nonEmpty)
assert(offer.changeAddress.value.nonEmpty)
}
accept <- walletB.acceptDLCOffer(offer.toTLV)
dlcB1Opt <- walletB.dlcDAO.read(paramHash)
_ = {
assert(dlcB1Opt.isDefined)
assert(dlcB1Opt.get.state == DLCState.Accepted)
assert(accept.fundingInputs.nonEmpty)
assert(
accept.fundingInputs
.map(_.output.value)
.sum >= accept.totalCollateral)
assert(
accept.totalCollateral == offer.contractInfo.max - offer.totalCollateral)
assert(accept.changeAddress.value.nonEmpty)
}
sign <- walletA.signDLC(accept.toTLV)
dlcA2Opt <- walletA.dlcDAO.read(paramHash)
_ = {
assert(dlcA2Opt.isDefined)
assert(dlcA2Opt.get.state == DLCState.Signed)
assert(sign.fundingSigs.length == offerData.fundingInputs.size)
}
dlcDb <- walletB.addDLCSigs(sign.toTLV)
_ = assert(dlcDb.state == DLCState.Signed)
outcomeSigs <- walletB.dlcSigsDAO.findByParamHash(offer.paramHash)
refundSigsA <-
walletA.dlcRefundSigDAO
.findByParamHash(paramHash)
.map(_.map(_.refundSig))
refundSigsB <-
walletB.dlcRefundSigDAO
.findByParamHash(paramHash)
.map(_.map(_.refundSig))
walletAChange <- walletA.addressDAO.read(offer.changeAddress)
walletAFinal <- walletA.addressDAO.read(offer.pubKeys.payoutAddress)
walletBChange <- walletB.addressDAO.read(accept.changeAddress)
walletBFinal <- walletB.addressDAO.read(accept.pubKeys.payoutAddress)
} yield {
assert(dlcDb.contractIdOpt.get == sign.contractId)
assert(refundSigsA.size == 2)
assert(refundSigsA.forall(refundSigsB.contains))
assert(sign.cetSigs.outcomeSigs.forall(sig =>
outcomeSigs.exists(dbSig => (dbSig.outcome, dbSig.signature) == sig)))
// Test that the Addresses are in the wallet's database
assert(walletAChange.isDefined)
assert(walletAFinal.isDefined)
assert(walletBChange.isDefined)
assert(walletBFinal.isDefined)
}
}
def getDLCReadyToAddSigs(
walletA: DLCWallet,
walletB: DLCWallet,
offerData: DLCOffer = DLCWalletUtil.sampleDLCOffer): Future[DLCSign] = {
for {
accept <- getDLCReadyToSign(walletA, walletB, offerData)
sign <- walletA.signDLC(accept)
} yield sign
}
def getDLCReadyToSign(
walletA: DLCWallet,
walletB: DLCWallet,
offerData: DLCOffer = DLCWalletUtil.sampleDLCOffer): Future[DLCAccept] = {
for {
offer <- walletA.createDLCOffer(
offerData.oracleInfo,
offerData.contractInfo,
offerData.totalCollateral,
Some(offerData.feeRate),
offerData.timeouts.contractMaturity.toUInt32,
offerData.timeouts.contractTimeout.toUInt32
)
accept <- walletB.acceptDLCOffer(offer)
} yield accept
}
def testDLCSignVerification(
walletA: DLCWallet,
walletB: DLCWallet,
makeDLCSignInvalid: DLCSign => DLCSign): Future[Assertion] = {
val failedAddSigsF = for {
sign <- getDLCReadyToAddSigs(walletA, walletB)
invalidSign = makeDLCSignInvalid(sign)
dlcDb <- walletB.addDLCSigs(invalidSign)
} yield dlcDb
recoverToSucceededIf[IllegalArgumentException](failedAddSigsF)
}
def testDLCAcceptVerification(
walletA: DLCWallet,
walletB: DLCWallet,
makeDLCAcceptInvalid: DLCAccept => DLCAccept): Future[Assertion] = {
val failedAddSigsF = for {
accept <- getDLCReadyToSign(walletA, walletB)
invalidSign = makeDLCAcceptInvalid(accept)
dlcDb <- walletA.signDLC(invalidSign)
} yield dlcDb
recoverToSucceededIf[IllegalArgumentException](failedAddSigsF)
}
it must "fail to add dlc funding sigs that are invalid" in {
FundedDLCWallets: (FundedDLCWallet, FundedDLCWallet) =>
val walletA = FundedDLCWallets._1.wallet
val walletB = FundedDLCWallets._2.wallet
testDLCSignVerification(
walletA,
walletB,
(sign: DLCSign) =>
sign.copy(fundingSigs = DLCWalletUtil.dummyFundingSignatures))
}
it must "fail to add dlc cet sigs that are invalid" in {
FundedDLCWallets: (FundedDLCWallet, FundedDLCWallet) =>
val walletA = FundedDLCWallets._1.wallet
val walletB = FundedDLCWallets._2.wallet
testDLCSignVerification(
walletA,
walletB,
(sign: DLCSign) =>
sign.copy(
cetSigs = CETSignatures(DLCWalletUtil.dummyOutcomeSigs,
sign.cetSigs.refundSig)))
}
it must "fail to add an invalid dlc refund sig" in {
FundedDLCWallets: (FundedDLCWallet, FundedDLCWallet) =>
val walletA = FundedDLCWallets._1.wallet
val walletB = FundedDLCWallets._2.wallet
testDLCSignVerification(
walletA,
walletB,
(sign: DLCSign) =>
sign.copy(
cetSigs = CETSignatures(sign.cetSigs.outcomeSigs,
DLCWalletUtil.dummyPartialSig)))
}
it must "fail to sign dlc with cet sigs that are invalid" in {
FundedDLCWallets: (FundedDLCWallet, FundedDLCWallet) =>
val walletA = FundedDLCWallets._1.wallet
val walletB = FundedDLCWallets._2.wallet
testDLCAcceptVerification(
walletA,
walletB,
(accept: DLCAccept) =>
accept.copy(
cetSigs = CETSignatures(DLCWalletUtil.dummyOutcomeSigs,
accept.cetSigs.refundSig)))
}
it must "fail to sign dlc with an invalid refund sig" in {
FundedDLCWallets: (FundedDLCWallet, FundedDLCWallet) =>
val walletA = FundedDLCWallets._1.wallet
val walletB = FundedDLCWallets._2.wallet
testDLCAcceptVerification(
walletA,
walletB,
(accept: DLCAccept) =>
accept.copy(
cetSigs = CETSignatures(accept.cetSigs.outcomeSigs,
DLCWalletUtil.dummyPartialSig)))
}
it must "setup and execute with lloyd's example" in {
FundedDLCWallets: (FundedDLCWallet, FundedDLCWallet) =>
val walletA = FundedDLCWallets._1.wallet
val walletB = FundedDLCWallets._2.wallet
val winStr = "Democrat_win"
val loseStr = "Republican_win"
val drawStr = "other"
val betSize = 10000
lazy val contractInfo: ContractInfo =
SingleNonceContractInfo.fromStringVec(
Vector(winStr -> Satoshis(betSize),
loseStr -> Satoshis.zero,
drawStr -> Satoshis(betSize / 2)))
val oraclePubKey = SchnorrPublicKey(
"156c7d1c7922f0aa1168d9e21ac77ea88bbbe05e24e70a08bbe0519778f2e5da")
val oracleNonce = SchnorrNonce(
"ea3a68d8749b81682513b0479418d289d17e24d4820df2ce979f1a56a63ca525")
val attestation = FieldElement(
"77a5aabd716936411bbe19219bd0b261fae8f0524367268feb264e0a3b215766")
val oracleInfo = SingleNonceOracleInfo(oraclePubKey, oracleNonce)
val offerData = DLCOffer(
OracleAndContractInfo(oracleInfo, contractInfo),
dummyDLCKeys,
Satoshis(5000),
Vector(dummyFundingInputs.head),
dummyAddress,
SatoshisPerVirtualByte(Satoshis(3)),
dummyTimeouts
)
val oracleSig = SchnorrDigitalSignature(oracleNonce, attestation)
val paramHash = DLCMessage.calcParamHash(offerData.oracleInfo,
offerData.contractInfo,
offerData.timeouts)
for {
offer <- walletA.createDLCOffer(
offerData.oracleInfo,
offerData.contractInfo,
offerData.totalCollateral,
Some(offerData.feeRate),
offerData.timeouts.contractMaturity.toUInt32,
offerData.timeouts.contractTimeout.toUInt32
)
_ = {
assert(offer.oracleInfo == offerData.oracleInfo)
assert(offer.contractInfo == offerData.contractInfo)
assert(offer.totalCollateral == offerData.totalCollateral)
assert(offer.feeRate == offerData.feeRate)
assert(offer.timeouts == offerData.timeouts)
assert(offer.fundingInputs.nonEmpty)
assert(offer.changeAddress.value.nonEmpty)
}
accept <- walletB.acceptDLCOffer(offer)
_ = {
assert(accept.fundingInputs.nonEmpty)
assert(
accept.totalCollateral == offer.contractInfo.max - offer.totalCollateral)
assert(accept.changeAddress.value.nonEmpty)
}
sign <- walletA.signDLC(accept)
_ = {
assert(sign.fundingSigs.length == offerData.fundingInputs.size)
}
dlcDb <- walletB.addDLCSigs(sign)
outcomeSigs <- walletB.dlcSigsDAO.findByParamHash(offer.paramHash)
refundSigsA <-
walletA.dlcRefundSigDAO
.findByParamHash(paramHash)
.map(_.map(_.refundSig))
refundSigsB <-
walletB.dlcRefundSigDAO
.findByParamHash(paramHash)
.map(_.map(_.refundSig))
walletAChange <- walletA.addressDAO.read(offer.changeAddress)
walletAFinal <- walletA.addressDAO.read(offer.pubKeys.payoutAddress)
walletBChange <- walletB.addressDAO.read(accept.changeAddress)
walletBFinal <- walletB.addressDAO.read(accept.pubKeys.payoutAddress)
_ = {
assert(dlcDb.contractIdOpt.get == sign.contractId)
assert(refundSigsA.size == 2)
assert(refundSigsA.forall(refundSigsB.contains))
assert(sign.cetSigs.outcomeSigs.forall(sig =>
outcomeSigs.exists(dbSig =>
(dbSig.outcome, dbSig.signature) == sig)))
// Test that the Addresses are in the wallet's database
assert(walletAChange.isDefined)
assert(walletAFinal.isDefined)
assert(walletBChange.isDefined)
assert(walletBFinal.isDefined)
}
tx <- walletB.broadcastDLCFundingTx(sign.contractId)
_ <- walletA.processTransaction(tx, None)
func =
(wallet: DLCWallet) => wallet.executeDLC(sign.contractId, oracleSig)
result <- dlcExecutionTest(dlcA = walletA,
dlcB = walletB,
asInitiator = true,
func = func,
expectedOutputs = 1)
} yield assert(result)
}
}

View file

@ -0,0 +1,93 @@
CREATE TABLE "wallet_dlcs"
(
"param_hash" TEXT NOT NULL UNIQUE,
"temp_contract_id" TEXT NOT NULL UNIQUE,
"contract_id" TEXT UNIQUE,
"state" TEXT NOT NULL,
"is_initiator" BOOLEAN NOT NULL,
"account" TEXT NOT NULL,
"key_index" INTEGER NOT NULL,
"oracle_sigs" TEXT,
"funding_outpoint" TEXT,
"funding_tx_id" TEXT,
"closing_tx_id" TEXT,
constraint "pk_dlc" primary key ("param_hash")
);
CREATE INDEX "wallet_dlcs_param_hash_index" on "wallet_dlcs" ("param_hash");
CREATE TABLE "wallet_dlc_offers"
(
"param_hash" VARCHAR(254) NOT NULL UNIQUE,
"temp_contract_id" TEXT NOT NULL UNIQUE,
"oracle_info_tlv" TEXT NOT NULL,
"contract_info" TEXT NOT NULL,
"contract_maturity" TEXT NOT NULL,
"contract_timeout" TEXT NOT NULL,
"funding_key" TEXT NOT NULL,
"payout_address" TEXT NOT NULL,
"total_collateral" INTEGER NOT NULL,
"fee_rate" TEXT,
"change_address" TEXT NOT NULL,
constraint "pk_dlc_offer" primary key ("param_hash"),
constraint "fk_param_hash" foreign key ("param_hash") references "wallet_dlcs" ("param_hash"),
constraint "fk_temp_contract_id" foreign key ("temp_contract_id") references "wallet_dlcs" ("temp_contract_id") on update NO ACTION on delete NO ACTION
);
CREATE INDEX "wallet_dlc_offers_param_hash_index" on "wallet_dlc_offers" ("param_hash");
CREATE TABLE "wallet_dlc_accepts"
(
"param_hash" VARCHAR(254) NOT NULL UNIQUE,
"temp_contract_id" TEXT NOT NULL UNIQUE,
"funding_key" TEXT NOT NULL,
"payout_address" TEXT NOT NULL,
"total_collateral" INTEGER NOT NULL,
"change_address" TEXT NOT NULL,
constraint "pk_dlc_accept" primary key ("param_hash"),
constraint "fk_param_hash" foreign key ("param_hash") references "wallet_dlcs" ("param_hash"),
constraint "fk_temp_contract_id" foreign key ("temp_contract_id") references "wallet_dlcs" ("temp_contract_id") on update NO ACTION on delete NO ACTION
);
CREATE INDEX "wallet_dlc_accepts_param_hash_index" on "wallet_dlc_accepts" ("param_hash");
CREATE TABLE "wallet_dlc_funding_inputs"
(
"param_hash" VARCHAR(254) NOT NULL UNIQUE,
"is_initiator" BOOLEAN NOT NULL,
"out_point" TEXT NOT NULL UNIQUE,
"output" TEXT NOT NULL,
"redeem_script_opt" TEXT,
"witness_script_opt" TEXT,
constraint "pk_dlc_input" primary key ("out_point"),
constraint "fk_param_hash" foreign key ("param_hash") references "wallet_dlcs" ("param_hash") on update NO ACTION on delete NO ACTION
);
CREATE TABLE "wallet_dlc_cet_sigs"
(
"param_hash" TEXT NOT NULL,
"is_initiator" INTEGER NOT NULL,
"outcome" TEXT NOT NULL,
"signature" TEXT NOT NULL,
constraint "fk_param_hash" foreign key ("param_hash") references "wallet_dlcs" ("param_hash") on update NO ACTION on delete NO ACTION
);
CREATE TABLE "wallet_dlc_refund_sigs"
(
"param_hash" TEXT NOT NULL,
"is_initiator" INTEGER NOT NULL,
"refund_sig" TEXT NOT NULL,
constraint "fk_param_hash" foreign key ("param_hash") references "wallet_dlcs" ("param_hash") on update NO ACTION on delete NO ACTION
);
CREATE INDEX "wallet_dlc_refund_sigs_param_hash_index" on "wallet_dlc_accepts" ("param_hash");
CREATE TABLE tx_table
(
"txIdBE" TEXT NOT NULL,
"transaction" TEXT NOT NULL,
"unsignedTxIdBE" TEXT NOT NULL,
"unsignedTx" TEXT NOT NULL,
"wTxIdBE" TEXT,
"totalOutput" BIGINT NOT NULL,
"numInputs" INTEGER NOT NULL,
"numOutputs" INTEGER NOT NULL,
locktime BIGINT NOT NULL,
constraint pk_tx primary key ("txIdBE")
);

View file

@ -0,0 +1 @@
ALTER TABLE "wallet_dlcs" ADD COLUMN "outcome" TEXT;

View file

@ -0,0 +1,86 @@
CREATE TABLE "wallet_dlcs"
(
"param_hash" VARCHAR(254) NOT NULL UNIQUE,
"temp_contract_id" VARCHAR(254) NOT NULL UNIQUE,
"contract_id" VARCHAR(254) UNIQUE,
"state" VARCHAR(254) NOT NULL,
"is_initiator" INTEGER NOT NULL,
"account" VARCHAR(254) NOT NULL,
"key_index" INTEGER NOT NULL,
"oracle_sigs" VARCHAR(254),
"funding_outpoint" VARCHAR(254),
"funding_tx_id" VARCHAR(254),
"closing_tx_id" VARCHAR(254)
);
CREATE INDEX "wallet_dlcs_param_hash_index" on "wallet_dlcs" ("param_hash");
CREATE TABLE "wallet_dlc_offers"
(
"param_hash" VARCHAR(254) NOT NULL UNIQUE,
"temp_contract_id" VARCHAR(254) NOT NULL UNIQUE,
"oracle_info_tlv" VARCHAR(254) NOT NULL,
"contract_info" VARCHAR(254) NOT NULL,
"contract_maturity" VARCHAR(254) NOT NULL,
"contract_timeout" VARCHAR(254) NOT NULL,
"funding_key" VARCHAR(254) NOT NULL,
"payout_address" VARCHAR(254) NOT NULL,
"total_collateral" INTEGER NOT NULL,
"fee_rate" VARCHAR(254),
"change_address" VARCHAR(254) NOT NULL,
constraint "fk_param_hash" foreign key ("param_hash") references "wallet_dlcs" ("param_hash") on update NO ACTION on delete NO ACTION
);
CREATE INDEX "wallet_dlc_offers_param_hash_index" on "wallet_dlc_offers" ("param_hash");
CREATE TABLE "wallet_dlc_accepts"
(
"param_hash" VARCHAR(254) NOT NULL UNIQUE,
"temp_contract_id" VARCHAR(254) NOT NULL UNIQUE,
"funding_key" VARCHAR(254) NOT NULL,
"payout_address" VARCHAR(254) NOT NULL,
"total_collateral" INTEGER NOT NULL,
"change_address" VARCHAR(254) NOT NULL,
constraint "fk_param_hash" foreign key ("param_hash") references "wallet_dlcs" ("param_hash") on update NO ACTION on delete NO ACTION
);
CREATE INDEX "wallet_dlc_accepts_param_hash_index" on "wallet_dlc_accepts" ("param_hash");
CREATE TABLE "wallet_dlc_funding_inputs"
(
"param_hash" VARCHAR(254) NOT NULL,
"is_initiator" INTEGER NOT NULL,
"out_point" VARCHAR(254) NOT NULL UNIQUE,
"output" VARCHAR(254) NOT NULL,
"redeem_script_opt" VARCHAR(254),
"witness_script_opt" VARCHAR(254),
constraint "fk_param_hash" foreign key ("param_hash") references "wallet_dlcs" ("param_hash") on update NO ACTION on delete NO ACTION
);
CREATE TABLE "wallet_dlc_cet_sigs"
(
"param_hash" VARCHAR(254) NOT NULL,
"is_initiator" INTEGER NOT NULL,
"outcome" VARCHAR(254) NOT NULL,
"signature" VARCHAR(254) NOT NULL,
constraint "fk_param_hash" foreign key ("param_hash") references "wallet_dlcs" ("param_hash") on update NO ACTION on delete NO ACTION
);
CREATE TABLE "wallet_dlc_refund_sigs"
(
"param_hash" VARCHAR(254) NOT NULL,
"is_initiator" INTEGER NOT NULL,
"refund_sig" VARCHAR(254) NOT NULL,
constraint "fk_param_hash" foreign key ("param_hash") references "wallet_dlcs" ("param_hash") on update NO ACTION on delete NO ACTION
);
CREATE INDEX "wallet_dlc_refund_sigs_param_hash_index" on "wallet_dlc_accepts" ("param_hash");
CREATE TABLE "dlc_remote_tx_table"
(
"txIdBE" VARCHAR(254) NOT NULL PRIMARY KEY,
"transaction" VARCHAR(254) NOT NULL,
"unsignedTxIdBE" VARCHAR(254) NOT NULL,
"unsignedTx" VARCHAR(254) NOT NULL,
"wTxIdBE" VARCHAR(254),
"totalOutput" INTEGER NOT NULL,
"numInputs" INTEGER NOT NULL,
"numOutputs" INTEGER NOT NULL,
"locktime" INTEGER NOT NULL
);

View file

@ -0,0 +1 @@
ALTER TABLE "wallet_dlcs" ADD COLUMN "outcome" TEXT;

View file

@ -0,0 +1,129 @@
package org.bitcoins.dlc.wallet
import java.nio.file.{Files, Path}
import com.typesafe.config.Config
import org.bitcoins.core.api.chain.ChainQueryApi
import org.bitcoins.core.api.feeprovider.FeeRateApi
import org.bitcoins.core.api.node.NodeApi
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.core.wallet.keymanagement.KeyManagerInitializeError
import org.bitcoins.db.{AppConfig, AppConfigFactory, JdbcProfileComponent}
import org.bitcoins.keymanager.bip39.{BIP39KeyManager, BIP39LockedKeyManager}
import org.bitcoins.wallet.config.WalletAppConfig
import org.bitcoins.wallet.{Wallet, WalletLogger}
import scala.concurrent.{ExecutionContext, Future}
/** Configuration for the Bitcoin-S wallet
*
* @param directory The data directory of the wallet
* @param conf Optional sequence of configuration overrides
*/
case class DLCAppConfig(private val directory: Path, private val conf: Config*)(
implicit override val ec: ExecutionContext)
extends AppConfig
with DLCDbManagement
with JdbcProfileComponent[DLCAppConfig] {
override protected[bitcoins] def configOverrides: List[Config] = conf.toList
override protected[bitcoins] def moduleName: String = "dlc"
override protected[bitcoins] type ConfigType = DLCAppConfig
override protected[bitcoins] def newConfigOfType(
configs: Seq[Config]): DLCAppConfig =
DLCAppConfig(directory, configs: _*)
protected[bitcoins] def baseDatadir: Path = directory
override def appConfig: DLCAppConfig = this
override def start(): Future[Unit] = {
logger.debug(s"Initializing dlc setup")
if (Files.notExists(datadir)) {
Files.createDirectories(datadir)
}
val numMigrations = {
migrate()
}
logger.info(s"Applied $numMigrations to the dlc project")
FutureUtil.unit
}
def createDLCWallet(
nodeApi: NodeApi,
chainQueryApi: ChainQueryApi,
feeRateApi: FeeRateApi)(implicit
walletConf: WalletAppConfig,
ec: ExecutionContext): Future[DLCWallet] = {
DLCAppConfig.createDLCWallet(nodeApi = nodeApi,
chainQueryApi = chainQueryApi,
feeRateApi = feeRateApi)(walletConf, this, ec)
}
}
object DLCAppConfig extends AppConfigFactory[DLCAppConfig] with WalletLogger {
/** Constructs a wallet configuration from the default Bitcoin-S
* data directory and given list of configuration overrides.
*/
override def fromDatadir(datadir: Path, confs: Vector[Config])(implicit
ec: ExecutionContext): DLCAppConfig =
DLCAppConfig(datadir, confs: _*)
/** Creates a wallet based on the given [[WalletAppConfig]] */
def createDLCWallet(
nodeApi: NodeApi,
chainQueryApi: ChainQueryApi,
feeRateApi: FeeRateApi)(implicit
walletConf: WalletAppConfig,
dlcConf: DLCAppConfig,
ec: ExecutionContext): Future[DLCWallet] = {
val aesPasswordOpt = walletConf.aesPasswordOpt
val bip39PasswordOpt = walletConf.bip39PasswordOpt
walletConf.hasWallet().flatMap { walletExists =>
if (walletExists) {
logger.info(s"Using pre-existing wallet")
// TODO change me when we implement proper password handling
BIP39LockedKeyManager.unlock(aesPasswordOpt,
bip39PasswordOpt,
walletConf.kmParams) match {
case Right(km) =>
val wallet =
DLCWallet(km, nodeApi, chainQueryApi, feeRateApi, km.creationTime)
Future.successful(wallet)
case Left(err) =>
sys.error(s"Error initializing key manager, err=${err}")
}
} else {
logger.info(s"Initializing key manager")
val keyManagerE: Either[KeyManagerInitializeError, BIP39KeyManager] =
BIP39KeyManager.initialize(aesPasswordOpt = aesPasswordOpt,
kmParams = walletConf.kmParams,
bip39PasswordOpt = bip39PasswordOpt)
val keyManager = keyManagerE match {
case Right(keyManager) => keyManager
case Left(err) =>
sys.error(s"Error initializing key manager, err=${err}")
}
logger.info(s"Creating new wallet")
val unInitializedWallet =
DLCWallet(keyManager,
nodeApi,
chainQueryApi,
feeRateApi,
keyManager.creationTime)
Wallet
.initialize(wallet = unInitializedWallet,
bip39PasswordOpt = bip39PasswordOpt)
.map(_.asInstanceOf[DLCWallet])
}
}
}
}

View file

@ -0,0 +1,57 @@
package org.bitcoins.dlc.wallet
import org.bitcoins.db.{DbManagement, JdbcProfileComponent}
import org.bitcoins.dlc.wallet.models._
import scala.concurrent.ExecutionContext
trait DLCDbManagement extends DbManagement {
_: JdbcProfileComponent[DLCAppConfig] =>
import profile.api._
def ec: ExecutionContext
private lazy val dlcTable: TableQuery[Table[_]] = {
DLCDAO()(ec, appConfig).table
}
private lazy val dlcOfferTable: TableQuery[Table[_]] = {
DLCOfferDAO()(ec, appConfig).table
}
private lazy val dlcAcceptTable: TableQuery[Table[_]] = {
DLCAcceptDAO()(ec, appConfig).table
}
private lazy val dlcFundingInputsTable: TableQuery[Table[_]] = {
DLCFundingInputDAO()(ec, appConfig).table
}
private lazy val dlcCETSigTable: TableQuery[Table[_]] = {
DLCCETSignatureDAO()(ec, appConfig).table
}
private lazy val dlcRefundSigTable: TableQuery[Table[_]] = {
DLCRefundSigDAO()(ec, appConfig).table
}
private lazy val dlcRemoteTxTable: TableQuery[Table[_]] = {
DLCRemoteTxDAO()(ec, appConfig).table
}
// Ordering matters here, tables with a foreign key should be listed after
// the table that key references
override lazy val allTables: List[TableQuery[Table[_]]] = {
List(
dlcTable,
dlcOfferTable,
dlcAcceptTable,
dlcFundingInputsTable,
dlcCETSigTable,
dlcRefundSigTable,
dlcRemoteTxTable
)
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,96 @@
package org.bitcoins.dlc.wallet
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage._
import org.bitcoins.commons.jsonmodels.dlc.DLCStatus
import org.bitcoins.core.api.wallet._
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.crypto.{SchnorrDigitalSignature, Sha256DigestBE}
import org.bitcoins.dlc.wallet.models.DLCDb
import scodec.bits.ByteVector
import scala.concurrent.Future
trait DLCWalletApi { self: WalletApi =>
def createDLCOffer(
oracleInfo: OracleInfo,
contractInfoTLV: ContractInfoTLV,
collateral: Satoshis,
feeRateOpt: Option[FeeUnit],
locktime: UInt32,
refundLT: UInt32): Future[DLCOffer] = {
val contractInfo = ContractInfo.fromTLV(contractInfoTLV)
createDLCOffer(oracleInfo,
contractInfo,
collateral,
feeRateOpt,
locktime,
refundLT)
}
def createDLCOffer(
oracleInfo: OracleInfo,
contractInfo: ContractInfo,
collateral: Satoshis,
feeRateOpt: Option[FeeUnit],
locktime: UInt32,
refundLT: UInt32): Future[DLCOffer]
def registerDLCOffer(dlcOffer: DLCOffer): Future[DLCOffer] = {
createDLCOffer(
dlcOffer.oracleInfo,
dlcOffer.contractInfo,
dlcOffer.totalCollateral,
Some(dlcOffer.feeRate),
dlcOffer.timeouts.contractMaturity.toUInt32,
dlcOffer.timeouts.contractTimeout.toUInt32
)
}
def acceptDLCOffer(dlcOfferTLV: DLCOfferTLV): Future[DLCAccept] = {
acceptDLCOffer(DLCOffer.fromTLV(dlcOfferTLV))
}
def acceptDLCOffer(dlcOffer: DLCOffer): Future[DLCAccept]
def signDLC(acceptTLV: DLCAcceptTLV): Future[DLCSign]
def signDLC(accept: DLCAccept): Future[DLCSign]
def addDLCSigs(signTLV: DLCSignTLV): Future[DLCDb]
def addDLCSigs(sigs: DLCSign): Future[DLCDb]
def getDLCFundingTx(contractId: ByteVector): Future[Transaction]
def broadcastDLCFundingTx(contractId: ByteVector): Future[Transaction]
/** Creates the CET for the given contractId and oracle signature, does not broadcast it */
def executeDLC(
contractId: ByteVector,
oracleSig: SchnorrDigitalSignature): Future[Transaction] =
executeDLC(contractId, Vector(oracleSig))
/** Creates the CET for the given contractId and oracle signature, does not broadcast it */
def executeDLC(
contractId: ByteVector,
oracleSigs: Vector[SchnorrDigitalSignature]): Future[Transaction]
/** Creates the refund transaction for the given contractId, does not broadcast it */
def executeDLCRefund(contractId: ByteVector): Future[Transaction]
def listDLCs(): Future[Vector[DLCStatus]]
def findDLC(paramHash: Sha256DigestBE): Future[Option[DLCStatus]]
}
/** An HDWallet that supports DLCs and both Neutrino and SPV methods of syncing */
trait AnyDLCHDWalletApi
extends HDWalletApi
with DLCWalletApi
with NeutrinoWalletApi
with SpvWalletApi

View file

@ -0,0 +1,99 @@
package org.bitcoins.dlc.wallet.models
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.crypto.{ECPublicKey, Sha256Digest, Sha256DigestBE}
import org.bitcoins.db.{CRUD, SlickUtil}
import org.bitcoins.dlc.wallet.DLCAppConfig
import slick.lifted.{ForeignKeyQuery, PrimaryKey, ProvenShape}
import scala.concurrent.{ExecutionContext, Future}
case class DLCAcceptDAO()(implicit
val ec: ExecutionContext,
override val appConfig: DLCAppConfig)
extends CRUD[DLCAcceptDb, Sha256DigestBE]
with SlickUtil[DLCAcceptDb, Sha256DigestBE] {
private val mappers = new org.bitcoins.db.DbCommonsColumnMappers(profile)
import mappers._
import profile.api._
override val table: TableQuery[DLCAcceptTable] = TableQuery[DLCAcceptTable]
private lazy val dlcTable: slick.lifted.TableQuery[DLCDAO#DLCTable] = {
DLCDAO().table
}
override def createAll(ts: Vector[DLCAcceptDb]): Future[Vector[DLCAcceptDb]] =
createAllNoAutoInc(ts, safeDatabase)
override protected def findByPrimaryKeys(
ids: Vector[Sha256DigestBE]): Query[DLCAcceptTable, DLCAcceptDb, Seq] =
table.filter(_.paramHash.inSet(ids))
override def findByPrimaryKey(
id: Sha256DigestBE): Query[DLCAcceptTable, DLCAcceptDb, Seq] = {
table
.filter(_.paramHash === id)
}
override def findAll(
dlcs: Vector[DLCAcceptDb]): Query[DLCAcceptTable, DLCAcceptDb, Seq] =
findByPrimaryKeys(dlcs.map(_.paramHash))
def findByParamHash(
paramHash: Sha256DigestBE): Future[Option[DLCAcceptDb]] = {
val q = table.filter(_.paramHash === paramHash)
safeDatabase.run(q.result).map {
case h +: Vector() =>
Some(h)
case Vector() =>
None
case dlcs: Vector[DLCAcceptDb] =>
throw new RuntimeException(
s"More than one DLCAccept per paramHash ($paramHash), got: $dlcs")
}
}
def findByParamHash(paramHash: Sha256Digest): Future[Option[DLCAcceptDb]] =
findByParamHash(paramHash.flip)
class DLCAcceptTable(tag: Tag)
extends Table[DLCAcceptDb](tag, "wallet_dlc_accepts") {
def paramHash: Rep[Sha256DigestBE] = column("Param_hash", O.PrimaryKey)
def tempContractId: Rep[Sha256Digest] =
column("temp_contract_id", O.Unique)
def fundingKey: Rep[ECPublicKey] = column("funding_key")
def payoutAddress: Rep[BitcoinAddress] = column("payout_address")
def totalCollateral: Rep[CurrencyUnit] = column("total_collateral")
def changeAddress: Rep[BitcoinAddress] = column("change_address")
def * : ProvenShape[DLCAcceptDb] =
(paramHash,
tempContractId,
fundingKey,
payoutAddress,
totalCollateral,
changeAddress).<>(DLCAcceptDb.tupled, DLCAcceptDb.unapply)
def primaryKey: PrimaryKey =
primaryKey(name = "pk_dlc_accept", sourceColumns = paramHash)
def fk: ForeignKeyQuery[_, DLCDb] =
foreignKey("fk_param_hash",
sourceColumns = paramHash,
targetTableQuery = dlcTable)(_.paramHash)
def fkTempContractId: ForeignKeyQuery[_, DLCDb] =
foreignKey("fk_temp_contract_id",
sourceColumns = tempContractId,
targetTableQuery = dlcTable)(_.tempContractId)
}
}

View file

@ -0,0 +1,74 @@
package org.bitcoins.dlc.wallet.models
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.{
DLCAccept,
DLCAcceptWithoutSigs
}
import org.bitcoins.commons.jsonmodels.dlc.{
CETSignatures,
DLCFundingInput,
DLCPublicKeys
}
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.tlv.DLCOutcomeType
import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature
import org.bitcoins.crypto.{
ECAdaptorSignature,
ECPublicKey,
Sha256Digest,
Sha256DigestBE
}
case class DLCAcceptDb(
paramHash: Sha256DigestBE,
tempContractId: Sha256Digest,
fundingKey: ECPublicKey,
finalAddress: BitcoinAddress,
totalCollateral: CurrencyUnit,
changeAddress: BitcoinAddress) {
def toDLCAccept(
fundingInputs: Vector[DLCFundingInput],
outcomeSigs: Vector[(DLCOutcomeType, ECAdaptorSignature)],
refundSig: PartialSignature): DLCAccept = {
val pubKeys =
DLCPublicKeys(fundingKey, finalAddress)
val cetSigs = CETSignatures(outcomeSigs, refundSig)
DLCAccept(totalCollateral.satoshis,
pubKeys,
fundingInputs,
changeAddress,
cetSigs,
tempContractId)
}
def toDLCAcceptWithoutSigs(
tempContractId: Sha256Digest,
fundingInputs: Vector[DLCFundingInput]): DLCAcceptWithoutSigs = {
val pubKeys =
DLCPublicKeys(fundingKey, finalAddress)
DLCAcceptWithoutSigs(totalCollateral.satoshis,
pubKeys,
fundingInputs,
changeAddress,
tempContractId)
}
}
object DLCAcceptDbHelper {
def fromDLCAccept(
paramHash: Sha256DigestBE,
accept: DLCAccept): DLCAcceptDb = {
DLCAcceptDb(
paramHash,
accept.tempContractId,
accept.pubKeys.fundingKey,
accept.pubKeys.payoutAddress,
accept.totalCollateral,
accept.changeAddress
)
}
}

View file

@ -0,0 +1,101 @@
package org.bitcoins.dlc.wallet.models
import org.bitcoins.core.protocol.tlv.DLCOutcomeType
import org.bitcoins.crypto.{ECAdaptorSignature, Sha256Digest, Sha256DigestBE}
import org.bitcoins.db.{CRUD, SlickUtil}
import org.bitcoins.dlc.wallet.DLCAppConfig
import slick.lifted.{ForeignKeyQuery, PrimaryKey, ProvenShape}
import scala.concurrent.{ExecutionContext, Future}
case class DLCCETSignatureDAO()(implicit
val ec: ExecutionContext,
override val appConfig: DLCAppConfig)
extends CRUD[DLCCETSignatureDb, (Sha256DigestBE, DLCOutcomeType)]
with SlickUtil[DLCCETSignatureDb, (Sha256DigestBE, DLCOutcomeType)] {
private val mappers = new org.bitcoins.db.DbCommonsColumnMappers(profile)
import mappers._
import profile.api._
override val table: TableQuery[DLCCETSignatureTable] =
TableQuery[DLCCETSignatureTable]
private lazy val dlcTable: slick.lifted.TableQuery[DLCDAO#DLCTable] = {
DLCDAO().table
}
override def createAll(
ts: Vector[DLCCETSignatureDb]): Future[Vector[DLCCETSignatureDb]] =
createAllNoAutoInc(ts, safeDatabase)
override protected def findByPrimaryKeys(ids: Vector[(
Sha256DigestBE,
DLCOutcomeType)]): Query[DLCCETSignatureTable, DLCCETSignatureDb, Seq] =
table
.filter(_.paramHash.inSet(ids.map(_._1)))
.filter(_.outcome.inSet(ids.map(_._2)))
override def findByPrimaryKey(id: (Sha256DigestBE, DLCOutcomeType)): Query[
DLCCETSignatureTable,
DLCCETSignatureDb,
Seq] = {
table
.filter(_.paramHash === id._1)
.filter(_.outcome === id._2)
}
override def findAll(dlcs: Vector[DLCCETSignatureDb]): Query[
DLCCETSignatureTable,
DLCCETSignatureDb,
Seq] =
findByPrimaryKeys(dlcs.map(sig => (sig.paramHash, sig.outcome)))
def findByParamHash(
paramHash: Sha256DigestBE): Future[Vector[DLCCETSignatureDb]] = {
val q = table.filter(_.paramHash === paramHash)
safeDatabase.runVec(q.result)
}
def findByParamHash(
paramHash: Sha256DigestBE,
isInit: Boolean): Future[Vector[DLCCETSignatureDb]] = {
val q = table
.filter(_.paramHash === paramHash)
.filter(_.isInitiator === isInit)
safeDatabase.runVec(q.result)
}
def findByParamHash(
paramHash: Sha256Digest): Future[Vector[DLCCETSignatureDb]] =
findByParamHash(paramHash.flip)
def deleteByParamHash(paramHash: Sha256DigestBE): Future[Int] = {
val q = table.filter(_.paramHash === paramHash)
safeDatabase.run(q.delete)
}
class DLCCETSignatureTable(tag: Tag)
extends Table[DLCCETSignatureDb](tag, "wallet_dlc_cet_sigs") {
def paramHash: Rep[Sha256DigestBE] = column("param_hash")
def isInitiator: Rep[Boolean] = column("is_initiator")
def outcome: Rep[DLCOutcomeType] = column("outcome")
def signature: Rep[ECAdaptorSignature] = column("signature")
def * : ProvenShape[DLCCETSignatureDb] =
(paramHash, isInitiator, outcome, signature).<>(DLCCETSignatureDb.tupled,
DLCCETSignatureDb.unapply)
def primaryKey: PrimaryKey =
primaryKey(name = "pk_dlc_cet_sigs",
sourceColumns = (paramHash, isInitiator, outcome))
def fk: ForeignKeyQuery[_, DLCDb] =
foreignKey("fk_param_hash",
sourceColumns = paramHash,
targetTableQuery = dlcTable)(_.paramHash)
}
}

View file

@ -0,0 +1,12 @@
package org.bitcoins.dlc.wallet.models
import org.bitcoins.core.protocol.tlv.DLCOutcomeType
import org.bitcoins.crypto.{ECAdaptorSignature, Sha256DigestBE}
case class DLCCETSignatureDb(
paramHash: Sha256DigestBE,
isInitiator: Boolean,
outcome: DLCOutcomeType,
signature: ECAdaptorSignature) {
def toTuple: (DLCOutcomeType, ECAdaptorSignature) = (outcome, signature)
}

View file

@ -0,0 +1,167 @@
package org.bitcoins.dlc.wallet.models
import org.bitcoins.commons.jsonmodels.dlc.DLCState
import org.bitcoins.core.hd.HDAccount
import org.bitcoins.core.protocol.tlv.DLCOutcomeType
import org.bitcoins.core.protocol.transaction.TransactionOutPoint
import org.bitcoins.crypto.{
DoubleSha256DigestBE,
SchnorrDigitalSignature,
Sha256Digest,
Sha256DigestBE
}
import org.bitcoins.db.{CRUD, SlickUtil}
import org.bitcoins.dlc.wallet.DLCAppConfig
import scodec.bits.ByteVector
import slick.lifted.{PrimaryKey, ProvenShape}
import scala.concurrent.{ExecutionContext, Future}
case class DLCDAO()(implicit
val ec: ExecutionContext,
override val appConfig: DLCAppConfig)
extends CRUD[DLCDb, Sha256DigestBE]
with SlickUtil[DLCDb, Sha256DigestBE] {
private val mappers = new org.bitcoins.db.DbCommonsColumnMappers(profile)
import mappers._
import profile.api._
override val table: TableQuery[DLCTable] = TableQuery[DLCTable]
override def createAll(ts: Vector[DLCDb]): Future[Vector[DLCDb]] =
createAllNoAutoInc(ts, safeDatabase)
override protected def findByPrimaryKeys(
ids: Vector[Sha256DigestBE]): Query[DLCTable, DLCDb, Seq] =
table.filter(_.paramHash.inSet(ids))
override def findByPrimaryKey(
id: Sha256DigestBE): Query[DLCTable, DLCDb, Seq] = {
table
.filter(_.paramHash === id)
}
override def findAll(dlcs: Vector[DLCDb]): Query[DLCTable, DLCDb, Seq] =
findByPrimaryKeys(dlcs.map(_.paramHash))
def findByTempContractId(
tempContractId: Sha256Digest): Future[Option[DLCDb]] = {
val q = table.filter(_.tempContractId === tempContractId)
safeDatabase.run(q.result).map {
case h +: Vector() =>
Some(h)
case Vector() =>
None
case dlcs: Vector[DLCDb] =>
throw new RuntimeException(
s"More than one DLC per tempContractId (${tempContractId.hex}), got: $dlcs")
}
}
def findByTempContractId(
tempContractId: Sha256DigestBE): Future[Option[DLCDb]] =
findByTempContractId(tempContractId.flip)
def findByContractId(contractId: ByteVector): Future[Option[DLCDb]] = {
val q = table.filter(_.contractId === contractId)
safeDatabase.run(q.result).map {
case h +: Vector() =>
Some(h)
case Vector() =>
None
case dlcs: Vector[DLCDb] =>
throw new RuntimeException(
s"More than one DLC per contractId (${contractId.toHex}), got: $dlcs")
}
}
def findByParamHash(paramHash: Sha256DigestBE): Future[Option[DLCDb]] = {
val q = table.filter(_.paramHash === paramHash)
safeDatabase.run(q.result).map {
case h +: Vector() =>
Some(h)
case Vector() =>
None
case dlcs: Vector[DLCDb] =>
throw new RuntimeException(
s"More than one DLC per paramHash (${paramHash.hex}), got: $dlcs")
}
}
def findByParamHash(paramHash: Sha256Digest): Future[Option[DLCDb]] =
findByParamHash(paramHash.flip)
def findByFundingOutPoint(
outPoint: TransactionOutPoint): Future[Option[DLCDb]] = {
val q = table.filter(_.fundingOutPointOpt === outPoint)
safeDatabase.run(q.result).map(_.headOption)
}
def findByFundingOutPoints(
outPoints: Vector[TransactionOutPoint]): Future[Vector[DLCDb]] = {
val q = table.filter(_.fundingOutPointOpt.inSet(outPoints))
safeDatabase.runVec(q.result)
}
def findByFundingTxIds(
txIds: Vector[DoubleSha256DigestBE]): Future[Vector[DLCDb]] = {
val q = table.filter(_.fundingTxIdOpt.inSet(txIds))
safeDatabase.runVec(q.result)
}
class DLCTable(tag: Tag) extends Table[DLCDb](tag, "wallet_dlcs") {
def paramHash: Rep[Sha256DigestBE] = column("param_hash", O.PrimaryKey)
def tempContractId: Rep[Sha256Digest] =
column("temp_contract_id", O.Unique)
def contractId: Rep[Option[ByteVector]] =
column("contract_id", O.Unique)
def state: Rep[DLCState] = column("state")
def isInitiator: Rep[Boolean] = column("is_initiator")
def account: Rep[HDAccount] = column("account")
def keyIndex: Rep[Int] = column("key_index")
def oracleSigsOpt: Rep[Option[Vector[SchnorrDigitalSignature]]] =
column("oracle_sigs")
def fundingOutPointOpt: Rep[Option[TransactionOutPoint]] =
column("funding_outpoint")
def fundingTxIdOpt: Rep[Option[DoubleSha256DigestBE]] =
column("funding_tx_id")
def closingTxIdOpt: Rep[Option[DoubleSha256DigestBE]] =
column("closing_tx_id")
def outcomeOpt: Rep[Option[DLCOutcomeType]] = column("outcome")
def * : ProvenShape[DLCDb] =
(paramHash,
tempContractId,
contractId,
state,
isInitiator,
account,
keyIndex,
oracleSigsOpt,
fundingOutPointOpt,
fundingTxIdOpt,
closingTxIdOpt,
outcomeOpt).<>(DLCDb.tupled, DLCDb.unapply)
def primaryKey: PrimaryKey =
primaryKey(name = "pk_dlc", sourceColumns = paramHash)
}
}

View file

@ -0,0 +1,38 @@
package org.bitcoins.dlc.wallet.models
import org.bitcoins.commons.jsonmodels.dlc.DLCState
import org.bitcoins.core.hd.HDAccount
import org.bitcoins.core.protocol.tlv.DLCOutcomeType
import org.bitcoins.core.protocol.transaction.TransactionOutPoint
import org.bitcoins.crypto.{
DoubleSha256DigestBE,
SchnorrDigitalSignature,
Sha256Digest,
Sha256DigestBE
}
import scodec.bits.ByteVector
case class DLCDb(
paramHash: Sha256DigestBE,
tempContractId: Sha256Digest,
contractIdOpt: Option[ByteVector],
state: DLCState,
isInitiator: Boolean,
account: HDAccount,
keyIndex: Int,
oracleSigsOpt: Option[Vector[SchnorrDigitalSignature]],
fundingOutPointOpt: Option[TransactionOutPoint],
fundingTxIdOpt: Option[DoubleSha256DigestBE],
closingTxIdOpt: Option[DoubleSha256DigestBE],
outcomeOpt: Option[DLCOutcomeType]
) {
def updateState(newState: DLCState): DLCDb = {
copy(state = newState)
}
def updateFundingOutPoint(outPoint: TransactionOutPoint): DLCDb = {
copy(fundingOutPointOpt = Some(outPoint),
fundingTxIdOpt = Some(outPoint.txIdBE))
}
}

View file

@ -0,0 +1,114 @@
package org.bitcoins.dlc.wallet.models
import org.bitcoins.core.protocol.script.{ScriptPubKey, ScriptWitness}
import org.bitcoins.core.protocol.transaction.{
TransactionOutPoint,
TransactionOutput
}
import org.bitcoins.crypto.{Sha256Digest, Sha256DigestBE}
import org.bitcoins.db.{CRUD, SlickUtil}
import org.bitcoins.dlc.wallet.DLCAppConfig
import slick.lifted.{ForeignKeyQuery, PrimaryKey, ProvenShape}
import scala.concurrent.{ExecutionContext, Future}
case class DLCFundingInputDAO()(implicit
val ec: ExecutionContext,
override val appConfig: DLCAppConfig)
extends CRUD[DLCFundingInputDb, TransactionOutPoint]
with SlickUtil[DLCFundingInputDb, TransactionOutPoint] {
private val mappers = new org.bitcoins.db.DbCommonsColumnMappers(profile)
import mappers._
import profile.api._
override val table: TableQuery[DLCFundingInputsTable] =
TableQuery[DLCFundingInputsTable]
private lazy val dlcTable: slick.lifted.TableQuery[DLCDAO#DLCTable] = {
DLCDAO().table
}
override def createAll(
ts: Vector[DLCFundingInputDb]): Future[Vector[DLCFundingInputDb]] =
createAllNoAutoInc(ts, safeDatabase)
override protected def findByPrimaryKeys(
outPoints: Vector[TransactionOutPoint]): Query[
DLCFundingInputsTable,
DLCFundingInputDb,
Seq] =
table.filter(_.outPoint.inSet(outPoints))
override def findByPrimaryKey(outPoint: TransactionOutPoint): Query[
DLCFundingInputsTable,
DLCFundingInputDb,
Seq] = {
table
.filter(_.outPoint === outPoint)
}
override def findAll(dlcs: Vector[DLCFundingInputDb]): Query[
DLCFundingInputsTable,
DLCFundingInputDb,
Seq] =
findByPrimaryKeys(dlcs.map(_.outPoint))
def findByParamHash(
paramHash: Sha256DigestBE): Future[Vector[DLCFundingInputDb]] = {
val q = table.filter(_.paramHash === paramHash)
safeDatabase.run(q.result).map(_.toVector)
}
def findByParamHash(
paramHash: Sha256Digest): Future[Vector[DLCFundingInputDb]] =
findByParamHash(paramHash.flip)
def findByParamHash(
paramHash: Sha256DigestBE,
isInitiator: Boolean): Future[Vector[DLCFundingInputDb]] = {
val q = table
.filter(_.paramHash === paramHash)
.filter(_.isInitiator === isInitiator)
safeDatabase.run(q.result).map(_.toVector)
}
def findByParamHash(
paramHash: Sha256Digest,
isInitiator: Boolean): Future[Vector[DLCFundingInputDb]] =
findByParamHash(paramHash.flip, isInitiator)
class DLCFundingInputsTable(tag: Tag)
extends Table[DLCFundingInputDb](tag, "wallet_dlc_funding_inputs") {
def paramHash: Rep[Sha256DigestBE] = column("param_hash")
def isInitiator: Rep[Boolean] = column("is_initiator")
def outPoint: Rep[TransactionOutPoint] = column("out_point", O.Unique)
def output: Rep[TransactionOutput] = column("output")
def redeemScriptOpt: Rep[Option[ScriptPubKey]] = column("redeem_script_opt")
def witnessScriptOpt: Rep[Option[ScriptWitness]] =
column("witness_script_opt")
def * : ProvenShape[DLCFundingInputDb] =
(paramHash,
isInitiator,
outPoint,
output,
redeemScriptOpt,
witnessScriptOpt).<>(DLCFundingInputDb.tupled, DLCFundingInputDb.unapply)
def primaryKey: PrimaryKey =
primaryKey(name = "pk_dlc_input", sourceColumns = outPoint)
def fk: ForeignKeyQuery[_, DLCDb] =
foreignKey("fk_param_hash",
sourceColumns = paramHash,
targetTableQuery = dlcTable)(_.paramHash)
}
}

View file

@ -0,0 +1,32 @@
package org.bitcoins.dlc.wallet.models
import org.bitcoins.commons.jsonmodels.dlc.{
DLCFundingInput,
DLCFundingInputP2WPKHV0
}
import org.bitcoins.core.protocol.script.{ScriptPubKey, ScriptWitness}
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.crypto.Sha256DigestBE
case class DLCFundingInputDb(
paramHash: Sha256DigestBE,
isInitiator: Boolean,
outPoint: TransactionOutPoint,
output: TransactionOutput,
redeemScriptOpt: Option[ScriptPubKey],
witnessScriptOpt: Option[ScriptWitness]) {
lazy val toOutputReference: OutputReference =
OutputReference(outPoint, output)
def toFundingInput(prevTx: Transaction): DLCFundingInput = {
require(
prevTx.txId == outPoint.txId,
s"Provided previous transaction didn't match database outpoint outpoint=${outPoint.txIdBE.hex} prevTx.txId=${prevTx.txIdBE.hex}"
)
DLCFundingInputP2WPKHV0(prevTx,
outPoint.vout,
TransactionConstants.sequence)
}
}

View file

@ -0,0 +1,115 @@
package org.bitcoins.dlc.wallet.models
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.protocol.tlv.{ContractInfoTLV, OracleInfoTLV}
import org.bitcoins.core.protocol.{BitcoinAddress, BlockTimeStamp}
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.crypto._
import org.bitcoins.db.{CRUD, SlickUtil}
import org.bitcoins.dlc.wallet.DLCAppConfig
import slick.lifted.{ForeignKeyQuery, PrimaryKey, ProvenShape}
import scala.concurrent.{ExecutionContext, Future}
case class DLCOfferDAO()(implicit
val ec: ExecutionContext,
override val appConfig: DLCAppConfig)
extends CRUD[DLCOfferDb, Sha256DigestBE]
with SlickUtil[DLCOfferDb, Sha256DigestBE] {
private val mappers = new org.bitcoins.db.DbCommonsColumnMappers(profile)
import mappers._
import profile.api._
override val table: TableQuery[DLCOfferTable] = TableQuery[DLCOfferTable]
private lazy val dlcTable: slick.lifted.TableQuery[DLCDAO#DLCTable] = {
DLCDAO().table
}
override def createAll(ts: Vector[DLCOfferDb]): Future[Vector[DLCOfferDb]] =
createAllNoAutoInc(ts, safeDatabase)
override protected def findByPrimaryKeys(
ids: Vector[Sha256DigestBE]): Query[DLCOfferTable, DLCOfferDb, Seq] =
table.filter(_.paramHash.inSet(ids))
override def findByPrimaryKey(
id: Sha256DigestBE): Query[DLCOfferTable, DLCOfferDb, Seq] = {
table
.filter(_.paramHash === id)
}
override def findAll(
dlcs: Vector[DLCOfferDb]): Query[DLCOfferTable, DLCOfferDb, Seq] =
findByPrimaryKeys(dlcs.map(_.paramHash))
def findByParamHash(paramHash: Sha256DigestBE): Future[Option[DLCOfferDb]] = {
val q = table.filter(_.paramHash === paramHash)
safeDatabase.run(q.result).map {
case h +: Vector() =>
Some(h)
case Vector() =>
None
case dlcs: Vector[DLCOfferDb] =>
throw new RuntimeException(
s"More than one DLCOffer per paramHash ($paramHash), got: $dlcs")
}
}
def findByParamHash(paramHash: Sha256Digest): Future[Option[DLCOfferDb]] =
findByParamHash(paramHash.flip)
class DLCOfferTable(tag: Tag)
extends Table[DLCOfferDb](tag, "wallet_dlc_offers") {
def paramHash: Rep[Sha256DigestBE] = column("param_hash", O.Unique)
def tempContractId: Rep[Sha256Digest] =
column("temp_contract_id", O.Unique)
def oracleInfoTLV: Rep[OracleInfoTLV] = column("oracle_info_tlv")
def contractInfoTLV: Rep[ContractInfoTLV] = column("contract_info")
def contractMaturity: Rep[BlockTimeStamp] = column("contract_maturity")
def contractTimeout: Rep[BlockTimeStamp] = column("contract_timeout")
def fundingKey: Rep[ECPublicKey] = column("funding_key")
def payoutAddress: Rep[BitcoinAddress] = column("payout_address")
def totalCollateral: Rep[CurrencyUnit] = column("total_collateral")
def feeRate: Rep[SatoshisPerVirtualByte] = column("fee_rate")
def changeAddress: Rep[BitcoinAddress] = column("change_address")
def * : ProvenShape[DLCOfferDb] =
(paramHash,
tempContractId,
oracleInfoTLV,
contractInfoTLV,
contractMaturity,
contractTimeout,
fundingKey,
payoutAddress,
totalCollateral,
feeRate,
changeAddress).<>(DLCOfferDb.tupled, DLCOfferDb.unapply)
def primaryKey: PrimaryKey =
primaryKey(name = "pk_dlc_offer", sourceColumns = paramHash)
def fk: ForeignKeyQuery[_, DLCDb] =
foreignKey("fk_paramHash",
sourceColumns = paramHash,
targetTableQuery = dlcTable)(_.paramHash)
def fkTempContractId: ForeignKeyQuery[_, DLCDb] =
foreignKey("fk_temp_contract_id",
sourceColumns = tempContractId,
targetTableQuery = dlcTable)(_.tempContractId)
}
}

View file

@ -0,0 +1,68 @@
package org.bitcoins.dlc.wallet.models
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage._
import org.bitcoins.commons.jsonmodels.dlc.{
DLCFundingInput,
DLCPublicKeys,
DLCTimeouts
}
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.protocol.tlv.{ContractInfoTLV, OracleInfoTLV}
import org.bitcoins.core.protocol.{BitcoinAddress, BlockTimeStamp}
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.crypto._
case class DLCOfferDb(
paramHash: Sha256DigestBE,
tempContractId: Sha256Digest,
oracleInfoTLV: OracleInfoTLV,
contractInfoTLV: ContractInfoTLV,
contractMaturity: BlockTimeStamp,
contractTimeout: BlockTimeStamp,
fundingKey: ECPublicKey,
payoutAddress: BitcoinAddress,
totalCollateral: CurrencyUnit,
feeRate: SatoshisPerVirtualByte,
changeAddress: BitcoinAddress) {
lazy val oracleInfo: OracleInfo = OracleInfo.fromTLV(oracleInfoTLV)
lazy val contractInfo: ContractInfo = ContractInfo.fromTLV(contractInfoTLV)
lazy val dlcPubKeys: DLCPublicKeys = DLCPublicKeys(fundingKey, payoutAddress)
lazy val dlcTimeouts: DLCTimeouts =
DLCTimeouts(contractMaturity, contractTimeout)
def toDLCOffer(fundingInputs: Vector[DLCFundingInput]): DLCOffer = {
DLCOffer(
OracleAndContractInfo(oracleInfo, contractInfo),
dlcPubKeys,
totalCollateral.satoshis,
fundingInputs,
changeAddress,
feeRate,
dlcTimeouts
)
}
}
object DLCOfferDbHelper {
def fromDLCOffer(offer: DLCOffer): DLCOfferDb = {
DLCOfferDb(
offer.paramHash,
offer.tempContractId,
offer.oracleInfo.toTLV,
offer.contractInfo.toTLV,
offer.timeouts.contractMaturity,
offer.timeouts.contractTimeout,
offer.pubKeys.fundingKey,
offer.pubKeys.payoutAddress,
offer.totalCollateral,
offer.feeRate,
offer.changeAddress
)
}
}

View file

@ -0,0 +1,93 @@
package org.bitcoins.dlc.wallet.models
import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature
import org.bitcoins.crypto.{Sha256Digest, Sha256DigestBE}
import org.bitcoins.db.{CRUD, SlickUtil}
import org.bitcoins.dlc.wallet.DLCAppConfig
import slick.lifted.{ForeignKeyQuery, PrimaryKey, ProvenShape}
import scala.concurrent.{ExecutionContext, Future}
case class DLCRefundSigDAO()(implicit
val ec: ExecutionContext,
override val appConfig: DLCAppConfig)
extends CRUD[DLCRefundSigDb, (Sha256DigestBE, Boolean)]
with SlickUtil[DLCRefundSigDb, (Sha256DigestBE, Boolean)] {
private val mappers = new org.bitcoins.db.DbCommonsColumnMappers(profile)
import mappers._
import profile.api._
override val table: TableQuery[DLCRefundSigTable] =
TableQuery[DLCRefundSigTable]
private lazy val dlcTable: slick.lifted.TableQuery[DLCDAO#DLCTable] = {
DLCDAO().table
}
override def createAll(
ts: Vector[DLCRefundSigDb]): Future[Vector[DLCRefundSigDb]] =
createAllNoAutoInc(ts, safeDatabase)
override protected def findByPrimaryKeys(ids: Vector[
(Sha256DigestBE, Boolean)]): Query[DLCRefundSigTable, DLCRefundSigDb, Seq] =
table.filter(_.paramHash.inSet(ids.map(_._1)))
override def findByPrimaryKey(id: (Sha256DigestBE, Boolean)): Query[
DLCRefundSigTable,
DLCRefundSigDb,
Seq] = {
val (paramHash, isInit) = id
table
.filter(_.paramHash === paramHash)
.filter(_.isInitiator === isInit)
}
override def findAll(dlcs: Vector[DLCRefundSigDb]): Query[
DLCRefundSigTable,
DLCRefundSigDb,
Seq] =
findByPrimaryKeys(dlcs.map(dlc => (dlc.paramHash, dlc.isInitiator)))
def findByParamHash(
paramHash: Sha256DigestBE): Future[Vector[DLCRefundSigDb]] = {
val q = table.filter(_.paramHash === paramHash)
safeDatabase.runVec(q.result)
}
def findByParamHash(
paramHash: Sha256DigestBE,
isInit: Boolean): Future[Option[DLCRefundSigDb]] = {
val q = table
.filter(_.paramHash === paramHash)
.filter(_.isInitiator === isInit)
safeDatabase.runVec(q.result).map(_.headOption)
}
def findByParamHash(paramHash: Sha256Digest): Future[Vector[DLCRefundSigDb]] =
findByParamHash(paramHash.flip)
class DLCRefundSigTable(tag: Tag)
extends Table[DLCRefundSigDb](tag, "wallet_dlc_refund_sigs") {
def paramHash: Rep[Sha256DigestBE] = column("param_hash")
def isInitiator: Rep[Boolean] = column("is_initiator")
def refundSig: Rep[PartialSignature] = column("refund_sig")
def * : ProvenShape[DLCRefundSigDb] =
(paramHash, isInitiator, refundSig).<>(DLCRefundSigDb.tupled,
DLCRefundSigDb.unapply)
def primaryKey: PrimaryKey =
primaryKey(name = "pk_dlc_refund_sig",
sourceColumns = (paramHash, isInitiator))
def fk: ForeignKeyQuery[_, DLCDb] =
foreignKey("fk_param_hash",
sourceColumns = paramHash,
targetTableQuery = dlcTable)(_.paramHash)
}
}

View file

@ -0,0 +1,9 @@
package org.bitcoins.dlc.wallet.models
import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature
import org.bitcoins.crypto.Sha256DigestBE
case class DLCRefundSigDb(
paramHash: Sha256DigestBE,
isInitiator: Boolean,
refundSig: PartialSignature)

View file

@ -0,0 +1,60 @@
package org.bitcoins.dlc.wallet.models
import org.bitcoins.core.api.wallet.db.TransactionDb
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.crypto.DoubleSha256DigestBE
import org.bitcoins.dlc.wallet.DLCAppConfig
import org.bitcoins.wallet.models.TxDAO
import slick.lifted.{PrimaryKey, ProvenShape}
import scala.concurrent.ExecutionContext
case class DLCRemoteTxDAO()(implicit
val ec: ExecutionContext,
override val appConfig: DLCAppConfig)
extends TxDAO[TransactionDb] {
import profile.api._
private val mappers = new org.bitcoins.db.DbCommonsColumnMappers(profile)
import mappers._
override val table = TableQuery[DLCRemoteTxTable]
class DLCRemoteTxTable(tag: Tag)
extends TxTable[TransactionDb](tag, schemaName, "dlc_remote_tx_table") {
def txIdBE: Rep[DoubleSha256DigestBE] = column("txIdBE", O.PrimaryKey)
def transaction: Rep[Transaction] = column("transaction")
def unsignedTxIdBE: Rep[DoubleSha256DigestBE] = column("unsignedTxIdBE")
def unsignedTx: Rep[Transaction] = column("unsignedTx")
def wTxIdBEOpt: Rep[Option[DoubleSha256DigestBE]] =
column("wTxIdBE")
def totalOutput: Rep[CurrencyUnit] = column("totalOutput")
def numInputs: Rep[Int] = column("numInputs")
def numOutputs: Rep[Int] = column("numOutputs")
def locktime: Rep[UInt32] = column("locktime")
def * : ProvenShape[TransactionDb] =
(txIdBE,
transaction,
unsignedTxIdBE,
unsignedTx,
wTxIdBEOpt,
totalOutput,
numInputs,
numOutputs,
locktime).<>(TransactionDb.tupled, TransactionDb.unapply)
def primaryKey: PrimaryKey =
primaryKey("pk_tx", sourceColumns = txIdBE)
}
}

1
dlc/README.md Normal file
View file

@ -0,0 +1 @@
#TODO

View file

@ -0,0 +1,79 @@
package org.bitcoins.dlc.builder
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.OracleAndContractInfo
import org.bitcoins.commons.jsonmodels.dlc.DLCTimeouts
import org.bitcoins.core.protocol.script.{
EmptyScriptSignature,
MultiSignatureScriptPubKey,
P2WSHWitnessV0,
ScriptPubKey
}
import org.bitcoins.core.protocol.tlv.DLCOutcomeType
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.wallet.builder.{
AddWitnessDataFinalizer,
FilterDustFinalizer,
RawTxBuilder
}
import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.core.wallet.utxo.{ConditionalPath, P2WSHV0InputInfo}
import org.bitcoins.crypto.ECPublicKey
import scala.concurrent.{ExecutionContext, Future}
/** Responsible for constructing unsigned
* Contract Execution Transactions (CETs)
*/
case class DLCCETBuilder(
oracleAndContractInfo: OracleAndContractInfo,
offerFundingKey: ECPublicKey,
offerFinalSPK: ScriptPubKey,
acceptFundingKey: ECPublicKey,
acceptFinalSPK: ScriptPubKey,
timeouts: DLCTimeouts,
feeRate: FeeUnit,
fundingOutputRef: OutputReference) {
private val fundingOutPoint = fundingOutputRef.outPoint
private val fundingKeys =
Vector(offerFundingKey, acceptFundingKey).sortBy(_.hex)
private val fundingInfo = P2WSHV0InputInfo(
outPoint = fundingOutPoint,
amount = fundingOutputRef.output.value,
scriptWitness = P2WSHWitnessV0(MultiSignatureScriptPubKey(2, fundingKeys)),
conditionalPath = ConditionalPath.NoCondition
)
/** Constructs a Contract Execution Transaction (CET)
* for a given outcome hash
*/
def buildCET(msg: DLCOutcomeType)(implicit
ec: ExecutionContext): Future[WitnessTransaction] = {
val builder = RawTxBuilder().setLockTime(timeouts.contractMaturity.toUInt32)
val (offerValue, acceptValue) = oracleAndContractInfo.getPayouts(msg)
builder += TransactionOutput(offerValue, offerFinalSPK)
builder += TransactionOutput(acceptValue, acceptFinalSPK)
builder += TransactionInput(fundingOutPoint,
EmptyScriptSignature,
TransactionConstants.disableRBFSequence)
val finalizer =
FilterDustFinalizer
.andThen(AddWitnessDataFinalizer(Vector(fundingInfo)))
val txF = finalizer.buildTx(builder.result())
txF.flatMap {
case _: NonWitnessTransaction =>
Future.failed(
new RuntimeException(
"Something went wrong with AddWitnessDataFinalizer"))
case wtx: WitnessTransaction => Future.successful(wtx)
}
}
}

View file

@ -0,0 +1,109 @@
package org.bitcoins.dlc.builder
import org.bitcoins.commons.jsonmodels.dlc.DLCFundingInput
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.protocol.script.{
EmptyScriptSignature,
MultiSignatureScriptPubKey,
P2SHScriptSignature,
P2WSHWitnessSPKV0,
ScriptPubKey
}
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.wallet.builder.{
DualFundingTxFinalizer,
RawTxBuilderWithFinalizer
}
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.crypto.ECPublicKey
import scala.concurrent.{ExecutionContext, Future}
/** Responsible for constructing an unsigned DLC funding transaction
* as well as all of its components (ScriptPubKeys, etc.)
*/
case class DLCFundingTxBuilder(
offerFundingKey: ECPublicKey,
acceptFundingKey: ECPublicKey,
feeRate: SatoshisPerVirtualByte,
offerInput: CurrencyUnit,
acceptInput: CurrencyUnit,
offerFundingInputs: Vector[DLCFundingInput],
acceptFundingInputs: Vector[DLCFundingInput],
offerChangeSPK: ScriptPubKey,
acceptChangeSPK: ScriptPubKey,
offerPayoutSPK: ScriptPubKey,
acceptPayoutSPK: ScriptPubKey) {
/** The total collateral of both parties combined */
val totalInput: CurrencyUnit = offerInput + acceptInput
/** The sum of all funding input amounts from the initiator */
val offerTotalFunding: CurrencyUnit =
offerFundingInputs.map(_.output.value).sum
/** The sum of all funding input amounts from the non-initiator */
val acceptTotalFunding: CurrencyUnit =
acceptFundingInputs.map(_.output.value).sum
require(
offerTotalFunding >= offerInput,
"Offer funding inputs must add up to at least offer's total collateral")
require(
acceptTotalFunding >= acceptInput,
"Accept funding inputs must add up to at least accept's total collateral")
private val fundingKeys =
Vector(offerFundingKey, acceptFundingKey).sortBy(_.hex)
/** The 2-of-2 MultiSignatureScriptPubKey to be wrapped in P2WSH and used as the funding output */
val fundingMultiSig: MultiSignatureScriptPubKey =
MultiSignatureScriptPubKey(2, fundingKeys)
/** The funding output's P2WSH(MultiSig) ScriptPubKey */
val fundingSPK: P2WSHWitnessSPKV0 = P2WSHWitnessSPKV0(fundingMultiSig)
val fundingTxFinalizer: DualFundingTxFinalizer = DualFundingTxFinalizer(
offerInputs = offerFundingInputs.map(_.toDualFundingInput),
offerPayoutSPK = offerPayoutSPK,
offerChangeSPK = offerChangeSPK,
acceptInputs = acceptFundingInputs.map(_.toDualFundingInput),
acceptPayoutSPK = acceptPayoutSPK,
acceptChangeSPK = acceptChangeSPK,
feeRate = feeRate,
fundingSPK = fundingSPK
)
def buildFundingTx()(implicit ec: ExecutionContext): Future[Transaction] = {
val builder = RawTxBuilderWithFinalizer(fundingTxFinalizer)
builder += TransactionOutput(totalInput, fundingSPK)
builder += TransactionOutput(offerTotalFunding - offerInput, offerChangeSPK)
builder += TransactionOutput(acceptTotalFunding - acceptInput,
acceptChangeSPK)
offerFundingInputs.foreach { ref =>
val scriptSig = ref.redeemScriptOpt match {
case Some(redeemScript) => P2SHScriptSignature(redeemScript)
case None => EmptyScriptSignature
}
builder += TransactionInput(ref.outPoint,
scriptSig,
TransactionConstants.sequence)
}
acceptFundingInputs.foreach { ref =>
val scriptSig = ref.redeemScriptOpt match {
case Some(redeemScript) => P2SHScriptSignature(redeemScript)
case None => EmptyScriptSignature
}
builder += TransactionInput(ref.outPoint,
scriptSig,
TransactionConstants.sequence)
}
builder.buildTx()
}
}

View file

@ -0,0 +1,66 @@
package org.bitcoins.dlc.builder
import org.bitcoins.commons.jsonmodels.dlc.DLCTimeouts
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.protocol.script.{
EmptyScriptSignature,
MultiSignatureScriptPubKey,
P2WSHWitnessV0,
ScriptPubKey
}
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.wallet.builder.{AddWitnessDataFinalizer, RawTxBuilder}
import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.core.wallet.utxo.{ConditionalPath, P2WSHV0InputInfo}
import org.bitcoins.crypto.ECPublicKey
import scala.concurrent.{ExecutionContext, Future}
case class DLCRefundTxBuilder(
offerInput: CurrencyUnit,
offerFundingKey: ECPublicKey,
offerFinalSPK: ScriptPubKey,
acceptInput: CurrencyUnit,
acceptFundingKey: ECPublicKey,
acceptFinalSPK: ScriptPubKey,
fundingOutputRef: OutputReference,
timeouts: DLCTimeouts,
feeRate: FeeUnit) {
private val OutputReference(fundingOutPoint, fundingOutput) = fundingOutputRef
private val fundingKeys =
Vector(offerFundingKey, acceptFundingKey).sortBy(_.hex)
private val fundingInfo = P2WSHV0InputInfo(
outPoint = fundingOutPoint,
amount = fundingOutput.value,
scriptWitness = P2WSHWitnessV0(MultiSignatureScriptPubKey(2, fundingKeys)),
conditionalPath = ConditionalPath.NoCondition
)
/** Constructs the unsigned DLC refund transaction */
def buildRefundTx()(implicit
ec: ExecutionContext): Future[WitnessTransaction] = {
val builder = RawTxBuilder().setLockTime(timeouts.contractTimeout.toUInt32)
builder += TransactionInput(fundingOutPoint,
EmptyScriptSignature,
TransactionConstants.disableRBFSequence)
builder += TransactionOutput(offerInput, offerFinalSPK)
builder += TransactionOutput(acceptInput, acceptFinalSPK)
val finalizer = AddWitnessDataFinalizer(Vector(fundingInfo))
val txF = finalizer.buildTx(builder.result())
txF.flatMap {
case _: NonWitnessTransaction =>
Future.failed(
new RuntimeException(
"Something went wrong with AddWitnessDataFinalizer"))
case wtx: WitnessTransaction => Future.successful(wtx)
}
}
}

View file

@ -0,0 +1,171 @@
package org.bitcoins.dlc.builder
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage._
import org.bitcoins.commons.jsonmodels.dlc.{
DLCFundingInput,
DLCPublicKeys,
DLCTimeouts
}
import org.bitcoins.core.config.BitcoinNetwork
import org.bitcoins.core.currency.{CurrencyUnit, Satoshis}
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.tlv.DLCOutcomeType
import org.bitcoins.core.protocol.transaction.{
OutputReference,
Transaction,
TransactionOutPoint,
WitnessTransaction
}
import org.bitcoins.core.protocol.{BitcoinAddress, BlockTimeStamp}
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.crypto._
import scodec.bits.ByteVector
import scala.concurrent.{ExecutionContext, Future}
case class DLCTxBuilder(offer: DLCOffer, accept: DLCAcceptWithoutSigs)(implicit
ec: ExecutionContext) {
val DLCOffer(_,
DLCPublicKeys(offerFundingKey: ECPublicKey,
offerFinalAddress: BitcoinAddress),
offerTotalCollateral: Satoshis,
offerFundingInputs: Vector[DLCFundingInput],
offerChangeAddress: BitcoinAddress,
feeRate: SatoshisPerVirtualByte,
DLCTimeouts(contractMaturity: BlockTimeStamp,
contractTimeout: BlockTimeStamp)) = offer
val network: BitcoinNetwork = offerFinalAddress.networkParameters match {
case network: BitcoinNetwork => network
}
val DLCAcceptWithoutSigs(acceptTotalCollateral: Satoshis,
DLCPublicKeys(acceptFundingKey: ECPublicKey,
acceptFinalAddress: BitcoinAddress),
acceptFundingInputs: Vector[DLCFundingInput],
acceptChangeAddress: BitcoinAddress,
tempContractId: Sha256Digest) = accept
val totalInput: CurrencyUnit = offerTotalCollateral + acceptTotalCollateral
// builder.offer.oracleAndContractInfo should not be used,
// builder.oracleAndContractInfo should be used instead in case a party
// is over-collateralized in which case payouts will be incorrect here.
private val oracleAndContractInfoBeforeAccept: OracleAndContractInfo =
offer.oracleAndContractInfo
val oracleAndContractInfo: OracleAndContractInfo =
oracleAndContractInfoBeforeAccept.updateTotalCollateral(totalInput.satoshis)
val offerTotalFunding: CurrencyUnit =
offerFundingInputs.map(_.output.value).sum
val acceptTotalFunding: CurrencyUnit =
acceptFundingInputs.map(_.output.value).sum
require(offer.tempContractId == tempContractId,
"Offer and accept (without sigs) must refer to same event")
require(acceptFinalAddress.networkParameters == network,
"Offer and accept (without sigs) must be on the same network")
require(offerChangeAddress.networkParameters == network,
"Offer change address must have same network as final address")
require(acceptChangeAddress.networkParameters == network,
"Accept change address must have same network as final address")
require(totalInput >= oracleAndContractInfo.offerContractInfo.max,
"Total collateral must add up to max winnings")
require(
offerTotalFunding >= offerTotalCollateral,
"Offer funding inputs must add up to at least offer's total collateral")
require(
acceptTotalFunding >= acceptTotalCollateral,
"Accept funding inputs must add up to at least accept's total collateral")
/** Returns the payouts for the signature as (toOffer, toAccept) */
def getPayouts(oracleSigs: Vector[SchnorrDigitalSignature]): (
CurrencyUnit,
CurrencyUnit) = {
oracleAndContractInfo.getPayouts(oracleSigs)
}
lazy val fundingTxBuilder: DLCFundingTxBuilder = {
DLCFundingTxBuilder(
offerFundingKey = offerFundingKey,
acceptFundingKey = acceptFundingKey,
feeRate = feeRate,
offerInput = offerTotalCollateral,
acceptInput = acceptTotalCollateral,
offerFundingInputs = offerFundingInputs,
acceptFundingInputs = acceptFundingInputs,
offerChangeSPK = offerChangeAddress.scriptPubKey,
acceptChangeSPK = acceptChangeAddress.scriptPubKey,
offerPayoutSPK = offerFinalAddress.scriptPubKey,
acceptPayoutSPK = acceptFinalAddress.scriptPubKey
)
}
/** Constructs the unsigned funding transaction */
lazy val buildFundingTx: Future[Transaction] = {
fundingTxBuilder.buildFundingTx()
}
lazy val calcContractId: Future[ByteVector] = {
buildFundingTx.map(_.txIdBE.bytes.xor(accept.tempContractId.bytes))
}
private lazy val cetBuilderF = {
for {
fundingTx <- buildFundingTx
} yield {
val fundingOutPoint = TransactionOutPoint(fundingTx.txId, UInt32.zero)
val fundingOutputRef =
OutputReference(fundingOutPoint, fundingTx.outputs.head)
DLCCETBuilder(
oracleAndContractInfo = oracleAndContractInfo,
offerFundingKey = offerFundingKey,
offerFinalSPK = offerFinalAddress.scriptPubKey,
acceptFundingKey = acceptFundingKey,
acceptFinalSPK = acceptFinalAddress.scriptPubKey,
timeouts = offer.timeouts,
feeRate = feeRate,
fundingOutputRef = fundingOutputRef
)
}
}
/** Constructs the unsigned Contract Execution Transaction (CET)
* for a given outcome hash
*/
def buildCET(msg: DLCOutcomeType): Future[WitnessTransaction] = {
for {
cetBuilder <- cetBuilderF
cet <- cetBuilder.buildCET(msg)
} yield cet
}
/** Constructs the unsigned refund transaction */
lazy val buildRefundTx: Future[WitnessTransaction] = {
val builderF = for {
fundingTx <- buildFundingTx
} yield {
val fundingOutPoint = TransactionOutPoint(fundingTx.txId, UInt32.zero)
val fundingOutputRef =
OutputReference(fundingOutPoint, fundingTx.outputs.head)
DLCRefundTxBuilder(
offerTotalCollateral,
offerFundingKey,
offerFinalAddress.scriptPubKey,
acceptTotalCollateral,
acceptFundingKey,
acceptFinalAddress.scriptPubKey,
fundingOutputRef,
offer.timeouts,
feeRate
)
}
builderF.flatMap(_.buildRefundTx())
}
}

View file

@ -0,0 +1,100 @@
package org.bitcoins.dlc.execution
import org.bitcoins.commons.jsonmodels.dlc.{CETSignatures, FundingSignatures}
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.crypto.SchnorrDigitalSignature
import org.bitcoins.dlc.builder.DLCTxBuilder
import org.bitcoins.dlc.sign.DLCTxSigner
import scala.concurrent.{ExecutionContext, Future}
/** Responsible for constructing SetupDLCs and DLCOutcomes */
case class DLCExecutor(signer: DLCTxSigner)(implicit ec: ExecutionContext) {
val builder: DLCTxBuilder = signer.builder
val isInitiator: Boolean = signer.isInitiator
/** Constructs the initiator's SetupDLC given the non-initiator's
* CETSignatures which should arrive in a DLC accept message
*/
def setupDLCOffer(cetSigs: CETSignatures): Future[SetupDLC] = {
require(isInitiator, "You should call setupDLCAccept")
setupDLC(cetSigs, None)
}
/** Constructs the non-initiator's SetupDLC given the initiator's
* CETSignatures and FundingSignatures which should arrive in
* a DLC sign message
*/
def setupDLCAccept(
cetSigs: CETSignatures,
fundingSigs: FundingSignatures): Future[SetupDLC] = {
require(!isInitiator, "You should call setupDLCOffer")
setupDLC(cetSigs, Some(fundingSigs))
}
/** Constructs a SetupDLC given the necessary signature information
* from the counter-party.
*/
def setupDLC(
cetSigs: CETSignatures,
fundingSigsOpt: Option[FundingSignatures]): Future[SetupDLC] = {
if (!isInitiator) {
require(fundingSigsOpt.isDefined,
"Accepting party must provide remote funding signatures")
}
val CETSignatures(outcomeSigs, refundSig) = cetSigs
val cetInfoFs = outcomeSigs.map {
case (msg, remoteAdaptorSig) =>
builder.buildCET(msg).map { cet =>
msg -> CETInfo(cet, remoteAdaptorSig)
}
}
for {
fundingTx <- {
fundingSigsOpt match {
case Some(fundingSigs) => signer.signFundingTx(fundingSigs)
case None => builder.buildFundingTx
}
}
cetInfos <- Future.sequence(cetInfoFs)
refundTx <- signer.signRefundTx(refundSig)
} yield {
SetupDLC(fundingTx, cetInfos.toMap, refundTx)
}
}
/** Return's this party's payout for a given oracle signature */
def getPayout(sigs: Vector[SchnorrDigitalSignature]): CurrencyUnit = {
signer.getPayout(sigs)
}
def executeDLC(
dlcSetup: SetupDLC,
oracleSigs: Vector[SchnorrDigitalSignature]): Future[
ExecutedDLCOutcome] = {
val SetupDLC(fundingTx, cetInfos, _) = dlcSetup
val msgOpt = builder.oracleAndContractInfo.findOutcome(oracleSigs)
val (msg, remoteAdaptorSig) = msgOpt match {
case Some(msg) =>
val cetInfo = cetInfos(msg)
(msg, cetInfo.remoteSignature)
case None =>
throw new IllegalArgumentException(
s"Signature does not correspond to any possible outcome! ${oracleSigs.map(_.hex).mkString(", ")}")
}
signer.signCET(msg, remoteAdaptorSig, oracleSigs).map { cet =>
ExecutedDLCOutcome(fundingTx, cet, msg)
}
}
def executeRefundDLC(dlcSetup: SetupDLC): RefundDLCOutcome = {
val SetupDLC(fundingTx, _, refundTx) = dlcSetup
RefundDLCOutcome(fundingTx, refundTx)
}
}

View file

@ -0,0 +1,19 @@
package org.bitcoins.dlc.execution
import org.bitcoins.core.protocol.tlv.DLCOutcomeType
import org.bitcoins.core.protocol.transaction.Transaction
sealed trait DLCOutcome {
def fundingTx: Transaction
}
case class ExecutedDLCOutcome(
override val fundingTx: Transaction,
cet: Transaction,
outcome: DLCOutcomeType)
extends DLCOutcome
case class RefundDLCOutcome(
override val fundingTx: Transaction,
refundTx: Transaction)
extends DLCOutcome

View file

@ -0,0 +1,34 @@
package org.bitcoins.dlc.execution
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.tlv.DLCOutcomeType
import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutPoint}
import org.bitcoins.crypto.ECAdaptorSignature
case class SetupDLC(
fundingTx: Transaction,
cets: Map[DLCOutcomeType, CETInfo],
refundTx: Transaction) {
cets.foreach {
case (msg, cetInfo) =>
require(
cetInfo.tx.inputs.size == 1,
s"CETs should only spend the funding input, local CET for $msg has ${cetInfo.tx.inputs.size} inputs")
require(
cetInfo.tx.inputs.head.previousOutput == TransactionOutPoint(
fundingTx.txId,
UInt32.zero),
s"CET is not spending the funding input, ${cetInfo.tx.inputs.head}"
)
}
require(
refundTx.inputs.size == 1,
s"RefundTx should only spend the funding input, refundTx has ${refundTx.inputs.size} inputs")
require(
refundTx.inputs.head.previousOutput == TransactionOutPoint(fundingTx.txId,
UInt32.zero),
s"RefundTx is not spending the funding input, ${refundTx.inputs.head}"
)
}
case class CETInfo(tx: Transaction, remoteSignature: ECAdaptorSignature)

View file

@ -0,0 +1,307 @@
package org.bitcoins.dlc.sign
import org.bitcoins.commons.jsonmodels.dlc.{
CETSignatures,
DLCFundingInput,
FundingSignatures
}
import org.bitcoins.core.config.BitcoinNetwork
import org.bitcoins.core.crypto.TransactionSignatureSerializer
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.tlv.DLCOutcomeType
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.protocol.{Bech32Address, BitcoinAddress}
import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature
import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.script.crypto.HashType
import org.bitcoins.core.wallet.signer.BitcoinSigner
import org.bitcoins.core.wallet.utxo._
import org.bitcoins.crypto._
import org.bitcoins.dlc.builder.DLCTxBuilder
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Success
/** Responsible for constructing all DLC signatures
* and signed transactions
*/
case class DLCTxSigner(
builder: DLCTxBuilder,
isInitiator: Boolean,
fundingKey: ECPrivateKey,
finalAddress: BitcoinAddress,
fundingUtxos: Vector[ScriptSignatureParams[InputInfo]])(implicit
ec: ExecutionContext) {
private val offer = builder.offer
private val accept = builder.accept
private val remoteFundingPubKey = if (isInitiator) {
accept.pubKeys.fundingKey
} else {
offer.pubKeys.fundingKey
}
private val fundingSPK: MultiSignatureScriptPubKey =
builder.fundingTxBuilder.fundingMultiSig
if (isInitiator) {
require(fundingKey.publicKey == offer.pubKeys.fundingKey &&
finalAddress == offer.pubKeys.payoutAddress,
"Given keys do not match public key and address in offer")
require(fundingUtxos.map(
DLCFundingInput.fromInputSigningInfo(_)) == offer.fundingInputs,
"Funding ScriptSignatureParams did not match offer funding inputs")
} else {
require(
fundingKey.publicKey == accept.pubKeys.fundingKey &&
finalAddress == accept.pubKeys.payoutAddress,
"Given keys do not match public key and address in accept"
)
require(fundingUtxos.map(
DLCFundingInput.fromInputSigningInfo(_)) == accept.fundingInputs,
"Funding ScriptSignatureParams did not match accept funding inputs")
}
/** Return's this party's payout for a given oracle signature */
def getPayout(sigs: Vector[SchnorrDigitalSignature]): CurrencyUnit = {
val (offerPayout, acceptPayout) = builder.getPayouts(sigs)
if (isInitiator) {
offerPayout
} else {
acceptPayout
}
}
/** Creates this party's FundingSignatures */
def createFundingTxSigs(): Future[FundingSignatures] = {
val sigFs =
Vector.newBuilder[Future[(TransactionOutPoint, ScriptWitnessV0)]]
for {
fundingTx <- builder.buildFundingTx
_ = {
fundingUtxos.foreach { utxo =>
val sigComponentF =
BitcoinSigner.sign(utxo, fundingTx, isDummySignature = false)
val witnessF = sigComponentF.flatMap { sigComponent =>
sigComponent.transaction match {
case wtx: WitnessTransaction =>
val witness = wtx.witness(sigComponent.inputIndex.toInt)
if (witness == EmptyScriptWitness) {
Future.failed(
new RuntimeException(
s"Funding Inputs must be SegWit: $utxo"))
} else {
Future.successful(witness)
}
case _: NonWitnessTransaction =>
Future.failed(
new RuntimeException(s"Funding Inputs must be SegWit: $utxo"))
}
}
sigFs += witnessF.flatMap {
case witness: ScriptWitnessV0 =>
Future.successful((utxo.outPoint, witness))
case witness: ScriptWitness =>
Future.failed(
new RuntimeException(s"Unrecognized script witness: $witness"))
}
}
}
sigs <- Future.sequence(sigFs.result())
} yield {
val sigsMap = sigs.toMap
val fundingInputs = if (isInitiator) {
offer.fundingInputs
} else {
accept.fundingInputs
}
val sigsVec = fundingInputs.map { input =>
input.outPoint -> sigsMap(input.outPoint)
}
FundingSignatures(sigsVec)
}
}
/** Constructs the signed DLC funding transaction given remote FundingSignatures */
def signFundingTx(remoteSigs: FundingSignatures): Future[Transaction] = {
val fundingInputs = offer.fundingInputs ++ accept.fundingInputs
val psbtF = for {
localSigs <- createFundingTxSigs()
allSigs = localSigs.merge(remoteSigs)
fundingTx <- builder.buildFundingTx
} yield {
fundingInputs.zipWithIndex.foldLeft(
PSBT.fromUnsignedTxWithP2SHScript(fundingTx)) {
case (psbt, (fundingInput, index)) =>
val witness = allSigs(fundingInput.outPoint)
psbt
.addUTXOToInput(fundingInput.prevTx, index)
.addFinalizedScriptWitnessToInput(fundingInput.scriptSignature,
witness,
index)
}
}
psbtF.flatMap { psbt =>
val finalizedT = if (psbt.isFinalized) {
Success(psbt)
} else {
psbt.finalizePSBT
}
val txT = finalizedT.flatMap(_.extractTransactionAndValidate)
Future.fromTry(txT)
}
}
private def findSigInPSBT(
psbt: PSBT,
pubKey: ECPublicKey): Future[PartialSignature] = {
val sigOpt = psbt.inputMaps.head.partialSignatures
.find(_.pubKey == pubKey)
sigOpt match {
case None =>
Future.failed(new RuntimeException("No signature found after signing"))
case Some(partialSig) => Future.successful(partialSig)
}
}
/** Signs remote's Contract Execution Transaction (CET) for a given outcome hash */
def createRemoteCETSig(msg: DLCOutcomeType): Future[ECAdaptorSignature] = {
val adaptorPoint = builder.oracleAndContractInfo.sigPointForOutcome(msg)
val hashType = HashType.sigHashAll
for {
fundingTx <- builder.buildFundingTx
fundingOutPoint = TransactionOutPoint(fundingTx.txId, UInt32.zero)
utx <- builder.buildCET(msg)
signingInfo = ECSignatureParams(
P2WSHV0InputInfo(outPoint = fundingOutPoint,
amount = fundingTx.outputs.head.value,
scriptWitness = P2WSHWitnessV0(fundingSPK),
conditionalPath = ConditionalPath.NoCondition),
fundingTx,
fundingKey,
hashType
)
utxWithData = TxUtil.addWitnessData(utx, signingInfo)
hashToSign = TransactionSignatureSerializer.hashForSignature(utxWithData,
signingInfo,
hashType)
} yield {
fundingKey.adaptorSign(adaptorPoint, hashToSign.bytes)
}
}
def signCET(
msg: DLCOutcomeType,
remoteAdaptorSig: ECAdaptorSignature,
oracleSigs: Vector[SchnorrDigitalSignature]): Future[Transaction] = {
val oracleSigSum = oracleSigs.map(_.sig).reduce(_.add(_)).toPrivateKey
val remoteSig =
oracleSigSum
.completeAdaptorSignature(remoteAdaptorSig, HashType.sigHashAll.byte)
val remotePartialSig = PartialSignature(remoteFundingPubKey, remoteSig)
for {
fundingTx <- builder.buildFundingTx
utx <- builder.buildCET(msg)
psbt <-
PSBT
.fromUnsignedTx(utx)
.addUTXOToInput(fundingTx, index = 0)
.addScriptWitnessToInput(P2WSHWitnessV0(fundingSPK), index = 0)
.addSignature(remotePartialSig, inputIndex = 0)
.sign(inputIndex = 0, fundingKey)
cetT = psbt.finalizePSBT.flatMap(_.extractTransactionAndValidate)
cet <- Future.fromTry(cetT)
} yield {
cet
}
}
/** Creates a PSBT of the refund transaction which contain's this
* party's signature
*/
def createPartiallySignedRefundTx(): Future[PSBT] = {
for {
fundingTx <- builder.buildFundingTx
utx <- builder.buildRefundTx
psbt <-
PSBT
.fromUnsignedTx(utx)
.addUTXOToInput(fundingTx, index = 0)
.addScriptWitnessToInput(P2WSHWitnessV0(fundingSPK), index = 0)
.sign(inputIndex = 0, fundingKey)
} yield {
psbt
}
}
/** Creates this party's signature of the refund transaction */
def createRefundSig(): Future[PartialSignature] = {
for {
psbt <- createPartiallySignedRefundTx()
signature <- findSigInPSBT(psbt, fundingKey.publicKey)
} yield {
signature
}
}
/** Constructs the signed refund transaction given remote's signature */
def signRefundTx(remoteSig: PartialSignature): Future[Transaction] = {
for {
unsignedPSBT <- createPartiallySignedRefundTx()
psbt = unsignedPSBT.addSignature(remoteSig, inputIndex = 0)
refundTxT = psbt.finalizePSBT.flatMap(_.extractTransactionAndValidate)
refundTx <- Future.fromTry(refundTxT)
} yield {
refundTx
}
}
/** Creates all of this party's CETSignatures */
def createCETSigs(): Future[CETSignatures] = {
val cetSigFs = builder.oracleAndContractInfo.allOutcomes.map { msg =>
// Need to wrap in another future so they are all started at once
// and do not block each other
Future(createRemoteCETSig(msg).map(msg -> _)).flatten
}
for {
cetSigs <- Future.sequence(cetSigFs)
refundSig <- createRefundSig()
} yield CETSignatures(cetSigs, refundSig)
}
}
object DLCTxSigner {
def apply(
builder: DLCTxBuilder,
isInitiator: Boolean,
fundingKey: ECPrivateKey,
payoutPrivKey: ECPrivateKey,
network: BitcoinNetwork,
fundingUtxos: Vector[ScriptSignatureParams[InputInfo]])(implicit
ec: ExecutionContext): DLCTxSigner = {
val payoutAddr =
Bech32Address(P2WPKHWitnessSPKV0(payoutPrivKey.publicKey), network)
DLCTxSigner(builder, isInitiator, fundingKey, payoutAddr, fundingUtxos)
}
}

View file

@ -0,0 +1,150 @@
package org.bitcoins.dlc.testgen
import org.bitcoins.core.currency.{CurrencyUnit, Satoshis}
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.wallet.builder.{
DualFundingInput,
DualFundingTxFinalizer
}
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.crypto.ECPublicKey
import play.api.libs.json._
import scodec.bits.ByteVector
case class DLCFeeTestVector(
inputs: DLCFeeTestVectorInput,
offerFundingFee: Satoshis,
offerClosingFee: Satoshis,
acceptFundingFee: Satoshis,
acceptClosingFee: Satoshis)
extends TestVector {
override def toJson: JsValue = {
Json.toJson(this)(DLCFeeTestVector.dlcFeeTestVectorFormat)
}
}
case class FundingFeeInfo(redeemScriptLen: Int, maxWitnessLen: Int) {
lazy val mockDualFundingInput: DualFundingInput = {
val scriptSig = if (redeemScriptLen == 0) {
EmptyScriptSignature
} else {
val wspk = if (redeemScriptLen == 22) {
P2WPKHWitnessSPKV0(ECPublicKey.freshPublicKey)
} else {
P2WSHWitnessSPKV0(EmptyScriptPubKey)
}
P2SHScriptSignature(wspk)
}
DualFundingInput(scriptSig, maxWitnessLen)
}
}
case class DLCFeeTestVectorInput(
offerInputs: Vector[FundingFeeInfo],
offerPayoutSPKLen: Int,
offerChangeSPKLen: Int,
acceptInputs: Vector[FundingFeeInfo],
acceptPayoutSPKLen: Int,
acceptChangeSPKLen: Int,
feeRate: SatoshisPerVirtualByte) {
lazy val mockDualFundingTxFinalizer: DualFundingTxFinalizer = {
def mockSPK(len: Int): ScriptPubKey = {
ScriptPubKey.fromAsmBytes(ByteVector.fill(len)(0x00.toByte))
}
DualFundingTxFinalizer(
offerInputs.map(_.mockDualFundingInput),
mockSPK(offerPayoutSPKLen),
mockSPK(offerChangeSPKLen),
acceptInputs.map(_.mockDualFundingInput),
mockSPK(acceptPayoutSPKLen),
mockSPK(acceptChangeSPKLen),
feeRate,
EmptyScriptPubKey
)
}
lazy val offerFundingFee: CurrencyUnit =
mockDualFundingTxFinalizer.offerFundingFee
lazy val offerClosingFee: CurrencyUnit =
mockDualFundingTxFinalizer.offerFutureFee
lazy val acceptFundingFee: CurrencyUnit =
mockDualFundingTxFinalizer.acceptFundingFee
lazy val acceptClosingFee: CurrencyUnit =
mockDualFundingTxFinalizer.acceptFutureFee
}
object DLCFeeTestVectorInput {
def fromJson(json: JsValue): JsResult[DLCFeeTestVectorInput] = {
json.validate(DLCFeeTestVector.dlcFeeTestVectorInputFormat)
}
}
object DLCFeeTestVector extends TestVectorParser[DLCFeeTestVector] {
def apply(inputs: DLCFeeTestVectorInput): DLCFeeTestVector = {
DLCFeeTestVector(
inputs,
inputs.offerFundingFee.satoshis,
inputs.offerClosingFee.satoshis,
inputs.acceptFundingFee.satoshis,
inputs.acceptClosingFee.satoshis
)
}
def apply(
offerInputs: Vector[FundingFeeInfo],
offerPayoutSPKLen: Int,
offerChangeSPKLen: Int,
acceptInputs: Vector[FundingFeeInfo],
acceptPayoutSPKLen: Int,
acceptChangeSPKLen: Int,
feeRate: SatoshisPerVirtualByte): DLCFeeTestVector = {
DLCFeeTestVector(
DLCFeeTestVectorInput(
offerInputs,
offerPayoutSPKLen,
offerChangeSPKLen,
acceptInputs,
acceptPayoutSPKLen,
acceptChangeSPKLen,
feeRate
)
)
}
implicit val fundingFeeInfoFormat: Format[FundingFeeInfo] =
Json.format[FundingFeeInfo]
implicit val satsPerVBFormat: Format[SatoshisPerVirtualByte] =
Format[SatoshisPerVirtualByte](
{
_.validate[Long].map(Satoshis.apply).map(SatoshisPerVirtualByte.apply)
},
{ satsPerVB => JsNumber(satsPerVB.toLong) }
)
implicit val dlcFeeTestVectorInputFormat: Format[DLCFeeTestVectorInput] =
Json.format[DLCFeeTestVectorInput]
implicit val satoshisFormat: Format[Satoshis] =
Format[Satoshis](
{ _.validate[Long].map(Satoshis.apply) },
{ sats => JsNumber(sats.toLong) }
)
implicit val dlcFeeTestVectorFormat: Format[DLCFeeTestVector] =
Json.format[DLCFeeTestVector]
override def fromJson(json: JsValue): JsResult[DLCFeeTestVector] = {
json.validate[DLCFeeTestVector]
}
}

View file

@ -0,0 +1,94 @@
package org.bitcoins.dlc.testgen
import java.io.File
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import play.api.libs.json.{JsResult, JsValue}
import scala.concurrent.Future
object DLCFeeTestVectorGen
extends TestVectorGen[DLCFeeTestVector, DLCFeeTestVectorInput] {
override val defaultTestFile: File = new File(
"dlc/src/main/scala/org/bitcoins/dlc/testgen/dlc_fee_test.json")
override val testVectorParser: DLCFeeTestVector.type =
DLCFeeTestVector
override def inputFromJson: JsValue => JsResult[DLCFeeTestVectorInput] =
DLCFeeTestVectorInput.fromJson
override val inputStr: String = "inputs"
override def generateFromInput: DLCFeeTestVectorInput => Future[
DLCFeeTestVector] = { input =>
Future.successful(DLCFeeTestVector(input))
}
override def generateTestVectors(): Future[Vector[DLCFeeTestVector]] = {
val redeemScriptLens = Vector(0, 22, 34)
val maxWitnessLens = Vector(108, 133, 218)
val feeFundingInfo1 = FundingFeeInfo(0, 108)
val feeFundingInfo2 = FundingFeeInfo(22, 108)
val feeFundingInfo3 = FundingFeeInfo(34, 218)
val oneInput = Vector(feeFundingInfo1)
val twoInputs = Vector(feeFundingInfo2, feeFundingInfo3)
val feeFundingInfos = redeemScriptLens.flatMap { redeemScriptLen =>
maxWitnessLens.flatMap { maxWitnessLen =>
if (
redeemScriptLen == 22 && (maxWitnessLen != 107 || maxWitnessLen != 108)
) {
None
} else {
Some(FundingFeeInfo(redeemScriptLen, maxWitnessLen))
}
}
}
val payoutSPKLens = Vector(22, 25, 34, 35, 71, 173)
val changeSPKLens = Vector(22, 34)
val feeRates = Vector(1L, 5L, 10L)
.map(Satoshis.apply)
.map(SatoshisPerVirtualByte.apply)
def allTests(
offerInputs: Vector[FundingFeeInfo],
acceptInputs: Vector[FundingFeeInfo]): Vector[DLCFeeTestVector] = {
for {
offerPayoutSPKLen <- payoutSPKLens
offerChangeSPKLen <- changeSPKLens
acceptPayoutSPKLen <- payoutSPKLens
acceptChangeSPKLen <- changeSPKLens
feeRate <- feeRates
} yield {
DLCFeeTestVector(
offerInputs,
offerPayoutSPKLen,
offerChangeSPKLen,
acceptInputs,
acceptPayoutSPKLen,
acceptChangeSPKLen,
feeRate
)
}
}
def someTests(
offerInputs: Vector[FundingFeeInfo],
acceptInputs: Vector[FundingFeeInfo]): Vector[DLCFeeTestVector] = {
allTests(offerInputs, acceptInputs)
.sortBy(_ => scala.util.Random.nextDouble())
.take(10)
}
val tests = allTests(oneInput, oneInput) ++
someTests(twoInputs, twoInputs) ++
someTests(oneInput, twoInputs) ++
someTests(twoInputs, oneInput) ++
someTests(feeFundingInfos, feeFundingInfos)
Future.successful(tests)
}
}

View file

@ -0,0 +1,442 @@
package org.bitcoins.dlc.testgen
import org.bitcoins.core.number.{UInt16, UInt64}
import org.bitcoins.core.protocol.BigSizeUInt
import org.bitcoins.core.protocol.script.EmptyScriptPubKey
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.crypto.{CryptoUtil, NetworkElement}
import org.bitcoins.dlc.testgen.ByteVectorWrapper._
import play.api.libs.json.{
JsArray,
JsError,
JsObject,
JsResult,
JsString,
JsSuccess,
JsValue
}
import scodec.bits.ByteVector
sealed trait DLCParsingTestVector extends TestVector {
def input: NetworkElement
def tpeName: String
def fields: Vector[(String, ByteVectorWrapper)]
override def toJson: JsValue = {
val jsonFields = fields.map {
case (name, byteWrapper) =>
name -> byteWrapper.toJson
}
JsObject(
Map(
"tpeName" -> JsString(tpeName),
"input" -> JsString(input.hex),
"fields" -> JsObject(jsonFields)
))
}
}
sealed trait ByteVectorWrapper {
def toJson: JsValue
}
object ByteVectorWrapper {
implicit class Element(val bytes: ByteVector) extends ByteVectorWrapper {
override def equals(obj: Any): Boolean = {
obj match {
case elem: Element => elem.bytes == bytes
case _ => false
}
}
override def toString: String = {
s"Element(${bytes.length} bytes, 0x${bytes.toHex})"
}
override def toJson: JsValue = {
JsString(bytes.toHex)
}
}
object Element {
def apply(element: NetworkElement): Element = {
element.bytes
}
}
case class MultiElement(elements: Vector[ByteVectorWrapper])
extends ByteVectorWrapper {
override def toString: String = {
s"MultiElement(${elements.mkString(",")})"
}
override def toJson: JsValue = {
JsArray(elements.map(_.toJson))
}
}
object MultiElement {
def apply(elements: ByteVectorWrapper*): MultiElement = {
new MultiElement(elements.toVector)
}
}
case class NamedMultiElement(elements: Vector[(String, ByteVectorWrapper)])
extends ByteVectorWrapper {
override def toString: String = {
s"NamedMultiElement(${elements
.map { case (name, bytes) => s"$name -> $bytes" }
.mkString(",")})"
}
override def toJson: JsValue = {
JsObject(elements.map { case (name, element) => name -> element.toJson })
}
}
object NamedMultiElement {
def apply(elements: (String, ByteVectorWrapper)*): NamedMultiElement = {
new NamedMultiElement(elements.toVector)
}
}
def fromJson(json: JsValue): JsResult[ByteVectorWrapper] = {
json.validate[String] match {
case JsSuccess(hex, _) => JsSuccess(ByteVector.fromValidHex(hex))
case JsError(_) =>
json.validate[Vector[JsValue]] match {
case JsSuccess(vec, _) =>
val nestedResult =
DLCParsingTestVector.flattenJsResult(vec.map(fromJson))
nestedResult.map(MultiElement(_))
case JsError(_) =>
json.validate[Map[String, JsValue]] match {
case JsSuccess(obj, _) =>
val jsResults = obj.map {
case (name, nestedJson) => fromJson(nestedJson).map(name -> _)
}.toVector
val nestedResult =
DLCParsingTestVector.flattenJsResult(jsResults)
nestedResult.map(NamedMultiElement(_))
case JsError(_) => JsError("Couldn't parse field bytes")
}
}
}
}
}
case class DLCTLVTestVector(
input: TLV,
tpeName: String,
fields: Vector[(String, ByteVectorWrapper)])
extends DLCParsingTestVector
case class DLCMessageTestVector(
input: LnMessage[TLV],
tpeName: String,
fields: Vector[(String, ByteVectorWrapper)])
extends DLCParsingTestVector
object DLCParsingTestVector extends TestVectorParser[DLCParsingTestVector] {
def apply(tlv: TLV): DLCParsingTestVector = {
tlv match {
case ContractInfoV0TLV(outcomes) =>
val fields = Vector(
"tpe" -> Element(ContractInfoV0TLV.tpe),
"length" -> Element(tlv.length),
"outcomes" -> MultiElement(outcomes.toVector.map {
case (outcome, amt) =>
NamedMultiElement("outcome" -> CryptoUtil.sha256(outcome).bytes,
"localPayout" -> amt.toUInt64.bytes)
})
)
DLCTLVTestVector(tlv, "contract_info_v0", fields)
case ContractInfoV1TLV(base, numDigits, totalCollateral, points) =>
val fields = Vector(
"tpe" -> Element(ContractInfoV1TLV.tpe),
"length" -> Element(tlv.length),
"base" -> Element(BigSizeUInt(base)),
"numDigits" -> Element(UInt16(numDigits)),
"totalCollateral" -> Element(UInt64(totalCollateral.toLong)),
"numPoints" -> Element(BigSizeUInt(points.length)),
"points" -> MultiElement(points.map { point =>
NamedMultiElement(
"isEndpoint" -> Element(ByteVector(point.leadingByte)),
"outcome" -> Element(BigSizeUInt(point.outcome)),
"value" -> Element(UInt64(point.value.toLong))
)
})
)
DLCTLVTestVector(tlv, "contract_info_v1", fields)
case OracleInfoV0TLV(pubKey, rValue) =>
val fields = Vector(
"tpe" -> Element(OracleInfoV0TLV.tpe),
"length" -> Element(tlv.length),
"pubKey" -> Element(pubKey),
"rValue" -> Element(rValue)
)
DLCTLVTestVector(tlv, "oracle_info_v0", fields)
case OracleInfoV1TLV(pubKey, nonces) =>
val fields = Vector(
"tpe" -> Element(OracleInfoV1TLV.tpe),
"length" -> Element(tlv.length),
"pubKey" -> Element(pubKey),
"nonces" -> MultiElement(nonces.map(Element(_)))
)
DLCTLVTestVector(tlv, "oracle_info_v1", fields)
case FundingInputV0TLV(prevTx,
prevTxVout,
sequence,
maxWitnessLen,
redeemScriptOpt) =>
val redeemScript =
redeemScriptOpt.getOrElse(EmptyScriptPubKey)
val fields = Vector(
"tpe" -> Element(FundingInputV0TLV.tpe),
"length" -> Element(tlv.length),
"prevTxLen" -> Element(UInt16(prevTx.byteSize)),
"prevTx" -> Element(prevTx),
"prevTxVout" -> Element(prevTxVout),
"sequence" -> Element(sequence),
"maxWitnessLen" -> Element(maxWitnessLen),
"redeemScriptLen" -> Element(UInt16(redeemScript.asmBytes.length)),
"redeemScript" -> Element(redeemScript.asmBytes)
)
DLCTLVTestVector(tlv, "funding_input_v0", fields)
case CETSignaturesV0TLV(sigs) =>
val fields = Vector(
"tpe" -> Element(CETSignaturesV0TLV.tpe),
"length" -> Element(tlv.length),
"sigs" -> MultiElement(
sigs.map(sig =>
NamedMultiElement("encryptedSig" -> sig.adaptedSig,
"dleqProof" -> sig.dleqProof)))
)
DLCTLVTestVector(tlv, "cet_adaptor_signatures_v0", fields)
case FundingSignaturesV0TLV(witnesses) =>
val fields = Vector(
"tpe" -> Element(FundingSignaturesV0TLV.tpe),
"length" -> Element(tlv.length),
"numWitnesses" -> Element(UInt16(witnesses.length)),
"witnesses" -> MultiElement(witnesses.map { witness =>
NamedMultiElement(
"stackLen" -> Element(UInt16(witness.stack.length)),
"stack" -> MultiElement(witness.stack.toVector.reverse.map {
stackElem =>
NamedMultiElement(
"stackElementLen" -> Element(UInt16(stackElem.length)),
"stackElement" -> stackElem)
})
)
})
)
DLCTLVTestVector(tlv, "funding_signatures_v0", fields)
case DLCOfferTLV(contractFlags,
chainHash,
contractInfo,
oracleInfo,
fundingPubKey,
payoutSPK,
totalCollateralSatoshis,
fundingInputs,
changeSPK,
feeRate,
contractMaturityBound,
contractTimeout) =>
val fields = Vector(
"tpe" -> Element(UInt16(DLCOfferTLV.tpe.toInt)),
"contractFlags" -> Element(ByteVector(contractFlags)),
"chainHash" -> Element(chainHash),
"contractInfo" -> Element(contractInfo),
"oracleInfo" -> Element(oracleInfo),
"fundingPubKey" -> Element(fundingPubKey),
"payoutSPKLen" -> Element(UInt16(payoutSPK.asmBytes.length)),
"payoutSPK" -> Element(payoutSPK.asmBytes),
"totalCollateralSatoshis" -> Element(
totalCollateralSatoshis.toUInt64),
"fundingInputsLen" -> Element(UInt16(fundingInputs.length)),
"fundingInputs" -> new MultiElement(
fundingInputs.map(input => Element(input.bytes))),
"changeSPKLen" -> Element(UInt16(changeSPK.asmBytes.length)),
"changeSPK" -> Element(changeSPK.asmBytes),
"feeRate" -> Element(feeRate.currencyUnit.satoshis.toUInt64),
"contractMaturityBound" -> Element(contractMaturityBound.toUInt32),
"contractTimeout" -> Element(contractTimeout.toUInt32)
)
DLCMessageTestVector(LnMessage(tlv), "offer_dlc_v0", fields)
case DLCAcceptTLV(tempContractId,
totalCollateralSatoshis,
fundingPubKey,
payoutSPK,
fundingInputs,
changeSPK,
cetSignatures,
refundSignature) =>
val fields = Vector(
"tpe" -> Element(UInt16(DLCAcceptTLV.tpe.toInt)),
"tempContractId" -> Element(tempContractId),
"totalCollateralSatoshis" -> Element(
totalCollateralSatoshis.toUInt64),
"fundingPubKey" -> Element(fundingPubKey),
"payoutSPKLen" -> Element(UInt16(payoutSPK.asmBytes.length)),
"payoutSPK" -> Element(payoutSPK.asmBytes),
"fundingInputsLen" -> Element(UInt16(fundingInputs.length)),
"fundingInputs" -> new MultiElement(
fundingInputs.map(input => Element(input.bytes))),
"changeSPKLen" -> Element(UInt16(changeSPK.asmBytes.length)),
"changeSPK" -> Element(changeSPK.asmBytes),
"cetSignatures" -> Element(cetSignatures),
"refundSignature" -> Element(refundSignature.toRawRS)
)
DLCMessageTestVector(LnMessage(tlv), "accept_dlc_v0", fields)
case DLCSignTLV(contractId,
cetSignatures,
refundSignature,
fundingSignatures) =>
val fields = Vector(
"tpe" -> Element(UInt16(DLCSignTLV.tpe.toInt)),
"contractId" -> Element(contractId),
"cetSignatures" -> Element(cetSignatures),
"refundSignature" -> Element(refundSignature.toRawRS),
"fundingSignatures" -> Element(fundingSignatures)
)
DLCMessageTestVector(LnMessage(tlv), "sign_dlc_v0", fields)
case EnumEventDescriptorV0TLV(outcomes) =>
val fields = Vector(
"tpe" -> Element(EnumEventDescriptorV0TLV.tpe),
"length" -> Element(tlv.length),
"numOutcomes" -> Element(UInt16(outcomes.size)),
"outcomes" -> MultiElement(outcomes.map { outcome =>
val outcomeBytes = CryptoUtil.serializeForHash(outcome)
NamedMultiElement(
"outcomeLen" -> Element(UInt16(outcomeBytes.length)),
"outcome" -> Element(outcomeBytes))
})
)
DLCTLVTestVector(tlv, "enum_event_descriptor_v0", fields)
case RangeEventDescriptorV0TLV(start, stop, step, units, precision) =>
val fields = Vector(
"tpe" -> Element(RangeEventDescriptorV0TLV.tpe),
"length" -> Element(tlv.length),
"start" -> Element(start),
"stop" -> Element(stop),
"step" -> Element(step),
"units" -> Element(CryptoUtil.serializeForHash(units)),
"precision" -> Element(precision)
)
DLCTLVTestVector(tlv, "range_event_descriptor_v0", fields)
case SignedDigitDecompositionEventDescriptor(base,
numDigits,
units,
precision) =>
val fields = Vector(
"tpe" -> Element(RangeEventDescriptorV0TLV.tpe),
"length" -> Element(tlv.length),
"base" -> Element(base),
"numDigits" -> Element(numDigits),
"isSigned" -> Element(ByteVector.fromByte(0x01)),
"units" -> Element(CryptoUtil.serializeForHash(units)),
"precision" -> Element(precision)
)
DLCTLVTestVector(tlv, "range_event_descriptor_v0", fields)
case UnsignedDigitDecompositionEventDescriptor(base,
numDigits,
units,
precision) =>
val fields = Vector(
"tpe" -> Element(RangeEventDescriptorV0TLV.tpe),
"length" -> Element(tlv.length),
"base" -> Element(base),
"numDigits" -> Element(numDigits),
"isSigned" -> Element(ByteVector.fromByte(0x00)),
"units" -> Element(CryptoUtil.serializeForHash(units)),
"precision" -> Element(precision)
)
DLCTLVTestVector(tlv, "range_event_descriptor_v0", fields)
case OracleEventV0TLV(nonces, eventMaturity, descriptor, uri) =>
val fields = Vector(
"tpe" -> Element(OracleEventV0TLV.tpe),
"length" -> Element(tlv.length),
"oracleNonces" -> MultiElement(nonces.map(Element(_))),
"eventMaturityEpoch" -> Element(eventMaturity),
"eventDescriptor" -> Element(descriptor),
"event_uri" -> Element(CryptoUtil.serializeForHash(uri))
)
DLCTLVTestVector(tlv, "oracle_event_v0", fields)
case OracleAnnouncementV0TLV(sig, pubkey, event) =>
val fields = Vector(
"tpe" -> Element(UInt16(OracleAnnouncementV0TLV.tpe.toInt)),
"signature" -> Element(sig),
"oraclePubKey" -> Element(pubkey),
"oracleEvent" -> Element(event)
)
DLCMessageTestVector(LnMessage(tlv), "oracle_announcement_v0", fields)
case _: UnknownTLV | _: ErrorTLV | _: PingTLV | _: PongTLV =>
throw new IllegalArgumentException(
s"DLCParsingTestVector is only defined for DLC messages and TLVs, got $tlv")
}
}
def flattenJsResult[T](vec: Vector[JsResult[T]]): JsResult[Vector[T]] = {
vec.foldLeft[JsResult[Vector[T]]](JsSuccess(Vector.empty)) {
case (vecResult, elemResult) =>
vecResult.flatMap { vec =>
elemResult.map { elem =>
vec :+ elem
}
}
}
}
override def fromJson(json: JsValue): JsResult[DLCParsingTestVector] = {
for {
outer <- json.validate[Map[String, JsValue]]
inputBytes <-
outer("input").validate[String].map(ByteVector.fromValidHex(_))
tpeName <- outer("tpeName").validate[String]
jsFields <- outer("fields").validate[JsObject].map(_.fields.toVector)
fields <- flattenJsResult {
jsFields.map {
case (name, field) =>
ByteVectorWrapper.fromJson(field).map(name -> _)
}
}
} yield {
val msgTpe = UInt16(inputBytes.take(2)).toInt
if (TLV.knownTypes.contains(BigSizeUInt(msgTpe))) {
DLCMessageTestVector(LnMessage(inputBytes), tpeName, fields)
} else {
DLCTLVTestVector(TLV(inputBytes), tpeName, fields)
}
}
}
def tlvFromJson(input: JsValue): JsResult[TLV] = {
val inputBytesResult =
input.validate[String].map(ByteVector.fromValidHex(_))
inputBytesResult.map { inputBytes =>
val msgTpe = UInt16(inputBytes.take(2)).toInt
if (TLV.knownTypes.contains(BigSizeUInt(msgTpe))) {
LnMessage(inputBytes).tlv
} else {
TLV(inputBytes)
}
}
}
}

View file

@ -0,0 +1,42 @@
package org.bitcoins.dlc.testgen
import java.io.File
import org.bitcoins.core.protocol.tlv.TLV
import play.api.libs.json.{JsResult, JsValue}
import scala.concurrent.Future
object DLCParsingTestVectorGen
extends TestVectorGen[DLCParsingTestVector, TLV] {
override val defaultTestFile: File = new File(
"dlc/src/main/scala/org/bitcoins/dlc/testgen/dlc_message_test.json")
override val testVectorParser: DLCParsingTestVector.type =
DLCParsingTestVector
override def inputFromJson: JsValue => JsResult[TLV] =
DLCParsingTestVector.tlvFromJson
override val inputStr: String = "input"
override def generateFromInput: TLV => Future[DLCParsingTestVector] = { tlv =>
Future.successful(DLCParsingTestVector(tlv))
}
override def generateTestVectors(): Future[Vector[DLCParsingTestVector]] = {
Future.successful(
Vector(
DLCTLVGen.contractInfoParsingTestVector(),
DLCTLVGen.oracleInfoParsingTestVector(),
DLCTLVGen.fundingInputParsingTestVector(),
DLCTLVGen.cetSigsParsingTestVector(),
DLCTLVGen.fundingSigsParsingTestVector(),
DLCTLVGen.dlcOfferParsingTestVector(),
DLCTLVGen.dlcAcceptParsingTestVector(),
DLCTLVGen.dlcSignParsingTestVector()
)
)
}
}

View file

@ -0,0 +1,398 @@
package org.bitcoins.dlc.testgen
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage._
import org.bitcoins.commons.jsonmodels.dlc._
import org.bitcoins.core.config.{NetworkParameters, RegTest}
import org.bitcoins.core.currency.{CurrencyUnit, CurrencyUnits, Satoshis}
import org.bitcoins.core.number.{UInt16, UInt32}
import org.bitcoins.core.protocol._
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature
import org.bitcoins.core.util.NumberUtil
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.crypto._
import org.bitcoins.dlc.builder.DLCTxBuilder
import scodec.bits.ByteVector
object DLCTLVGen {
val defaultAmt: Satoshis = CurrencyUnits.oneBTC.satoshis
def hash(bytes: ByteVector = NumberUtil.randomBytes(32)): Sha256Digest = {
CryptoUtil.sha256(bytes)
}
def genContractInfo(
outcomes: Vector[String] = DLCTestUtil.genOutcomes(3),
totalInput: CurrencyUnit = defaultAmt * 2): SingleNonceContractInfo = {
DLCTestUtil.genContractInfos(outcomes, totalInput)._1
}
def contractInfoParsingTestVector(
outcomes: Vector[String] = DLCTestUtil.genOutcomes(3),
totalInput: CurrencyUnit = defaultAmt * 2): DLCParsingTestVector = {
DLCParsingTestVector(genContractInfo(outcomes, totalInput).toTLV)
}
def genOracleInfo(
oraclePubKey: SchnorrPublicKey =
ECPublicKey.freshPublicKey.schnorrPublicKey,
oracleRValue: SchnorrNonce =
ECPublicKey.freshPublicKey.schnorrNonce): SingleNonceOracleInfo = {
SingleNonceOracleInfo(oraclePubKey, oracleRValue)
}
def genOracleAndContractInfo(
oraclePubKey: SchnorrPublicKey =
ECPublicKey.freshPublicKey.schnorrPublicKey,
oracleRValue: SchnorrNonce = ECPublicKey.freshPublicKey.schnorrNonce,
outcomes: Vector[String] = DLCTestUtil.genOutcomes(3),
totalInput: CurrencyUnit = defaultAmt * 2): OracleAndContractInfo = {
OracleAndContractInfo(genOracleInfo(oraclePubKey, oracleRValue),
genContractInfo(outcomes, totalInput))
}
def oracleInfoParsingTestVector(
oraclePubKey: SchnorrPublicKey =
ECPublicKey.freshPublicKey.schnorrPublicKey,
oracleRValue: SchnorrNonce =
ECPublicKey.freshPublicKey.schnorrNonce): DLCParsingTestVector = {
DLCParsingTestVector(genOracleInfo(oraclePubKey, oracleRValue).toTLV)
}
def p2wpkh(
pubKey: ECPublicKey = ECPublicKey.freshPublicKey): P2WPKHWitnessSPKV0 = {
P2WPKHWitnessSPKV0(pubKey)
}
def address(
spk: ScriptPubKey = p2wpkh(),
network: NetworkParameters = RegTest): BitcoinAddress = {
spk match {
case wspk: WitnessScriptPubKey => Bech32Address(wspk, network)
case p2sh: P2SHScriptPubKey => P2SHAddress(p2sh, network)
case p2pkh: P2PKHScriptPubKey => P2PKHAddress(p2pkh, network)
case _: RawScriptPubKey =>
throw new IllegalArgumentException(s"$spk is not valid for an address")
}
}
def inputTransaction(
input: CurrencyUnit = defaultAmt,
spk: ScriptPubKey = p2wpkh()): Transaction = {
BaseTransaction(
TransactionConstants.validLockVersion,
Vector.empty,
Vector(TransactionOutput(input * 2, spk)),
UInt32.zero
)
}
def outputReference(
input: CurrencyUnit = defaultAmt,
spk: ScriptPubKey =
P2WPKHWitnessSPKV0(ECPublicKey.freshPublicKey)): OutputReference = {
val tx = inputTransaction(input, spk)
OutputReference(TransactionOutPoint(tx.txIdBE, UInt32.zero),
tx.outputs.head)
}
def fundingInput(
prevTx: Transaction = inputTransaction(),
prevTxVout: UInt32 = UInt32.zero,
sequence: UInt32 = TransactionConstants.sequence,
maxWitnessLen: UInt16 = UInt16(107),
redeemScriptOpt: Option[WitnessScriptPubKey] = None): DLCFundingInput = {
DLCFundingInput(prevTx,
prevTxVout,
sequence,
maxWitnessLen,
redeemScriptOpt)
}
def fundingInputParsingTestVector(
prevTx: Transaction = inputTransaction(),
prevTxVout: UInt32 = UInt32.zero,
sequence: UInt32 = TransactionConstants.sequence,
maxWitnessLen: UInt16 = UInt16(107),
redeemScriptOpt: Option[WitnessScriptPubKey] =
None): DLCParsingTestVector = {
DLCParsingTestVector(
fundingInput(prevTx,
prevTxVout,
sequence,
maxWitnessLen,
redeemScriptOpt).toTLV)
}
def adaptorSig: ECAdaptorSignature = {
ECAdaptorSignature(
ECPublicKey.freshPublicKey,
ECPrivateKey.freshPrivateKey.fieldElement,
ECPublicKey.freshPublicKey,
ECPrivateKey.freshPrivateKey.fieldElement,
ECPrivateKey.freshPrivateKey.fieldElement
)
}
def ecdsaSig(sigHashByte: Boolean = true): ECDigitalSignature = {
val sigWithoutSigHash = ECDigitalSignature.fromRS(
ECPrivateKey.freshPrivateKey.fieldElement.toBigInteger,
ECPrivateKey.freshPrivateKey.fieldElement.toBigInteger)
if (sigHashByte) {
ECDigitalSignature(sigWithoutSigHash.bytes :+ 0x01)
} else {
sigWithoutSigHash
}
}
def partialSig(
pubKey: ECPublicKey = ECPublicKey.freshPublicKey,
sigHashByte: Boolean = true): PartialSignature = {
PartialSignature(pubKey, ecdsaSig(sigHashByte))
}
def p2wpkhWitnessV0(
pubKey: ECPublicKey = ECPublicKey.freshPublicKey,
sigHashByte: Boolean = true): P2WPKHWitnessV0 = {
P2WPKHWitnessV0(pubKey, ecdsaSig(sigHashByte))
}
def cetSigs(
outcomes: Vector[DLCOutcomeType] =
DLCTestUtil.genOutcomes(3).map(EnumOutcome.apply),
fundingPubKey: ECPublicKey =
ECPublicKey.freshPublicKey): CETSignatures = {
CETSignatures(outcomes.map(outcome => outcome -> adaptorSig),
partialSig(fundingPubKey, sigHashByte = false))
}
def cetSigsParsingTestVector(numOutcomes: Int = 3): DLCParsingTestVector = {
DLCParsingTestVector(
CETSignaturesV0TLV((0 until numOutcomes).toVector.map(_ => adaptorSig)))
}
def fundingSigs(
outPoints: Vector[TransactionOutPoint] = Vector(
outputReference().outPoint)): FundingSignatures = {
FundingSignatures(outPoints.map(outpoint => outpoint -> p2wpkhWitnessV0()))
}
def fundingSigsParsingTestVector(
outPoints: Vector[TransactionOutPoint] = Vector(
outputReference().outPoint)): DLCParsingTestVector = {
DLCParsingTestVector(fundingSigs(outPoints).toTLV)
}
def dlcOffer(
oracleAndContractInfo: OracleAndContractInfo = genOracleAndContractInfo(),
fundingPubKey: ECPublicKey = ECPublicKey.freshPublicKey,
payoutAddress: BitcoinAddress = address(),
totalCollateral: Satoshis = defaultAmt,
fundingInputs: Vector[DLCFundingInput] = Vector(fundingInput()),
changeAddress: BitcoinAddress = address(),
feeRate: SatoshisPerVirtualByte = SatoshisPerVirtualByte.one,
contractMaturityBound: BlockTimeStamp = BlockTimeStamp(100),
contractTimeout: BlockTimeStamp = BlockTimeStamp(200)): DLCOffer = {
DLCOffer(
oracleAndContractInfo,
DLCPublicKeys(fundingPubKey, payoutAddress),
totalCollateral,
fundingInputs,
changeAddress,
feeRate,
DLCTimeouts(contractMaturityBound, contractTimeout)
)
}
def dlcOfferTLV(
oracleAndContractInfo: OracleAndContractInfo = genOracleAndContractInfo(),
fundingPubKey: ECPublicKey = ECPublicKey.freshPublicKey,
payoutAddress: BitcoinAddress = address(),
totalCollateral: Satoshis = defaultAmt,
fundingInputs: Vector[DLCFundingInput] = Vector(fundingInput()),
changeAddress: BitcoinAddress = address(),
feeRate: SatoshisPerVirtualByte = SatoshisPerVirtualByte.one,
contractMaturityBound: BlockTimeStamp = BlockTimeStamp(100),
contractTimeout: BlockTimeStamp = BlockTimeStamp(200)): DLCOfferTLV = {
dlcOffer(oracleAndContractInfo,
fundingPubKey,
payoutAddress,
totalCollateral,
fundingInputs,
changeAddress,
feeRate,
contractMaturityBound,
contractTimeout).toTLV
}
def dlcOfferParsingTestVector(
oracleAndContractInfo: OracleAndContractInfo = genOracleAndContractInfo(),
fundingPubKey: ECPublicKey = ECPublicKey.freshPublicKey,
payoutAddress: BitcoinAddress = address(),
totalCollateral: Satoshis = defaultAmt,
fundingInputs: Vector[DLCFundingInput] = Vector(fundingInput()),
changeAddress: BitcoinAddress = address(),
feeRate: SatoshisPerVirtualByte = SatoshisPerVirtualByte.one,
contractMaturityBound: BlockTimeStamp = BlockTimeStamp(100),
contractTimeout: BlockTimeStamp =
BlockTimeStamp(200)): DLCParsingTestVector = {
DLCParsingTestVector(
dlcOfferTLV(oracleAndContractInfo,
fundingPubKey,
payoutAddress,
totalCollateral,
fundingInputs,
changeAddress,
feeRate,
contractMaturityBound,
contractTimeout))
}
def dlcAccept(
totalCollateral: Satoshis = defaultAmt,
fundingPubKey: ECPublicKey = ECPublicKey.freshPublicKey,
payoutAddress: BitcoinAddress = address(),
fundingInputs: Vector[DLCFundingInput] = Vector(fundingInput()),
changeAddress: BitcoinAddress = address(),
cetSignatures: CETSignatures = cetSigs(),
tempContractId: Sha256Digest = hash()): DLCAccept = {
DLCAccept(totalCollateral,
DLCPublicKeys(fundingPubKey, payoutAddress),
fundingInputs,
changeAddress,
cetSignatures,
tempContractId)
}
def dlcAcceptTLV(
totalCollateral: Satoshis = defaultAmt,
fundingPubKey: ECPublicKey = ECPublicKey.freshPublicKey,
payoutAddress: BitcoinAddress = address(),
fundingInputs: Vector[DLCFundingInput] = Vector(fundingInput()),
changeAddress: BitcoinAddress = address(),
cetSignatures: CETSignatures = cetSigs(),
tempContractId: Sha256Digest = hash()): DLCAcceptTLV = {
dlcAccept(totalCollateral,
fundingPubKey,
payoutAddress,
fundingInputs,
changeAddress,
cetSignatures,
tempContractId).toTLV
}
def dlcAcceptParsingTestVector(
totalCollateral: Satoshis = defaultAmt,
fundingPubKey: ECPublicKey = ECPublicKey.freshPublicKey,
payoutAddress: BitcoinAddress = address(),
fundingInputs: Vector[DLCFundingInput] = Vector(fundingInput()),
changeAddress: BitcoinAddress = address(),
cetSignatures: CETSignatures = cetSigs(),
tempContractId: Sha256Digest = hash()): DLCParsingTestVector = {
DLCParsingTestVector(
dlcAcceptTLV(totalCollateral,
fundingPubKey,
payoutAddress,
fundingInputs,
changeAddress,
cetSignatures,
tempContractId))
}
def dlcAcceptFromOffer(
offer: DLCOffer,
overCollateral: Satoshis = Satoshis.zero,
fundingPubKey: ECPublicKey = ECPublicKey.freshPublicKey,
payoutAddress: BitcoinAddress = address(),
fundingInputs: Vector[DLCFundingInput] = Vector(fundingInput()),
changeAddress: BitcoinAddress = address()): DLCAccept = {
val totalCollateral =
offer.contractInfo.max - offer.totalCollateral + overCollateral
val cetSignatures =
cetSigs(offer.oracleAndContractInfo.allOutcomes, fundingPubKey)
val tempContractId = offer.tempContractId
DLCAccept(totalCollateral.satoshis,
DLCPublicKeys(fundingPubKey, payoutAddress),
fundingInputs,
changeAddress,
cetSignatures,
tempContractId)
}
def dlcAcceptTLVFromOffer(
offer: DLCOffer,
overCollateral: Satoshis = Satoshis.zero,
fundingPubKey: ECPublicKey = ECPublicKey.freshPublicKey,
payoutAddress: BitcoinAddress = address(),
fundingInputs: Vector[DLCFundingInput] = Vector(fundingInput()),
changeAddress: BitcoinAddress = address()): DLCAcceptTLV = {
dlcAcceptFromOffer(offer,
overCollateral,
fundingPubKey,
payoutAddress,
fundingInputs,
changeAddress).toTLV
}
def dlcSign(
cetSignatures: CETSignatures = cetSigs(),
fundingSignatures: FundingSignatures = fundingSigs(),
contractId: ByteVector = hash().bytes): DLCSign = {
DLCSign(cetSignatures, fundingSignatures, contractId)
}
def dlcSignTLV(
cetSignatures: CETSignatures = cetSigs(),
fundingSignatures: FundingSignatures = fundingSigs(),
contractId: ByteVector = hash().bytes): DLCSignTLV = {
dlcSign(cetSignatures, fundingSignatures, contractId).toTLV
}
def dlcSignParsingTestVector(
cetSignatures: CETSignatures = cetSigs(),
fundingSignatures: FundingSignatures = fundingSigs(),
contractId: ByteVector = hash().bytes): DLCParsingTestVector = {
DLCParsingTestVector(
dlcSignTLV(cetSignatures, fundingSignatures, contractId))
}
def dlcSignFromOffer(
offer: DLCOffer,
contractId: ByteVector = hash().bytes): DLCSign = {
val cetSignatures =
cetSigs(offer.oracleAndContractInfo.allOutcomes, offer.pubKeys.fundingKey)
val fundingSignatures = fundingSigs(offer.fundingInputs.map(_.outPoint))
DLCSign(cetSignatures, fundingSignatures, contractId)
}
def dlcSignTLVFromOffer(
offer: DLCOffer,
contractId: ByteVector = hash().bytes): DLCSignTLV = {
dlcSignFromOffer(offer, contractId).toTLV
}
def dlcSignFromOfferAndAccept(offer: DLCOffer, accept: DLCAccept): DLCSign = {
import scala.concurrent.duration.DurationInt
import scala.concurrent.{Await, ExecutionContext}
val builder =
DLCTxBuilder(offer, accept.withoutSigs)(ExecutionContext.global)
val fundingTx = Await.result(builder.buildFundingTx, 5.seconds)
val contractId = fundingTx.txIdBE.bytes.xor(accept.tempContractId.bytes)
dlcSignFromOffer(offer, contractId)
}
def dlcSignTLVFromOfferAndAccept(
offer: DLCOffer,
accept: DLCAccept): DLCSignTLV = {
dlcSignFromOfferAndAccept(offer, accept).toTLV
}
}

View file

@ -0,0 +1,151 @@
package org.bitcoins.dlc.testgen
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.{
MultiNonceContractInfo,
SingleNonceContractInfo
}
import org.bitcoins.commons.jsonmodels.dlc.{
CETSignatures,
FundingSignatures,
OutcomeValueFunction,
OutcomeValuePoint
}
import org.bitcoins.core.currency.{CurrencyUnit, Satoshis}
import org.bitcoins.core.protocol.script.{
EmptyScriptPubKey,
P2WPKHWitnessV0,
P2WSHWitnessV0,
ScriptWitnessV0
}
import org.bitcoins.core.protocol.tlv.EnumOutcome
import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature
import org.bitcoins.core.util.NumberUtil
import org.bitcoins.crypto.{ECAdaptorSignature, ECDigitalSignature}
import scodec.bits.ByteVector
object DLCTestUtil {
def genOutcomes(size: Int): Vector[String] = {
(0 until size).map(_ => scala.util.Random.nextLong().toString).toVector
}
def genValues(size: Int, totalAmount: CurrencyUnit): Vector[Satoshis] = {
val vals = if (size < 2) {
throw new IllegalArgumentException(
s"Size must be at least two, got $size")
} else if (size == 2) {
Vector(totalAmount.satoshis, Satoshis.zero)
} else {
(0 until size - 2).map { _ =>
Satoshis(NumberUtil.randomLong(totalAmount.satoshis.toLong))
}.toVector :+ totalAmount.satoshis :+ Satoshis.zero
}
val valsWithOrder = vals.map(_ -> scala.util.Random.nextDouble())
valsWithOrder.sortBy(_._2).map(_._1)
}
def genContractInfos(outcomes: Vector[String], totalInput: CurrencyUnit): (
SingleNonceContractInfo,
SingleNonceContractInfo) = {
val outcomeMap =
outcomes
.map(EnumOutcome.apply)
.zip(DLCTestUtil.genValues(outcomes.length, totalInput))
val info = SingleNonceContractInfo(outcomeMap)
val remoteInfo = info.flip(totalInput.satoshis)
(info, remoteInfo)
}
/** Generates a collared forward contract */
def genMultiDigitContractInfo(
numDigits: Int,
totalCollateral: CurrencyUnit): (
MultiNonceContractInfo,
MultiNonceContractInfo) = {
val overMaxValue = Math.pow(10, numDigits).toLong
// Left collar goes from [0, botCollar]
val botCollar = NumberUtil.randomLong(overMaxValue / 2)
val halfWindow = scala.math.min(overMaxValue / 4, 2500)
val topCollarDiff = NumberUtil.randomLong(halfWindow)
// Right collar goes from [topCollar, overMaxValue)
val topCollar = botCollar + halfWindow + topCollarDiff
val isGoingLong = scala.util.Random.nextBoolean()
// leftVal and rightVal determine whether the contract shape
// goes from total to 0 or 0 to total
val (leftVal, rightVal) =
if (isGoingLong) (Satoshis.zero, totalCollateral.satoshis)
else (totalCollateral.satoshis, Satoshis.zero)
val func = OutcomeValueFunction(
Vector(
OutcomeValuePoint(0, leftVal, isEndpoint = true),
OutcomeValuePoint(botCollar, leftVal, isEndpoint = true),
OutcomeValuePoint(topCollar, rightVal, isEndpoint = true),
OutcomeValuePoint(overMaxValue - 1, rightVal, isEndpoint = true)
))
val info = MultiNonceContractInfo(func,
base = 10,
numDigits,
totalCollateral.satoshis)
val remoteInfo = info.flip(totalCollateral.satoshis)
(info, remoteInfo)
}
def flipAtIndex(bytes: ByteVector, byteIndex: Int): ByteVector = {
val (front, backWithToFlip) = bytes.splitAt(byteIndex)
val (toFlip, back) = backWithToFlip.splitAt(1)
front ++ toFlip.xor(ByteVector.fromByte(1)) ++ back
}
def flipBit(signature: ECDigitalSignature): ECDigitalSignature = {
ECDigitalSignature(flipAtIndex(signature.bytes, 60))
}
def flipBit(partialSignature: PartialSignature): PartialSignature = {
partialSignature.copy(signature = flipBit(partialSignature.signature))
}
def flipBit(adaptorSignature: ECAdaptorSignature): ECAdaptorSignature = {
ECAdaptorSignature(flipAtIndex(adaptorSignature.bytes, 40))
}
def flipBit(witness: ScriptWitnessV0): ScriptWitnessV0 = {
witness match {
case p2wpkh: P2WPKHWitnessV0 =>
P2WPKHWitnessV0(p2wpkh.pubKey, flipBit(p2wpkh.signature))
case p2wsh: P2WSHWitnessV0 =>
val sigOpt = p2wsh.stack.zipWithIndex.find {
case (bytes, _) =>
bytes.length >= 67 && bytes.length <= 73
}
sigOpt match {
case Some((sig, index)) =>
P2WSHWitnessV0(
EmptyScriptPubKey,
p2wsh.stack.updated(index,
flipBit(ECDigitalSignature(sig)).bytes))
case None =>
P2WSHWitnessV0(
EmptyScriptPubKey,
p2wsh.stack.updated(0, flipAtIndex(p2wsh.stack.head, 0)))
}
}
}
def flipBit(fundingSigs: FundingSignatures): FundingSignatures = {
val (firstOutPoint, witness) = fundingSigs.head
val badWitness = flipBit(witness)
FundingSignatures(fundingSigs.tail.toVector.+:(firstOutPoint -> badWitness))
}
def flipBit(cetSigs: CETSignatures): CETSignatures = {
val badOutcomeSigs = cetSigs.outcomeSigs.map {
case (outcome, sig) => outcome -> flipBit(sig)
}
val badRefundSig = flipBit(cetSigs.refundSig)
CETSignatures(badOutcomeSigs, badRefundSig)
}
}

View file

@ -0,0 +1,330 @@
package org.bitcoins.dlc.testgen
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage._
import org.bitcoins.commons.jsonmodels.dlc.{
DLCFundingInput,
DLCPublicKeys,
DLCTimeouts
}
import org.bitcoins.core.currency.{CurrencyUnit, Satoshis}
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.script.{
ScriptWitness,
ScriptWitnessV0,
WitnessScriptPubKey
}
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.core.protocol.transaction.{
OutputReference,
Transaction,
TransactionOutPoint
}
import org.bitcoins.core.protocol.{BitcoinAddress, BlockTimeStamp}
import org.bitcoins.core.script.crypto.HashType
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo.{
ConditionalPath,
InputInfo,
ScriptSignatureParams
}
import org.bitcoins.crypto._
import org.bitcoins.dlc.builder.DLCTxBuilder
import play.api.libs.json._
import scodec.bits.ByteVector
import scala.concurrent.{ExecutionContext, Future}
sealed trait DLCTestVector extends TestVector
object DLCTestVector extends TestVectorParser[DLCTestVector] {
def fromJson(json: JsValue): JsResult[DLCTestVector] = {
SuccessTestVector.fromJson(json)
}
}
case class FundingInputTx(
tx: Transaction,
idx: Int,
inputKeys: Vector[ECPrivateKey],
redeemScript: Option[WitnessScriptPubKey],
scriptWitness: ScriptWitnessV0) {
val outputRef: OutputReference =
OutputReference(TransactionOutPoint(tx.txId, UInt32(idx)), tx.outputs(idx))
lazy val scriptSignatureParams: ScriptSignatureParams[InputInfo] = {
ScriptSignatureParams(
InputInfo(TransactionOutPoint(tx.txId, UInt32(idx)),
tx.outputs(idx),
redeemScript,
Some(scriptWitness),
ConditionalPath.NoCondition),
tx,
inputKeys,
HashType.sigHashAll
)
}
def toFundingInput: DLCFundingInput = {
DLCFundingInput.fromInputSigningInfo(scriptSignatureParams)
}
def toSerializedFundingInputTx: SerializedFundingInputTx = {
SerializedFundingInputTx(tx,
idx,
inputKeys,
redeemScript,
scriptWitness,
scriptSignatureParams.maxWitnessLen)
}
}
case class SerializedFundingInputTx(
tx: Transaction,
idx: Int,
inputKeys: Vector[ECPrivateKey],
redeemScript: Option[WitnessScriptPubKey],
scriptWitness: ScriptWitnessV0,
maxWitnessLen: Int) {
def toFundingInputTx: FundingInputTx = {
FundingInputTx(tx, idx, inputKeys, redeemScript, scriptWitness)
}
}
// Currently only supports P2WPKH inputs
case class DLCPartyParams(
collateral: CurrencyUnit,
fundingInputTxs: Vector[FundingInputTx],
changeAddress: BitcoinAddress,
fundingPrivKey: ECPrivateKey,
payoutAddress: BitcoinAddress) {
def fundingInputs: Vector[DLCFundingInput] =
fundingInputTxs.map(_.toFundingInput)
lazy val fundingScriptSigParams: Vector[ScriptSignatureParams[InputInfo]] = {
fundingInputTxs.map(_.scriptSignatureParams)
}
def toOffer(params: DLCParams): DLCOffer = {
DLCOffer(
OracleAndContractInfo(
params.oracleInfo,
SingleNonceContractInfo(params.contractInfo.map(_.toMapEntry))),
DLCPublicKeys(fundingPrivKey.publicKey, payoutAddress),
collateral.satoshis,
fundingInputs,
changeAddress,
params.feeRate,
DLCTimeouts(params.contractMaturityBound, params.contractTimeout)
)
}
}
case class SerializedContractInfoEntry(
preImage: String,
outcome: Sha256Digest,
localPayout: CurrencyUnit) {
def toMapEntry: (EnumOutcome, Satoshis) = {
EnumOutcome(preImage) -> localPayout.satoshis
}
}
object SerializedContractInfoEntry {
def fromContractInfo(contractInfo: SingleNonceContractInfo): Vector[
SerializedContractInfoEntry] = {
contractInfo.map {
case (EnumOutcome(str), amt) =>
SerializedContractInfoEntry(str, CryptoUtil.sha256(str), amt)
}.toVector
}
}
case class DLCParams(
oracleInfo: OracleInfo,
contractInfo: Vector[SerializedContractInfoEntry],
contractMaturityBound: BlockTimeStamp,
contractTimeout: BlockTimeStamp,
feeRate: SatoshisPerVirtualByte,
realOutcome: Sha256Digest,
oracleSignature: SchnorrDigitalSignature)
case class ValidTestInputs(
params: DLCParams,
offerParams: DLCPartyParams,
acceptParams: DLCPartyParams) {
def offer: DLCOffer = offerParams.toOffer(params)
def accept: DLCAcceptWithoutSigs =
DLCAcceptWithoutSigs(
acceptParams.collateral.satoshis,
DLCPublicKeys(acceptParams.fundingPrivKey.publicKey,
acceptParams.payoutAddress),
acceptParams.fundingInputs,
acceptParams.changeAddress,
offer.tempContractId
)
def builder(implicit ec: ExecutionContext): DLCTxBuilder =
DLCTxBuilder(offer, accept)
def buildTransactions(implicit
ec: ExecutionContext): Future[DLCTransactions] = {
val builder = this.builder
for {
fundingTx <- builder.buildFundingTx
cetFs =
params.contractInfo
.map(_.preImage)
.map(EnumOutcome.apply)
.map(builder.buildCET)
cets <- Future.sequence(cetFs)
refundTx <- builder.buildRefundTx
} yield DLCTransactions(fundingTx, cets, refundTx)
}
}
object ValidTestInputs {
def fromJson(json: JsValue): JsResult[ValidTestInputs] = {
Json.fromJson(json)(SuccessTestVector.validTestInputsFormat)
}
}
case class DLCTransactions(
fundingTx: Transaction,
cets: Vector[Transaction],
refundTx: Transaction)
case class SuccessTestVector(
testInputs: ValidTestInputs,
offer: LnMessage[DLCOfferTLV],
accept: LnMessage[DLCAcceptTLV],
sign: LnMessage[DLCSignTLV],
unsignedTxs: DLCTransactions,
signedTxs: DLCTransactions)
extends DLCTestVector {
override def toJson: JsValue = {
Json.toJson(this)(SuccessTestVector.successTestVectorFormat)
}
}
object SuccessTestVector extends TestVectorParser[SuccessTestVector] {
def hexFormat[T <: NetworkElement](factory: Factory[T]): Format[T] =
Format[T](
{ hex => hex.validate[String].map(factory.fromHex) },
{ element => JsString(element.hex) }
)
implicit val oracleInfoFormat: Format[OracleInfo] = Format[OracleInfo](
{
_.validate[Map[String, String]]
.map(map =>
SingleNonceOracleInfo(SchnorrPublicKey(map("publicKey")),
SchnorrNonce(map("nonce"))))
},
{ info =>
Json.toJson(
Map("publicKey" -> info.pubKey.hex, "nonce" -> info.nonces.head.hex))
}
)
implicit val blockTimeStampFormat: Format[BlockTimeStamp] =
Format[BlockTimeStamp](
{ _.validate[Long].map(UInt32.apply).map(BlockTimeStamp.apply) },
{ stamp => JsNumber(stamp.toUInt32.toLong) }
)
implicit val satsPerVBFormat: Format[SatoshisPerVirtualByte] =
Format[SatoshisPerVirtualByte](
{
_.validate[Long].map(Satoshis.apply).map(SatoshisPerVirtualByte.apply)
},
{ satsPerVB => JsNumber(satsPerVB.toLong) }
)
implicit val sha256DigestFormat: Format[Sha256Digest] = hexFormat(
Sha256Digest)
implicit val schnorrDigitalSignatureFormat: Format[SchnorrDigitalSignature] =
hexFormat(SchnorrDigitalSignature)
implicit val currencyUnitFormat: Format[CurrencyUnit] =
Format[CurrencyUnit](
{ _.validate[Long].map(Satoshis.apply) },
{ currency => JsNumber(currency.satoshis.toLong) }
)
implicit val transactionFormat: Format[Transaction] = hexFormat(Transaction)
implicit val ecPrivKeyFormat: Format[ECPrivateKey] = hexFormat(ECPrivateKey)
implicit val witnessScriptPubKeyFormat: Format[WitnessScriptPubKey] =
Format[WitnessScriptPubKey](
{ json => json.validate[String].map(WitnessScriptPubKey.fromAsmHex) },
{ wspk => JsString(wspk.asmBytes.toHex) }
)
implicit val scriptWitnessV0Format: Format[ScriptWitnessV0] =
Format[ScriptWitnessV0](
{
_.validate[String]
.map(ByteVector.fromValidHex(_))
.map(ScriptWitness.fromBytes)
.map(_.asInstanceOf[ScriptWitnessV0])
},
{ witness => JsString(witness.hex) }
)
implicit val serializedFundingInputTx: Format[SerializedFundingInputTx] =
Json.format[SerializedFundingInputTx]
implicit val fundingInputTxFormat: Format[FundingInputTx] =
Format[FundingInputTx](
{ _.validate[SerializedFundingInputTx].map(_.toFundingInputTx) },
{ inputTx =>
Json.toJson(inputTx.toSerializedFundingInputTx)
}
)
implicit val addressFormat: Format[BitcoinAddress] =
Format[BitcoinAddress](
{ _.validate[String].map(BitcoinAddress.fromString) },
{ address => JsString(address.toString) }
)
implicit val contractInfoFormat: Format[SerializedContractInfoEntry] =
Json.format[SerializedContractInfoEntry]
implicit val dlcParamFormat: Format[DLCParams] = Json.format[DLCParams]
implicit val DLCPartyParamsFormat: Format[DLCPartyParams] =
Json.format[DLCPartyParams]
implicit val offerMsgFormat: Format[LnMessage[DLCOfferTLV]] = hexFormat(
LnMessageFactory(DLCOfferTLV))
implicit val acceptMsgFormat: Format[LnMessage[DLCAcceptTLV]] = hexFormat(
LnMessageFactory(DLCAcceptTLV))
implicit val signMsgFormat: Format[LnMessage[DLCSignTLV]] = hexFormat(
LnMessageFactory(DLCSignTLV))
implicit val validTestInputsFormat: Format[ValidTestInputs] =
Json.format[ValidTestInputs]
implicit val dlcTransactionsFormat: Format[DLCTransactions] =
Json.format[DLCTransactions]
implicit val successTestVectorFormat: Format[SuccessTestVector] =
Json.format[SuccessTestVector]
override def fromJson(json: JsValue): JsResult[SuccessTestVector] = {
json.validate[SuccessTestVector]
}
}

View file

@ -0,0 +1,38 @@
package org.bitcoins.dlc.testgen
import java.io.File
import play.api.libs.json._
import scala.concurrent.Future
object DLCTestVectorGen extends TestVectorGen[DLCTestVector, ValidTestInputs] {
override val defaultTestFile: File = new File(
"dlc/src/main/scala/org/bitcoins/dlc/testgen/dlc_test.json")
override val testVectorParser: DLCTestVector.type = DLCTestVector
override def inputFromJson: JsValue => JsResult[ValidTestInputs] =
ValidTestInputs.fromJson
override val inputStr: String = "testInputs"
override def generateFromInput: ValidTestInputs => Future[DLCTestVector] =
DLCTxGen.successTestVector(_)
override def generateTestVectors(): Future[Vector[DLCTestVector]] = {
// Happy Path
val numOutcomesTests =
Vector(2, 3, 5, 8, 100).map(DLCTxGen.randomSuccessTestVector)
val nonP2WPKHInputTests =
DLCTxGen.nonP2WPKHInputs.map(DLCTxGen.successTestVector(_))
val multiInputTests = DLCTxGen
.multiInputTests(Vector(1, 2, 5))
.map(DLCTxGen.successTestVector(_))
Future.sequence(numOutcomesTests ++ nonP2WPKHInputTests ++ multiInputTests)
}
}

View file

@ -0,0 +1,277 @@
package org.bitcoins.dlc.testgen
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.{
DLCSign,
SingleNonceContractInfo,
SingleNonceOracleInfo
}
import org.bitcoins.core.currency.{CurrencyUnit, Satoshis}
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.tlv.EnumOutcome
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.protocol.{BitcoinAddress, BlockTimeStamp}
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.crypto.{CryptoUtil, ECPrivateKey, ECPublicKey}
import org.bitcoins.dlc.sign.DLCTxSigner
import scodec.bits.ByteVector
import scala.concurrent.{ExecutionContext, Future}
object DLCTxGen {
import DLCTLVGen._
def dlcParams(
contractInfo: SingleNonceContractInfo = genContractInfo(),
contractMaturityBound: BlockTimeStamp = BlockTimeStamp(100),
contractTimeout: BlockTimeStamp = BlockTimeStamp(200),
feeRate: SatoshisPerVirtualByte =
SatoshisPerVirtualByte(Satoshis(5))): DLCParams = {
val privKey = ECPrivateKey.freshPrivateKey
val kVal = ECPrivateKey.freshPrivateKey
val oracleInfo =
SingleNonceOracleInfo(privKey.schnorrPublicKey, kVal.schnorrNonce)
val realOutcome = contractInfo.keys(contractInfo.size / 2)
val sig =
privKey.schnorrSignWithNonce(CryptoUtil.sha256(realOutcome.outcome).bytes,
kVal)
DLCParams(
oracleInfo,
SerializedContractInfoEntry.fromContractInfo(contractInfo),
contractMaturityBound,
contractTimeout,
feeRate,
CryptoUtil.sha256(realOutcome.outcome),
sig
)
}
private val dummyTransactionInput = TransactionInput(
TransactionOutPoint(CryptoUtil.doubleSHA256(ByteVector("DLC".getBytes)),
UInt32.zero),
EmptyScriptSignature,
UInt32.zero)
def fundingInputTx(
inputs: Vector[TransactionInput] = Vector(dummyTransactionInput),
idx: Int = 0,
privKeys: Vector[ECPrivateKey] = Vector(ECPrivateKey.freshPrivateKey),
redeemScriptOpt: Option[WitnessScriptPubKeyV0] = None,
scriptWitness: ScriptWitnessV0 = P2WPKHWitnessV0(
ECPublicKey.freshPublicKey),
amt: CurrencyUnit = defaultAmt * 2,
lockTime: UInt32 = UInt32.zero): FundingInputTx = {
val (spk, scriptWit) = redeemScriptOpt match {
case Some(wspk) => (P2SHScriptPubKey(wspk), scriptWitness)
case None =>
scriptWitness match {
case p2wpkh: P2WPKHWitnessV0 =>
val pubKey = if (privKeys.head.publicKey != p2wpkh.pubKey) {
privKeys.head.publicKey
} else {
p2wpkh.pubKey
}
(P2WPKHWitnessSPKV0(pubKey), P2WPKHWitnessV0(pubKey))
case p2wsh: P2WSHWitnessV0 =>
(P2WSHWitnessSPKV0(p2wsh.redeemScript), p2wsh)
}
}
val outputs =
Vector
.fill(idx)(TransactionOutput(defaultAmt, EmptyScriptPubKey)) :+
TransactionOutput(amt, spk)
val tx = BaseTransaction(TransactionConstants.validLockVersion,
inputs,
outputs,
lockTime)
FundingInputTx(tx, idx, privKeys, redeemScriptOpt, scriptWit)
}
def multiSigFundingInputTx(
privKeys: Vector[ECPrivateKey] =
Vector(ECPrivateKey.freshPrivateKey, ECPrivateKey.freshPrivateKey),
requiredSigs: Int = 2,
p2shNested: Boolean = false,
idx: Int = 0,
amt: CurrencyUnit = defaultAmt * 2,
lockTime: UInt32 = UInt32.zero): FundingInputTx = {
val multiSig =
MultiSignatureScriptPubKey(requiredSigs, privKeys.map(_.publicKey))
val redeemScriptOpt = if (p2shNested) {
Some(P2WSHWitnessSPKV0(multiSig))
} else None
val scriptWitness = P2WSHWitnessV0(multiSig)
fundingInputTx(idx = idx,
privKeys = privKeys,
redeemScriptOpt = redeemScriptOpt,
scriptWitness = scriptWitness,
amt = amt,
lockTime = lockTime)
}
def dlcPartyParams(
collateral: CurrencyUnit = defaultAmt,
fundingInputTxs: Vector[FundingInputTx] = Vector(fundingInputTx()),
changeAddress: BitcoinAddress = address(),
fundingPrivKey: ECPrivateKey = ECPrivateKey.freshPrivateKey,
payoutAddress: BitcoinAddress = address()): DLCPartyParams = {
DLCPartyParams(collateral,
fundingInputTxs,
changeAddress,
fundingPrivKey,
payoutAddress)
}
def validTestInputs(
params: DLCParams = dlcParams(),
offerParams: DLCPartyParams = dlcPartyParams(),
acceptParams: DLCPartyParams = dlcPartyParams()): ValidTestInputs = {
ValidTestInputs(params, offerParams, acceptParams)
}
def validTestInputsForInputs(
offerInputs: Vector[FundingInputTx],
acceptInputs: Vector[FundingInputTx],
numOutcomes: Int = 3): ValidTestInputs = {
val outcomes = DLCTestUtil.genOutcomes(numOutcomes)
val contractInfo = genContractInfo(outcomes)
validTestInputs(
params = dlcParams(contractInfo = contractInfo),
offerParams = dlcPartyParams(fundingInputTxs = offerInputs),
acceptParams = dlcPartyParams(fundingInputTxs = acceptInputs)
)
}
def vecProd[T](vec1: Vector[T], vec2: Vector[T]): Vector[(T, T)] = {
vec1.flatMap(x => vec2.map((x, _)))
}
val allInputs = Vector(0, 1, 2)
def inputFromKind(n: Int): FundingInputTx = {
if (n == 0) fundingInputTx()
else if (n == 1) multiSigFundingInputTx()
else multiSigFundingInputTx(p2shNested = true)
}
def inputs(n: Int): Vector[FundingInputTx] = {
(0 until n).toVector.map { _ =>
inputFromKind(scala.util.Random.nextInt(3))
}
}
def nonP2WPKHInputs: Vector[ValidTestInputs] = {
vecProd(allInputs, allInputs).tail.map {
case (offerInputKind, acceptInputKind) =>
validTestInputsForInputs(
offerInputs = Vector(inputFromKind(offerInputKind)),
acceptInputs = Vector(inputFromKind(acceptInputKind))
)
}
}
def multiInputTests(numInputOptions: Vector[Int]): Vector[ValidTestInputs] = {
vecProd(numInputOptions, numInputOptions).tail.map {
case (offerNumInputs, acceptNumInputs) =>
validTestInputsForInputs(
offerInputs = inputs(offerNumInputs),
acceptInputs = inputs(acceptNumInputs)
)
}
}
def dlcTxTestVector(inputs: ValidTestInputs = validTestInputs())(implicit
ec: ExecutionContext): Future[DLCTxTestVector] = {
DLCTxTestVector.fromInputs(inputs)
}
def dlcTxTestVectorWithTxInputs(
offerInputs: Vector[FundingInputTx],
acceptInputs: Vector[FundingInputTx],
numOutcomes: Int = 3)(implicit
ec: ExecutionContext): Future[DLCTxTestVector] = {
dlcTxTestVector(
validTestInputsForInputs(offerInputs, acceptInputs, numOutcomes))
}
def randomTxTestVector(numOutcomes: Int)(implicit
ec: ExecutionContext): Future[DLCTxTestVector] = {
val outcomes = DLCTestUtil.genOutcomes(numOutcomes)
val contractInfo = genContractInfo(outcomes)
dlcTxTestVector(validTestInputs(dlcParams(contractInfo = contractInfo)))
}
def successTestVector(inputs: ValidTestInputs = validTestInputs())(implicit
ec: ExecutionContext): Future[SuccessTestVector] = {
val offer = inputs.offer
val acceptWithoutSigs = inputs.accept
val builder = inputs.builder
val offerSigner = DLCTxSigner(builder,
isInitiator = true,
inputs.offerParams.fundingPrivKey,
inputs.offerParams.payoutAddress,
inputs.offerParams.fundingScriptSigParams)
val acceptSigner = DLCTxSigner(builder,
isInitiator = false,
inputs.acceptParams.fundingPrivKey,
inputs.acceptParams.payoutAddress,
inputs.acceptParams.fundingScriptSigParams)
val outcomeStr = inputs.params.contractInfo
.find(_.outcome == inputs.params.realOutcome)
.map(_.preImage)
.get
val outcome = EnumOutcome(outcomeStr)
for {
accpetCETSigs <- acceptSigner.createCETSigs()
offerCETSigs <- offerSigner.createCETSigs()
offerFundingSigs <- offerSigner.createFundingTxSigs()
DLCTransactions(fundingTx, cets, refundTx) <- inputs.buildTransactions
signedFundingTx <- acceptSigner.signFundingTx(offerFundingSigs)
signedRefundTx <- offerSigner.signRefundTx(accpetCETSigs.refundSig)
offerSignedCET <- offerSigner.signCET(
outcome,
accpetCETSigs(outcome),
Vector(inputs.params.oracleSignature))
acceptSignedCET <- acceptSigner.signCET(
outcome,
offerCETSigs(outcome),
Vector(inputs.params.oracleSignature))
} yield {
val accept = acceptWithoutSigs.withSigs(accpetCETSigs)
val contractId = fundingTx.txIdBE.bytes.xor(accept.tempContractId.bytes)
val sign = DLCSign(offerCETSigs, offerFundingSigs, contractId)
SuccessTestVector(
inputs,
offer.toMessage,
accept.toMessage,
sign.toMessage,
DLCTransactions(fundingTx, cets, refundTx),
DLCTransactions(signedFundingTx,
Vector(offerSignedCET, acceptSignedCET),
signedRefundTx)
)
}
}
def randomSuccessTestVector(numOutcomes: Int)(implicit
ec: ExecutionContext): Future[SuccessTestVector] = {
val outcomes = DLCTestUtil.genOutcomes(numOutcomes)
val contractInfo = genContractInfo(outcomes)
successTestVector(validTestInputs(dlcParams(contractInfo = contractInfo)))
}
}

View file

@ -0,0 +1,30 @@
package org.bitcoins.dlc.testgen
import play.api.libs.json.{Format, JsResult, JsValue, Json}
import scala.concurrent.{ExecutionContext, Future}
case class DLCTxTestVector(inputs: ValidTestInputs, txs: DLCTransactions)
extends TestVector {
override def toJson: JsValue =
Json.toJson(this)(DLCTxTestVector.dlcTxTestVectorFormat)
}
object DLCTxTestVector extends TestVectorParser[DLCTxTestVector] {
def fromInputs(inputs: ValidTestInputs)(implicit
ec: ExecutionContext): Future[DLCTxTestVector] = {
inputs.buildTransactions.map(txs => DLCTxTestVector(inputs, txs))
}
import SuccessTestVector.validTestInputsFormat
import SuccessTestVector.dlcTransactionsFormat
implicit val dlcTxTestVectorFormat: Format[DLCTxTestVector] =
Json.format[DLCTxTestVector]
override def fromJson(json: JsValue): JsResult[DLCTxTestVector] = {
json.validate[DLCTxTestVector]
}
}

View file

@ -0,0 +1,38 @@
package org.bitcoins.dlc.testgen
import java.io.File
import play.api.libs.json.{JsResult, JsValue}
import scala.concurrent.Future
object DLCTxTestVectorGen
extends TestVectorGen[DLCTxTestVector, ValidTestInputs] {
override val defaultTestFile: File = new File(
"dlc/src/main/scala/org/bitcoins/dlc/testgen/dlc_tx_test.json")
override val testVectorParser: DLCTxTestVector.type = DLCTxTestVector
override def inputFromJson: JsValue => JsResult[ValidTestInputs] =
ValidTestInputs.fromJson
override val inputStr: String = "inputs"
override def generateFromInput: ValidTestInputs => Future[DLCTxTestVector] =
DLCTxTestVector.fromInputs
override def generateTestVectors(): Future[Vector[DLCTxTestVector]] = {
val numOutcomesTests = Vector(2, 3, 5, 8).map(DLCTxGen.randomTxTestVector)
val nonP2WPKHInputTests =
DLCTxGen.nonP2WPKHInputs.map(DLCTxGen.dlcTxTestVector(_))
val numInputs = Vector(1, 2, 3, 8)
val multiInputTests =
DLCTxGen.multiInputTests(numInputs).map(DLCTxGen.dlcTxTestVector(_))
Future.sequence(numOutcomesTests ++ nonP2WPKHInputTests ++ multiInputTests)
}
}

View file

@ -0,0 +1,99 @@
package org.bitcoins.dlc.testgen
import org.bitcoins.crypto._
import play.api.libs.json._
case class SchnorrSigPointTestVector(
inputs: SchnorrSigPointTestVectorInput,
pubKey: SchnorrPublicKey,
pubNonce: SchnorrNonce,
signature: SchnorrDigitalSignature,
sigPoint: ECPublicKey)
extends TestVector {
require(signature.sig.getPublicKey == sigPoint,
s"Signature ($signature) does not match Signature Point ($sigPoint)")
def privKey: ECPrivateKey = inputs.privKey
def privNonce: ECPrivateKey = inputs.privNonce
def msgHash: Sha256Digest = inputs.msgHash
override def toJson: JsValue = {
Json.toJson(this)(SchnorrSigPointTestVector.schnorrSigPointTestVectorFormat)
}
}
case class SchnorrSigPointTestVectorInput(
privKey: ECPrivateKey,
privNonce: ECPrivateKey,
msgHash: Sha256Digest)
object SchnorrSigPointTestVectorInput {
def fromJson(json: JsValue): JsResult[SchnorrSigPointTestVectorInput] = {
json.validate[SchnorrSigPointTestVectorInput](
SchnorrSigPointTestVector.schnorrSigPointTestVectorInputFormat)
}
}
object SchnorrSigPointTestVector
extends TestVectorParser[SchnorrSigPointTestVector] {
def apply(
input: SchnorrSigPointTestVectorInput): SchnorrSigPointTestVector = {
SchnorrSigPointTestVector(input.privKey, input.privNonce, input.msgHash)
}
def apply(
privKey: ECPrivateKey,
privNonce: ECPrivateKey,
msgHash: Sha256Digest): SchnorrSigPointTestVector = {
val signature = privKey.schnorrSignWithNonce(msgHash.bytes, privNonce)
val signaturePoint = signature.sig.getPublicKey
SchnorrSigPointTestVector(
SchnorrSigPointTestVectorInput(privKey, privNonce, msgHash),
privKey.schnorrPublicKey,
privNonce.schnorrNonce,
signature,
signaturePoint)
}
def networkElementFormat[T <: NetworkElement](
factory: Factory[T]): Format[T] = {
Format[T]({ json =>
json.validate[String].map(factory.fromHex)
},
{ element =>
JsString(element.hex)
})
}
implicit val privKeyFormat: Format[ECPrivateKey] = networkElementFormat(
ECPrivateKey)
implicit val schnorrPubKeyFormat: Format[SchnorrPublicKey] =
networkElementFormat(SchnorrPublicKey)
implicit val schnorrNonceFormat: Format[SchnorrNonce] = networkElementFormat(
SchnorrNonce)
implicit val hashFormat: Format[Sha256Digest] = networkElementFormat(
Sha256Digest)
implicit val signatureFormat: Format[SchnorrDigitalSignature] =
networkElementFormat(SchnorrDigitalSignature)
implicit val pubKeyFromat: Format[ECPublicKey] = networkElementFormat(
ECPublicKey)
implicit val schnorrSigPointTestVectorInputFormat: Format[
SchnorrSigPointTestVectorInput] =
Json.format[SchnorrSigPointTestVectorInput]
implicit val schnorrSigPointTestVectorFormat: Format[
SchnorrSigPointTestVector] = Json.format[SchnorrSigPointTestVector]
override def fromJson(json: JsValue): JsResult[SchnorrSigPointTestVector] = {
json.validate[SchnorrSigPointTestVector]
}
}

View file

@ -0,0 +1,46 @@
package org.bitcoins.dlc.testgen
import java.io.File
import org.bitcoins.crypto.{ECPrivateKey, Sha256Digest}
import play.api.libs.json.{JsResult, JsValue}
import scala.concurrent.Future
object SchnorrSigPointTestVectorGen
extends TestVectorGen[
SchnorrSigPointTestVector,
SchnorrSigPointTestVectorInput] {
override val defaultTestFile: File = new File(
"dlc/src/main/scala/org/bitcoins/dlc/testgen/dlc_schnorr_test.json")
override val testVectorParser: SchnorrSigPointTestVector.type =
SchnorrSigPointTestVector
override def inputFromJson: JsValue => JsResult[
SchnorrSigPointTestVectorInput] =
SchnorrSigPointTestVectorInput.fromJson
override val inputStr: String = "inputs"
override def generateFromInput: SchnorrSigPointTestVectorInput => Future[
SchnorrSigPointTestVector] = { inputs =>
Future.successful(SchnorrSigPointTestVector(inputs))
}
override def generateTestVectors(): Future[
Vector[SchnorrSigPointTestVector]] = {
def generateTest: SchnorrSigPointTestVector = {
SchnorrSigPointTestVector(
ECPrivateKey.freshPrivateKey,
ECPrivateKey.freshPrivateKey,
Sha256Digest(ECPrivateKey.freshPrivateKey.bytes)
)
}
Future.successful(
Vector.fill(5)(generateTest)
)
}
}

View file

@ -0,0 +1,209 @@
package org.bitcoins.dlc.testgen
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.{ContractInfo, OracleInfo}
import org.bitcoins.commons.jsonmodels.dlc._
import org.bitcoins.core.config.{BitcoinNetwork, RegTest}
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.tlv.DLCOutcomeType
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo.{InputInfo, ScriptSignatureParams}
import org.bitcoins.crypto._
import org.bitcoins.dlc.builder.DLCTxBuilder
import org.bitcoins.dlc.execution.{
DLCExecutor,
ExecutedDLCOutcome,
RefundDLCOutcome,
SetupDLC
}
import org.bitcoins.dlc.sign.DLCTxSigner
import scala.concurrent.duration.DurationInt
import scala.concurrent.{Await, ExecutionContext, Future}
/** This case class allows for the construction and execution of
* Discreet Log Contracts between two parties running on this machine (for tests).
*
* @param offer The DLCOffer associated with this DLC
* @param accept The DLCAccept (without sigs) associated with this DLC
* @param isInitiator True if this client sends the offer message
* @param fundingPrivKey This client's funding private key for this event
* @param payoutPrivKey This client's payout private key for this event
* @param fundingUtxos This client's funding BitcoinUTXOSpendingInfo collection
*/
case class TestDLCClient(
offer: DLCMessage.DLCOffer,
accept: DLCMessage.DLCAcceptWithoutSigs,
isInitiator: Boolean,
fundingPrivKey: ECPrivateKey,
payoutPrivKey: ECPrivateKey,
fundingUtxos: Vector[ScriptSignatureParams[InputInfo]])(implicit
ec: ExecutionContext)
extends BitcoinSLogger {
val dlcTxBuilder: DLCTxBuilder = DLCTxBuilder(offer, accept)
val dlcTxSigner: DLCTxSigner = DLCTxSigner(dlcTxBuilder,
isInitiator,
fundingPrivKey,
payoutPrivKey,
RegTest,
fundingUtxos)
private val dlcExecutor = DLCExecutor(dlcTxSigner)
val messages: Vector[DLCOutcomeType] = offer.oracleAndContractInfo.allOutcomes
val timeouts: DLCTimeouts = offer.timeouts
lazy val fundingTx: Transaction =
Await.result(dlcTxBuilder.buildFundingTx, 5.seconds)
lazy val fundingTxIdBE: DoubleSha256DigestBE = fundingTx.txIdBE
/** Sets up the non-initiator's DLC given functions for sending
* CETSignatures to the initiator as well as receiving CETSignatures
* and FundingSignatures from them
*/
def setupDLCAccept(
sendSigs: CETSignatures => Future[Unit],
getSigs: Future[(CETSignatures, FundingSignatures)]): Future[SetupDLC] = {
require(!isInitiator, "You should call setupDLCOffer")
for {
remoteCetSigs <- dlcTxSigner.createCETSigs()
_ <- sendSigs(remoteCetSigs)
(cetSigs, fundingSigs) <- getSigs
setupDLC <- dlcExecutor.setupDLCAccept(cetSigs, fundingSigs)
} yield {
setupDLC
}
}
/** Sets up the initiator's DLC given functions for getting CETSignatures
* from the non-initiator as well as sending signatures to them, and lastly
* a Future which will be populated with the broadcasted (or relayed) fully
* signed funding transaction
*/
def setupDLCOffer(
getSigs: Future[CETSignatures],
sendSigs: (CETSignatures, FundingSignatures) => Future[Unit],
getFundingTx: Future[Transaction]): Future[SetupDLC] = {
require(isInitiator, "You should call setupDLCAccept")
for {
cetSigs <- getSigs
setupDLCWithoutFundingTxSigs <- dlcExecutor.setupDLCOffer(cetSigs)
cetSigs <- dlcTxSigner.createCETSigs()
localFundingSigs <- dlcTxSigner.createFundingTxSigs()
_ <- sendSigs(cetSigs, localFundingSigs)
fundingTx <- getFundingTx
} yield {
setupDLCWithoutFundingTxSigs.copy(fundingTx = fundingTx)
}
}
def executeDLC(
dlcSetup: SetupDLC,
oracleSigsF: Future[Vector[SchnorrDigitalSignature]]): Future[
ExecutedDLCOutcome] = {
oracleSigsF.flatMap { oracleSigs =>
dlcExecutor.executeDLC(dlcSetup, oracleSigs)
}
}
def executeRefundDLC(dlcSetup: SetupDLC): RefundDLCOutcome = {
dlcExecutor.executeRefundDLC(dlcSetup)
}
}
object TestDLCClient {
def apply(
outcomes: ContractInfo,
oracleInfo: OracleInfo,
isInitiator: Boolean,
fundingPrivKey: ECPrivateKey,
payoutPrivKey: ECPrivateKey,
remotePubKeys: DLCPublicKeys,
input: CurrencyUnit,
remoteInput: CurrencyUnit,
fundingUtxos: Vector[ScriptSignatureParams[InputInfo]],
remoteFundingInputs: Vector[DLCFundingInput],
timeouts: DLCTimeouts,
feeRate: SatoshisPerVirtualByte,
changeSPK: ScriptPubKey,
remoteChangeSPK: ScriptPubKey,
network: BitcoinNetwork)(implicit ec: ExecutionContext): TestDLCClient = {
val pubKeys = DLCPublicKeys.fromPrivKeys(
fundingPrivKey,
payoutPrivKey,
network
)
val remoteOutcomes: ContractInfo =
outcomes.flip((input + remoteInput).satoshis)
val changeAddress = BitcoinAddress.fromScriptPubKey(changeSPK, network)
val remoteChangeAddress =
BitcoinAddress.fromScriptPubKey(remoteChangeSPK, network)
val (offerOutcomes,
offerPubKeys,
offerInput,
offerFundingInputs,
offerChangeAddress,
acceptPubKeys,
acceptInput,
acceptFundingInputs,
acceptChangeAddress) = if (isInitiator) {
(outcomes,
pubKeys,
input,
fundingUtxos.map(DLCFundingInput.fromInputSigningInfo(_)),
changeAddress,
remotePubKeys,
remoteInput,
remoteFundingInputs,
remoteChangeAddress)
} else {
(remoteOutcomes,
remotePubKeys,
remoteInput,
remoteFundingInputs,
remoteChangeAddress,
pubKeys,
input,
fundingUtxos.map(DLCFundingInput.fromInputSigningInfo(_)),
changeAddress)
}
val offer = DLCMessage.DLCOffer(
oracleAndContractInfo =
DLCMessage.OracleAndContractInfo(oracleInfo, offerOutcomes),
pubKeys = offerPubKeys,
totalCollateral = offerInput.satoshis,
fundingInputs = offerFundingInputs,
changeAddress = offerChangeAddress,
feeRate = feeRate,
timeouts = timeouts
)
val accept = DLCMessage.DLCAcceptWithoutSigs(
totalCollateral = acceptInput.satoshis,
pubKeys = acceptPubKeys,
fundingInputs = acceptFundingInputs,
changeAddress = acceptChangeAddress,
tempContractId = offer.tempContractId
)
TestDLCClient(offer,
accept,
isInitiator,
fundingPrivKey,
payoutPrivKey,
fundingUtxos)
}
}

View file

@ -0,0 +1,19 @@
package org.bitcoins.dlc.testgen
import play.api.libs.json.{JsResult, JsValue, Json}
trait TestVector {
def toJson: JsValue
def toJsonStr: String = {
Json.prettyPrint(toJson)
}
}
trait TestVectorParser[T <: TestVector] {
def fromJson(json: JsValue): JsResult[T]
def fromString(str: String): JsResult[T] = {
fromJson(Json.parse(str))
}
}

View file

@ -0,0 +1,121 @@
package org.bitcoins.dlc.testgen
import java.io.{File, PrintWriter}
import play.api.libs.json.{JsArray, JsError, JsResult, JsSuccess, JsValue, Json}
import scala.concurrent.{ExecutionContext, Future}
import scala.io.Source
import scala.util.{Failure, Success, Try}
trait TestVectorGen[T <: TestVector, Input] {
def testVectorParser: TestVectorParser[T]
def inputFromJson: JsValue => JsResult[Input]
def inputStr: String
def generateFromInput: Input => Future[T]
implicit def ec: ExecutionContext = ExecutionContext.global
def defaultTestFile: File
private def writeToFile(json: JsValue, outFile: File): Unit = {
val writer = new PrintWriter(outFile)
writer.print(Json.prettyPrint(json))
writer.close()
}
def writeTestVectorsToFile(
vecs: Vector[T],
file: File = defaultTestFile): Unit = {
val arr = JsArray(vecs.map(_.toJson))
writeToFile(arr, file)
}
def readFromDefaultTestFile(): JsResult[Vector[T]] = {
val source = Source.fromFile(defaultTestFile)
val str = source.getLines().reduce(_ ++ _)
source.close()
Json.parse(str).validate[JsArray].flatMap { arr =>
arr.value
.foldLeft[JsResult[Vector[T]]](JsSuccess(Vector.empty)) {
case (jsResultAccum, json) =>
jsResultAccum.flatMap { accum =>
testVectorParser.fromJson(json).map { testVec =>
accum :+ testVec
}
}
}
}
}
def readInputsFromDefaultTestFile(): JsResult[Vector[Input]] = {
val source = Source.fromFile(defaultTestFile)
val str = source.getLines().reduce(_ ++ _)
source.close()
Json.parse(str).validate[JsArray].flatMap { arr =>
arr.value
.foldLeft[JsResult[Vector[Input]]](JsSuccess(Vector.empty)) {
case (jsResultAccum, json) =>
jsResultAccum.flatMap { accum =>
inputFromJson((json \ inputStr).get).map { testVec =>
accum :+ testVec
}
}
}
}
}
/** Returns true if anything has changed, false otherwise */
def regenerateTestFile(): Future[Boolean] = {
val testVecInputs = readInputsFromDefaultTestFile()
val testVecResultT = Try(readFromDefaultTestFile())
testVecInputs match {
case JsSuccess(inputs, _) =>
val newTestVecsF =
Future.sequence(inputs.map(input => generateFromInput(input)))
newTestVecsF.flatMap { newTestVecs =>
val noChange = testVecResultT match {
case Failure(_) | Success(JsError(_)) => false
case Success(JsSuccess(testVecs, _)) =>
newTestVecs.zip(testVecs).foldLeft(true) {
case (sameSoFar, (oldVec, newVec)) =>
sameSoFar && (oldVec == newVec)
}
}
if (noChange) {
Future.successful(false)
} else {
val successfulDelete = defaultTestFile.delete()
if (successfulDelete) {
writeTestVectorsToFile(newTestVecs)
Future.successful(true)
} else {
Future.failed(
new RuntimeException(
s"Was unable to delete ${defaultTestFile.getAbsolutePath}"))
}
}
}
case JsError(err) =>
Future.failed(
new IllegalArgumentException(s"Could not read json from file: $err"))
}
}
def generateTestVectors(): Future[Vector[T]]
def generateAndWriteTestVectors(
file: File = defaultTestFile): Future[Unit] = {
generateTestVectors().map { testVectors =>
writeTestVectorsToFile(testVectors, file)
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,45 @@
[ {
"byteLen" : 0,
"script" : "",
"description" : "empty"
}, {
"byteLen" : 22,
"script" : "00146f47307cd1d7e61b89a6fe24e660714d31e4aca6",
"description" : "p2wpkh spk"
}, {
"byteLen" : 25,
"script" : "76a914a5745456754abf104d8d1a8852f9ef4cbb0af3ee88ac",
"description" : "p2pkh spk"
}, {
"byteLen" : 34,
"script" : "0020e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"description" : "p2wsh spk"
}, {
"byteLen" : 35,
"script" : "21022a5c418a642d70ab3f21029f510931a5af44617d58fcc50195acc136ec90b75cac",
"description" : "p2pk spk"
}, {
"byteLen" : 71,
"script" : "522102a48558c126c5e3eda2188e743ab31cf9b3e956bb5cc6b9a9eeceb17c69ccffc42103f9be793aa26af2acb555ced3702e360e3c65b8c040c0fc0cf85844bce904cf7452ae",
"description" : "2-of-2 multisig spk"
}, {
"byteLen" : 173,
"script" : "532103828103a5eb0a8663fffd122ccc52ab7705197686cb09abc8a3d25bdae7426a7a2102fdfc0fbf0148587cb076bbeea390fad9f110ef4b5f5c916b68f0b846ccd8892d2102ab454385715ca02c7cc3aad97b202fd84375906612798b88d386e1c9a7e5bb5421020af3417e4c0a9ddd5a0adbbf460ca05301a9a13d61dcd7ac8bf43b49f06d4d2c21034f5385fdeaaaa725231983765e7fe4c1dbab5c54c051b02b1d4b68c57b8eb67555ae",
"description" : "3-of-5 multisig spk"
}, {
"byteLen" : 107,
"script" : "02473045022100dc049370a3e1e2f1c30976f97b503ce00e840b10603cf7da86cbbb7a3362501302206104618664922452bfbf40951deb1cccba49f659e3a082761e360d1e1f6abff921029a5276b84f01538373a67033942bb9c4078d8ee2cb72b6dd148fc25e075109cf",
"description" : "p2wpkh (low R) witness"
}, {
"byteLen" : 108,
"script" : "024830460221009efb9e799b6ddbad4040bf6ae214e3320064e68ccba81dc5a19c516eb0cb8148022100dc049370a3e1e2f1c30976f97b503ce00e840b10603cf7da86cbbb7a336250132103b000bc3cae94b64f26375a8f5d5700f320beeeeb19f82cbb1494cd22f9bf6325",
"description" : "p2wpkh (high R) witness"
}, {
"byteLen" : 133,
"script" : "03473045022100dc049370a3e1e2f1c30976f97b503ce00e840b10603cf7da86cbbb7a3362501302206104618664922452bfbf40951deb1cccba49f659e3a082761e360d1e1f6abff92102d96d94954612b0ca242247baa8f09ebc4a98f9d4fe335e1b2d1efcecc4ec2f351976a9141969d7b6c106a848d1330652290cd128705d28da88ac",
"description" : "p2wsh(p2pkh) witness"
}, {
"byteLen" : 218,
"script" : "0400473045022100dc049370a3e1e2f1c30976f97b503ce00e840b10603cf7da86cbbb7a3362501302206104618664922452bfbf40951deb1cccba49f659e3a082761e360d1e1f6abff94730450221009efb9e799b6ddbad4040bf6ae214e3320064e68ccba81dc5a19c516eb0cb8148022023fb6c8f5c1e1d0e3cf6890684afc31eac2ad1d64f0ba8613906a3129cd3f12e47522102a48558c126c5e3eda2188e743ab31cf9b3e956bb5cc6b9a9eeceb17c69ccffc42103f9be793aa26af2acb555ced3702e360e3c65b8c040c0fc0cf85844bce904cf7452ae",
"description" : "p2wsh(2-of-2 multsig) witness"
} ]

View file

@ -0,0 +1,124 @@
[ {
"tpeName" : "contract_info_v0",
"input" : "fda71055133132313134373033383237313332333337363600000000060ef950142d35363531383531393237393637343536383538000000000bebc20013383138393936393137343533303334303738300000000000000000",
"fields" : {
"tpe" : "fda710",
"length" : "55",
"outcomes" : [ {
"outcome" : "3dfeb356412d78912b58e37a2c195c807bb400203597d53f6287df741aa6d4c0",
"localPayout" : "00000000060ef950"
}, {
"outcome" : "0c57d8013614d9252c6c194ade3c93c16804e43757461f0091ddb6d879fe7176",
"localPayout" : "000000000bebc200"
}, {
"outcome" : "b9a960c1a908078a3822db4ea912185904bc9c6dcda04e2de6d01489f0b0dc1a",
"localPayout" : "0000000000000000"
} ]
}
}, {
"tpeName" : "oracle_info_v0",
"input" : "fda71240c59730cfae37472dc50a08e4d2804a57dd1506be6b0aafaa3728673f79183a9f737f4c83ba94f58dc605307dfb8a94f26e5a98121e6f6b05947e187b66c6b6b6",
"fields" : {
"tpe" : "fda712",
"length" : "40",
"pubKey" : "c59730cfae37472dc50a08e4d2804a57dd1506be6b0aafaa3728673f79183a9f",
"rValue" : "737f4c83ba94f58dc605307dfb8a94f26e5a98121e6f6b05947e187b66c6b6b6"
}
}, {
"tpeName" : "funding_input_v0",
"input" : "fda71437002902000000000100c2eb0b00000000160014aaa42dcd06d4e6a5a05f3d9eb561f73dc86edd1d0000000000000000ffffffff006b0000",
"fields" : {
"tpe" : "fda714",
"length" : "37",
"prevTxLen" : "0029",
"prevTx" : "02000000000100c2eb0b00000000160014aaa42dcd06d4e6a5a05f3d9eb561f73dc86edd1d00000000",
"prevTxVout" : "00000000",
"sequence" : "ffffffff",
"maxWitnessLen" : "006b",
"redeemScriptLen" : "0000",
"redeemScript" : ""
}
}, {
"tpeName" : "cet_adaptor_signatures_v0",
"input" : "fda716fd01e6004a7265e664e6c8b3511e2abeb9701e2ad5ac0fcd5669060dfabab493b2bcb04156e26fb3eb66afa4190c9feb99d248560bb5b8ced8c51643e35fcc8b1045097c01daefe8add5430e7b57d275c9b4cd18423d64226fd8f3f66a95e56e664d2ca8fb8dad3a5a231418cbf59650958551e02b599a6ab69b669055f1d1412f2dec44d0cdf6ff4eb48cad1eb04d647364038a87a3fba1a5814644ab9dcc2a9ad3b1c5ac0053fdc89d260f8935f3658d5444084d13e479e958353338bbfcdd35f8c49c0c8dffc56fde0a6b3d18067750e155341e3a9245524dd9c1498e08358fb354ae6c53015a9d0801dcdc5a69a78426763b88fc015f28459db3acb7ce60e3409aca8248cd2d6934cd64e9d1b4bc8a619f2f3781858c0d25e0663a25df00d526c4ef06388dbbd104b5b1514fe90d81ac3ff65a8f7cd2d3b4e0fb13477bcd074414985c1d6a001520cf43dc1b4c04f472c14dd6fd19ad3e57abd671a3fd7f6ceaabfc0227f31c5a14dcfbfd248d409af7077ab0b303875fcb4fc2789a5d06f379241c932b5e7501eca7f50ea45fe68301085a2ed27aad9979fb92dff666f4e6b2a9547080f643763a516e5904b86b828dec6ca860ea956acd70fb8f99e1ec3af83887123b679a8e95e9f64bfb5c4800f563efb82fcdedeeb0841e58d4860a96460da99d6ffbfb97",
"fields" : {
"tpe" : "fda716",
"length" : "fd01e6",
"sigs" : [ {
"encryptedSig" : "004a7265e664e6c8b3511e2abeb9701e2ad5ac0fcd5669060dfabab493b2bcb04156e26fb3eb66afa4190c9feb99d248560bb5b8ced8c51643e35fcc8b1045097c",
"dleqProof" : "01daefe8add5430e7b57d275c9b4cd18423d64226fd8f3f66a95e56e664d2ca8fb8dad3a5a231418cbf59650958551e02b599a6ab69b669055f1d1412f2dec44d0cdf6ff4eb48cad1eb04d647364038a87a3fba1a5814644ab9dcc2a9ad3b1c5ac"
}, {
"encryptedSig" : "0053fdc89d260f8935f3658d5444084d13e479e958353338bbfcdd35f8c49c0c8dffc56fde0a6b3d18067750e155341e3a9245524dd9c1498e08358fb354ae6c53",
"dleqProof" : "015a9d0801dcdc5a69a78426763b88fc015f28459db3acb7ce60e3409aca8248cd2d6934cd64e9d1b4bc8a619f2f3781858c0d25e0663a25df00d526c4ef06388dbbd104b5b1514fe90d81ac3ff65a8f7cd2d3b4e0fb13477bcd074414985c1d6a"
}, {
"encryptedSig" : "001520cf43dc1b4c04f472c14dd6fd19ad3e57abd671a3fd7f6ceaabfc0227f31c5a14dcfbfd248d409af7077ab0b303875fcb4fc2789a5d06f379241c932b5e75",
"dleqProof" : "01eca7f50ea45fe68301085a2ed27aad9979fb92dff666f4e6b2a9547080f643763a516e5904b86b828dec6ca860ea956acd70fb8f99e1ec3af83887123b679a8e95e9f64bfb5c4800f563efb82fcdedeeb0841e58d4860a96460da99d6ffbfb97"
} ]
}
}, {
"tpeName" : "funding_signatures_v0",
"input" : "fda718710001000200483045022078b7589954b3fa6131629827f14284727f743ad788364766ca58b4c38aa89e4d022100e298f649009182b4421a19019c52c559427344ad9bf57a0baa0997579d98279d010021020101a1b3fd70748dea06df8005b6de26b298e516e2ffa561745cf300747f92ca",
"fields" : {
"tpe" : "fda718",
"length" : "71",
"numWitnesses" : "0001",
"witnesses" : [ {
"stackLen" : "0002",
"stack" : [ {
"stackElementLen" : "0048",
"stackElement" : "3045022078b7589954b3fa6131629827f14284727f743ad788364766ca58b4c38aa89e4d022100e298f649009182b4421a19019c52c559427344ad9bf57a0baa0997579d98279d01"
}, {
"stackElementLen" : "0021",
"stackElement" : "020101a1b3fd70748dea06df8005b6de26b298e516e2ffa561745cf300747f92ca"
} ]
} ]
}
}, {
"tpeName" : "offer_dlc_v0",
"input" : "a71a0006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910ffda71057142d31393234383830323932373336303236323438000000000bebc200142d343839363234313333323636303437363633360000000000000000142d313234333930313230313134383130393638360000000002852271fda71240158da3789a8ff6d08e8e643626f9dd725d86ccac99bcd2dc71f4aea3f8dcd95073b57cddccb0f8514adbe3ff9b54a6a1edb4bd6a60dd7c7be57958348d4d6176031f1b96f96e86a14473ff71c3f9cb1d0f4dd5c007f45f55464d6a666a33117ea5001600143191cecb4b5fce7849f4e511f164285cefaeb3550000000005f5e1000001fda71437002902000000000100c2eb0b00000000160014f0507a3f11e5691b9c718a257ed5868d052759440000000000000000ffffffff006b000000160014878b745749da0bb3be65d98ce8615de0cbc6cd9d000000000000000100000064000000c8",
"fields" : {
"tpe" : "a71a",
"contractFlags" : "00",
"chainHash" : "06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f",
"contractInfo" : "fda71057142d31393234383830323932373336303236323438000000000bebc200142d343839363234313333323636303437363633360000000000000000142d313234333930313230313134383130393638360000000002852271",
"oracleInfo" : "fda71240158da3789a8ff6d08e8e643626f9dd725d86ccac99bcd2dc71f4aea3f8dcd95073b57cddccb0f8514adbe3ff9b54a6a1edb4bd6a60dd7c7be57958348d4d6176",
"fundingPubKey" : "031f1b96f96e86a14473ff71c3f9cb1d0f4dd5c007f45f55464d6a666a33117ea5",
"payoutSPKLen" : "0016",
"payoutSPK" : "00143191cecb4b5fce7849f4e511f164285cefaeb355",
"totalCollateralSatoshis" : "0000000005f5e100",
"fundingInputsLen" : "0001",
"fundingInputs" : [ "fda71437002902000000000100c2eb0b00000000160014f0507a3f11e5691b9c718a257ed5868d052759440000000000000000ffffffff006b0000" ],
"changeSPKLen" : "0016",
"changeSPK" : "0014878b745749da0bb3be65d98ce8615de0cbc6cd9d",
"feeRate" : "0000000000000001",
"contractMaturityBound" : "00000064",
"contractTimeout" : "000000c8"
}
}, {
"tpeName" : "accept_dlc_v0",
"input" : "a71c31fcbdd62eebf5b120b404e1a2ce06f8145301cc8d0217bd60cf58d18c6af10e0000000005f5e10002674c81da205b2a5c610f169858894388a98ce7dc3584e7b5136459ba946fd35100160014ff3c1088156462dad4fcd8fac9391036da82ec7b0001fda71437002902000000000100c2eb0b000000001600141e24a1b77e4ad65b2ea3297eace4a9c7e33409680000000000000000ffffffff006b000000160014567eb85ad8dd5bf69404e0d2398c0b205ece8072fda716fd01e6011b888867c15b9cdcd79d3c2f6dc9dc726496c67d2e69ac7de17fd0a65c6bbd778db8219b43535e90e7d4366398ad6f0bd6bc878caefbc5e59a96fae0fdfc24d8013ef3096baec015a9d878108173425e781f1a294ee2021c107da320352fb7c501d650725df33dbecac781322f5d4d7f4ffe73addb066bf632e174dc6df05adb3f32ae91b17695dc3093df654ebfc92b082919078e4b400f188141b8317957bff00070d23d6981cdc49eee7505c9521d496fc497de767064c1037712a8aa0977832377aaa26280842b9439adfdcb92c8a340841d307e2f25e67b28c678c916b37236007397c6b88f211ff3031f390540bf3d8d6d35b17e8af8d1d7d88a6d9f26533e0a891fa2593853b93d8ef483b53a16fd7039baabde9e9d17b0e5e1a67e2b634ec3833c2db0b847d1d2380b5edff29d51e7f0110ba3898a3500deab034a3769b78e01d212099997a1f1e3eed594bf9d6b5b6ed530c7269cd32ee86aba7c500a26f8d5ffefa35740054a9c9df4338d305b381c2d799cd140f99280ae85eed645e724a90164d049c1b580251f4112723f182a51d9ae66253d6b0b0b2ddbf2800e955ec125012d6656da15748bcb6080e8b90e668a5248357a3f047e29dbafe655ba9a952ed74b150cd252e3fb5a9a030400d139f3c50ccf5fbd0539e6234643feece4ee46a5d47dca0370c39f94e43782ee38994689507b90f84340582445ab7a4459afb5ccdfaf1bc57bfa938413228cc276280e76bfdbb7261b33e1b8b6243768877849",
"fields" : {
"tpe" : "a71c",
"tempContractId" : "31fcbdd62eebf5b120b404e1a2ce06f8145301cc8d0217bd60cf58d18c6af10e",
"totalCollateralSatoshis" : "0000000005f5e100",
"fundingPubKey" : "02674c81da205b2a5c610f169858894388a98ce7dc3584e7b5136459ba946fd351",
"payoutSPKLen" : "0016",
"payoutSPK" : "0014ff3c1088156462dad4fcd8fac9391036da82ec7b",
"fundingInputsLen" : "0001",
"fundingInputs" : [ "fda71437002902000000000100c2eb0b000000001600141e24a1b77e4ad65b2ea3297eace4a9c7e33409680000000000000000ffffffff006b0000" ],
"changeSPKLen" : "0016",
"changeSPK" : "0014567eb85ad8dd5bf69404e0d2398c0b205ece8072",
"cetSignatures" : "fda716fd01e6011b888867c15b9cdcd79d3c2f6dc9dc726496c67d2e69ac7de17fd0a65c6bbd778db8219b43535e90e7d4366398ad6f0bd6bc878caefbc5e59a96fae0fdfc24d8013ef3096baec015a9d878108173425e781f1a294ee2021c107da320352fb7c501d650725df33dbecac781322f5d4d7f4ffe73addb066bf632e174dc6df05adb3f32ae91b17695dc3093df654ebfc92b082919078e4b400f188141b8317957bff00070d23d6981cdc49eee7505c9521d496fc497de767064c1037712a8aa0977832377aaa26280842b9439adfdcb92c8a340841d307e2f25e67b28c678c916b37236007397c6b88f211ff3031f390540bf3d8d6d35b17e8af8d1d7d88a6d9f26533e0a891fa2593853b93d8ef483b53a16fd7039baabde9e9d17b0e5e1a67e2b634ec3833c2db0b847d1d2380b5edff29d51e7f0110ba3898a3500deab034a3769b78e01d212099997a1f1e3eed594bf9d6b5b6ed530c7269cd32ee86aba7c500a26f8d5ffefa35740054a9c9df4338d305b381c2d799cd140f99280ae85eed645e724a90164d049c1b580251f4112723f182a51d9ae66253d6b0b0b2ddbf2800e955ec125012d6656da15748bcb6080e8b90e668a5248357a3f047e29dbafe655ba9a952ed74b150cd252e3fb5a9a030400d139f3c50ccf5fbd0539e6234643feece4ee46",
"refundSignature" : "a5d47dca0370c39f94e43782ee38994689507b90f84340582445ab7a4459afb5ccdfaf1bc57bfa938413228cc276280e76bfdbb7261b33e1b8b6243768877849"
}
}, {
"tpeName" : "sign_dlc_v0",
"input" : "a71efee82c5d269c4b0788e3e4649c4ede28575ee423681d3665fb85298bc55aedbcfda716fd01e60052bb42a1d5d068708bbdc8b8879c40337dc05561b4434b4983c11aae69906b4ae81ac36d8d6fd358a414beb767bd5c23d75817d2d9bba4cfd9ddc737fd427c8901ddb650e20332551b5903655c17f4dbf93a123997935d5a5f8f761892a917fe691d891ee5d209bbc6f9d82af5f14db9ee23ee70096c02865adad7e5fa06345f00739389cc3079f559e724c60b81203ba42551b687a0516c2034bc1929ce7daa0100d452b9afbb3e5956668ee5e56163994227b5d503de16083d2f7499bcbf15e461e9dd76a0efbe263f7e1756625a016be03afaf692da734894e0e10e7c069427d1017e6bf2aee98c9be40e7509fc33471f2ad4519086644d09300c5d54660d64832e1e9aeb747fad999ade748edaba68de873252779e29f390c90cc8cd2ad52ec545e6ec7dd92324daf17577a1e3ed2ca0145db7a3c4f45a85724f5a5a1541b1a0fb0163a32cf351d480dc67886e084b4fb6dad2aea26f467c7362018adc0884999de4a806af353f53ccaaa488985aaad4f8b93d31b7f692e9c68e8e8b1c31a5755393004d602f85b139bca252092d08b855b89925056361e9d6828e9910522daa5e6ba74ac6eaa5b28eb7764fc76c0bbfa6635059ce955e55e4991b932b8bdf8748d1f0655898afe4e603e0ca147ff6c7f597e18b7c30e567383cc7265f2ecccce15775ab115bb73f8bb53642da6230b0426853ac837e4e090a244ac329c1743fd8974aa87f33cc5be7340fed5bf026684cb7eb72a86128ea01b5d91257bce3e9a9d434fda718720001000200493046022100f3eb25691ac6c2c993d52e775f43259dd9bc0565edff45fcf70f268d73bf69ae022100dadadb0eff6e32ccecee60fec37b22ace543c0a5251dcdd30d744622c64904fa01002102e6f5c02b455c541a098d3043ff0f744c5a71a298884f82155805f8558673fc52",
"fields" : {
"tpe" : "a71e",
"contractId" : "fee82c5d269c4b0788e3e4649c4ede28575ee423681d3665fb85298bc55aedbc",
"cetSignatures" : "fda716fd01e60052bb42a1d5d068708bbdc8b8879c40337dc05561b4434b4983c11aae69906b4ae81ac36d8d6fd358a414beb767bd5c23d75817d2d9bba4cfd9ddc737fd427c8901ddb650e20332551b5903655c17f4dbf93a123997935d5a5f8f761892a917fe691d891ee5d209bbc6f9d82af5f14db9ee23ee70096c02865adad7e5fa06345f00739389cc3079f559e724c60b81203ba42551b687a0516c2034bc1929ce7daa0100d452b9afbb3e5956668ee5e56163994227b5d503de16083d2f7499bcbf15e461e9dd76a0efbe263f7e1756625a016be03afaf692da734894e0e10e7c069427d1017e6bf2aee98c9be40e7509fc33471f2ad4519086644d09300c5d54660d64832e1e9aeb747fad999ade748edaba68de873252779e29f390c90cc8cd2ad52ec545e6ec7dd92324daf17577a1e3ed2ca0145db7a3c4f45a85724f5a5a1541b1a0fb0163a32cf351d480dc67886e084b4fb6dad2aea26f467c7362018adc0884999de4a806af353f53ccaaa488985aaad4f8b93d31b7f692e9c68e8e8b1c31a5755393004d602f85b139bca252092d08b855b89925056361e9d6828e9910522daa5e6ba74ac6eaa5b28eb7764fc76c0bbfa6635059ce955e55e4991b932b8bdf8748d1f0655898afe4e603e0ca147ff6c7f597e18b7c30e567383cc7265f2ecccce15775",
"refundSignature" : "ab115bb73f8bb53642da6230b0426853ac837e4e090a244ac329c1743fd8974aa87f33cc5be7340fed5bf026684cb7eb72a86128ea01b5d91257bce3e9a9d434",
"fundingSignatures" : "fda718720001000200493046022100f3eb25691ac6c2c993d52e775f43259dd9bc0565edff45fcf70f268d73bf69ae022100dadadb0eff6e32ccecee60fec37b22ace543c0a5251dcdd30d744622c64904fa01002102e6f5c02b455c541a098d3043ff0f744c5a71a298884f82155805f8558673fc52"
}
} ]

View file

@ -0,0 +1,51 @@
[ {
"inputs" : {
"privKey" : "5376a94490ff9b07387511351fdf9fb56d0f704effaa9e55218ac82f712f8a26",
"privNonce" : "f32827363379a82bedd1724197ebbae0b0e58719d3014dacc353f0c45109830e",
"msgHash" : "b27019d1912cb97b679eee4c01f9203e00da8443767173df076a529a66e707cf"
},
"pubKey" : "ce9a3088688eecd98db77c90637c25e6801fc56b0436e7e0103cee82ec63d508",
"pubNonce" : "0273ebfee82296afd16b9a6c7cf2485ef83b0cba1b6b66dc7edfbfb1071e8317",
"signature" : "0273ebfee82296afd16b9a6c7cf2485ef83b0cba1b6b66dc7edfbfb1071e8317ee2b16e43e08393bcbe087c792b30c902ff136775323877fc832f64bf5935781",
"sigPoint" : "020dddc643adbc3c8d745f6e9c028bf4abf22cfc97568b60e4c3419cbb72502690"
}, {
"inputs" : {
"privKey" : "b339569c68f2de370ba4774203c2d01cfbe2af1a23958accaaa227e64cae4e5f",
"privNonce" : "467a0383c6116b9c65ddc8aa2d3577a0f597027b07163f5ea9e891d068c9545a",
"msgHash" : "ecc549855e17ce7dbce3759ff9ffd224ba34e40befe3df69d6b7a5450c82fc07"
},
"pubKey" : "639fd0e002f476a1ba3dd3bb40d007544cf9e09ff1a23bd8c66d1cb8980fed8c",
"pubNonce" : "1e90814df446c16b854494aac8b1f05771611228b52ddd6b1eb1dd29dab973b7",
"signature" : "1e90814df446c16b854494aac8b1f05771611228b52ddd6b1eb1dd29dab973b7cfadd5b1004918214713cc052b84821f675eccd12388008b25dc18b6596aa132",
"sigPoint" : "02987faf504b29c90ffa0e83f9c9c9919d7c56ad5564c0b7eaa63d92b7cf3e51ca"
}, {
"inputs" : {
"privKey" : "a6080050e59f3b7ebbd14a3d058f44978ece37bb896543c789b89ca86dabc74d",
"privNonce" : "84e9e4edfcd67a92f326f4d96f48d311793e96c2b542e96e5738fe987820d5e4",
"msgHash" : "6744b92461c47f4a7f7b785c6bb38daa40b7c1d9e532b5480e5b73c6c0096011"
},
"pubKey" : "4c2a9fd1473302f23d28c398d54e4bef5f3d6d01341387bac5872796b89a0ef9",
"pubNonce" : "7fa3a59374116c93b1ea0d2c9408b40768e99e43562f9bd7205986e567e961b5",
"signature" : "7fa3a59374116c93b1ea0d2c9408b40768e99e43562f9bd7205986e567e961b59dd00ba70a497851153afc5faf774f8ed3f8b6b2b421c94121f7c85dab3d6225",
"sigPoint" : "03b6e6aee8ef20a761bc9bbb227d0a49c1eea80a0e6df62a79c576b5f08ad88ec5"
}, {
"inputs" : {
"privKey" : "cfe7c7ed47b0ce4885838a53cb6b102ae09b37b2705147607d886e62988190be",
"privNonce" : "a0d084b608e5d1b218901212ed3fa15a8692de99c37c6cae6648285156a5feda",
"msgHash" : "143bc33b165b4f7e66a2d997e1f20e4d880416be6d7aaa0d8d385d1ab9482470"
},
"pubKey" : "43766d34751895463a0a9a3979b8ebc300132c3117ef20de0a209ffc1e5d06cb",
"pubNonce" : "53c53d2f7eae94c4766ec322c018be27f65fb65478212f4983b08b8f40765018",
"signature" : "53c53d2f7eae94c4766ec322c018be27f65fb65478212f4983b08b8f407650188a01c30884bcd27157191fa4046944c6fc035a69765fd09dccd7a02816143a84",
"sigPoint" : "035041aedc6a8fb0c911507af5cdfe5393761361a28e88d5f4f4197f39a495b6ab"
}, {
"inputs" : {
"privKey" : "7582012d1fa17f723754927109828c71c5d7dd3fadcc8f20d6a79dde27fc985f",
"privNonce" : "dd1d13c185e27172e83fd615f6ace25b89e7360ca74888b15b7ac4b639d9edb8",
"msgHash" : "1c2aedbbbd3d8a425fc688730a00c8da2e5b0f0be90eee1f90a2099cac5edb50"
},
"pubKey" : "d0816bd521ce59ae060eb43cd6e8d0fc8047f338a23f0038a986b92f77a038fe",
"pubNonce" : "dae00f16a8c375fb8f0848e96ddfb77ea57b2c6c6f499ffbe6534ba53d6193ae",
"signature" : "dae00f16a8c375fb8f0848e96ddfb77ea57b2c6c6f499ffbe6534ba53d6193aef8c6936867b1addd66517eaf933e92fb5f6299975ffcf09901439a012ad2ad83",
"sigPoint" : "020ddbf600b1cef6cb9f9cdabdb1951c42f1a6b7fa7dee0ef9fb3bdda258e5b2c6"
} ]

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,135 @@
package org.bitcoins.dlc.verify
import org.bitcoins.commons.jsonmodels.dlc.FundingSignatures
import org.bitcoins.core.crypto.{
TransactionSignatureChecker,
TransactionSignatureSerializer,
WitnessTxSigComponentRaw
}
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.policy.Policy
import org.bitcoins.core.protocol.tlv.DLCOutcomeType
import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature
import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.script.crypto.HashType
import org.bitcoins.core.util.{BitcoinSLogger, FutureUtil}
import org.bitcoins.crypto.ECAdaptorSignature
import org.bitcoins.dlc.builder.DLCTxBuilder
import scodec.bits.ByteVector
import scala.concurrent.{Await, ExecutionContext, Future}
import scala.concurrent.duration.DurationInt
import scala.util.{Failure, Success}
/** Responsible for verifying all DLC signatures */
case class DLCSignatureVerifier(builder: DLCTxBuilder, isInitiator: Boolean)
extends BitcoinSLogger {
private lazy val fundingTx = Await.result(builder.buildFundingTx, 5.seconds)
def verifyRemoteFundingSigs(remoteSigs: FundingSignatures): Boolean = {
val (remoteTweak, remoteFundingInputs) = if (isInitiator) {
(builder.offerFundingInputs.length, builder.acceptFundingInputs)
} else {
(0, builder.offerFundingInputs)
}
val psbt = PSBT.fromUnsignedTxWithP2SHScript(fundingTx)
remoteSigs.zipWithIndex
.foldLeft(true) {
case (ret, ((outPoint, witness), index)) =>
val idx = index + remoteTweak
if (ret) {
if (psbt.transaction.inputs(idx).previousOutput != outPoint) {
logger.error("Adding signature for incorrect input")
false
} else {
val fundingInput = remoteFundingInputs(index)
psbt
.addUTXOToInput(fundingInput.prevTx, idx)
.addFinalizedScriptWitnessToInput(fundingInput.scriptSignature,
witness,
idx)
.finalizeInput(idx) match {
case Success(finalized) =>
finalized.verifyFinalizedInput(idx)
case Failure(_) =>
false
}
}
} else false
}
}
/** Verifies remote's CET signature for a given outcome hash */
def verifyCETSig(
outcome: DLCOutcomeType,
sig: ECAdaptorSignature): Boolean = {
val remoteFundingPubKey = if (isInitiator) {
builder.acceptFundingKey
} else {
builder.offerFundingKey
}
val adaptorPoint = builder.oracleAndContractInfo.sigPointForOutcome(outcome)
val cet = Await.result(builder.buildCET(outcome), 5.seconds)
val sigComponent = WitnessTxSigComponentRaw(transaction = cet,
inputIndex = UInt32.zero,
output = fundingTx.outputs.head,
flags = Policy.standardFlags)
val hashType = HashType(
ByteVector(0.toByte, 0.toByte, 0.toByte, HashType.sigHashAll.byte))
val hash =
TransactionSignatureSerializer.hashForSignature(sigComponent, hashType)
remoteFundingPubKey.adaptorVerify(hash.bytes, adaptorPoint, sig)
}
def verifyCETSigs(sigs: Vector[(DLCOutcomeType, ECAdaptorSignature)])(implicit
ec: ExecutionContext): Future[Boolean] = {
val correctNumberOfSigs =
sigs.size >= builder.oracleAndContractInfo.allOutcomes.length
def runVerify(
outcomeSigs: Vector[(DLCOutcomeType, ECAdaptorSignature)]): Future[
Boolean] = {
Future {
outcomeSigs.foldLeft(true) {
case (ret, (outcome, sig)) =>
ret && verifyCETSig(outcome, sig)
}
}
}
if (correctNumberOfSigs) {
FutureUtil
.batchAndParallelExecute(sigs, runVerify, 25)
.map(_.forall(res => res))
} else Future.successful(false)
}
/** Verifies remote's refund signature */
def verifyRefundSig(sig: PartialSignature): Boolean = {
val refundTx = Await.result(builder.buildRefundTx, 5.seconds)
val sigComponent = WitnessTxSigComponentRaw(transaction = refundTx,
inputIndex = UInt32.zero,
output = fundingTx.outputs.head,
flags = Policy.standardFlags)
TransactionSignatureChecker
.checkSignature(
sigComponent,
sigComponent.output.scriptPubKey.asm.toVector,
sig.pubKey,
sig.signature,
Policy.standardFlags
)
.isValid
}
}

View file

@ -4,7 +4,7 @@ import scala.util.Properties
version in ThisBuild ~= { version =>
val withoutSuffix = version.dropRight(8)
withoutSuffix + "SCHNORR-DLC-SNAPSHOT"
withoutSuffix + "ADAPTOR-ECDSA-DLC-SNAPSHOT"
}
val scala2_12 = "2.12.12"

View file

@ -56,7 +56,10 @@ case class BIP39KeyManager(
private val privVersion: ExtKeyPrivVersion =
HDUtil.getXprivVersion(kmParams.purpose, kmParams.network)
private val rootExtPrivKey = seed.toExtPrivateKey(privVersion)
/** FIXME Temporary public, should be private, This is still required to make the
* BinaryOutcomeDLCClient from the wallet, can change back to private val when we change the client to use a Sign instead
*/
val rootExtPrivKey: ExtPrivateKey = seed.toExtPrivateKey(privVersion)
/** Converts a non-sensitive DB representation of a UTXO into
* a signable (and sensitive) real-world UTXO

Some files were not shown because too many files have changed in this diff Show more