mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-01-18 21:34:39 +01:00
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:
parent
3ba8fb6dd4
commit
93c5121632
@ -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
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 = {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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[
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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)
|
@ -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),
|
||||
|
@ -355,7 +355,7 @@ trait TLVGen {
|
||||
|
||||
def cetSignaturesV0TLV: Gen[CETSignaturesV0TLV] = {
|
||||
Gen
|
||||
.listOf(CryptoGenerators.adaptorSignature)
|
||||
.nonEmptyListOf(CryptoGenerators.adaptorSignature)
|
||||
.map(sigs => CETSignaturesV0TLV(sigs.toVector))
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user