2021 01 06 tlv invariants (#3965)

* Add invariant to CETSignaturesTLV so we don't have empty adaptor sigs

* Add invariant to make sure FundingSignaturesTLV witnesses are not empty

* Add invariant that contractOraclePairs aren't empty

* Add fundingInputs and ordered announcement invariants

* fix docs

* Move invaraints from TLVs to in memory data structures

* WIP

* WIP2

* Modify return type of DLCDataManagement.executorAndSetupFromDb() to return an Option. The None case represents when we have pruned CET signatures from the database

* Add some comments, clean up a bit

* more cleanup
This commit is contained in:
Chris Stewart 2022-01-08 16:21:58 -06:00 committed by GitHub
parent 3ba8fb6dd4
commit 93c5121632
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 219 additions and 116 deletions

View File

@ -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

View File

@ -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")

View File

@ -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)

View File

@ -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 = {

View File

@ -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

View File

@ -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)

View File

@ -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[

View File

@ -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
*/

View File

@ -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)

View File

@ -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),

View File

@ -355,7 +355,7 @@ trait TLVGen {
def cetSignaturesV0TLV: Gen[CETSignaturesV0TLV] = {
Gen
.listOf(CryptoGenerators.adaptorSignature)
.nonEmptyListOf(CryptoGenerators.adaptorSignature)
.map(sigs => CETSignaturesV0TLV(sigs.toVector))
}