diff --git a/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/ContractInfo.scala b/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/ContractInfo.scala index cb6a0deaa4..8581d11bc4 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/ContractInfo.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/ContractInfo.scala @@ -370,6 +370,8 @@ object SingleContractInfo case class DisjointUnionContractInfo(contracts: Vector[SingleContractInfo]) extends ContractInfo with TLVSerializable[ContractInfoV1TLV] { + require(contracts.nonEmpty, + s"Cannot have empty contract oracle pairs for ContractInfoV1TLV") override val totalCollateral: Satoshis = contracts.head.totalCollateral diff --git a/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/DLCMessage.scala b/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/DLCMessage.scala index 74052c8f95..0a2fa58029 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/DLCMessage.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/DLCMessage.scala @@ -90,6 +90,8 @@ object DLCMessage { timeouts: DLCTimeouts) extends DLCSetupMessage { + require(fundingInputs.nonEmpty, s"DLCOffer fundingINnputs cannot be empty") + require( fundingInputs.map(_.inputSerialId).distinct.size == fundingInputs.size, "All funding input serial ids must be unique") diff --git a/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/DLCSignatures.scala b/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/DLCSignatures.scala index 509aacfcc3..b383b57832 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/DLCSignatures.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/DLCSignatures.scala @@ -14,6 +14,8 @@ case class FundingSignatures( extends SeqWrapper[(TransactionOutPoint, ScriptWitnessV0)] with DLCSignatures { + require(sigs.nonEmpty, s"FundingSignatures.sigs cannot be empty") + override protected def wrapped: Vector[ (TransactionOutPoint, ScriptWitnessV0)] = sigs @@ -38,6 +40,9 @@ case class CETSignatures( outcomeSigs: Vector[(ECPublicKey, ECAdaptorSignature)], refundSig: PartialSignature) extends DLCSignatures { + + require(outcomeSigs.nonEmpty, + s"CETSignatures cannot have outcomeSigs be empty") lazy val keys: Vector[ECPublicKey] = outcomeSigs.map(_._1) lazy val adaptorSigs: Vector[ECAdaptorSignature] = outcomeSigs.map(_._2) diff --git a/core/src/main/scala/org/bitcoins/core/protocol/tlv/TLV.scala b/core/src/main/scala/org/bitcoins/core/protocol/tlv/TLV.scala index e564dd6c74..2bc2981a9c 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/tlv/TLV.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/tlv/TLV.scala @@ -1493,6 +1493,7 @@ case class ContractInfoV1TLV( totalCollateral: Satoshis, contractOraclePairs: Vector[(ContractDescriptorTLV, OracleInfoTLV)]) extends ContractInfoTLV { + override val tpe: BigSizeUInt = ContractInfoV0TLV.tpe override val value: ByteVector = { @@ -1598,12 +1599,20 @@ object FundingInputV0TLV extends TLVFactory[FundingInputV0TLV] { } override val typeName: String = "FundingInputV0TLV" + + val dummy: FundingInputV0TLV = FundingInputV0TLV(UInt64.zero, + EmptyTransaction, + prevTxVout = UInt32.zero, + UInt32.zero, + UInt16.zero, + None) } sealed trait CETSignaturesTLV extends DLCSetupPieceTLV case class CETSignaturesV0TLV(sigs: Vector[ECAdaptorSignature]) extends CETSignaturesTLV { + override val tpe: BigSizeUInt = CETSignaturesV0TLV.tpe override val value: ByteVector = { diff --git a/core/src/main/scala/org/bitcoins/core/util/sorted/OrderedAnnouncements.scala b/core/src/main/scala/org/bitcoins/core/util/sorted/OrderedAnnouncements.scala index af5dcd93e9..49cb811d4a 100644 --- a/core/src/main/scala/org/bitcoins/core/util/sorted/OrderedAnnouncements.scala +++ b/core/src/main/scala/org/bitcoins/core/util/sorted/OrderedAnnouncements.scala @@ -8,7 +8,9 @@ import org.bitcoins.core.protocol.tlv._ case class OrderedAnnouncements(vec: Vector[OracleAnnouncementTLV]) extends SortedVec[OracleAnnouncementTLV, OracleAnnouncementTLV]( vec, - SortedVec.forOrdered(vec)) + SortedVec.forOrdered(vec)) { + require(vec.nonEmpty, s"Cannot have empty OrderedAnnouncements") +} /** Represents an ordered set of OracleAnnouncementV0TLV * The ordering represents the ranked preference of the user 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 273aa3f263..8781475e96 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 @@ -1272,15 +1272,57 @@ abstract class DLCWallet contractId: ByteVector, oracleSigs: Vector[OracleSignatures]): Future[Transaction] = { require(oracleSigs.nonEmpty, "Must provide at least one oracle signature") + val executorWithSetupOptF = executorAndSetupFromDb(contractId) for { - (executor, setup) <- executorAndSetupFromDb(contractId) + executorWithSetupOpt <- executorWithSetupOptF + tx <- { + executorWithSetupOpt match { + case Some(executorWithSetup) => + buildExecutionTxWithExecutor(executorWithSetup, + oracleSigs, + contractId) + case None => + //means we don't have cet sigs in the db anymore + //can we retrieve the CET some other way? - executed = executor.executeDLC(setup, oracleSigs) - (tx, outcome, sigsUsed) = - (executed.cet, executed.outcome, executed.sigsUsed) - _ = logger.info( - s"Created DLC execution transaction ${tx.txIdBE.hex} for contract ${contractId.toHex}") + //lets try to retrieve it from our transactionDAO + val dlcDbOptF = dlcDAO.findByContractId(contractId) + for { + dlcDbOpt <- dlcDbOptF + _ = require( + dlcDbOpt.isDefined, + s"Could not find dlc associated with this contractId=${contractId.toHex}") + dlcDb = dlcDbOpt.get + _ = require( + dlcDb.closingTxIdOpt.isDefined, + s"If we don't have CET signatures, the closing tx must be defined, contractId=${contractId.toHex}") + closingTxId = dlcDb.closingTxIdOpt.get + closingTxOpt <- transactionDAO.findByTxId(closingTxId) + } yield { + require( + closingTxOpt.isDefined, + s"Could not find closing tx for DLC in db, contactId=${contractId.toHex} closingTxId=${closingTxId.hex}") + closingTxOpt.get.transaction + } + } + } + } yield tx + } + + private def buildExecutionTxWithExecutor( + executorWithSetup: DLCExecutorWithSetup, + oracleSigs: Vector[OracleSignatures], + contractId: ByteVector): Future[Transaction] = { + val executor = executorWithSetup.executor + val setup = executorWithSetup.setup + val executed = executor.executeDLC(setup, oracleSigs) + val (tx, outcome, sigsUsed) = + (executed.cet, executed.outcome, executed.sigsUsed) + logger.info( + s"Created DLC execution transaction ${tx.txIdBE.hex} for contract ${contractId.toHex}") + + for { _ <- updateDLCOracleSigs(sigsUsed) _ <- updateDLCState(contractId, DLCState.Claimed) dlcDb <- updateClosingTxId(contractId, tx.txIdBE) diff --git a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/internal/DLCDataManagement.scala b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/internal/DLCDataManagement.scala index 9af5ba8ba4..3bbd2e646b 100644 --- a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/internal/DLCDataManagement.scala +++ b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/internal/DLCDataManagement.scala @@ -263,7 +263,7 @@ private[bitcoins] trait DLCDataManagement { self: DLCWallet => DLCRefundSigsDb, ContractInfo, Vector[DLCFundingInputDb], - Vector[DLCCETSignaturesDb])] = { + Option[Vector[DLCCETSignaturesDb]])] = { for { dlcDbOpt <- dlcDAO.findByContractId(contractId) dlcDb = dlcDbOpt.get @@ -295,7 +295,7 @@ private[bitcoins] trait DLCDataManagement { self: DLCWallet => DLCRefundSigsDb, ContractInfo, Vector[DLCFundingInputDb], - Vector[DLCCETSignaturesDb])] = { + Option[Vector[DLCCETSignaturesDb]])] = { val safeDatabase = dlcRefundSigDAO.safeDatabase val refundSigDLCs = dlcRefundSigDAO.findByDLCIdAction(dlcId) val sigDLCs = dlcSigsDAO.findByDLCIdAction(dlcId) @@ -307,14 +307,19 @@ private[bitcoins] trait DLCDataManagement { self: DLCWallet => (dlcDb, contractData, dlcOffer, dlcAccept, fundingInputs, contractInfo) <- getDLCFundingData(dlcId) (refundSigs, outcomeSigs) <- refundAndOutcomeSigsF - } yield (dlcDb, - contractData, - dlcOffer, - dlcAccept, - refundSigs.head, - contractInfo, - fundingInputs, - outcomeSigs) + } yield { + + val sigsOpt = if (outcomeSigs.isEmpty) None else Some(outcomeSigs) + + (dlcDb, + contractData, + dlcOffer, + dlcAccept, + refundSigs.head, + contractInfo, + fundingInputs, + sigsOpt) + } } private[wallet] def fundingUtxosFromDb( @@ -542,8 +547,11 @@ private[bitcoins] trait DLCDataManagement { self: DLCWallet => signerFromDb(dlcId).map(DLCExecutor.apply) } + /** Builds an [[DLCExecutor]] and [[SetupDLC]] for a given contract id + * @return the executor and setup if we still have CET signatures else return None + */ private[wallet] def executorAndSetupFromDb( - contractId: ByteVector): Future[(DLCExecutor, SetupDLC)] = { + contractId: ByteVector): Future[Option[DLCExecutorWithSetup]] = { getAllDLCData(contractId).flatMap { case (dlcDb, contractData, @@ -552,15 +560,24 @@ private[bitcoins] trait DLCDataManagement { self: DLCWallet => refundSigs, contractInfo, fundingInputsDb, - outcomeSigsDbs) => - executorAndSetupFromDb(dlcDb, - contractData, - dlcOffer, - dlcAccept, - refundSigs, - contractInfo, - fundingInputsDb, - outcomeSigsDbs) + outcomeSigsDbsOpt) => + outcomeSigsDbsOpt match { + case Some(outcomeSigsDbs) => + executorAndSetupFromDb(dlcDb, + contractData, + dlcOffer, + dlcAccept, + refundSigs, + contractInfo, + fundingInputsDb, + outcomeSigsDbs).map(Some(_)) + case None => + //means we cannot re-create messages because + //we don't have the cets in the database anymore + Future.successful(None) + + } + } } @@ -573,59 +590,61 @@ private[bitcoins] trait DLCDataManagement { self: DLCWallet => contractInfo: ContractInfo, fundingInputs: Vector[DLCFundingInputDb], outcomeSigsDbs: Vector[DLCCETSignaturesDb]): Future[ - (DLCExecutor, SetupDLC)] = { + DLCExecutorWithSetup] = { - executorFromDb(dlcDb, - contractDataDb, - dlcOffer, - dlcAccept, - fundingInputs, - contractInfo) - .flatMap { executor => - // Filter for only counterparty's outcome sigs - val outcomeSigs = - if (dlcDb.isInitiator) { - outcomeSigsDbs - .map { dbSig => - dbSig.sigPoint -> dbSig.accepterSig - } - } else { - outcomeSigsDbs - .map { dbSig => - dbSig.sigPoint -> dbSig.initiatorSig.get - } + val dlcExecutorF = executorFromDb(dlcDb, + contractDataDb, + dlcOffer, + dlcAccept, + fundingInputs, + contractInfo) + + dlcExecutorF.flatMap { executor => + // Filter for only counterparty's outcome sigs + val outcomeSigs = if (dlcDb.isInitiator) { + outcomeSigsDbs + .map { dbSig => + dbSig.sigPoint -> dbSig.accepterSig + } + } else { + outcomeSigsDbs + .map { dbSig => + dbSig.sigPoint -> dbSig.initiatorSig.get } - - val refundSig = if (dlcDb.isInitiator) { - refundSigsDb.accepterSig - } else refundSigsDb.initiatorSig.get - - val cetSigs = CETSignatures(outcomeSigs, refundSig) - - val setupF = if (dlcDb.isInitiator) { - // Note that the funding tx in this setup is not signed - executor.setupDLCOffer(cetSigs) - } else { - val fundingSigs = - fundingInputs - .filter(_.isInitiator) - .map { input => - input.witnessScriptOpt match { - case Some(witnessScript) => - witnessScript match { - case EmptyScriptWitness => - throw new RuntimeException( - "Script witness cannot be empty") - case witness: ScriptWitnessV0 => (input.outPoint, witness) - } - case None => throw new RuntimeException("") - } - } - executor.setupDLCAccept(cetSigs, FundingSignatures(fundingSigs), None) - } - - Future.fromTry(setupF.map((executor, _))) } + val refundSig = if (dlcDb.isInitiator) { + refundSigsDb.accepterSig + } else refundSigsDb.initiatorSig.get + + //sometimes we do not have cet signatures, for instance + //if we have settled a DLC, we prune the cet signatures + //from the database + val cetSigs = CETSignatures(outcomeSigs, refundSig) + + val setupF = if (dlcDb.isInitiator) { + // Note that the funding tx in this setup is not signed + executor.setupDLCOffer(cetSigs) + } else { + val fundingSigs = + fundingInputs + .filter(_.isInitiator) + .map { input => + input.witnessScriptOpt match { + case Some(witnessScript) => + witnessScript match { + case EmptyScriptWitness => + throw new RuntimeException( + "Script witness cannot be empty") + case witness: ScriptWitnessV0 => (input.outPoint, witness) + } + case None => throw new RuntimeException("") + } + } + executor.setupDLCAccept(cetSigs, FundingSignatures(fundingSigs), None) + } + + Future.fromTry(setupF.map(DLCExecutorWithSetup(executor, _))) + } } def getCetAndRefundSigsAction(dlcId: Sha256Digest): DBIOAction[ diff --git a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/internal/DLCTransactionProcessing.scala b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/internal/DLCTransactionProcessing.scala index 57361c0fc1..6581dd54c5 100644 --- a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/internal/DLCTransactionProcessing.scala +++ b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/internal/DLCTransactionProcessing.scala @@ -2,6 +2,7 @@ package org.bitcoins.dlc.wallet.internal import org.bitcoins.core.api.dlc.wallet.db._ import org.bitcoins.core.api.wallet.db.SpendingInfoDb +import org.bitcoins.core.protocol.dlc.execution.SetupDLC import org.bitcoins.core.protocol.dlc.models.DLCMessage._ import org.bitcoins.core.protocol.dlc.models._ import org.bitcoins.core.protocol.script._ @@ -31,50 +32,66 @@ private[bitcoins] trait DLCTransactionProcessing extends TransactionProcessing { def calculateAndSetState(dlcDb: DLCDb): Future[DLCDb] = { (dlcDb.contractIdOpt, dlcDb.closingTxIdOpt) match { case (Some(id), Some(txId)) => - executorAndSetupFromDb(id).flatMap { case (_, setup) => - val updatedF = if (txId == setup.refundTx.txIdBE) { - Future.successful(dlcDb.copy(state = DLCState.Refunded)) - } else if (dlcDb.state == DLCState.Claimed) { - Future.successful(dlcDb.copy(state = DLCState.Claimed)) - } else { - val withState = dlcDb.updateState(DLCState.RemoteClaimed) - if (dlcDb.state != DLCState.RemoteClaimed) { - for { - // update so we can calculate correct DLCStatus - _ <- dlcDAO.update(withState) - withOutcome <- calculateAndSetOutcome(withState) - dlc <- findDLC(dlcDb.dlcId) - _ = dlcConfig.walletCallbacks.executeOnDLCStateChange(logger, - dlc.get) - } yield withOutcome - } else Future.successful(withState) + val executorWithSetupOptF = executorAndSetupFromDb(id) + executorWithSetupOptF.flatMap { case executorWithSetupOpt => + executorWithSetupOpt match { + case Some(exeutorWithSetup) => + calculateAndSetStateWithSetupDLC(exeutorWithSetup.setup, + dlcDb, + txId) + case None => + //this means we have already deleted the cet sigs + //just return the dlcdb given to us + Future.successful(dlcDb) } - - for { - updated <- updatedF - - _ <- { - updated.state match { - case DLCState.Claimed | DLCState.RemoteClaimed | - DLCState.Refunded => - val contractId = updated.contractIdOpt.get.toHex - logger.info( - s"Deleting unneeded DLC signatures for contract $contractId") - - dlcSigsDAO.deleteByDLCId(updated.dlcId) - case DLCState.Offered | DLCState.Accepted | DLCState.Signed | - DLCState.Broadcasted | DLCState.Confirmed => - FutureUtil.unit - } - } - - } yield updated } case (None, None) | (None, Some(_)) | (Some(_), None) => Future.successful(dlcDb) } } + /** Calculates the new closing state for a DLC if we still + * have adaptor signatures available to us in the database + */ + private def calculateAndSetStateWithSetupDLC( + setup: SetupDLC, + dlcDb: DLCDb, + closingTxId: DoubleSha256DigestBE): Future[DLCDb] = { + val updatedF = if (closingTxId == setup.refundTx.txIdBE) { + Future.successful(dlcDb.copy(state = DLCState.Refunded)) + } else if (dlcDb.state == DLCState.Claimed) { + Future.successful(dlcDb.copy(state = DLCState.Claimed)) + } else { + val withState = dlcDb.updateState(DLCState.RemoteClaimed) + if (dlcDb.state != DLCState.RemoteClaimed) { + for { + // update so we can calculate correct DLCStatus + _ <- dlcDAO.update(withState) + withOutcome <- calculateAndSetOutcome(withState) + dlc <- findDLC(dlcDb.dlcId) + _ = dlcConfig.walletCallbacks.executeOnDLCStateChange(logger, dlc.get) + } yield withOutcome + } else Future.successful(withState) + } + + for { + updated <- updatedF + _ <- { + updated.state match { + case DLCState.Claimed | DLCState.RemoteClaimed | DLCState.Refunded => + val contractId = updated.contractIdOpt.get.toHex + logger.info( + s"Deleting unneeded DLC signatures for contract $contractId") + + dlcSigsDAO.deleteByDLCId(updated.dlcId) + case DLCState.Offered | DLCState.Accepted | DLCState.Signed | + DLCState.Broadcasted | DLCState.Confirmed => + FutureUtil.unit + } + } + } yield updated + } + /** Calculates the outcome used for execution * based on the closing transaction */ diff --git a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/models/DLCExecutorWithSetup.scala b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/models/DLCExecutorWithSetup.scala new file mode 100644 index 0000000000..c2b5f5474a --- /dev/null +++ b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/models/DLCExecutorWithSetup.scala @@ -0,0 +1,5 @@ +package org.bitcoins.dlc.wallet.models + +import org.bitcoins.core.protocol.dlc.execution.{DLCExecutor, SetupDLC} + +case class DLCExecutorWithSetup(executor: DLCExecutor, setup: SetupDLC) diff --git a/docs/core/dlc.md b/docs/core/dlc.md index 9ba80d42c2..2c93ffe218 100644 --- a/docs/core/dlc.md +++ b/docs/core/dlc.md @@ -182,7 +182,7 @@ val offerTLV = DLCOfferTLV( payoutSPK = EmptyScriptPubKey, payoutSerialId = UInt64(1), totalCollateralSatoshis = Satoshis(500), - fundingInputs = Vector.empty, + fundingInputs = Vector(FundingInputV0TLV.dummy), changeSPK = EmptyScriptPubKey, changeSerialId = UInt64(2), fundOutputSerialId = UInt64(3), diff --git a/testkit-core/src/main/scala/org/bitcoins/testkitcore/gen/TLVGen.scala b/testkit-core/src/main/scala/org/bitcoins/testkitcore/gen/TLVGen.scala index 65497c7063..b6768bb4e0 100644 --- a/testkit-core/src/main/scala/org/bitcoins/testkitcore/gen/TLVGen.scala +++ b/testkit-core/src/main/scala/org/bitcoins/testkitcore/gen/TLVGen.scala @@ -355,7 +355,7 @@ trait TLVGen { def cetSignaturesV0TLV: Gen[CETSignaturesV0TLV] = { Gen - .listOf(CryptoGenerators.adaptorSignature) + .nonEmptyListOf(CryptoGenerators.adaptorSignature) .map(sigs => CETSignaturesV0TLV(sigs.toVector)) }