Prevent DB state corruption while accepting the same offer multiple times (#4067)

* Prevent db state corruption while accepting the same offer multiple times

* cleanup
This commit is contained in:
rorp 2022-02-11 10:23:17 -08:00 committed by GitHub
parent 2166faf61d
commit 6cfbf67812
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 117 additions and 37 deletions

View File

@ -9,7 +9,10 @@ import org.bitcoins.core.protocol.tlv._
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo.TxoState import org.bitcoins.core.wallet.utxo.TxoState
import org.bitcoins.crypto._ 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.dlc.wallet.internal.DLCDataManagement
import org.bitcoins.testkit.wallet.DLCWalletUtil._ import org.bitcoins.testkit.wallet.DLCWalletUtil._
import org.bitcoins.testkit.wallet.FundWalletUtil.FundedDLCWallet import org.bitcoins.testkit.wallet.FundWalletUtil.FundedDLCWallet
@ -837,6 +840,77 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
} yield succeed } 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 => it must "not be able to sign its own accept" in { wallets =>
val walletA = wallets._1.wallet val walletA = wallets._1.wallet
val walletB = wallets._2.wallet val walletB = wallets._2.wallet

View File

@ -480,11 +480,6 @@ abstract class DLCWallet
val chainType = HDChainType.External val chainType = HDChainType.External
//filter announcements that we already have in the db
val groupedAnnouncementsF: Future[AnnouncementGrouping] = {
groupByExistingAnnouncements(announcements)
}
getDlcDbOfferDbContractDataDb(offer.tempContractId).flatMap { getDlcDbOfferDbContractDataDb(offer.tempContractId).flatMap {
case Some((dlcDb, dlcOffer, contractDataDb)) => case Some((dlcDb, dlcOffer, contractDataDb)) =>
Future.successful((dlcDb, dlcOffer, contractDataDb)) Future.successful((dlcDb, dlcOffer, contractDataDb))
@ -502,7 +497,7 @@ abstract class DLCWallet
dlcId, dlcId,
offer) offer)
_ <- writeDLCKeysToAddressDb(account, chainType, nextIndex) _ <- writeDLCKeysToAddressDb(account, chainType, nextIndex)
groupedAnnouncements <- groupedAnnouncementsF groupedAnnouncements <- groupByExistingAnnouncements(announcements)
dlcDbAction = dlcDAO.createAction(dlc) dlcDbAction = dlcDAO.createAction(dlc)
dlcOfferAction = dlcOfferDAO.createAction(dlcOfferDb) dlcOfferAction = dlcOfferDAO.createAction(dlcOfferDb)
contractAction = contractDataDAO.createAction(contractDataDb) contractAction = contractDataDAO.createAction(contractDataDb)
@ -617,38 +612,28 @@ abstract class DLCWallet
private def createNewDLCAccept( private def createNewDLCAccept(
collateral: CurrencyUnit, collateral: CurrencyUnit,
offer: DLCOffer): Future[DLCAccept] = { offer: DLCOffer): Future[DLCAccept] = Future {
DLCWallet.AcceptingOffersLatch.startAccepting(offer.tempContractId)
logger.info( logger.info(
s"Creating DLC Accept for tempContractId ${offer.tempContractId.hex}") s"Creating DLC Accept for tempContractId ${offer.tempContractId.hex}")
val accountF = getDefaultAccountForType(AddressType.SegWit) def getFundingPrivKey(account: AccountDb, dlc: DLCDb): AdaptorSign = {
val bip32Path = BIP32Path(
val dlcDbOfferDbF: Future[(DLCDb, DLCOfferDb, DLCContractDataDb)] = { account.hdAccount.path ++ Vector(BIP32Node(0, hardened = false),
for { BIP32Node(dlc.keyIndex,
accountDb <- accountF hardened = false)))
(dlcDb, offerDb, contractDataDb) <- initDLCForAccept(offer, accountDb) val privKeyPath = HDPath.fromString(bip32Path.toString)
} yield (dlcDb, offerDb, contractDataDb) keyManager.toSign(privKeyPath)
} }
val fundingPrivKeyF: Future[AdaptorSign] = { val result = for {
for { account <- getDefaultAccountForType(AddressType.SegWit)
(dlc, _, _) <- dlcDbOfferDbF (dlc, offerDb, contractDataDb) <- initDLCForAccept(offer, account)
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
(txBuilder, spendingInfos) <- fundDLCAcceptMsg(offer = offer, (txBuilder, spendingInfos) <- fundDLCAcceptMsg(offer = offer,
collateral = collateral, collateral = collateral,
account = account) account = account)
fundingPrivKey <- fundingPrivKeyF fundingPrivKey = getFundingPrivKey(account, dlc)
(acceptWithoutSigs, dlcPubKeys) = DLCAcceptUtil.buildAcceptWithoutSigs( (acceptWithoutSigs, dlcPubKeys) = DLCAcceptUtil.buildAcceptWithoutSigs(
dlc = dlc, dlc = dlc,
offer = offer, offer = offer,
@ -673,11 +658,8 @@ abstract class DLCWallet
spkDb = ScriptPubKeyDb(builder.fundingSPK) spkDb = ScriptPubKeyDb(builder.fundingSPK)
// only update spk db if we don't have it // only update spk db if we don't have it
spkDbOpt <- scriptPubKeyDAO.findScriptPubKey(spkDb.scriptPubKey) _ <- scriptPubKeyDAO.createIfNotExists(spkDb)
_ <- spkDbOpt match {
case Some(_) => Future.unit
case None => scriptPubKeyDAO.create(spkDb)
}
_ = logger.info(s"Creating CET Sigs for ${contractId.toHex}") _ = logger.info(s"Creating CET Sigs for ${contractId.toHex}")
//emit websocket event that we are now computing adaptor signatures //emit websocket event that we are now computing adaptor signatures
status = DLCStatusBuilder.buildInProgressDLCStatus( status = DLCStatusBuilder.buildInProgressDLCStatus(
@ -775,7 +757,10 @@ abstract class DLCWallet
UInt32(builder.fundOutputIndex)) UInt32(builder.fundOutputIndex))
_ <- updateFundingOutPoint(dlcDb.contractIdOpt.get, outPoint) _ <- updateFundingOutPoint(dlcDb.contractIdOpt.get, outPoint)
} yield accept } yield accept
} result.onComplete(_ =>
DLCWallet.AcceptingOffersLatch.doneAccepting(offer.tempContractId))
result
}.flatten
def registerDLCAccept( def registerDLCAccept(
accept: DLCAccept): Future[(DLCDb, Vector[DLCCETSignaturesDb])] = { accept: DLCAccept): Future[(DLCDb, Vector[DLCCETSignaturesDb])] = {
@ -1743,6 +1728,9 @@ abstract class DLCWallet
object DLCWallet extends WalletLogger { object DLCWallet extends WalletLogger {
case class DuplicateOfferException(message: String)
extends RuntimeException(message)
case class InvalidAnnouncementSignature(message: String) case class InvalidAnnouncementSignature(message: String)
extends RuntimeException(message) extends RuntimeException(message)
@ -1765,4 +1753,22 @@ object DLCWallet extends WalletLogger {
ec: ExecutionContext): DLCWallet = { ec: ExecutionContext): DLCWallet = {
DLCWalletImpl(nodeApi, chainQueryApi, feeRateApi) 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)
}
}
} }