Add ordering of funding inputs to DLC Wallet (#3647)

* Add ordering of funding inputs to DLC Wallet

* Fix tests
This commit is contained in:
benthecarman 2021-09-11 08:25:24 -05:00 committed by GitHub
parent 5089e901bb
commit 099e4469b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 215 additions and 88 deletions

View File

@ -82,13 +82,13 @@ class DbManagementTest extends BitcoinSAsyncTest with EmbeddedPg {
val result = dlcDbManagement.migrate()
dlcAppConfig.driver match {
case SQLite =>
val expected = 3
val expected = 4
assert(result == expected)
val flywayInfo = dlcAppConfig.info()
assert(flywayInfo.applied().length == expected)
assert(flywayInfo.pending().length == 0)
case PostgreSQL =>
val expected = 3
val expected = 4
assert(result == expected)
val flywayInfo = dlcAppConfig.info()

View File

@ -74,6 +74,7 @@ class DLCDAOTest extends BitcoinSWalletTest with DLCDAOFixture {
val input = DLCFundingInputDb(
dlcId = dlcId,
isInitiator = true,
index = 0,
inputSerialId = UInt64.zero,
outPoint = TransactionOutPoint(testBlockHash, UInt32.zero),
output = TransactionOutput(Satoshis.one, EmptyScriptPubKey),
@ -95,6 +96,7 @@ class DLCDAOTest extends BitcoinSWalletTest with DLCDAOFixture {
DLCFundingInputDb(
dlcId = dlcId,
isInitiator = true,
index = 0,
inputSerialId = UInt64.zero,
outPoint = TransactionOutPoint(testBlockHash, UInt32.zero),
output = TransactionOutput(Satoshis.one, EmptyScriptPubKey),
@ -106,6 +108,7 @@ class DLCDAOTest extends BitcoinSWalletTest with DLCDAOFixture {
DLCFundingInputDb(
dlcId = dlcId,
isInitiator = false,
index = 0,
inputSerialId = UInt64.one,
outPoint = TransactionOutPoint(testBlockHash, UInt32.one),
output = TransactionOutput(Satoshis.one, EmptyScriptPubKey),
@ -117,6 +120,7 @@ class DLCDAOTest extends BitcoinSWalletTest with DLCDAOFixture {
DLCFundingInputDb(
dlcId = dlcId,
isInitiator = true,
index = 1,
inputSerialId = UInt64(2),
outPoint = TransactionOutPoint(testBlockHash, UInt32(3)),
output = TransactionOutput(Satoshis.one, EmptyScriptPubKey),

View File

@ -1,6 +1,6 @@
package org.bitcoins.dlc.wallet
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.currency._
import org.bitcoins.core.number.{UInt32, UInt64}
import org.bitcoins.core.protocol.dlc.models.DLCMessage._
import org.bitcoins.core.protocol.dlc.models._
@ -75,7 +75,7 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
_ = {
assert(dlcA2Opt.isDefined)
assert(dlcA2Opt.get.state == DLCState.Signed)
assert(sign.fundingSigs.length == offerData.fundingInputs.size)
assert(sign.fundingSigs.length == offer.fundingInputs.size)
}
dlcDb <- walletB.addDLCSigs(sign)
@ -136,6 +136,120 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
testNegotiate(fundedDLCWallets, DLCWalletUtil.sampleMultiNonceDLCOffer)
}
// This could happen inputs can end up in different orders when
// using postgres or using different coin selection algos
it must "correctly negotiate a dlc with reordered inputs" in {
fundedDLCWallets: (FundedDLCWallet, FundedDLCWallet) =>
// construct a contract info that uses many inputs
val totalCol = Bitcoins(11).satoshis
val col = totalCol / Satoshis(2)
val outcomes: Vector[(EnumOutcome, Satoshis)] =
Vector(EnumOutcome(winStr) -> totalCol,
EnumOutcome(loseStr) -> Satoshis.zero)
val oraclePair: ContractOraclePair.EnumPair =
ContractOraclePair.EnumPair(EnumContractDescriptor(outcomes),
sampleOracleInfo)
val contractInfo: ContractInfo = ContractInfo(totalCol, oraclePair)
val offerData =
sampleDLCOffer.copy(contractInfo = contractInfo,
totalCollateral = col.satoshis)
val walletA = fundedDLCWallets._1.wallet
val walletB = fundedDLCWallets._2.wallet
def reorderInputDbs(
wallet: DLCWallet,
dlcId: Sha256Digest): Future[Unit] = {
for {
inputDbs <- wallet.dlcInputsDAO.findByDLCId(dlcId)
_ <- wallet.dlcInputsDAO.deleteByDLCId(dlcId)
_ <- wallet.dlcInputsDAO.createAll(inputDbs.reverse)
} yield ()
}
for {
offer <- walletA.createDLCOffer(
offerData.contractInfo,
offerData.totalCollateral,
Some(offerData.feeRate),
offerData.timeouts.contractMaturity.toUInt32,
offerData.timeouts.contractTimeout.toUInt32
)
dlcId = calcDLCId(offer.fundingInputs.map(_.outPoint))
accept <- walletB.acceptDLCOffer(offer)
// reorder dlc inputs in wallets
_ <- reorderInputDbs(walletA, dlcId)
_ <- reorderInputDbs(walletB, dlcId)
sign <- walletA.signDLC(accept)
dlcDb <- walletB.addDLCSigs(sign)
} yield assert(dlcDb.state == DLCState.Signed)
}
// This could happen inputs can end up in different orders when
// using postgres or using different coin selection algos
it must "correctly negotiate a dlc with tlvs & with reordered inputs" in {
fundedDLCWallets: (FundedDLCWallet, FundedDLCWallet) =>
// construct a contract info that uses many inputs
val totalCol = Bitcoins(11).satoshis
val col = totalCol / Satoshis(2)
val outcomes: Vector[(EnumOutcome, Satoshis)] =
Vector(EnumOutcome(winStr) -> totalCol,
EnumOutcome(loseStr) -> Satoshis.zero)
val oraclePair: ContractOraclePair.EnumPair =
ContractOraclePair.EnumPair(EnumContractDescriptor(outcomes),
sampleOracleInfo)
val contractInfo: ContractInfo = ContractInfo(totalCol, oraclePair)
val offerData =
sampleDLCOffer.copy(contractInfo = contractInfo,
totalCollateral = col.satoshis)
val walletA = fundedDLCWallets._1.wallet
val walletB = fundedDLCWallets._2.wallet
def reorderInputDbs(
wallet: DLCWallet,
dlcId: Sha256Digest): Future[Unit] = {
for {
inputDbs <- wallet.dlcInputsDAO.findByDLCId(dlcId)
_ <- wallet.dlcInputsDAO.deleteByDLCId(dlcId)
_ <- wallet.dlcInputsDAO.createAll(inputDbs.reverse)
} yield ()
}
for {
offer <- walletA.createDLCOffer(
offerData.contractInfo,
offerData.totalCollateral,
Some(offerData.feeRate),
offerData.timeouts.contractMaturity.toUInt32,
offerData.timeouts.contractTimeout.toUInt32
)
dlcId = calcDLCId(offer.fundingInputs.map(_.outPoint))
accept <- walletB.acceptDLCOffer(offer.toTLV)
// reorder dlc inputs in wallets
_ <- reorderInputDbs(walletA, dlcId)
_ <- reorderInputDbs(walletB, dlcId)
sign <- walletA.signDLC(accept.toTLV)
dlcDb <- walletB.addDLCSigs(sign.toTLV)
} yield assert(dlcDb.state == DLCState.Signed)
}
it must "correctly negotiate a dlc using TLVs" in {
fundedDLCWallets: (FundedDLCWallet, FundedDLCWallet) =>
val walletA = fundedDLCWallets._1.wallet
@ -185,7 +299,7 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
_ = {
assert(dlcA2Opt.isDefined)
assert(dlcA2Opt.get.state == DLCState.Signed)
assert(sign.fundingSigs.length == offerData.fundingInputs.size)
assert(sign.fundingSigs.length == offer.fundingInputs.size)
}
dlcDb <- walletB.addDLCSigs(sign.toTLV)

View File

@ -0,0 +1,2 @@
ALTER TABLE "funding_inputs"
ADD COLUMN "index" INTEGER NOT NULL DEFAULT 0;

View File

@ -0,0 +1,2 @@
ALTER TABLE "funding_inputs"
ADD COLUMN "index" INTEGER NOT NULL DEFAULT 0;

View File

@ -422,18 +422,20 @@ abstract class DLCWallet
_ <- dlcAnnouncementDAO.createAll(dlcAnnouncementDbs)
dlcOfferDb = DLCOfferDbHelper.fromDLCOffer(dlcId, offer)
dlcInputs = spendingInfos.zip(utxos).map { case (utxo, fundingInput) =>
DLCFundingInputDb(
dlcId = dlcId,
isInitiator = true,
inputSerialId = fundingInput.inputSerialId,
outPoint = utxo.outPoint,
output = utxo.output,
nSequence = fundingInput.sequence,
maxWitnessLength = fundingInput.maxWitnessLen.toLong,
redeemScriptOpt = InputInfo.getRedeemScript(utxo.inputInfo),
witnessScriptOpt = InputInfo.getScriptWitness(utxo.inputInfo)
)
dlcInputs = spendingInfos.zip(utxos).zipWithIndex.map {
case ((utxo, fundingInput), idx) =>
DLCFundingInputDb(
dlcId = dlcId,
isInitiator = true,
index = idx,
inputSerialId = fundingInput.inputSerialId,
outPoint = utxo.outPoint,
output = utxo.output,
nSequence = fundingInput.sequence,
maxWitnessLength = fundingInput.maxWitnessLen.toLong,
redeemScriptOpt = InputInfo.getRedeemScript(utxo.inputInfo),
witnessScriptOpt = InputInfo.getScriptWitness(utxo.inputInfo)
)
}
_ = logger.info(
@ -685,35 +687,40 @@ abstract class DLCWallet
dlcOfferDb = DLCOfferDbHelper.fromDLCOffer(dlc.dlcId, offer)
offerInputs = offer.fundingInputs.map(funding =>
DLCFundingInputDb(
dlcId = dlc.dlcId,
isInitiator = true,
inputSerialId = funding.inputSerialId,
outPoint = funding.outPoint,
output = funding.output,
nSequence = funding.sequence,
maxWitnessLength = funding.maxWitnessLen.toLong,
redeemScriptOpt = funding.redeemScriptOpt,
witnessScriptOpt = None
))
offerInputs = offer.fundingInputs.zipWithIndex.map {
case (funding, idx) =>
DLCFundingInputDb(
dlcId = dlc.dlcId,
isInitiator = true,
index = idx,
inputSerialId = funding.inputSerialId,
outPoint = funding.outPoint,
output = funding.output,
nSequence = funding.sequence,
maxWitnessLength = funding.maxWitnessLen.toLong,
redeemScriptOpt = funding.redeemScriptOpt,
witnessScriptOpt = None
)
}
offerPrevTxs = offer.fundingInputs.map(funding =>
TransactionDbHelper.fromTransaction(funding.prevTx,
blockHashOpt = None))
acceptInputs = spendingInfos.zip(utxos).map { case (utxo, fundingInput) =>
DLCFundingInputDb(
dlcId = dlc.dlcId,
isInitiator = false,
inputSerialId = fundingInput.inputSerialId,
outPoint = utxo.outPoint,
output = utxo.output,
nSequence = fundingInput.sequence,
maxWitnessLength = fundingInput.maxWitnessLen.toLong,
redeemScriptOpt = InputInfo.getRedeemScript(utxo.inputInfo),
witnessScriptOpt = InputInfo.getScriptWitness(utxo.inputInfo)
)
acceptInputs = spendingInfos.zip(utxos).zipWithIndex.map {
case ((utxo, fundingInput), idx) =>
DLCFundingInputDb(
dlcId = dlc.dlcId,
isInitiator = false,
index = idx,
inputSerialId = fundingInput.inputSerialId,
outPoint = utxo.outPoint,
output = utxo.output,
nSequence = fundingInput.sequence,
maxWitnessLength = fundingInput.maxWitnessLen.toLong,
redeemScriptOpt = InputInfo.getRedeemScript(utxo.inputInfo),
witnessScriptOpt = InputInfo.getScriptWitness(utxo.inputInfo)
)
}
accept =
@ -772,18 +779,21 @@ abstract class DLCWallet
val dlcId = dlc.dlcId
lazy val dlcAcceptDb = DLCAcceptDbHelper.fromDLCAccept(dlcId, accept)
lazy val acceptInputs = accept.fundingInputs.map(funding =>
DLCFundingInputDb(
dlcId = dlcId,
isInitiator = false,
inputSerialId = funding.inputSerialId,
outPoint = funding.outPoint,
output = funding.output,
nSequence = funding.sequence,
maxWitnessLength = funding.maxWitnessLen.toLong,
redeemScriptOpt = funding.redeemScriptOpt,
witnessScriptOpt = None
))
lazy val acceptInputs = accept.fundingInputs.zipWithIndex.map {
case (funding, idx) =>
DLCFundingInputDb(
dlcId = dlcId,
isInitiator = false,
index = idx,
inputSerialId = funding.inputSerialId,
outPoint = funding.outPoint,
output = funding.output,
nSequence = funding.sequence,
maxWitnessLength = funding.maxWitnessLen.toLong,
redeemScriptOpt = funding.redeemScriptOpt,
witnessScriptOpt = None
)
}
lazy val acceptPrevTxs = accept.fundingInputs.map { funding =>
TransactionDbHelper.fromTransaction(funding.prevTx,
@ -933,13 +943,20 @@ abstract class DLCWallet
_ = logger.info(s"Creating funding sigs for ${contractId.toHex}")
fundingSigs <- Future.fromTry(signer.signFundingTx())
// order fundingSigs by index
inputDbs <- dlcInputsDAO.findByDLCId(dlc.dlcId, isInitiator = true)
sortedSigVec = inputDbs.sortBy(_.index).map { db =>
val sig = fundingSigs(db.outPoint)
(db.outPoint, sig)
}
updatedRefundSigsDb = refundSigsDb.copy(initiatorSig =
Some(cetSigs.refundSig))
_ <- dlcRefundSigDAO.update(updatedRefundSigsDb)
_ <- updateDLCState(dlc.contractIdOpt.get, DLCState.Signed)
_ = logger.info(s"DLC ${contractId.toHex} is signed")
} yield DLCSign(cetSigs, fundingSigs, contractId)
} yield DLCSign(cetSigs, FundingSignatures(sortedSigVec), contractId)
}
def verifyCETSigs(accept: DLCAccept): Future[Boolean] = {
@ -981,7 +998,7 @@ abstract class DLCWallet
def addFundingSigs(sign: DLCSign): Future[Vector[DLCFundingInputDb]] = {
for {
dlc <- dlcDAO.findByContractId(sign.contractId).map(_.get)
inputs <- dlcInputsDAO.findByDLCId(dlc.dlcId)
inputs <- dlcInputsDAO.findByDLCId(dlc.dlcId).map(_.sortBy(_.index))
_ = logger.info(
s"Verifying ${sign.fundingSigs.length} funding sigs for contract ${sign.contractId.toHex}")
@ -1019,36 +1036,16 @@ abstract class DLCWallet
Future.failed(new RuntimeException(
s"No DLC found with corresponding contractId ${contractId.toHex}"))
}
// .get should be safe now
contractData <- contractDataDAO.read(dlcDb.dlcId).map(_.get)
offerDbOpt <- dlcOfferDAO.findByDLCId(dlcDb.dlcId)
offerDb = offerDbOpt.get
fundingInputDbs <- dlcInputsDAO.findByDLCId(dlcDb.dlcId,
isInitiator = true)
(_, contractData, dlcOffer, dlcAccept, fundingInputs, contractInfo) <-
getDLCFundingData(dlcDb.dlcId)
builder <- builderFromDbData(dlcDb,
contractData,
dlcOffer,
dlcAccept,
fundingInputs,
contractInfo)
txIds = fundingInputDbs.map(_.outPoint.txIdBE)
remotePrevTxs <- remoteTxDAO.findByTxIdBEs(txIds)
(announcements, announcementData, nonceDbs) <- getDLCAnnouncementDbs(
dlcDb.dlcId)
prevTxs = remotePrevTxs.map(_.transaction)
txs = prevTxs.groupBy(_.txIdBE)
fundingInputs = fundingInputDbs.map(input =>
input.toFundingInput(txs(input.outPoint.txIdBE).head))
contractInfo = getContractInfo(contractData,
announcements,
announcementData,
nonceDbs)
offer = offerDb.toDLCOffer(contractInfo,
fundingInputs,
dlcDb,
contractData)
sign = DLCSign.fromTLV(signTLV, offer)
sign = DLCSign.fromTLV(signTLV, builder.offer)
result <- addDLCSigs(sign)
} yield result
}

View File

@ -219,7 +219,9 @@ private[bitcoins] trait DLCDataManagement { self: DLCWallet =>
announcements,
announcementData,
nonceDbs)
} yield (dlcDb, contractData, dlcOffer, fundingInputs, contractInfo)
sortedInputs = fundingInputs.sortBy(_.index)
} yield (dlcDb, contractData, dlcOffer, sortedInputs, contractInfo)
}
private[wallet] def getDLCFundingData(dlcId: Sha256Digest): Future[
@ -421,7 +423,7 @@ private[bitcoins] trait DLCDataManagement { self: DLCWallet =>
private[wallet] def matchPrevTxsWithInputs(
inputs: Vector[DLCFundingInputDb],
prevTxs: Vector[TransactionDb]): Vector[DLCFundingInput] = {
inputs.map { i =>
inputs.sortBy(_.index).map { i =>
prevTxs.find(_.txId == i.outPoint.txId) match {
case Some(txDb) => i.toFundingInput(txDb.transaction)
case None =>

View File

@ -79,7 +79,9 @@ private[bitcoins] trait DLCTransactionProcessing extends TransactionProcessing {
contractData <- contractDataDAO.read(dlcId).map(_.get)
offerDbOpt <- dlcOfferDAO.findByDLCId(dlcId)
acceptDbOpt <- dlcAcceptDAO.findByDLCId(dlcId)
fundingInputDbs <- dlcInputsDAO.findByDLCId(dlcId)
fundingInputDbs <- dlcInputsDAO
.findByDLCId(dlcId)
.map(_.sortBy(_.index))
txIds = fundingInputDbs.map(_.outPoint.txIdBE)
remotePrevTxs <- remoteTxDAO.findByTxIdBEs(txIds)
localPrevTxs <- transactionDAO.findByTxIdBEs(txIds)
@ -154,7 +156,7 @@ private[bitcoins] trait DLCTransactionProcessing extends TransactionProcessing {
DLCStatus.calculateOutcomeAndSig(isInit, offer, accept, sign, cet).get
}
(outcomes, oracleInfos) = getOutcomeDbInfo(outcome)
(_, oracleInfos) = getOutcomeDbInfo(outcome)
noncesByAnnouncement = nonceDbs
.groupBy(_.announcementId)

View File

@ -85,6 +85,8 @@ case class DLCFundingInputDAO()(implicit
def inputSerialId: Rep[UInt64] = column("input_serial_id")
def index: Rep[Int] = column("index")
def isInitiator: Rep[Boolean] = column("is_initiator")
def output: Rep[TransactionOutput] = column("output")
@ -101,6 +103,7 @@ case class DLCFundingInputDAO()(implicit
def * : ProvenShape[DLCFundingInputDb] =
(dlcId,
isInitiator,
index,
inputSerialId,
outPoint,
output,

View File

@ -12,6 +12,7 @@ import org.bitcoins.crypto.Sha256Digest
case class DLCFundingInputDb(
dlcId: Sha256Digest,
isInitiator: Boolean,
index: Int,
inputSerialId: UInt64,
outPoint: TransactionOutPoint,
output: TransactionOutput,