From 5777ec1c316a3e160a3706cf9fd119c2e7436452 Mon Sep 17 00:00:00 2001 From: rorp Date: Fri, 18 Feb 2022 07:29:00 -0800 Subject: [PATCH] Add an ability to set custom payout and change addresses (#4101) * Add an ability to set custom payout and change addresses * config changes * formatting * respond to the comments --- .../org/bitcoins/server/RoutesSpec.scala | 28 +- .../bitcoins/server/ServerJsonModels.scala | 137 ++++++-- .../org/bitcoins/server/WalletRoutes.scala | 33 +- .../src/universal/docker-application.conf | 3 + .../core/api/dlc/wallet/DLCWalletApi.scala | 35 +- .../core/protocol/dlc/compute/DLCUtil.scala | 24 +- db-commons/src/main/resources/reference.conf | 4 + .../dlc/node/DLCNegotiationTest.scala | 6 +- .../org/bitcoins/dlc/node/DLCNodeTest.scala | 4 +- .../bitcoins/dlc/node/DLCDataHandler.scala | 2 +- .../dlc/wallet/DLCExecutionTest.scala | 18 +- .../dlc/wallet/MultiWalletDLCTest.scala | 19 +- .../dlc/wallet/WalletDLCSetupTest.scala | 205 ++++++++--- .../org/bitcoins/dlc/wallet/DLCWallet.scala | 328 ++++++++++-------- .../dlc/wallet/util/DLCAcceptUtil.scala | 21 +- docs/config/configuration.md | 4 + .../testkit/BitcoinSTestAppConfig.scala | 4 +- .../testkit/wallet/DLCWalletUtil.scala | 15 +- .../wallet/config/WalletAppConfig.scala | 3 + 19 files changed, 596 insertions(+), 297 deletions(-) diff --git a/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala b/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala index 314a0de1ee..0c027c122e 100644 --- a/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala +++ b/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala @@ -952,18 +952,26 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { ) "create a dlc offer" in { - (mockWalletApi - .createDLCOffer(_: ContractInfoTLV, - _: Satoshis, - _: Option[SatoshisPerVirtualByte], - _: UInt32, - _: UInt32)) + ( + mockWalletApi + .createDLCOffer( + _: ContractInfoTLV, + _: Satoshis, + _: Option[SatoshisPerVirtualByte], + _: UInt32, + _: UInt32, + _: Option[BitcoinAddress], + _: Option[BitcoinAddress] + ) + ) .expects( contractInfoTLV, Satoshis(2500), Some(SatoshisPerVirtualByte(Satoshis.one)), UInt32(contractMaturity), - UInt32(contractTimeout) + UInt32(contractTimeout), + None, + None ) .returning(Future.successful(offer)) @@ -1021,8 +1029,10 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { "accept a dlc offer" in { (mockWalletApi - .acceptDLCOffer(_: DLCOfferTLV)) - .expects(offer.toTLV) + .acceptDLCOffer(_: DLCOfferTLV, + _: Option[BitcoinAddress], + _: Option[BitcoinAddress])) + .expects(offer.toTLV, None, None) .returning(Future.successful(accept)) val route = walletRoutes.handleCommand( diff --git a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala index 228403a2b8..aa11b325bd 100644 --- a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala +++ b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala @@ -2,7 +2,7 @@ package org.bitcoins.server import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.LockUnspentOutputParameter import org.bitcoins.commons.jsonmodels.cli.ContractDescriptorParser -import org.bitcoins.commons.serializers.{JsonReaders} +import org.bitcoins.commons.serializers.JsonReaders import org.bitcoins.core.api.wallet.CoinSelectionAlgo import org.bitcoins.core.crypto._ import org.bitcoins.core.currency.{Bitcoins, Satoshis} @@ -655,26 +655,67 @@ case class CreateDLCOffer( collateral: Satoshis, feeRateOpt: Option[SatoshisPerVirtualByte], locktime: UInt32, - refundLocktime: UInt32) + refundLocktime: UInt32, + externalPayoutAddressOpt: Option[BitcoinAddress], + externalChangeAddressOpt: Option[BitcoinAddress]) object CreateDLCOffer extends ServerJsonModels { def fromJsArr(jsArr: ujson.Arr): Try[CreateDLCOffer] = { + def parseParameters( + contractInfoJs: Value, + collateralJs: Value, + feeRateOptJs: Value, + locktimeJs: Value, + refundLTJs: Value, + payoutAddressJs: Value, + changeAddressJs: Value) = Try { + val contractInfoTLV = jsToContractInfoTLV(contractInfoJs) + val collateral = jsToSatoshis(collateralJs) + val feeRate = jsToSatoshisPerVirtualByteOpt(feeRateOptJs) + val locktime = jsToUInt32(locktimeJs) + val refundLT = jsToUInt32(refundLTJs) + val payoutAddressJsOpt = nullToOpt(payoutAddressJs) + val payoutAddressOpt = + payoutAddressJsOpt.map(js => jsToBitcoinAddress(js)) + val changeAddressJsOpt = nullToOpt(changeAddressJs) + val changeAddressOpt = + changeAddressJsOpt.map(js => jsToBitcoinAddress(js)) + CreateDLCOffer(contractInfoTLV, + collateral, + feeRate, + locktime, + refundLT, + payoutAddressOpt, + changeAddressOpt) + } + jsArr.arr.toList match { case contractInfoJs :: collateralJs :: feeRateOptJs :: locktimeJs :: refundLTJs :: Nil => - Try { - val contractInfoTLV = jsToContractInfoTLV(contractInfoJs) - val collateral = jsToSatoshis(collateralJs) - val feeRate = jsToSatoshisPerVirtualByteOpt(feeRateOptJs) - val locktime = jsToUInt32(locktimeJs) - val refundLT = jsToUInt32(refundLTJs) - CreateDLCOffer(contractInfoTLV, - collateral, - feeRate, - locktime, - refundLT) - } + parseParameters(contractInfoJs, + collateralJs, + feeRateOptJs, + locktimeJs, + refundLTJs, + Null, + Null) + case contractInfoJs :: collateralJs :: feeRateOptJs :: locktimeJs :: refundLTJs :: payoutAddressJs :: Nil => + parseParameters(contractInfoJs, + collateralJs, + feeRateOptJs, + locktimeJs, + refundLTJs, + payoutAddressJs, + Null) + case contractInfoJs :: collateralJs :: feeRateOptJs :: locktimeJs :: refundLTJs :: payoutAddressJs :: changeAddressJs :: Nil => + parseParameters(contractInfoJs, + collateralJs, + feeRateOptJs, + locktimeJs, + refundLTJs, + payoutAddressJs, + changeAddressJs) case other => Failure( new IllegalArgumentException( @@ -839,17 +880,35 @@ object DecodeAttestations extends ServerJsonModels { } } -case class AcceptDLCOffer(offer: LnMessage[DLCOfferTLV]) +case class AcceptDLCOffer( + offer: LnMessage[DLCOfferTLV], + externalPayoutAddressOpt: Option[BitcoinAddress], + externalChangeAddressOpt: Option[BitcoinAddress]) object AcceptDLCOffer extends ServerJsonModels { def fromJsArr(jsArr: ujson.Arr): Try[AcceptDLCOffer] = { + def parseParameters( + offerJs: Value, + payoutAddressJs: Value, + changeAddressJs: Value) = Try { + val offer = LnMessageFactory(DLCOfferTLV).fromHex(offerJs.str) + val payoutAddressJsOpt = nullToOpt(payoutAddressJs) + val payoutAddressOpt = + payoutAddressJsOpt.map(js => jsToBitcoinAddress(js)) + val changeAddressJsOpt = nullToOpt(changeAddressJs) + val changeAddressOpt = + changeAddressJsOpt.map(js => jsToBitcoinAddress(js)) + AcceptDLCOffer(offer, payoutAddressOpt, changeAddressOpt) + } + jsArr.arr.toList match { case offerJs :: Nil => - Try { - val offer = LnMessageFactory(DLCOfferTLV).fromHex(offerJs.str) - AcceptDLCOffer(offer) - } + parseParameters(offerJs, Null, Null) + case offerJs :: payoutAddressJs :: Nil => + parseParameters(offerJs, payoutAddressJs, Null) + case offerJs :: payoutAddressJs :: changeAddressJs :: Nil => + parseParameters(offerJs, payoutAddressJs, changeAddressJs) case Nil => Failure(new IllegalArgumentException("Missing offer argument")) @@ -930,24 +989,42 @@ object AddDLCSigs extends ServerJsonModels { } } -case class DLCDataFromFile(path: Path, destinationOpt: Option[Path]) +case class DLCDataFromFile( + path: Path, + destinationOpt: Option[Path], + externalPayoutAddressOpt: Option[BitcoinAddress], + externalChangeAddressOpt: Option[BitcoinAddress]) object DLCDataFromFile extends ServerJsonModels { def fromJsArr(jsArr: ujson.Arr): Try[DLCDataFromFile] = { + def parseParameters( + pathJs: Value, + destJs: Value, + payoutAddressJs: Value, + changeAddressJs: Value) = Try { + val path = new File(pathJs.str).toPath + val destJsOpt = nullToOpt(destJs) + val destOpt = destJsOpt.map(js => new File(js.str).toPath) + val payoutAddressJsOpt = nullToOpt(payoutAddressJs) + val payoutAddressOpt = + payoutAddressJsOpt.map(js => jsToBitcoinAddress(js)) + val changeAddressJsOpt = nullToOpt(changeAddressJs) + val changeAddressOpt = + changeAddressJsOpt.map(js => jsToBitcoinAddress(js)) + + DLCDataFromFile(path, destOpt, payoutAddressOpt, changeAddressOpt) + } + jsArr.arr.toList match { case pathJs :: Nil => - Try { - val path = new File(pathJs.str).toPath - DLCDataFromFile(path, None) - } + parseParameters(pathJs, Null, Null, Null) case pathJs :: destJs :: Nil => - Try { - val path = new File(pathJs.str).toPath - val destJsOpt = nullToOpt(destJs) - val destOpt = destJsOpt.map(js => new File(js.str).toPath) - DLCDataFromFile(path, destOpt) - } + parseParameters(pathJs, destJs, Null, Null) + case pathJs :: destJs :: payoutAddressJs :: Nil => + parseParameters(pathJs, destJs, payoutAddressJs, Null) + case pathJs :: destJs :: payoutAddressJs :: changeAddressJs :: Nil => + parseParameters(pathJs, destJs, payoutAddressJs, changeAddressJs) case Nil => Failure(new IllegalArgumentException("Missing path argument")) case other => diff --git a/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala b/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala index 5d9aff979e..1db767f683 100644 --- a/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala +++ b/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala @@ -6,14 +6,14 @@ import akka.http.scaladsl.server._ import akka.stream.Materializer import grizzled.slf4j.Logging import org.bitcoins.commons.serializers.Picklers._ +import org.bitcoins.core.api.dlc.wallet.AnyDLCHDWalletApi 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.fee.SatoshisPerVirtualByte import org.bitcoins.core.wallet.utxo.{AddressLabelTagType, TxoState} import org.bitcoins.crypto.NetworkElement -import org.bitcoins.core.api.dlc.wallet.AnyDLCHDWalletApi -import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte import org.bitcoins.keymanager._ import org.bitcoins.keymanager.config.KeyManagerAppConfig import org.bitcoins.server.routes.{Server, ServerCommand, ServerRoute} @@ -298,7 +298,9 @@ case class WalletRoutes(wallet: AnyDLCHDWalletApi)(implicit collateral, feeRateOpt, locktime, - refundLT)) => + refundLT, + payoutAddressOpt, + changeAddressOpt)) => complete { val announcements = contractInfo.oracleInfo match { case OracleInfoV0TLV(announcement) => Vector(announcement) @@ -316,7 +318,9 @@ case class WalletRoutes(wallet: AnyDLCHDWalletApi)(implicit collateral, feeRateOpt, locktime, - refundLT) + refundLT, + payoutAddressOpt, + changeAddressOpt) .map { offer => Server.httpSuccess(offer.toMessage.hex) } @@ -327,10 +331,11 @@ case class WalletRoutes(wallet: AnyDLCHDWalletApi)(implicit AcceptDLCOffer.fromJsArr(arr) match { case Failure(exception) => complete(Server.httpBadRequest(exception)) - case Success(AcceptDLCOffer(offer)) => + case Success( + AcceptDLCOffer(offer, payoutAddressOpt, changeAddressOpt)) => complete { wallet - .acceptDLCOffer(offer.tlv) + .acceptDLCOffer(offer.tlv, payoutAddressOpt, changeAddressOpt) .map { accept => Server.httpSuccess(accept.toMessage.hex) } @@ -341,7 +346,11 @@ case class WalletRoutes(wallet: AnyDLCHDWalletApi)(implicit DLCDataFromFile.fromJsArr(arr) match { case Failure(exception) => complete(Server.httpBadRequest(exception)) - case Success(DLCDataFromFile(path, destOpt)) => + case Success( + DLCDataFromFile(path, + destOpt, + payoutAddressOpt, + changeAddressOpt)) => complete { val hex = Files.readAllLines(path).get(0) @@ -349,7 +358,9 @@ case class WalletRoutes(wallet: AnyDLCHDWalletApi)(implicit val offerMessage = LnMessageFactory(DLCOfferTLV).fromHex(hex) wallet - .acceptDLCOffer(offerMessage.tlv) + .acceptDLCOffer(offerMessage.tlv, + payoutAddressOpt, + changeAddressOpt) .map { accept => val ret = handleDestinationOpt(accept.toMessage.hex, destOpt) Server.httpSuccess(ret) @@ -375,7 +386,7 @@ case class WalletRoutes(wallet: AnyDLCHDWalletApi)(implicit DLCDataFromFile.fromJsArr(arr) match { case Failure(exception) => complete(Server.httpBadRequest(exception)) - case Success(DLCDataFromFile(path, destOpt)) => + case Success(DLCDataFromFile(path, destOpt, _, _)) => complete { val hex = Files.readAllLines(path).get(0) @@ -408,7 +419,7 @@ case class WalletRoutes(wallet: AnyDLCHDWalletApi)(implicit DLCDataFromFile.fromJsArr(arr) match { case Failure(exception) => complete(Server.httpBadRequest(exception)) - case Success(DLCDataFromFile(path, _)) => + case Success(DLCDataFromFile(path, _, _, _)) => complete { val hex = Files.readAllLines(path).get(0) @@ -439,7 +450,7 @@ case class WalletRoutes(wallet: AnyDLCHDWalletApi)(implicit DLCDataFromFile.fromJsArr(arr) match { case Failure(exception) => complete(Server.httpBadRequest(exception)) - case Success(DLCDataFromFile(path, _)) => + case Success(DLCDataFromFile(path, _, _, _)) => val hex = Files.readAllLines(path).get(0) val signMessage = LnMessageFactory(DLCSignTLV).fromHex(hex) diff --git a/app/server/src/universal/docker-application.conf b/app/server/src/universal/docker-application.conf index b2e49d9f9b..52beb860f9 100644 --- a/app/server/src/universal/docker-application.conf +++ b/app/server/src/universal/docker-application.conf @@ -32,5 +32,8 @@ bitcoin-s.dlcnode.proxy.socks5 = ${?BITCOIN_S_DLCNODE_PROXY_SOCKS5} bitcoin-s.dlcnode.tor.enabled = ${?BITCOIN_S_DLCNODE_TOR_ENABLED} bitcoin-s.dlcnode.external-ip = ${?BITCOIN_S_DLCNODE_EXTERNAL_IP} +bitcoin-s.wallet.allowExternalDLCAddresses = false +bitcoin-s.wallet.allowExternalDLCAddresses = ${?BITCOIN_S_ALLOW_EXT_DLC_ADDRESSES} + bitcoin-s.tor.enabled = ${?BITCOIN_S_TOR_ENABLED} bitcoin-s.tor.provided = ${?BITCOIN_S_TOR_PROVIDED} \ No newline at end of file diff --git a/core/src/main/scala/org/bitcoins/core/api/dlc/wallet/DLCWalletApi.scala b/core/src/main/scala/org/bitcoins/core/api/dlc/wallet/DLCWalletApi.scala index a2c4d74696..f2eb50b14f 100644 --- a/core/src/main/scala/org/bitcoins/core/api/dlc/wallet/DLCWalletApi.scala +++ b/core/src/main/scala/org/bitcoins/core/api/dlc/wallet/DLCWalletApi.scala @@ -5,6 +5,7 @@ import org.bitcoins.core.api.wallet._ import org.bitcoins.core.currency.Satoshis import org.bitcoins.core.dlc.accounting._ import org.bitcoins.core.number.UInt32 +import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.protocol.dlc.models.DLCMessage._ import org.bitcoins.core.protocol.dlc.models._ import org.bitcoins.core.protocol.tlv._ @@ -22,9 +23,17 @@ trait DLCWalletApi { self: WalletApi => collateral: Satoshis, feeRateOpt: Option[SatoshisPerVirtualByte], locktime: UInt32, - refundLT: UInt32): Future[DLCOffer] = { + refundLT: UInt32, + externalPayoutAddressOpt: Option[BitcoinAddress], + externalChangeAddressOpt: Option[BitcoinAddress]): Future[DLCOffer] = { val contractInfo = ContractInfo.fromTLV(contractInfoTLV) - createDLCOffer(contractInfo, collateral, feeRateOpt, locktime, refundLT) + createDLCOffer(contractInfo, + collateral, + feeRateOpt, + locktime, + refundLT, + externalPayoutAddressOpt, + externalChangeAddressOpt) } def createDLCOffer( @@ -32,7 +41,9 @@ trait DLCWalletApi { self: WalletApi => collateral: Satoshis, feeRateOpt: Option[SatoshisPerVirtualByte], locktime: UInt32, - refundLT: UInt32): Future[DLCOffer] + refundLT: UInt32, + externalPayoutAddressOpt: Option[BitcoinAddress], + externalChangeAddressOpt: Option[BitcoinAddress]): Future[DLCOffer] def registerDLCOffer(dlcOffer: DLCOffer): Future[DLCOffer] = { createDLCOffer( @@ -40,15 +51,25 @@ trait DLCWalletApi { self: WalletApi => dlcOffer.totalCollateral, Some(dlcOffer.feeRate), dlcOffer.timeouts.contractMaturity.toUInt32, - dlcOffer.timeouts.contractTimeout.toUInt32 + dlcOffer.timeouts.contractTimeout.toUInt32, + None, + None ) } - def acceptDLCOffer(dlcOfferTLV: DLCOfferTLV): Future[DLCAccept] = { - acceptDLCOffer(DLCOffer.fromTLV(dlcOfferTLV)) + def acceptDLCOffer( + dlcOfferTLV: DLCOfferTLV, + externalPayoutAddressOpt: Option[BitcoinAddress], + externalChangeAddressOpt: Option[BitcoinAddress]): Future[DLCAccept] = { + acceptDLCOffer(DLCOffer.fromTLV(dlcOfferTLV), + externalPayoutAddressOpt, + externalChangeAddressOpt) } - def acceptDLCOffer(dlcOffer: DLCOffer): Future[DLCAccept] + def acceptDLCOffer( + dlcOffer: DLCOffer, + externalPayoutAddressOpt: Option[BitcoinAddress], + externalChangeAddressOpt: Option[BitcoinAddress]): Future[DLCAccept] def signDLC(acceptTLV: DLCAcceptTLV): Future[DLCSign] diff --git a/core/src/main/scala/org/bitcoins/core/protocol/dlc/compute/DLCUtil.scala b/core/src/main/scala/org/bitcoins/core/protocol/dlc/compute/DLCUtil.scala index 4b0c11aed7..0a2e4ed493 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/dlc/compute/DLCUtil.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/dlc/compute/DLCUtil.scala @@ -5,6 +5,7 @@ import org.bitcoins.core.crypto.ExtPublicKey import org.bitcoins.core.hd.{BIP32Path, HDChainType} import org.bitcoins.core.number.UInt16 import org.bitcoins.core.policy.Policy +import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.protocol.dlc.build.DLCTxBuilder import org.bitcoins.core.protocol.dlc.models.DLCMessage.{ DLCAccept, @@ -324,7 +325,8 @@ object DLCUtil { xpub: ExtPublicKey, chainType: HDChainType, keyIndex: Int, - networkParameters: NetworkParameters): DLCPublicKeys = { + networkParameters: NetworkParameters, + externalPayoutAddressOpt: Option[BitcoinAddress]): DLCPublicKeys = { val chainIndex = chainType.index val fundingKey = xpub @@ -332,16 +334,20 @@ object DLCUtil { .get .key - val payoutKey = - xpub - .deriveChildPubKey( - BIP32Path.fromString(s"m/$chainIndex/${keyIndex + 1}")) - .get - .key - networkParameters match { case bitcoinNetwork: BitcoinNetwork => - DLCPublicKeys.fromPubKeys(fundingKey, payoutKey, bitcoinNetwork) + externalPayoutAddressOpt match { + case None => + val payoutKey = + xpub + .deriveChildPubKey( + BIP32Path.fromString(s"m/$chainIndex/${keyIndex + 1}")) + .get + .key + DLCPublicKeys.fromPubKeys(fundingKey, payoutKey, bitcoinNetwork) + case Some(externalPayoutAddress) => + DLCPublicKeys(fundingKey, externalPayoutAddress) + } } } } diff --git a/db-commons/src/main/resources/reference.conf b/db-commons/src/main/resources/reference.conf index 8c89fffde8..3f60b8bc3e 100644 --- a/db-commons/src/main/resources/reference.conf +++ b/db-commons/src/main/resources/reference.conf @@ -134,6 +134,10 @@ bitcoin-s { # before we timeout addressQueueTimeout = 5 seconds + # Allow external payout and change addresses in DLCs + # By default all DLC addresses are generated by the wallet itself + allowExternalDLCAddresses = false + # this config key is read by Slick db { name = walletdb diff --git a/dlc-node-test/src/test/scala/org/bitcoins/dlc/node/DLCNegotiationTest.scala b/dlc-node-test/src/test/scala/org/bitcoins/dlc/node/DLCNegotiationTest.scala index 5e3ee91ed8..241d840fee 100644 --- a/dlc-node-test/src/test/scala/org/bitcoins/dlc/node/DLCNegotiationTest.scala +++ b/dlc-node-test/src/test/scala/org/bitcoins/dlc/node/DLCNegotiationTest.scala @@ -57,8 +57,10 @@ class DLCNegotiationTest extends BitcoinSDualWalletTest { half, Some(SatoshisPerVirtualByte.one), UInt32.zero, - UInt32.one) - accept <- walletA.acceptDLCOffer(offer) + UInt32.one, + None, + None) + accept <- walletA.acceptDLCOffer(offer, None, None) // Send accept message to begin p2p _ = handler ! accept.toMessage diff --git a/dlc-node-test/src/test/scala/org/bitcoins/dlc/node/DLCNodeTest.scala b/dlc-node-test/src/test/scala/org/bitcoins/dlc/node/DLCNodeTest.scala index 4516f0bbd3..60bb1402b7 100644 --- a/dlc-node-test/src/test/scala/org/bitcoins/dlc/node/DLCNodeTest.scala +++ b/dlc-node-test/src/test/scala/org/bitcoins/dlc/node/DLCNodeTest.scala @@ -35,7 +35,9 @@ class DLCNodeTest extends BitcoinSDLCNodeTest { half, Some(SatoshisPerVirtualByte.one), UInt32.zero, - UInt32.one) + UInt32.one, + None, + None) _ <- nodeB.acceptDLCOffer(addrA, offer.toMessage) diff --git a/dlc-node/src/main/scala/org/bitcoins/dlc/node/DLCDataHandler.scala b/dlc-node/src/main/scala/org/bitcoins/dlc/node/DLCDataHandler.scala index 5adb7be492..8e00167bfc 100644 --- a/dlc-node/src/main/scala/org/bitcoins/dlc/node/DLCDataHandler.scala +++ b/dlc-node/src/main/scala/org/bitcoins/dlc/node/DLCDataHandler.scala @@ -47,7 +47,7 @@ class DLCDataHandler(dlcWalletApi: DLCWalletApi, connectionHandler: ActorRef) Future.unit case dlcOffer: DLCOfferTLV => val f = for { - accept <- dlcWalletApi.acceptDLCOffer(dlcOffer) + accept <- dlcWalletApi.acceptDLCOffer(dlcOffer, None, None) _ = connectionHandler ! accept.toMessage } yield () f diff --git a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCExecutionTest.scala b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCExecutionTest.scala index 63462d5dac..a9fbc6967d 100644 --- a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCExecutionTest.scala +++ b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCExecutionTest.scala @@ -362,11 +362,15 @@ class DLCExecutionTest extends BitcoinSDualWalletTest { //helper method to make an offer def makeOffer(): Future[DLCOffer] = { - walletA.createDLCOffer(contractInfoTLV = contractInfo, - collateral = totalCollateral, - feeRateOpt = feeRateOpt, - locktime = UInt32.zero, - refundLT = UInt32.one) + walletA.createDLCOffer( + contractInfoTLV = contractInfo, + collateral = totalCollateral, + feeRateOpt = feeRateOpt, + locktime = UInt32.zero, + refundLT = UInt32.one, + externalPayoutAddressOpt = None, + externalChangeAddressOpt = None + ) } //simply try to make 2 offers with the same contract info @@ -414,7 +418,9 @@ class DLCExecutionTest extends BitcoinSDualWalletTest { status.localCollateral.satoshis, None, UInt32.zero, - UInt32.one) + UInt32.one, + None, + None) _ <- walletA.listDLCs() } yield succeed diff --git a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/MultiWalletDLCTest.scala b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/MultiWalletDLCTest.scala index 5e2ef0fc9b..78d57c62df 100644 --- a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/MultiWalletDLCTest.scala +++ b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/MultiWalletDLCTest.scala @@ -51,7 +51,9 @@ class MultiWalletDLCTest extends BitcoinSWalletTest { half, Some(SatoshisPerVirtualByte.one), UInt32.zero, - UInt32.one) + UInt32.one, + None, + None) dlcsA <- walletA.listDLCs() dlcsB <- walletB.listDLCs() @@ -68,12 +70,15 @@ class MultiWalletDLCTest extends BitcoinSWalletTest { fundedWallet: FundedDLCWallet => //see: https://github.com/bitcoin-s/bitcoin-s/issues/3813#issue-1051117559 val wallet = fundedWallet.wallet - val offerF = wallet.createDLCOffer(contractInfo = sampleContractInfo, - collateral = half, - feeRateOpt = - Some(SatoshisPerVirtualByte.one), - locktime = UInt32.zero, - refundLocktime = UInt32.one) + val offerF = wallet.createDLCOffer( + contractInfo = sampleContractInfo, + collateral = half, + feeRateOpt = Some(SatoshisPerVirtualByte.one), + locktime = UInt32.zero, + refundLocktime = UInt32.one, + externalPayoutAddressOpt = None, + externalChangeAddressOpt = None + ) //now unreserve the utxo val reservedUtxoF = for { diff --git a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/WalletDLCSetupTest.scala b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/WalletDLCSetupTest.scala index 7c2ab63bea..4f7eb94159 100644 --- a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/WalletDLCSetupTest.scala +++ b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/WalletDLCSetupTest.scala @@ -2,6 +2,7 @@ package org.bitcoins.dlc.wallet import org.bitcoins.core.currency._ import org.bitcoins.core.number.{UInt32, UInt64} +import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.protocol.dlc.models.DLCMessage._ import org.bitcoins.core.protocol.dlc.models._ import org.bitcoins.core.protocol.script.P2WPKHWitnessV0 @@ -44,7 +45,9 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { offerData.totalCollateral, Some(offerData.feeRate), offerData.timeouts.contractMaturity.toUInt32, - offerData.timeouts.contractTimeout.toUInt32 + offerData.timeouts.contractTimeout.toUInt32, + None, + None ) dlcId = calcDLCId(offer.fundingInputs.map(_.outPoint)) dlcA1Opt <- walletA.dlcDAO.read(dlcId) @@ -61,7 +64,7 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { assert(offer.changeAddress.value.nonEmpty) } - accept <- walletB.acceptDLCOffer(offer) + accept <- walletB.acceptDLCOffer(offer, None, None) dlcB1Opt <- walletB.dlcDAO.read(dlcId) _ = { assert(dlcB1Opt.isDefined) @@ -185,11 +188,13 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { offerData.totalCollateral, Some(offerData.feeRate), offerData.timeouts.contractMaturity.toUInt32, - offerData.timeouts.contractTimeout.toUInt32 + offerData.timeouts.contractTimeout.toUInt32, + None, + None ) dlcId = calcDLCId(offer.fundingInputs.map(_.outPoint)) - accept <- walletB.acceptDLCOffer(offer) + accept <- walletB.acceptDLCOffer(offer, None, None) // reorder dlc inputs in wallets _ <- reorderInputDbs(walletA, dlcId) @@ -242,11 +247,13 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { offerData.totalCollateral, Some(offerData.feeRate), offerData.timeouts.contractMaturity.toUInt32, - offerData.timeouts.contractTimeout.toUInt32 + offerData.timeouts.contractTimeout.toUInt32, + None, + None ) dlcId = calcDLCId(offer.fundingInputs.map(_.outPoint)) - accept <- walletB.acceptDLCOffer(offer.toTLV) + accept <- walletB.acceptDLCOffer(offer.toTLV, None, None) // reorder dlc inputs in wallets _ <- reorderInputDbs(walletA, dlcId) @@ -271,7 +278,9 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { offerData.totalCollateral, Some(offerData.feeRate), offerData.timeouts.contractMaturity.toUInt32, - offerData.timeouts.contractTimeout.toUInt32 + offerData.timeouts.contractTimeout.toUInt32, + None, + None ) dlcId = calcDLCId(offer.fundingInputs.map(_.outPoint)) dlcA1Opt <- walletA.dlcDAO.read(dlcId) @@ -287,7 +296,7 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { assert(offer.changeAddress.value.nonEmpty) } - accept <- walletB.acceptDLCOffer(offer.toTLV) + accept <- walletB.acceptDLCOffer(offer.toTLV, None, None) dlcB1Opt <- walletB.dlcDAO.read(dlcId) _ = { assert(dlcB1Opt.isDefined) @@ -365,10 +374,12 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { offerData.totalCollateral, Some(offerData.feeRate), offerData.timeouts.contractMaturity.toUInt32, - offerData.timeouts.contractTimeout.toUInt32 + offerData.timeouts.contractTimeout.toUInt32, + None, + None ) - accept <- walletB.acceptDLCOffer(offer) + accept <- walletB.acceptDLCOffer(offer, None, None) } yield accept } @@ -509,7 +520,9 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { offerData.totalCollateral, Some(offerData.feeRate), offerData.timeouts.contractMaturity.toUInt32, - offerData.timeouts.contractTimeout.toUInt32 + offerData.timeouts.contractTimeout.toUInt32, + None, + None ) dlcId = calcDLCId(offer.fundingInputs.map(_.outPoint)) @@ -552,9 +565,11 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { offerData.totalCollateral, Some(offerData.feeRate), offerData.timeouts.contractMaturity.toUInt32, - offerData.timeouts.contractTimeout.toUInt32 + offerData.timeouts.contractTimeout.toUInt32, + None, + None ) - _ <- walletB.acceptDLCOffer(offer) + _ <- walletB.acceptDLCOffer(offer, None, None) dlcId = calcDLCId(offer.fundingInputs.map(_.outPoint)) @@ -593,9 +608,11 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { offerData.totalCollateral, Some(offerData.feeRate), offerData.timeouts.contractMaturity.toUInt32, - offerData.timeouts.contractTimeout.toUInt32 + offerData.timeouts.contractTimeout.toUInt32, + None, + None ) - accept <- walletB.acceptDLCOffer(offer) + accept <- walletB.acceptDLCOffer(offer, None, None) sign <- walletA.signDLC(accept) _ <- walletB.addDLCSigs(sign) @@ -635,9 +652,11 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { offerData.totalCollateral, Some(offerData.feeRate), offerData.timeouts.contractMaturity.toUInt32, - offerData.timeouts.contractTimeout.toUInt32 + offerData.timeouts.contractTimeout.toUInt32, + None, + None ) - accept <- walletB.acceptDLCOffer(offer) + accept <- walletB.acceptDLCOffer(offer, None, None) sign <- walletA.signDLC(accept) _ <- walletB.addDLCSigs(sign) @@ -668,9 +687,11 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { offerData.totalCollateral, Some(offerData.feeRate), offerData.timeouts.contractMaturity.toUInt32, - UInt32.max + UInt32.max, + None, + None ) - accept <- walletB.acceptDLCOffer(offer) + accept <- walletB.acceptDLCOffer(offer, None, None) sign <- walletA.signDLC(accept) _ <- walletB.addDLCSigs(sign) @@ -733,7 +754,9 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { offerData.totalCollateral, Some(offerData.feeRate), offerData.timeouts.contractMaturity.toUInt32, - offerData.timeouts.contractTimeout.toUInt32 + offerData.timeouts.contractTimeout.toUInt32, + None, + None ) _ = { assert(offer.oracleInfos == offerData.oracleInfos) @@ -747,7 +770,7 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { dlcId = calcDLCId(offer.fundingInputs.map(_.outPoint)) - accept <- walletB.acceptDLCOffer(offer) + accept <- walletB.acceptDLCOffer(offer, None, None) _ = { assert(accept.fundingInputs.nonEmpty) assert( @@ -824,19 +847,23 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { val totalCollateral = Satoshis(5000) def makeOffer(contractInfo: ContractInfoV0TLV): Future[DLCOffer] = { - walletA.createDLCOffer(contractInfoTLV = contractInfo, - collateral = totalCollateral, - feeRateOpt = feeRateOpt, - locktime = UInt32.zero, - refundLT = UInt32.one) + walletA.createDLCOffer( + contractInfoTLV = contractInfo, + collateral = totalCollateral, + feeRateOpt = feeRateOpt, + locktime = UInt32.zero, + refundLT = UInt32.one, + externalPayoutAddressOpt = None, + externalChangeAddressOpt = None + ) } for { offerA <- makeOffer(contractInfoA) offerB <- makeOffer(contractInfoB) - _ <- walletB.acceptDLCOffer(offerA) - _ <- walletB.acceptDLCOffer(offerB) + _ <- walletB.acceptDLCOffer(offerA, None, None) + _ <- walletB.acceptDLCOffer(offerB, None, None) } yield succeed } @@ -868,17 +895,21 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { val totalCollateral = Satoshis(100000) def makeOffer(contractInfo: ContractInfoV0TLV): Future[DLCOffer] = { - walletA.createDLCOffer(contractInfoTLV = contractInfo, - collateral = totalCollateral, - feeRateOpt = feeRateOpt, - locktime = UInt32.zero, - refundLT = UInt32.one) + walletA.createDLCOffer( + contractInfoTLV = contractInfo, + collateral = totalCollateral, + feeRateOpt = feeRateOpt, + locktime = UInt32.zero, + refundLT = UInt32.one, + externalPayoutAddressOpt = None, + externalChangeAddressOpt = None + ) } for { offer <- makeOffer(contractInfoA) - accept1F = walletB.acceptDLCOffer(offer) - accept2F = walletB.acceptDLCOffer(offer) + accept1F = walletB.acceptDLCOffer(offer, None, None) + accept2F = walletB.acceptDLCOffer(offer, None, None) _ <- recoverToSucceededIf[DuplicateOfferException]( Future.sequence(Seq(accept1F, accept2F))) } yield { @@ -895,17 +926,21 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { val totalCollateral = Satoshis(100000) def makeOffer(contractInfo: ContractInfoV0TLV): Future[DLCOffer] = { - walletA.createDLCOffer(contractInfoTLV = contractInfo, - collateral = totalCollateral, - feeRateOpt = feeRateOpt, - locktime = UInt32.zero, - refundLT = UInt32.one) + walletA.createDLCOffer( + contractInfoTLV = contractInfo, + collateral = totalCollateral, + feeRateOpt = feeRateOpt, + locktime = UInt32.zero, + refundLT = UInt32.one, + externalPayoutAddressOpt = None, + externalChangeAddressOpt = None + ) } for { offer <- makeOffer(contractInfoA) - accept1 <- walletB.acceptDLCOffer(offer) - accept2 <- walletB.acceptDLCOffer(offer) + accept1 <- walletB.acceptDLCOffer(offer, None, None) + accept2 <- walletB.acceptDLCOffer(offer, None, None) } yield { assert(accept1 == accept2) } @@ -923,9 +958,11 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { offerData.totalCollateral, Some(offerData.feeRate), offerData.timeouts.contractMaturity.toUInt32, - UInt32.max + UInt32.max, + None, + None ) - accept <- walletB.acceptDLCOffer(offer) + accept <- walletB.acceptDLCOffer(offer, None, None) res <- recoverToSucceededIf[IllegalArgumentException]( walletB.signDLC(accept)) } yield res @@ -944,7 +981,9 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { offerData.totalCollateral, Some(offerData.feeRate), offerData.timeouts.contractMaturity.toUInt32, - UInt32.max + UInt32.max, + None, + None )) } yield { res @@ -963,18 +1002,82 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { val totalCollateral = Satoshis(5000) for { - offer <- walletA.createDLCOffer(contractInfoTLV = contractInfo, - collateral = totalCollateral, - feeRateOpt = feeRateOpt, - locktime = UInt32.zero, - refundLT = UInt32.one) + offer <- walletA.createDLCOffer( + contractInfoTLV = contractInfo, + collateral = totalCollateral, + feeRateOpt = feeRateOpt, + locktime = UInt32.zero, + refundLT = UInt32.one, + externalPayoutAddressOpt = None, + externalChangeAddressOpt = None + ) invalidOffer = offer.copy(contractInfo = invalidContractInfo) res <- recoverToSucceededIf[InvalidAnnouncementSignature]( - walletB.acceptDLCOffer(invalidOffer)) + walletB.acceptDLCOffer(invalidOffer, None, None)) } yield { res } } + it must "use external payout and change addresses when they are provided" in { + wallets => + val walletA = wallets._1.wallet + val walletB = wallets._2.wallet + + //https://test.oracle.suredbits.com/contract/enum/75b08299654dca23b80cf359db6afb6cfd6e55bc898b5397d3c0fe796dfc13f0/12fb3e5f091086329ed0d2a12c3fcfa80111a36ef3fc1ac9c2567076a57d6a73 + val contractInfo = ContractInfoV0TLV.fromHex( + "fdd82eeb00000000000186a0fda71026030359455300000000000186a0024e4f0000000000000000056f746865720000000000000000fda712b5fdd824b1596ec40d0dae3fdf54d9795ad51ec069970c6863a02d244663d39fd6bedadc0070349e1ba2e17583ee2d1cb3ae6fffaaa1c45039b61c5c4f1d0d864221c461745d1bcfab252c6dd9edd7aea4c5eeeef138f7ff7346061ea40143a9f5ae80baa9fdd8224d0001fa5b84283852400b21a840d5d5ca1cc31867c37326ad521aa50bebf3df4eea1a60b03280fdd8060f000303594553024e4f056f74686572135465746865722d52657365727665732d363342") + val contractInfo1 = DLCWalletUtil.sampleDLCOffer.contractInfo.toTLV + + val feeRateOpt = Some(SatoshisPerVirtualByte(Satoshis.one)) + val totalCollateral = Satoshis(5000) + val feeRateOpt1 = Some(SatoshisPerVirtualByte(Satoshis(2))) + val totalCollateral1 = Satoshis(10000) + + // random testnet addresses + val payoutAddressAOpt = Some( + BitcoinAddress.fromString("tb1qw98mrsxpqtz25xe332khnvlapvl09ejnzk7c3f")) + val changeAddressAOpt = Some( + BitcoinAddress.fromString("tb1qkfaglsvpcwe5pm9ktqs80u9d9jd0qzgqjqd240")) + val payoutAddressBOpt = + Some(BitcoinAddress.fromString("2MsM67NLa71fHvTUBqNENW15P68nHB2vVXb")) + val changeAddressBOpt = + Some(BitcoinAddress.fromString("2N4YXTxKEso3yeYXNn5h42Vqu3FzTTQ8Lq5")) + + for { + offer <- walletA.createDLCOffer( + contractInfoTLV = contractInfo, + collateral = totalCollateral, + feeRateOpt = feeRateOpt, + locktime = UInt32.zero, + refundLT = UInt32.one, + externalPayoutAddressOpt = payoutAddressAOpt, + externalChangeAddressOpt = changeAddressAOpt + ) + accept <- walletB.acceptDLCOffer(offer, + payoutAddressBOpt, + changeAddressBOpt) + offer1 <- walletA.createDLCOffer( + contractInfoTLV = contractInfo1, + collateral = totalCollateral1, + feeRateOpt = feeRateOpt1, + locktime = UInt32.zero, + refundLT = UInt32.one, + externalPayoutAddressOpt = None, + externalChangeAddressOpt = None + ) + accept1 <- walletB.acceptDLCOffer(offer1, None, None) + } yield { + assert(offer.pubKeys.payoutAddress == payoutAddressAOpt.get) + assert(offer.changeAddress == changeAddressAOpt.get) + assert(accept.pubKeys.payoutAddress == payoutAddressBOpt.get) + assert(accept.changeAddress == changeAddressBOpt.get) + assert(offer1.pubKeys.payoutAddress != payoutAddressAOpt.get) + assert(offer1.changeAddress != changeAddressAOpt.get) + assert(accept1.pubKeys.payoutAddress != payoutAddressBOpt.get) + assert(accept1.changeAddress != changeAddressBOpt.get) + } + } + } diff --git a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWallet.scala b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWallet.scala index f12a2032d9..8e8eb49ffe 100644 --- a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWallet.scala +++ b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWallet.scala @@ -268,8 +268,16 @@ abstract class DLCWallet collateral: Satoshis, feeRateOpt: Option[SatoshisPerVirtualByte], locktime: UInt32, - refundLocktime: UInt32): Future[DLCOffer] = { + refundLocktime: UInt32, + externalPayoutAddressOpt: Option[BitcoinAddress], + externalChangeAddressOpt: Option[BitcoinAddress]): Future[DLCOffer] = { logger.info("Creating DLC Offer") + if ( + !walletConfig.allowExternalDLCAddresses && (externalPayoutAddressOpt.nonEmpty || externalChangeAddressOpt.nonEmpty) + ) { + return Future.failed( + new IllegalArgumentException("External DLC addresses are not allowed")) + } if (!validateAnnouncementSignatures(contractInfo.oracleInfos)) { return Future.failed( InvalidAnnouncementSignature( @@ -279,19 +287,15 @@ abstract class DLCWallet val announcements = contractInfo.oracleInfos.head.singleOracleInfos.map(_.announcement) - //hack for now to get around https://github.com/bitcoin-s/bitcoin-s/issues/3127 - //filter announcements that we already have in the db - val groupedAnnouncementsF: Future[AnnouncementGrouping] = { - groupByExistingAnnouncements(announcements) - } - val feeRateF = determineFeeRate(feeRateOpt).map { fee => SatoshisPerVirtualByte(fee.currencyUnit) } for { feeRate <- feeRateF - groupedAnnouncements <- groupedAnnouncementsF + //hack for now to get around https://github.com/bitcoin-s/bitcoin-s/issues/3127 + //filter announcements that we already have in the db + groupedAnnouncements <- groupByExistingAnnouncements(announcements) announcementDataDbs <- announcementDAO.createAll( groupedAnnouncements.newAnnouncements) allAnnouncementDbs = @@ -341,14 +345,17 @@ abstract class DLCWallet used = false) } - changeSPK = - txBuilder.finalizer.changeSPK - changeAddr = BitcoinAddress.fromScriptPubKey(changeSPK, networkParameters) + changeAddr = externalChangeAddressOpt.getOrElse { + val changeSPK = txBuilder.finalizer.changeSPK + BitcoinAddress.fromScriptPubKey(changeSPK, networkParameters) + } dlcPubKeys = DLCUtil.calcDLCPubKeys(xpub = account.xpub, chainType = chainType, keyIndex = nextIndex, - networkParameters = networkParameters) + networkParameters = networkParameters, + externalPayoutAddressOpt = + externalPayoutAddressOpt) _ = logger.debug( s"DLC Offer data collected, creating database entry, ${dlcId.hex}") @@ -552,8 +559,17 @@ abstract class DLCWallet * * This is the first step of the recipient */ - override def acceptDLCOffer(offer: DLCOffer): Future[DLCAccept] = { + override def acceptDLCOffer( + offer: DLCOffer, + externalPayoutAddressOpt: Option[BitcoinAddress], + externalChangeAddressOpt: Option[BitcoinAddress]): Future[DLCAccept] = { logger.debug("Calculating relevant wallet data for DLC Accept") + if ( + !walletConfig.allowExternalDLCAddresses && (externalPayoutAddressOpt.nonEmpty || externalChangeAddressOpt.nonEmpty) + ) { + return Future.failed( + new IllegalArgumentException("External DLC addresses are not allowed")) + } if (!validateAnnouncementSignatures(offer.oracleInfos)) { return Future.failed(InvalidAnnouncementSignature( s"Offer ${offer.tempContractId.hex} contains invalid announcement signature(s)")) @@ -573,7 +589,11 @@ abstract class DLCWallet dlcAccept <- { dlcAcceptOpt match { case Some(accept) => Future.successful(accept) - case None => createNewDLCAccept(collateral, offer) + case None => + createNewDLCAccept(collateral, + offer, + externalPayoutAddressOpt, + externalChangeAddressOpt) } } status <- findDLC(dlcId) @@ -612,155 +632,161 @@ abstract class DLCWallet private def createNewDLCAccept( collateral: CurrencyUnit, - offer: DLCOffer): Future[DLCAccept] = Future { - DLCWallet.AcceptingOffersLatch.startAccepting(offer.tempContractId) + offer: DLCOffer, + externalPayoutAddressOpt: Option[BitcoinAddress], + externalChangeAddressOpt: Option[BitcoinAddress]): Future[DLCAccept] = + Future { + DLCWallet.AcceptingOffersLatch.startAccepting(offer.tempContractId) - logger.info( - s"Creating DLC Accept for tempContractId ${offer.tempContractId.hex}") + logger.info( + s"Creating DLC Accept for tempContractId ${offer.tempContractId.hex}") - def getFundingPrivKey(account: AccountDb, dlc: DLCDb): AdaptorSign = { - val bip32Path = BIP32Path( - account.hdAccount.path ++ Vector(BIP32Node(0, hardened = false), - BIP32Node(dlc.keyIndex, - hardened = false))) - val privKeyPath = HDPath.fromString(bip32Path.toString) - keyManager.toSign(privKeyPath) - } - - val result = for { - account <- getDefaultAccountForType(AddressType.SegWit) - (dlc, offerDb, contractDataDb) <- initDLCForAccept(offer, account) - (txBuilder, spendingInfos) <- fundDLCAcceptMsg(offer = offer, - collateral = collateral, - account = account) - fundingPrivKey = getFundingPrivKey(account, dlc) - (acceptWithoutSigs, dlcPubKeys) = DLCAcceptUtil.buildAcceptWithoutSigs( - dlc = dlc, - offer = offer, - txBuilder = txBuilder, - spendingInfos = spendingInfos, - account = account, - fundingPrivKey = fundingPrivKey, - collateral = collateral, - networkParameters = networkParameters - ) - builder = DLCTxBuilder(offer, acceptWithoutSigs) - - contractId = builder.calcContractId - - dlcDbWithContractId = dlc.copy(contractIdOpt = Some(contractId)) - - signer = DLCTxSigner(builder = builder, - isInitiator = false, - fundingKey = fundingPrivKey, - finalAddress = dlcPubKeys.payoutAddress, - fundingUtxos = spendingInfos) - - spkDb = ScriptPubKeyDb(builder.fundingSPK) - // only update spk db if we don't have it - _ <- scriptPubKeyDAO.createIfNotExists(spkDb) - - _ = logger.info(s"Creating CET Sigs for ${contractId.toHex}") - //emit websocket event that we are now computing adaptor signatures - status = DLCStatusBuilder.buildInProgressDLCStatus( - dlcDb = dlcDbWithContractId, - contractInfo = offer.contractInfo, - contractData = contractDataDb, - offerDb = offerDb) - _ = dlcConfig.walletCallbacks.executeOnDLCStateChange(logger, status) - cetSigs <- signer.createCETSigsAsync() - refundSig = signer.signRefundTx - _ = logger.debug( - s"DLC Accept data collected, creating database entry, ${dlc.dlcId.hex}") - - dlcAcceptDb = DLCAcceptDb( - dlcId = dlc.dlcId, - fundingKey = dlcPubKeys.fundingKey, - payoutAddress = dlcPubKeys.payoutAddress, - payoutSerialId = acceptWithoutSigs.payoutSerialId, - collateral = collateral, - changeAddress = acceptWithoutSigs.changeAddress, - changeSerialId = acceptWithoutSigs.changeSerialId, - negotiationFieldsTLV = NoNegotiationFields.toTLV - ) - - sigsDbs = cetSigs.outcomeSigs.zipWithIndex.map { case (sig, index) => - DLCCETSignaturesDb(dlc.dlcId, index = index, sig._1, sig._2, None) + def getFundingPrivKey(account: AccountDb, dlc: DLCDb): AdaptorSign = { + val bip32Path = BIP32Path( + account.hdAccount.path ++ Vector(BIP32Node(0, hardened = false), + BIP32Node(dlc.keyIndex, + hardened = false))) + val privKeyPath = HDPath.fromString(bip32Path.toString) + keyManager.toSign(privKeyPath) } - refundSigsDb = - DLCRefundSigsDb(dlc.dlcId, refundSig, None) + val result = for { + account <- getDefaultAccountForType(AddressType.SegWit) + (dlc, offerDb, contractDataDb) <- initDLCForAccept(offer, account) + (txBuilder, spendingInfos) <- fundDLCAcceptMsg(offer = offer, + collateral = collateral, + account = account) + fundingPrivKey = getFundingPrivKey(account, dlc) + (acceptWithoutSigs, dlcPubKeys) = DLCAcceptUtil.buildAcceptWithoutSigs( + dlc = dlc, + offer = offer, + txBuilder = txBuilder, + spendingInfos = spendingInfos, + account = account, + fundingPrivKey = fundingPrivKey, + collateral = collateral, + networkParameters = networkParameters, + externalPayoutAddressOpt = externalPayoutAddressOpt, + externalChangeAddressOpt = externalChangeAddressOpt + ) + builder = DLCTxBuilder(offer, acceptWithoutSigs) - offerInputs = offer.fundingInputs.zipWithIndex.map { - case (funding, idx) => - DLCFundingInputDb( - dlcId = dlc.dlcId, - isInitiator = true, - index = idx, - inputSerialId = funding.inputSerialId, - outPoint = funding.outPoint, - output = funding.output, - nSequence = funding.sequence, - maxWitnessLength = funding.maxWitnessLen.toLong, - redeemScriptOpt = funding.redeemScriptOpt, - witnessScriptOpt = None - ) - } + contractId = builder.calcContractId - offerPrevTxs = offer.fundingInputs.map(funding => - TransactionDbHelper.fromTransaction(funding.prevTx, - blockHashOpt = None)) + dlcDbWithContractId = dlc.copy(contractIdOpt = Some(contractId)) - acceptInputs = spendingInfos - .zip(acceptWithoutSigs.fundingInputs) - .zipWithIndex - .map { case ((utxo, fundingInput), idx) => - DLCFundingInputDb( - dlcId = dlc.dlcId, - isInitiator = false, - index = idx, - inputSerialId = fundingInput.inputSerialId, - outPoint = utxo.outPoint, - output = utxo.output, - nSequence = fundingInput.sequence, - maxWitnessLength = fundingInput.maxWitnessLen.toLong, - redeemScriptOpt = InputInfo.getRedeemScript(utxo.inputInfo), - witnessScriptOpt = InputInfo.getScriptWitness(utxo.inputInfo) - ) + signer = DLCTxSigner(builder = builder, + isInitiator = false, + fundingKey = fundingPrivKey, + finalAddress = dlcPubKeys.payoutAddress, + fundingUtxos = spendingInfos) + + spkDb = ScriptPubKeyDb(builder.fundingSPK) + // only update spk db if we don't have it + _ <- scriptPubKeyDAO.createIfNotExists(spkDb) + + _ = logger.info(s"Creating CET Sigs for ${contractId.toHex}") + //emit websocket event that we are now computing adaptor signatures + status = DLCStatusBuilder.buildInProgressDLCStatus( + dlcDb = dlcDbWithContractId, + contractInfo = offer.contractInfo, + contractData = contractDataDb, + offerDb = offerDb) + _ = dlcConfig.walletCallbacks.executeOnDLCStateChange(logger, status) + cetSigs <- signer.createCETSigsAsync() + refundSig = signer.signRefundTx + _ = logger.debug( + s"DLC Accept data collected, creating database entry, ${dlc.dlcId.hex}") + + dlcAcceptDb = DLCAcceptDb( + dlcId = dlc.dlcId, + fundingKey = dlcPubKeys.fundingKey, + payoutAddress = dlcPubKeys.payoutAddress, + payoutSerialId = acceptWithoutSigs.payoutSerialId, + collateral = collateral, + changeAddress = acceptWithoutSigs.changeAddress, + changeSerialId = acceptWithoutSigs.changeSerialId, + negotiationFieldsTLV = NoNegotiationFields.toTLV + ) + + sigsDbs = cetSigs.outcomeSigs.zipWithIndex.map { case (sig, index) => + DLCCETSignaturesDb(dlc.dlcId, index = index, sig._1, sig._2, None) } - accept = - dlcAcceptDb.toDLCAccept(tempContractId = dlc.tempContractId, - fundingInputs = acceptWithoutSigs.fundingInputs, - outcomeSigs = cetSigs.outcomeSigs, - refundSig = refundSig) + refundSigsDb = + DLCRefundSigsDb(dlc.dlcId, refundSig, None) - _ = require(accept.tempContractId == offer.tempContractId, - "Offer and Accept have differing tempContractIds!") + offerInputs = offer.fundingInputs.zipWithIndex.map { + case (funding, idx) => + DLCFundingInputDb( + dlcId = dlc.dlcId, + isInitiator = true, + index = idx, + inputSerialId = funding.inputSerialId, + outPoint = funding.outPoint, + output = funding.output, + nSequence = funding.sequence, + maxWitnessLength = funding.maxWitnessLen.toLong, + redeemScriptOpt = funding.redeemScriptOpt, + witnessScriptOpt = None + ) + } - _ <- remoteTxDAO.upsertAll(offerPrevTxs) - actions = actionBuilder.buildCreateAcceptAction( - dlcDb = dlcDbWithContractId.updateState(DLCState.Accepted), - dlcAcceptDb = dlcAcceptDb, - offerInputs = offerInputs, - acceptInputs = acceptInputs, - cetSigsDb = sigsDbs, - refundSigsDb = refundSigsDb - ) - _ <- safeDatabase.run(actions) - dlcDb <- updateDLCContractIds(offer, accept) - _ = logger.info( - s"Created DLCAccept for tempContractId ${offer.tempContractId.hex} with contract Id ${contractId.toHex}") + offerPrevTxs = offer.fundingInputs.map(funding => + TransactionDbHelper.fromTransaction(funding.prevTx, + blockHashOpt = None)) - fundingTx = builder.buildFundingTx - outPoint = TransactionOutPoint(fundingTx.txId, - UInt32(builder.fundOutputIndex)) - _ <- updateFundingOutPoint(dlcDb.contractIdOpt.get, outPoint) - } yield accept - result.onComplete(_ => - DLCWallet.AcceptingOffersLatch.doneAccepting(offer.tempContractId)) - result - }.flatten + acceptInputs = spendingInfos + .zip(acceptWithoutSigs.fundingInputs) + .zipWithIndex + .map { case ((utxo, fundingInput), idx) => + DLCFundingInputDb( + dlcId = dlc.dlcId, + isInitiator = false, + index = idx, + inputSerialId = fundingInput.inputSerialId, + outPoint = utxo.outPoint, + output = utxo.output, + nSequence = fundingInput.sequence, + maxWitnessLength = fundingInput.maxWitnessLen.toLong, + redeemScriptOpt = InputInfo.getRedeemScript(utxo.inputInfo), + witnessScriptOpt = InputInfo.getScriptWitness(utxo.inputInfo) + ) + } + + accept = + dlcAcceptDb.toDLCAccept(tempContractId = dlc.tempContractId, + fundingInputs = + acceptWithoutSigs.fundingInputs, + outcomeSigs = cetSigs.outcomeSigs, + refundSig = refundSig) + + _ = require(accept.tempContractId == offer.tempContractId, + "Offer and Accept have differing tempContractIds!") + + _ <- remoteTxDAO.upsertAll(offerPrevTxs) + actions = actionBuilder.buildCreateAcceptAction( + dlcDb = dlcDbWithContractId.updateState(DLCState.Accepted), + dlcAcceptDb = dlcAcceptDb, + offerInputs = offerInputs, + acceptInputs = acceptInputs, + cetSigsDb = sigsDbs, + refundSigsDb = refundSigsDb + ) + _ <- safeDatabase.run(actions) + dlcDb <- updateDLCContractIds(offer, accept) + _ = logger.info( + s"Created DLCAccept for tempContractId ${offer.tempContractId.hex} with contract Id ${contractId.toHex}") + + fundingTx = builder.buildFundingTx + outPoint = TransactionOutPoint(fundingTx.txId, + UInt32(builder.fundOutputIndex)) + _ <- updateFundingOutPoint(dlcDb.contractIdOpt.get, outPoint) + } yield accept + result.onComplete(_ => + DLCWallet.AcceptingOffersLatch.doneAccepting(offer.tempContractId)) + result + }.flatten def registerDLCAccept( accept: DLCAccept): Future[(DLCDb, Vector[DLCCETSignaturesDb])] = { diff --git a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/util/DLCAcceptUtil.scala b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/util/DLCAcceptUtil.scala index 8f2511ccf8..60da5fa068 100644 --- a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/util/DLCAcceptUtil.scala +++ b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/util/DLCAcceptUtil.scala @@ -38,7 +38,9 @@ object DLCAcceptUtil extends Logging { account: AccountDb, fundingPrivKey: AdaptorSign, collateral: CurrencyUnit, - networkParameters: NetworkParameters): ( + networkParameters: NetworkParameters, + externalPayoutAddressOpt: Option[BitcoinAddress], + externalChangeAddressOpt: Option[BitcoinAddress]): ( DLCAcceptWithoutSigs, DLCPublicKeys) = { val serialIds = DLCMessage.genSerialIds( @@ -49,15 +51,18 @@ object DLCAcceptUtil extends Logging { .fromInputSigningInfo(utxo, id, TransactionConstants.enableRBFSequence) } - val changeSPK = txBuilder.finalizer.changeSPK - val changeAddr = + val changeAddr = externalChangeAddressOpt.getOrElse { + val changeSPK = txBuilder.finalizer.changeSPK BitcoinAddress.fromScriptPubKey(changeSPK, networkParameters) + } - val dlcPubKeys = DLCUtil.calcDLCPubKeys(xpub = account.xpub, - chainType = dlc.changeIndex, - keyIndex = dlc.keyIndex, - networkParameters = - networkParameters) + val dlcPubKeys = DLCUtil.calcDLCPubKeys( + xpub = account.xpub, + chainType = dlc.changeIndex, + keyIndex = dlc.keyIndex, + networkParameters = networkParameters, + externalPayoutAddressOpt = externalPayoutAddressOpt + ) require(dlcPubKeys.fundingKey == fundingPrivKey.publicKey, "Did not derive the same funding private and public key") diff --git a/docs/config/configuration.md b/docs/config/configuration.md index 9d80e8f0c3..88f363b892 100644 --- a/docs/config/configuration.md +++ b/docs/config/configuration.md @@ -242,6 +242,10 @@ bitcoin-s { # before we timeout addressQueueTimeout = 5 seconds + # Allow external payout and change addresses in DLCs + # By default all DLC addresses are generated by the wallet itself + allowExternalDLCAddresses = false + # How often the wallet will rebroadcast unconfirmed transactions rebroadcastFrequency = 4 hours diff --git a/testkit/src/main/scala/org/bitcoins/testkit/BitcoinSTestAppConfig.scala b/testkit/src/main/scala/org/bitcoins/testkit/BitcoinSTestAppConfig.scala index 0b34d68689..5e3dcd5f23 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/BitcoinSTestAppConfig.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/BitcoinSTestAppConfig.scala @@ -46,7 +46,9 @@ object BitcoinSTestAppConfig { | node { | mode = spv | } - | + | wallet { + | allowExternalDLCAddresses = true + | } | proxy.enabled = $torEnabled | tor.enabled = $torEnabled | tor.use-random-ports = false diff --git a/testkit/src/main/scala/org/bitcoins/testkit/wallet/DLCWalletUtil.scala b/testkit/src/main/scala/org/bitcoins/testkit/wallet/DLCWalletUtil.scala index 9c358fc0ec..53295ce854 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/wallet/DLCWalletUtil.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/wallet/DLCWalletUtil.scala @@ -274,7 +274,12 @@ object DLCWalletUtil extends Logging { def initDLC( fundedWalletA: FundedDLCWallet, fundedWalletB: FundedDLCWallet, - contractInfo: ContractInfo)(implicit ec: ExecutionContext): Future[ + contractInfo: ContractInfo, + payoutAddressAOpt: Option[BitcoinAddress] = None, + changeAddressAOpt: Option[BitcoinAddress] = None, + payoutAddressBOpt: Option[BitcoinAddress] = None, + changeAddressBOpt: Option[BitcoinAddress] = None)(implicit + ec: ExecutionContext): Future[ (InitializedDLCWallet, InitializedDLCWallet)] = { val walletA = fundedWalletA.wallet val walletB = fundedWalletB.wallet @@ -285,9 +290,13 @@ object DLCWalletUtil extends Logging { collateral = half, feeRateOpt = Some(SatoshisPerVirtualByte.fromLong(10)), locktime = dummyTimeouts.contractMaturity.toUInt32, - refundLocktime = dummyTimeouts.contractTimeout.toUInt32 + refundLocktime = dummyTimeouts.contractTimeout.toUInt32, + externalPayoutAddressOpt = payoutAddressAOpt, + externalChangeAddressOpt = changeAddressAOpt ) - accept <- walletB.acceptDLCOffer(offer) + accept <- walletB.acceptDLCOffer(offer, + payoutAddressBOpt, + changeAddressBOpt) sigs <- walletA.signDLC(accept) _ <- walletB.addDLCSigs(sigs) tx <- walletB.broadcastDLCFundingTx(sigs.contractId) diff --git a/wallet/src/main/scala/org/bitcoins/wallet/config/WalletAppConfig.scala b/wallet/src/main/scala/org/bitcoins/wallet/config/WalletAppConfig.scala index 5fbc124985..438c7dd479 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/config/WalletAppConfig.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/config/WalletAppConfig.scala @@ -145,6 +145,9 @@ case class WalletAppConfig(baseDatadir: Path, configOverrides: Vector[Config])( lazy val feeProviderTargetOpt: Option[Int] = config.getIntOpt("bitcoin-s.fee-provider.target") + lazy val allowExternalDLCAddresses: Boolean = + config.getBoolean("bitcoin-s.wallet.allowExternalDLCAddresses") + lazy val bip39PasswordOpt: Option[String] = kmConf.bip39PasswordOpt lazy val aesPasswordOpt: Option[AesPassword] = kmConf.aesPasswordOpt