mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-03-18 21:22:04 +01:00
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:
parent
ddd352d844
commit
935145d46b
115 changed files with 27207 additions and 100 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
name := "bitcoin-s-sbclient-test"
|
||||
|
||||
publish / skip := true
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3
app/dlc-suredbits-client/dlc-suredbits-client.sbt
Normal file
3
app/dlc-suredbits-client/dlc-suredbits-client.sbt
Normal file
|
@ -0,0 +1,3 @@
|
|||
name := "bitcoin-s-dlc-suredbits-client"
|
||||
|
||||
libraryDependencies ++= Deps.dlcSuredbitsClient
|
|
@ -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)
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"}"""
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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`)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(_ => ())
|
||||
}
|
||||
|
|
|
@ -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] = {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
74
build.sbt
74
build.sbt
|
@ -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)
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
722
dlc-test/src/test/scala/org/bitcoins/dlc/DLCClientTest.scala
Normal file
722
dlc-test/src/test/scala/org/bitcoins/dlc/DLCClientTest.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
116
dlc-test/src/test/scala/org/bitcoins/dlc/DLCFeeTestUtil.scala
Normal file
116
dlc-test/src/test/scala/org/bitcoins/dlc/DLCFeeTestUtil.scala
Normal 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))
|
||||
}
|
||||
}
|
76
dlc-test/src/test/scala/org/bitcoins/dlc/SetupDLCTest.scala
Normal file
76
dlc-test/src/test/scala/org/bitcoins/dlc/SetupDLCTest.scala
Normal 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))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
);
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "wallet_dlcs" ADD COLUMN "outcome" TEXT;
|
|
@ -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
|
||||
);
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "wallet_dlcs" ADD COLUMN "outcome" TEXT;
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
1601
dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWallet.scala
Normal file
1601
dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWallet.scala
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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
1
dlc/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
#TODO
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
171
dlc/src/main/scala/org/bitcoins/dlc/builder/DLCTxBuilder.scala
Normal file
171
dlc/src/main/scala/org/bitcoins/dlc/builder/DLCTxBuilder.scala
Normal 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())
|
||||
}
|
||||
}
|
100
dlc/src/main/scala/org/bitcoins/dlc/execution/DLCExecutor.scala
Normal file
100
dlc/src/main/scala/org/bitcoins/dlc/execution/DLCExecutor.scala
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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
|
34
dlc/src/main/scala/org/bitcoins/dlc/execution/SetupDLC.scala
Normal file
34
dlc/src/main/scala/org/bitcoins/dlc/execution/SetupDLC.scala
Normal 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)
|
307
dlc/src/main/scala/org/bitcoins/dlc/sign/DLCTxSigner.scala
Normal file
307
dlc/src/main/scala/org/bitcoins/dlc/sign/DLCTxSigner.scala
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
398
dlc/src/main/scala/org/bitcoins/dlc/testgen/DLCTLVGen.scala
Normal file
398
dlc/src/main/scala/org/bitcoins/dlc/testgen/DLCTLVGen.scala
Normal 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
|
||||
}
|
||||
}
|
151
dlc/src/main/scala/org/bitcoins/dlc/testgen/DLCTestUtil.scala
Normal file
151
dlc/src/main/scala/org/bitcoins/dlc/testgen/DLCTestUtil.scala
Normal 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)
|
||||
}
|
||||
}
|
330
dlc/src/main/scala/org/bitcoins/dlc/testgen/DLCTestVector.scala
Normal file
330
dlc/src/main/scala/org/bitcoins/dlc/testgen/DLCTestVector.scala
Normal 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]
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
277
dlc/src/main/scala/org/bitcoins/dlc/testgen/DLCTxGen.scala
Normal file
277
dlc/src/main/scala/org/bitcoins/dlc/testgen/DLCTxGen.scala
Normal 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)))
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
209
dlc/src/main/scala/org/bitcoins/dlc/testgen/TestDLCClient.scala
Normal file
209
dlc/src/main/scala/org/bitcoins/dlc/testgen/TestDLCClient.scala
Normal 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)
|
||||
}
|
||||
}
|
19
dlc/src/main/scala/org/bitcoins/dlc/testgen/TestVector.scala
Normal file
19
dlc/src/main/scala/org/bitcoins/dlc/testgen/TestVector.scala
Normal 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))
|
||||
}
|
||||
}
|
121
dlc/src/main/scala/org/bitcoins/dlc/testgen/TestVectorGen.scala
Normal file
121
dlc/src/main/scala/org/bitcoins/dlc/testgen/TestVectorGen.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
9861
dlc/src/main/scala/org/bitcoins/dlc/testgen/dlc_fee_test.json
Normal file
9861
dlc/src/main/scala/org/bitcoins/dlc/testgen/dlc_fee_test.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
} ]
|
|
@ -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"
|
||||
}
|
||||
} ]
|
|
@ -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"
|
||||
} ]
|
1996
dlc/src/main/scala/org/bitcoins/dlc/testgen/dlc_test.json
Normal file
1996
dlc/src/main/scala/org/bitcoins/dlc/testgen/dlc_test.json
Normal file
File diff suppressed because one or more lines are too long
2110
dlc/src/main/scala/org/bitcoins/dlc/testgen/dlc_tx_test.json
Normal file
2110
dlc/src/main/scala/org/bitcoins/dlc/testgen/dlc_tx_test.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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
Loading…
Add table
Reference in a new issue