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 b72abd95f0..7c2ab63bea 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 @@ -9,7 +9,10 @@ import org.bitcoins.core.protocol.tlv._ import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte import org.bitcoins.core.wallet.utxo.TxoState import org.bitcoins.crypto._ -import org.bitcoins.dlc.wallet.DLCWallet.InvalidAnnouncementSignature +import org.bitcoins.dlc.wallet.DLCWallet.{ + DuplicateOfferException, + InvalidAnnouncementSignature +} import org.bitcoins.dlc.wallet.internal.DLCDataManagement import org.bitcoins.testkit.wallet.DLCWalletUtil._ import org.bitcoins.testkit.wallet.FundWalletUtil.FundedDLCWallet @@ -837,6 +840,77 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { } yield succeed } + //https://test.oracle.suredbits.com/contract/numeric/022428196c6a60673d1f2b90e7be14ac615986dce5571246e8bcc9d2d689d57b + private val numericContractInfo = ContractInfoV0TLV.fromHex( + """fdd82efd033f00000000000186a0fda720540012fda72648000501000000000000000000000001fd88b80000000000000000000001 + |fdafc8000000000000c350000001fdd6d800000000000186a0000001fe0003ffff00000000000186a00000fda724020000fda712fd + |02d9fdd824fd02d391177fd623a72d56e7bc12e3903f8d6bce7f07a25226d54009cd7e670f5e7a7320b0704286580d8b6a7f31ab7b + |f71356a13c28aa609a021111b2e3d2b2db26bc120bd29248895b81f76b07d85a21b86021f22352d6376d19bbf5c93f918828f1fdd8 + |22fd026d0012fad0cde50a2258efa25cbba60ef0b6677cd29654802937c112097edb64bd205beea02263d6461e60a9ca8e08209c8b + |d5552863156014d5418cad91ac590bbf13a847f105db9899d560e5040f9565c043c9e7fdf3782ad2708c5b99646c722b4118547472 + |48fb52e6486cce3eca5ddf9d64ecbe0864501a446efd378863f9a4055fab50d2112320ff14d8747a72467589822103f197063b49e7 + |7b90d82d3a8d49b63c3ceb9bd3328398a53989d4237216a24a1d12364efa2d2aec59cdc87909b115dca5b07106b70032ff78072f82 + |ceeaf2e20db55086e9a2e5e5cac864992d747fd40f4b26bc3d7de958ee02460d1199ff81438c9b76b3934cbc4566d10f242563b95e + |7df79c28d52c9c46b617676a4ee84a549ee1f0f53865c9ef4d0ff825e2f438384c5f6238d0734beb570a1a49d035d9f86ee31c23f1 + |e97bd34fba3f586c0fdf29997530e528b3200a0d7e34f865dc4ca7bfd722bf77c0478ddd25bfa2dc6a4ab973d0c1b7a9ff38283b7c + |19bbe02677a9b628f3ee3d5198d66c1623c9608293093c126d4124a445bb6412f720493b6ffa411db53923895fd50c9563d50a97a8 + |6188084fe4c0f15ce50438ff0b00e1a9470185fd7c96296ae2be12056f61ceaeee7ced48314a3b6855bc9aa3b446b9dfad68553f53 + |02c60670a95fb9bdc5e72db7f1e9d42e4f4baca1bcbb22612db6417b45cc3d78b1ef33fc362a68db56df00ab1ee0700bd900200f6a + |24882101e71de7e18a7fb0d7da27b340de52f97d96239f359cfe31afcaf69cc9ddfcbfbdb2267e673ad728a29dd22d31d1a1187162 + |037480fdd80a100002000642544355534400000000001212446572696269742d4254432d394645423232""".stripMargin) + + it must "fail accepting an offer twice simultaneously" in { wallets => + val walletA = wallets._1.wallet + val walletB = wallets._2.wallet + + val contractInfoA = numericContractInfo + val feeRateOpt = Some(SatoshisPerVirtualByte(Satoshis.one)) + val totalCollateral = Satoshis(100000) + + def makeOffer(contractInfo: ContractInfoV0TLV): Future[DLCOffer] = { + walletA.createDLCOffer(contractInfoTLV = contractInfo, + collateral = totalCollateral, + feeRateOpt = feeRateOpt, + locktime = UInt32.zero, + refundLT = UInt32.one) + } + + for { + offer <- makeOffer(contractInfoA) + accept1F = walletB.acceptDLCOffer(offer) + accept2F = walletB.acceptDLCOffer(offer) + _ <- recoverToSucceededIf[DuplicateOfferException]( + Future.sequence(Seq(accept1F, accept2F))) + } yield { + succeed + } + } + + it must "accept an offer twice sequentially" in { wallets => + val walletA = wallets._1.wallet + val walletB = wallets._2.wallet + + val contractInfoA = numericContractInfo + val feeRateOpt = Some(SatoshisPerVirtualByte(Satoshis.one)) + val totalCollateral = Satoshis(100000) + + def makeOffer(contractInfo: ContractInfoV0TLV): Future[DLCOffer] = { + walletA.createDLCOffer(contractInfoTLV = contractInfo, + collateral = totalCollateral, + feeRateOpt = feeRateOpt, + locktime = UInt32.zero, + refundLT = UInt32.one) + } + + for { + offer <- makeOffer(contractInfoA) + accept1 <- walletB.acceptDLCOffer(offer) + accept2 <- walletB.acceptDLCOffer(offer) + } yield { + assert(accept1 == accept2) + } + } + it must "not be able to sign its own accept" in { wallets => val walletA = wallets._1.wallet val walletB = wallets._2.wallet 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 1bc8d54eae..f12a2032d9 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 @@ -480,11 +480,6 @@ abstract class DLCWallet val chainType = HDChainType.External - //filter announcements that we already have in the db - val groupedAnnouncementsF: Future[AnnouncementGrouping] = { - groupByExistingAnnouncements(announcements) - } - getDlcDbOfferDbContractDataDb(offer.tempContractId).flatMap { case Some((dlcDb, dlcOffer, contractDataDb)) => Future.successful((dlcDb, dlcOffer, contractDataDb)) @@ -502,7 +497,7 @@ abstract class DLCWallet dlcId, offer) _ <- writeDLCKeysToAddressDb(account, chainType, nextIndex) - groupedAnnouncements <- groupedAnnouncementsF + groupedAnnouncements <- groupByExistingAnnouncements(announcements) dlcDbAction = dlcDAO.createAction(dlc) dlcOfferAction = dlcOfferDAO.createAction(dlcOfferDb) contractAction = contractDataDAO.createAction(contractDataDb) @@ -617,38 +612,28 @@ abstract class DLCWallet private def createNewDLCAccept( collateral: CurrencyUnit, - offer: DLCOffer): Future[DLCAccept] = { + offer: DLCOffer): Future[DLCAccept] = Future { + DLCWallet.AcceptingOffersLatch.startAccepting(offer.tempContractId) + logger.info( s"Creating DLC Accept for tempContractId ${offer.tempContractId.hex}") - val accountF = getDefaultAccountForType(AddressType.SegWit) - - val dlcDbOfferDbF: Future[(DLCDb, DLCOfferDb, DLCContractDataDb)] = { - for { - accountDb <- accountF - (dlcDb, offerDb, contractDataDb) <- initDLCForAccept(offer, accountDb) - } yield (dlcDb, offerDb, contractDataDb) + 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 fundingPrivKeyF: Future[AdaptorSign] = { - for { - (dlc, _, _) <- dlcDbOfferDbF - account <- accountF - bip32Path = BIP32Path( - account.hdAccount.path ++ Vector(BIP32Node(0, hardened = false), - BIP32Node(dlc.keyIndex, - hardened = false))) - privKeyPath = HDPath.fromString(bip32Path.toString) - } yield keyManager.toSign(privKeyPath) - } - - for { - (dlc, offerDb, contractDataDb) <- dlcDbOfferDbF - account <- accountF + val result = for { + account <- getDefaultAccountForType(AddressType.SegWit) + (dlc, offerDb, contractDataDb) <- initDLCForAccept(offer, account) (txBuilder, spendingInfos) <- fundDLCAcceptMsg(offer = offer, collateral = collateral, account = account) - fundingPrivKey <- fundingPrivKeyF + fundingPrivKey = getFundingPrivKey(account, dlc) (acceptWithoutSigs, dlcPubKeys) = DLCAcceptUtil.buildAcceptWithoutSigs( dlc = dlc, offer = offer, @@ -673,11 +658,8 @@ abstract class DLCWallet spkDb = ScriptPubKeyDb(builder.fundingSPK) // only update spk db if we don't have it - spkDbOpt <- scriptPubKeyDAO.findScriptPubKey(spkDb.scriptPubKey) - _ <- spkDbOpt match { - case Some(_) => Future.unit - case None => scriptPubKeyDAO.create(spkDb) - } + _ <- 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( @@ -775,7 +757,10 @@ abstract class DLCWallet 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])] = { @@ -1743,6 +1728,9 @@ abstract class DLCWallet object DLCWallet extends WalletLogger { + case class DuplicateOfferException(message: String) + extends RuntimeException(message) + case class InvalidAnnouncementSignature(message: String) extends RuntimeException(message) @@ -1765,4 +1753,22 @@ object DLCWallet extends WalletLogger { ec: ExecutionContext): DLCWallet = { DLCWalletImpl(nodeApi, chainQueryApi, feeRateApi) } + + private object AcceptingOffersLatch { + + private val tempContractIds = + new java.util.concurrent.ConcurrentHashMap[Sha256Digest, Sha256Digest]() + + def startAccepting(tempContractId: Sha256Digest): Unit = { + if (tempContractIds.putIfAbsent(tempContractId, tempContractId) != null) { + throw DuplicateOfferException( + s"Offer with temporary contract ID ${tempContractId.hex} is already being accepted") + } + } + + def doneAccepting(tempContractId: Sha256Digest): Unit = { + val _ = tempContractIds.remove(tempContractId) + } + + } }