Rework findDLC() (#3214)

* Rework findDLC(), break things out into separate objects to make things more testable and correct

* Address ben's code review

* Add caching of oracle outcomes we know are valid and broadcast
This commit is contained in:
Chris Stewart 2021-06-01 14:54:00 -05:00 committed by GitHub
parent 549d840d02
commit 2269a052b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 427 additions and 223 deletions

View file

@ -6,49 +6,57 @@ sealed abstract class DLCState
object DLCState extends StringFactory[DLCState] {
sealed abstract class InProgressState extends DLCState
/** Means that someone has attempted to claim the DLC */
sealed abstract class ClosedState extends DLCState
/** A state that requires an oracle outcome to be valid */
sealed trait ClosedViaOracleOutcomeState extends ClosedState
/** The state where an offer has been created but no
* accept message has yet been created/received.
*/
final case object Offered extends DLCState
final case object Offered extends InProgressState
/** The state where an offer has been accepted but
* no sign message has yet been created/received.
*/
final case object Accepted extends DLCState
final case object Accepted extends InProgressState
/** The state where the initiating party has created
* a sign message in response to an accept message
* but the DLC funding transaction has not yet been
* broadcasted to the network.
*/
final case object Signed extends DLCState
final case object Signed extends InProgressState
/** The state where the accepting (non-initiating)
* party has broadcasted the DLC funding transaction
* to the blockchain, and it has not yet been confirmed.
*/
final case object Broadcasted extends DLCState
final case object Broadcasted extends InProgressState
/** The state where the DLC funding transaction has been
* confirmed on-chain and no execution paths have yet been
* initiated.
*/
final case object Confirmed extends DLCState
final case object Confirmed extends InProgressState
/** The state where one of the CETs has been accepted by the network
* and executed by ourselves.
*/
final case object Claimed extends DLCState
final case object Claimed extends ClosedViaOracleOutcomeState
/** The state where one of the CETs has been accepted by the network
* and executed by a remote party.
*/
final case object RemoteClaimed extends DLCState
final case object RemoteClaimed extends ClosedViaOracleOutcomeState
/** The state where the DLC refund transaction has been
* accepted by the network.
*/
final case object Refunded extends DLCState
final case object Refunded extends ClosedState
val all: Vector[DLCState] = Vector(Offered,
Accepted,

View file

@ -254,4 +254,5 @@ object DLCStatus {
localAdaptorSigs,
cet)
}
}

View file

@ -7,14 +7,12 @@ import org.bitcoins.core.api.wallet.db._
import org.bitcoins.core.config.BitcoinNetwork
import org.bitcoins.core.crypto.ExtPublicKey
import org.bitcoins.core.currency._
import org.bitcoins.core.dlc.accounting.DLCAccounting
import org.bitcoins.core.hd._
import org.bitcoins.core.number._
import org.bitcoins.core.protocol._
import org.bitcoins.core.protocol.dlc.build.DLCTxBuilder
import org.bitcoins.core.protocol.dlc.models.DLCMessage.DLCAccept._
import org.bitcoins.core.protocol.dlc.models.DLCMessage._
import org.bitcoins.core.protocol.dlc.models.DLCStatus._
import org.bitcoins.core.protocol.dlc.models._
import org.bitcoins.core.protocol.dlc.sign._
import org.bitcoins.core.protocol.script._
@ -26,6 +24,7 @@ import org.bitcoins.core.wallet.utxo._
import org.bitcoins.crypto._
import org.bitcoins.dlc.wallet.internal._
import org.bitcoins.dlc.wallet.models._
import org.bitcoins.dlc.wallet.util.DLCStatusBuilder
import org.bitcoins.keymanager.bip39.BIP39KeyManager
import org.bitcoins.wallet.config.WalletAppConfig
import org.bitcoins.wallet.models.TransactionDAO
@ -1235,228 +1234,105 @@ abstract class DLCWallet
}
override def findDLC(dlcId: Sha256Digest): Future[Option[DLCStatus]] = {
for {
dlcDbOpt <- dlcDAO.read(dlcId)
contractDataOpt <- contractDataDAO.read(dlcId)
offerDbOpt <- dlcOfferDAO.read(dlcId)
acceptDbOpt <- dlcAcceptDAO.read(dlcId)
(announcements, announcementData, nonceDbs) <- getDLCAnnouncementDbs(
dlcId)
closingTxFOpt <- dlcDbOpt.map(dlcDb => getClosingTxOpt(dlcDb)) match {
case None => Future.successful(None)
case Some(closingTxIdOpt) => closingTxIdOpt
val start = System.currentTimeMillis()
val dlcDbOptF = dlcDAO.read(dlcId)
val contractDataOptF = contractDataDAO.read(dlcId)
val offerDbOptF = dlcOfferDAO.read(dlcId)
val acceptDbOptF = dlcAcceptDAO.read(dlcId)
val closingTxOptF: Future[Option[TransactionDb]] = for {
dlcDbOpt <- dlcDbOptF
closingTxFOpt <- {
dlcDbOpt.map(dlcDb => getClosingTxOpt(dlcDb)) match {
case None => Future.successful(None)
case Some(closingTxIdOpt) => closingTxIdOpt
}
}
} yield (dlcDbOpt, contractDataOpt, offerDbOpt) match {
case (Some(dlcDb), Some(contractData), Some(offerDb)) =>
val totalCollateral = contractData.totalCollateral
} yield closingTxFOpt
val localCollateral = if (dlcDb.isInitiator) {
offerDb.collateral
} else {
totalCollateral - offerDb.collateral
}
val contractInfo =
getContractInfo(contractData,
announcements,
announcementData,
nonceDbs)
// Only called when safe
lazy val (oracleOutcome, sigs) = {
val aggSig = dlcDb.aggregateSignatureOpt.get
val outcome =
contractInfo.sigPointMap(aggSig.sig.toPrivateKey.publicKey)
val sigs = nonceDbs.flatMap(_.signatureOpt)
(outcome, sigs)
}
val status = dlcDb.state match {
case DLCState.Offered =>
Offered(
dlcId,
dlcDb.isInitiator,
dlcDb.tempContractId,
contractInfo,
contractData.dlcTimeouts,
dlcDb.feeRate,
totalCollateral,
localCollateral
)
case DLCState.Accepted =>
Accepted(
dlcId,
dlcDb.isInitiator,
dlcDb.tempContractId,
dlcDb.contractIdOpt.get,
contractInfo,
contractData.dlcTimeouts,
dlcDb.feeRate,
totalCollateral,
localCollateral
)
case DLCState.Signed =>
Signed(
dlcId,
dlcDb.isInitiator,
dlcDb.tempContractId,
dlcDb.contractIdOpt.get,
contractInfo,
contractData.dlcTimeouts,
dlcDb.feeRate,
totalCollateral,
localCollateral
)
case DLCState.Broadcasted =>
Broadcasted(
dlcId,
dlcDb.isInitiator,
dlcDb.tempContractId,
dlcDb.contractIdOpt.get,
contractInfo,
contractData.dlcTimeouts,
dlcDb.feeRate,
totalCollateral,
localCollateral,
dlcDb.fundingTxIdOpt.get
)
case DLCState.Confirmed =>
Confirmed(
dlcId,
dlcDb.isInitiator,
dlcDb.tempContractId,
dlcDb.contractIdOpt.get,
contractInfo,
contractData.dlcTimeouts,
dlcDb.feeRate,
totalCollateral,
localCollateral,
dlcDb.fundingTxIdOpt.get
)
case DLCState.Claimed =>
require(acceptDbOpt.isDefined,
s"Must have acceptDb to be in state=${DLCState.Claimed}")
val closingTxDb = closingTxFOpt.get
val accounting: DLCAccounting =
calculatePnl(dlcDb,
val dlcOptF: Future[Option[DLCStatus]] = for {
dlcDbOpt <- dlcDbOptF
contractDataOpt <- contractDataOptF
offerDbOpt <- offerDbOptF
acceptDbOpt <- acceptDbOptF
closingTxOpt <- closingTxOptF
result <- {
(dlcDbOpt, contractDataOpt, offerDbOpt) match {
case (Some(dlcDb), Some(contractData), Some(offerDb)) =>
buildDLCStatus(dlcDb,
contractData,
offerDb,
acceptDbOpt.get,
closingTxDb.transaction)
Claimed(
dlcId,
dlcDb.isInitiator,
dlcDb.tempContractId,
dlcDb.contractIdOpt.get,
contractInfo,
contractData.dlcTimeouts,
dlcDb.feeRate,
totalCollateral,
localCollateral,
dlcDb.fundingTxIdOpt.get,
dlcDb.closingTxIdOpt.get,
sigs,
oracleOutcome,
myPayout = accounting.myPayout,
counterPartyPayout = accounting.theirPayout
)
case DLCState.RemoteClaimed =>
require(
acceptDbOpt.isDefined,
s"Must have acceptDb to be in state=${DLCState.RemoteClaimed}")
val closingTxDb = closingTxFOpt.get
val accounting: DLCAccounting =
calculatePnl(dlcDb,
offerDb,
acceptDbOpt.get,
closingTxDb.transaction)
RemoteClaimed(
dlcId,
dlcDb.isInitiator,
dlcDb.tempContractId,
dlcDb.contractIdOpt.get,
contractInfo,
contractData.dlcTimeouts,
dlcDb.feeRate,
totalCollateral,
localCollateral,
dlcDb.fundingTxIdOpt.get,
dlcDb.closingTxIdOpt.get,
dlcDb.aggregateSignatureOpt.get,
oracleOutcome,
myPayout = accounting.myPayout,
counterPartyPayout = accounting.theirPayout
)
case DLCState.Refunded =>
require(acceptDbOpt.isDefined,
s"Must have acceptDb to be in state=${DLCState.Refunded}")
val closingTxDb = closingTxFOpt.get
val accounting: DLCAccounting =
calculatePnl(dlcDb,
offerDb,
acceptDbOpt.get,
closingTxDb.transaction)
Refunded(
dlcId,
dlcDb.isInitiator,
dlcDb.tempContractId,
dlcDb.contractIdOpt.get,
contractInfo,
contractData.dlcTimeouts,
dlcDb.feeRate,
totalCollateral,
localCollateral,
dlcDb.fundingTxIdOpt.get,
dlcDb.closingTxIdOpt.get,
myPayout = accounting.myPayout,
counterPartyPayout = accounting.theirPayout
)
acceptDbOpt,
closingTxOpt)
case (_, _, _) => Future.successful(None)
}
}
} yield result
Some(status)
case (_, _, _) => None
}
dlcOptF.foreach(_ =>
logger.debug(
s"Done finding dlc=$dlcId, it took=${System.currentTimeMillis() - start}ms"))
dlcOptF
}
private def calculatePnl(
/** Helper method to assemble a [[DLCStatus]] */
private def buildDLCStatus(
dlcDb: DLCDb,
contractData: DLCContractDataDb,
offerDb: DLCOfferDb,
acceptDb: DLCAcceptDb,
closingTx: Transaction): DLCAccounting = {
val (myCollateral, theirCollateral, myPayoutAddress, theirPayoutAddress) = {
if (dlcDb.isInitiator) {
val myCollateral = offerDb.collateral
val theirCollateral = acceptDb.collateral
val myPayoutAddress = offerDb.payoutAddress
val theirPayoutAddress = acceptDb.payoutAddress
(myCollateral, theirCollateral, myPayoutAddress, theirPayoutAddress)
acceptDbOpt: Option[DLCAcceptDb],
closingTxOpt: Option[TransactionDb]): Future[Option[DLCStatus]] = {
val dlcId = dlcDb.dlcId
val aggregatedF: Future[(
Vector[DLCAnnouncementDb],
Vector[OracleAnnouncementDataDb],
Vector[OracleNonceDb])] = getDLCAnnouncementDbs(dlcDb.dlcId)
} else {
val myCollateral = acceptDb.collateral
val theirCollateral = offerDb.collateral
val myPayoutAddress = acceptDb.payoutAddress
val theirPayoutAddress = offerDb.payoutAddress
(myCollateral, theirCollateral, myPayoutAddress, theirPayoutAddress)
val contractInfoF: Future[ContractInfo] = {
aggregatedF.map { case (announcements, announcementData, nonceDbs) =>
getContractInfo(contractData, announcements, announcementData, nonceDbs)
}
}
val myPayout = closingTx.outputs
.filter(_.scriptPubKey == myPayoutAddress.scriptPubKey)
.map(_.value)
.sum
val theirPayout = closingTx.outputs
.filter(_.scriptPubKey == theirPayoutAddress.scriptPubKey)
.map(_.value)
.sum
DLCAccounting(
dlcId = dlcDb.dlcId,
myCollateral = myCollateral,
theirCollateral = theirCollateral,
myPayout = myPayout,
theirPayout = theirPayout
)
val statusF: Future[DLCStatus] = for {
contractInfo <- contractInfoF
(_, _, nonceDbs) <- aggregatedF
status <- {
dlcDb.state match {
case _: DLCState.InProgressState =>
val inProgress = DLCStatusBuilder.buildInProgressDLCStatus(
dlcDb = dlcDb,
contractInfo = contractInfo,
contractData = contractData,
offerDb = offerDb)
Future.successful(inProgress)
case _: DLCState.ClosedState =>
(acceptDbOpt, closingTxOpt) match {
case (Some(acceptDb), Some(closingTx)) =>
val statusF = DLCStatusBuilder.buildClosedDLCStatus(
dlcDb = dlcDb,
contractInfo = contractInfo,
contractData = contractData,
nonceDbs = nonceDbs,
offerDb = offerDb,
acceptDb = acceptDb,
closingTx = closingTx.transaction
)
statusF
case (None, None) =>
Future.failed(new RuntimeException(
s"Could not find acceptDb or closingTx for closing state=${dlcDb.state} dlcId=$dlcId"))
case (Some(_), None) =>
Future.failed(new RuntimeException(
s"Could not find closingTx for state=${dlcDb.state} dlcId=$dlcId"))
case (None, Some(_)) =>
Future.failed(new RuntimeException(
s"Cannot find acceptDb for dlcId=$dlcId. This likely means we have data corruption"))
}
}
}
} yield status
statusF.map(Some(_))
}
/** @param newAnnouncements announcements we do not have in our db

View file

@ -0,0 +1,48 @@
package org.bitcoins.dlc.wallet.accounting
import org.bitcoins.core.dlc.accounting.DLCAccounting
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.dlc.wallet.models.{DLCAcceptDb, DLCDb, DLCOfferDb}
object AccountingUtil {
/** Calculates the profit and loss for the given dlc */
def calculatePnl(
dlcDb: DLCDb,
offerDb: DLCOfferDb,
acceptDb: DLCAcceptDb,
closingTx: Transaction): DLCAccounting = {
val (myCollateral, theirCollateral, myPayoutAddress, theirPayoutAddress) = {
if (dlcDb.isInitiator) {
val myCollateral = offerDb.collateral
val theirCollateral = acceptDb.collateral
val myPayoutAddress = offerDb.payoutAddress
val theirPayoutAddress = acceptDb.payoutAddress
(myCollateral, theirCollateral, myPayoutAddress, theirPayoutAddress)
} else {
val myCollateral = acceptDb.collateral
val theirCollateral = offerDb.collateral
val myPayoutAddress = acceptDb.payoutAddress
val theirPayoutAddress = offerDb.payoutAddress
(myCollateral, theirCollateral, myPayoutAddress, theirPayoutAddress)
}
}
val myPayout = closingTx.outputs
.filter(_.scriptPubKey == myPayoutAddress.scriptPubKey)
.map(_.value)
.sum
val theirPayout = closingTx.outputs
.filter(_.scriptPubKey == theirPayoutAddress.scriptPubKey)
.map(_.value)
.sum
DLCAccounting(
dlcId = dlcDb.dlcId,
myCollateral = myCollateral,
theirCollateral = theirCollateral,
myPayout = myPayout,
theirPayout = theirPayout
)
}
}

View file

@ -26,11 +26,21 @@ private[bitcoins] trait DLCDataManagement { self: DLCWallet =>
Vector[DLCAnnouncementDb],
Vector[OracleAnnouncementDataDb],
Vector[OracleNonceDb])] = {
for {
announcements <- dlcAnnouncementDAO.findByDLCId(dlcId)
val announcementsF = dlcAnnouncementDAO.findByDLCId(dlcId)
val announcementIdsF = for {
announcements <- announcementsF
announcementIds = announcements.map(_.announcementId)
announcementData <- announcementDAO.findByIds(announcementIds)
nonceDbs <- oracleNonceDAO.findByAnnouncementIds(announcementIds)
} yield announcementIds
val announcementDataF =
announcementIdsF.flatMap(ids => announcementDAO.findByIds(ids))
val nonceDbsF =
announcementIdsF.flatMap(ids => oracleNonceDAO.findByAnnouncementIds(ids))
for {
announcements <- announcementsF
announcementData <- announcementDataF
nonceDbs <- nonceDbsF
} yield (announcements, announcementData, nonceDbs)
}

View file

@ -0,0 +1,261 @@
package org.bitcoins.dlc.wallet.util
import org.bitcoins.core.dlc.accounting.DLCAccounting
import org.bitcoins.core.protocol.dlc.models.{
ClosedDLCStatus,
ContractInfo,
DLCState,
DLCStatus,
OracleOutcome
}
import org.bitcoins.core.protocol.dlc.models.DLCStatus.{
Accepted,
Broadcasted,
Claimed,
Confirmed,
Offered,
Refunded,
RemoteClaimed,
Signed
}
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.crypto.SchnorrDigitalSignature
import org.bitcoins.dlc.wallet.accounting.AccountingUtil
import org.bitcoins.dlc.wallet.models.{
DLCAcceptDb,
DLCContractDataDb,
DLCDb,
DLCOfferDb,
OracleNonceDb
}
import scala.collection.mutable
import scala.concurrent.{ExecutionContext, Future}
object DLCStatusBuilder {
/** Helper method to convert a bunch of indepdendent datastructures into a in progress dlc status */
def buildInProgressDLCStatus(
dlcDb: DLCDb,
contractInfo: ContractInfo,
contractData: DLCContractDataDb,
offerDb: DLCOfferDb): DLCStatus = {
require(
dlcDb.state.isInstanceOf[DLCState.InProgressState],
s"Cannot have divergent states beteween dlcDb and the parameter state, got= dlcDb.state=${dlcDb.state} state=${dlcDb.state}"
)
val dlcId = dlcDb.dlcId
val totalCollateral = contractData.totalCollateral
val localCollateral = if (dlcDb.isInitiator) {
offerDb.collateral
} else {
totalCollateral - offerDb.collateral
}
val status = dlcDb.state.asInstanceOf[DLCState.InProgressState] match {
case DLCState.Offered =>
Offered(
dlcId,
dlcDb.isInitiator,
dlcDb.tempContractId,
contractInfo,
contractData.dlcTimeouts,
dlcDb.feeRate,
totalCollateral,
localCollateral
)
case DLCState.Accepted =>
Accepted(
dlcId,
dlcDb.isInitiator,
dlcDb.tempContractId,
dlcDb.contractIdOpt.get,
contractInfo,
contractData.dlcTimeouts,
dlcDb.feeRate,
totalCollateral,
localCollateral
)
case DLCState.Signed =>
Signed(
dlcId,
dlcDb.isInitiator,
dlcDb.tempContractId,
dlcDb.contractIdOpt.get,
contractInfo,
contractData.dlcTimeouts,
dlcDb.feeRate,
totalCollateral,
localCollateral
)
case DLCState.Broadcasted =>
Broadcasted(
dlcId,
dlcDb.isInitiator,
dlcDb.tempContractId,
dlcDb.contractIdOpt.get,
contractInfo,
contractData.dlcTimeouts,
dlcDb.feeRate,
totalCollateral,
localCollateral,
dlcDb.fundingTxIdOpt.get
)
case DLCState.Confirmed =>
Confirmed(
dlcId,
dlcDb.isInitiator,
dlcDb.tempContractId,
dlcDb.contractIdOpt.get,
contractInfo,
contractData.dlcTimeouts,
dlcDb.feeRate,
totalCollateral,
localCollateral,
dlcDb.fundingTxIdOpt.get
)
}
status
}
def buildClosedDLCStatus(
dlcDb: DLCDb,
contractInfo: ContractInfo,
contractData: DLCContractDataDb,
nonceDbs: Vector[OracleNonceDb],
offerDb: DLCOfferDb,
acceptDb: DLCAcceptDb,
closingTx: Transaction)(implicit
ec: ExecutionContext): Future[ClosedDLCStatus] = {
require(
dlcDb.state.isInstanceOf[DLCState.ClosedState],
s"Cannot have divergent states beteween dlcDb and the parameter state, got= dlcDb.state=${dlcDb.state} state=${dlcDb.state}"
)
val dlcId = dlcDb.dlcId
val accounting: DLCAccounting =
AccountingUtil.calculatePnl(dlcDb, offerDb, acceptDb, closingTx)
//start calculation up here in parallel as this is a bottleneck currently
val outcomesOptF: Future[
Option[(OracleOutcome, Vector[SchnorrDigitalSignature])]] =
for {
oracleOutcomeSigsOpt <- getOracleOutcomeAndSigs(dlcDb = dlcDb,
contractInfo =
contractInfo,
nonceDbs = nonceDbs)
} yield oracleOutcomeSigsOpt
val totalCollateral = contractData.totalCollateral
val localCollateral = if (dlcDb.isInitiator) {
offerDb.collateral
} else {
totalCollateral - offerDb.collateral
}
val statusF = dlcDb.state.asInstanceOf[DLCState.ClosedState] match {
case DLCState.Refunded =>
//no oracle information in the refund case
val refund = Refunded(
dlcId,
dlcDb.isInitiator,
dlcDb.tempContractId,
dlcDb.contractIdOpt.get,
contractInfo,
contractData.dlcTimeouts,
dlcDb.feeRate,
totalCollateral,
localCollateral,
dlcDb.fundingTxIdOpt.get,
closingTx.txIdBE,
myPayout = accounting.myPayout,
counterPartyPayout = accounting.theirPayout
)
Future.successful(refund)
case oracleOutcomeState: DLCState.ClosedViaOracleOutcomeState =>
//a state that requires an oracle outcome
//the .get below should always be valid
for {
outcomesOpt <- outcomesOptF
(oracleOutcome, sigs) = outcomesOpt.get
} yield {
oracleOutcomeState match {
case DLCState.Claimed =>
Claimed(
dlcId,
dlcDb.isInitiator,
dlcDb.tempContractId,
dlcDb.contractIdOpt.get,
contractInfo,
contractData.dlcTimeouts,
dlcDb.feeRate,
totalCollateral,
localCollateral,
dlcDb.fundingTxIdOpt.get,
closingTx.txIdBE,
sigs,
oracleOutcome,
myPayout = accounting.myPayout,
counterPartyPayout = accounting.theirPayout
)
case DLCState.RemoteClaimed =>
RemoteClaimed(
dlcId,
dlcDb.isInitiator,
dlcDb.tempContractId,
dlcDb.contractIdOpt.get,
contractInfo,
contractData.dlcTimeouts,
dlcDb.feeRate,
totalCollateral,
localCollateral,
dlcDb.fundingTxIdOpt.get,
closingTx.txIdBE,
dlcDb.aggregateSignatureOpt.get,
oracleOutcome,
myPayout = accounting.myPayout,
counterPartyPayout = accounting.theirPayout
)
}
}
}
statusF
}
/** Calculates oracle outcome and signatures. Returns none if the dlc is not in a valid state to
* calculate the outcome
*/
def getOracleOutcomeAndSigs(
dlcDb: DLCDb,
contractInfo: ContractInfo,
nonceDbs: Vector[OracleNonceDb])(implicit ec: ExecutionContext): Future[
Option[(OracleOutcome, Vector[SchnorrDigitalSignature])]] = {
Future {
dlcDb.aggregateSignatureOpt match {
case Some(aggSig) =>
val oracleOutcome = sigPointCache.get(aggSig) match {
case Some(outcome) => outcome //return cached outcome
case None =>
val o =
contractInfo.sigPointMap(aggSig.sig.toPrivateKey.publicKey)
sigPointCache.+=((aggSig, o))
o
}
val sigs = nonceDbs.flatMap(_.signatureOpt)
Some((oracleOutcome, sigs))
case None => None
}
}
}
/** A performance optimization to cache sigpoints we know map to oracle outcomes.
* This is needed as a workaround for issue 3213
* @see https://github.com/bitcoin-s/bitcoin-s/issues/3213
*/
private val sigPointCache: mutable.Map[
SchnorrDigitalSignature,
OracleOutcome] = mutable.Map.empty[SchnorrDigitalSignature, OracleOutcome]
}