diff --git a/build.sbt b/build.sbt index 6af8859cce..b927df6eb7 100644 --- a/build.sbt +++ b/build.sbt @@ -749,7 +749,7 @@ lazy val dlcWalletTest = project name := "bitcoin-s-dlc-wallet-test", libraryDependencies ++= Deps.dlcWalletTest ) - .dependsOn(coreJVM % testAndCompile, dlcWallet, testkit, dlcTest) + .dependsOn(coreJVM % testAndCompile, dlcWallet, testkit, testkitCoreJVM, dlcTest) lazy val dlcNode = project .in(file("dlc-node")) diff --git a/core/src/main/scala/org/bitcoins/core/protocol/dlc/compute/DLCUtil.scala b/core/src/main/scala/org/bitcoins/core/protocol/dlc/compute/DLCUtil.scala index 2c29486235..d71e4bc8e9 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/dlc/compute/DLCUtil.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/dlc/compute/DLCUtil.scala @@ -8,9 +8,14 @@ import org.bitcoins.core.protocol.dlc.models.DLCMessage.{ DLCAcceptWithoutSigs, DLCOffer } -import org.bitcoins.core.protocol.dlc.models.{ContractInfo, OracleOutcome} +import org.bitcoins.core.protocol.dlc.models._ import org.bitcoins.core.protocol.script.P2WSHWitnessV0 +import org.bitcoins.core.protocol.tlv.{ + OracleAnnouncementTLV, + OracleAttestmentTLV +} import org.bitcoins.core.protocol.transaction.{Transaction, WitnessTransaction} +import org.bitcoins.core.util.sorted.OrderedAnnouncements import org.bitcoins.crypto._ import scodec.bits.ByteVector @@ -187,4 +192,98 @@ object DLCUtil { outputIdx = fundingOutputIdx, tempContractId = offer.tempContractId) } + + /** Checks that the oracles signatures given to us are correct + * Things we need to check + * 1. We have all the oracle signatures + * 2. The oracle signatures are for one of the contracts in the [[ContractInfo]] + * @see https://github.com/bitcoin-s/bitcoin-s/issues/4032 + */ + def checkOracleSignaturesAgainstContract( + contractInfo: ContractInfo, + oracleSigs: Vector[OracleSignatures]): Boolean = { + contractInfo match { + case single: SingleContractInfo => + checkSingleContractInfoOracleSigs(single, oracleSigs) + case disjoint: DisjointUnionContractInfo => + //at least one disjoint union contract + //has to have matching signatures + disjoint.contracts.exists { single: SingleContractInfo => + checkSingleContractInfoOracleSigs(single, oracleSigs) + } + } + } + + /** Check if the given [[SingleContractInfo]] has one [[OracleSignatures]] + * matches it inside of oracleSignatures. + */ + private def checkSingleContractInfoOracleSigs( + contractInfo: SingleContractInfo, + oracleSignatures: Vector[OracleSignatures]): Boolean = { + require(oracleSignatures.nonEmpty, s"Signatures cannot be empty") + matchOracleSignatures(contractInfo, oracleSignatures).isDefined + } + + /** Matches a [[SingleContractInfo]] to its oracle's signatures */ + def matchOracleSignatures( + contractInfo: SingleContractInfo, + oracleSignatures: Vector[OracleSignatures]): Option[OracleSignatures] = { + matchOracleSignatures(contractInfo.announcements, oracleSignatures) + } + + def matchOracleSignatures( + announcements: Vector[OracleAnnouncementTLV], + oracleSignatures: Vector[OracleSignatures]): Option[OracleSignatures] = { + val announcementNonces: Vector[Vector[SchnorrNonce]] = { + announcements + .map(_.eventTLV.nonces) + .map(_.vec) + } + val resultOpt = oracleSignatures.find { case oracleSignature => + val oracleSigNonces: Vector[SchnorrNonce] = oracleSignature.sigs.map(_.rx) + announcementNonces.contains(oracleSigNonces) + } + resultOpt + } + + /** Checks to see if the given oracle signatures and announcement have the same nonces */ + private def matchOracleSignaturesForAnnouncements( + announcement: OracleAnnouncementTLV, + signature: OracleSignatures): Option[OracleSignatures] = { + matchOracleSignatures( + Vector(announcement), + Vector(signature) + ) + } + + /** Builds a set of oracle signatures from given announcements + * and attestations. This method discards attestments + * that do not have a matching announcement. Those attestments + * are not included in the returned set of [[OracleSignatures]] + */ + def buildOracleSignatures( + announcements: OrderedAnnouncements, + attestments: Vector[OracleAttestmentTLV]): Vector[OracleSignatures] = { + val result: Vector[OracleSignatures] = { + val init = Vector.empty[OracleSignatures] + attestments + .foldLeft(init) { (acc, attestment) => + val r: Vector[OracleSignatures] = announcements.flatMap { ann => + val oracleSig = + OracleSignatures(SingleOracleInfo(ann), attestment.sigs) + val isMatch = matchOracleSignaturesForAnnouncements(ann, oracleSig) + isMatch match { + case Some(matchedSig) => + acc.:+(matchedSig) + case None => + //don't add it, skip it + acc + } + }.toVector + r + } + } + + result + } } diff --git a/core/src/main/scala/org/bitcoins/core/protocol/dlc/execution/DLCExecutor.scala b/core/src/main/scala/org/bitcoins/core/protocol/dlc/execution/DLCExecutor.scala index e3067f4763..64bbc3bf12 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/dlc/execution/DLCExecutor.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/dlc/execution/DLCExecutor.scala @@ -2,7 +2,7 @@ package org.bitcoins.core.protocol.dlc.execution import org.bitcoins.core.currency.CurrencyUnit import org.bitcoins.core.protocol.dlc.build.DLCTxBuilder -import org.bitcoins.core.protocol.dlc.compute.CETCalculator +import org.bitcoins.core.protocol.dlc.compute.{CETCalculator, DLCUtil} import org.bitcoins.core.protocol.dlc.models._ import org.bitcoins.core.protocol.dlc.sign.DLCTxSigner import org.bitcoins.core.protocol.transaction.{Transaction, WitnessTransaction} @@ -136,6 +136,9 @@ object DLCExecutor { fundingTx: Transaction, fundOutputIndex: Int ): ExecutedDLCOutcome = { + require( + DLCUtil.checkOracleSignaturesAgainstContract(contractInfo, oracleSigs), + s"Incorrect oracle signatures and contract combination") val sigOracles = oracleSigs.map(_.oracle) val oracleInfoOpt = contractInfo.oracleInfos.find { oracleInfo => diff --git a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCExecutionTest.scala b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCExecutionTest.scala index 70fd1c89ca..4d7411542d 100644 --- a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCExecutionTest.scala +++ b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCExecutionTest.scala @@ -3,16 +3,16 @@ package org.bitcoins.dlc.wallet import org.bitcoins.core.currency.Satoshis import org.bitcoins.core.number.UInt32 import org.bitcoins.core.protocol.dlc.models.DLCMessage.DLCOffer -import org.bitcoins.core.protocol.dlc.models.{ - DLCState, - DisjointUnionContractInfo, - SingleContractInfo -} import org.bitcoins.core.protocol.dlc.models.DLCStatus.{ Claimed, Refunded, RemoteClaimed } +import org.bitcoins.core.protocol.dlc.models.{ + DLCState, + DisjointUnionContractInfo, + SingleContractInfo +} import org.bitcoins.core.protocol.tlv._ import org.bitcoins.core.script.interpreter.ScriptInterpreter import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte @@ -416,4 +416,41 @@ class DLCExecutionTest extends BitcoinSDualWalletTest { _ <- walletA.listDLCs() } yield succeed } + + it must "throw an exception for a enum contract when do not have all the oracle signatures/outcomes" in { + wallets => + val walletA = wallets._1.wallet + val resultF = for { + contractId <- getContractId(walletA) + status <- getDLCStatus(walletA) + (goodAttestment, _) = { + status.contractInfo match { + case single: SingleContractInfo => + DLCWalletUtil.getSigs(single) + case disjoint: DisjointUnionContractInfo => + sys.error( + s"Cannot retrieve sigs for disjoint union contract, got=$disjoint") + } + } + //purposefully drop these + //we cannot drop just a sig, or just an outcome because + //of invariants in OracleAttestmentV0TLV + badSigs = goodAttestment.sigs.dropRight(1) + badOutcomes = goodAttestment.outcomes.dropRight(1) + badAttestment = OracleAttestmentV0TLV(eventId = goodAttestment.eventId, + publicKey = + goodAttestment.publicKey, + sigs = badSigs, + outcomes = badOutcomes) + func = (wallet: DLCWallet) => + wallet.executeDLC(contractId, badAttestment) + + result <- dlcExecutionTest(wallets = wallets, + asInitiator = true, + func = func, + expectedOutputs = 1) + } yield assert(result) + + recoverToSucceededIf[IllegalArgumentException](resultF) + } } diff --git a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCMultiOracleExactNumericExecutionTest.scala b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCMultiOracleExactNumericExecutionTest.scala index a8ed49be3a..85f6cff618 100644 --- a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCMultiOracleExactNumericExecutionTest.scala +++ b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCMultiOracleExactNumericExecutionTest.scala @@ -74,27 +74,7 @@ class DLCMultiOracleExactNumericExecutionTest extends BitcoinSDualWalletTest { initiatorWinVec, None) - val initiatorWinSigs = - privateKeys.zip(kValues).flatMap { case (priv, kValues) => - val outcomeOpt = initWinOutcomes.oraclesAndOutcomes.find( - _._1.publicKey == priv.schnorrPublicKey) - - outcomeOpt.map { case (oracleInfo, outcome) => - val sigs = outcome.digits.zip(kValues).map { case (num, kValue) => - val hash = CryptoUtil.sha256DLCAttestation(num.toString).bytes - priv.schnorrSignWithNonce(hash, kValue) - } - val eventId = oracleInfo.announcement.eventTLV match { - case v0: OracleEventV0TLV => v0.eventId - } - - OracleAttestmentV0TLV(eventId, - priv.schnorrPublicKey, - sigs, - outcome.digits.map(_.toString)) - } - } - + val initiatorWinSigs = buildAttestments(initWinOutcomes) val recipientChosenOracles = Random.shuffle(oracleIndices).take(oracleInfo.threshold).sorted @@ -114,26 +94,7 @@ class DLCMultiOracleExactNumericExecutionTest extends BitcoinSDualWalletTest { recipientWinVec, None) - val recipientWinSigs = - privateKeys.zip(kValues).flatMap { case (priv, kValues) => - val outcomeOpt = recipientWinOutcomes.oraclesAndOutcomes.find( - _._1.publicKey == priv.schnorrPublicKey) - - outcomeOpt.map { case (oracleInfo, outcome) => - val sigs = outcome.digits.zip(kValues).map { case (num, kValue) => - val hash = CryptoUtil.sha256DLCAttestation(num.toString).bytes - priv.schnorrSignWithNonce(hash, kValue) - } - val eventId = oracleInfo.announcement.eventTLV match { - case v0: OracleEventV0TLV => v0.eventId - } - - OracleAttestmentV0TLV(eventId, - priv.schnorrPublicKey, - sigs, - outcome.digits.map(_.toString)) - } - } + val recipientWinSigs = buildAttestments(recipientWinOutcomes) // Shuffle to make sure ordering doesn't matter (Random.shuffle(initiatorWinSigs), Random.shuffle(recipientWinSigs)) @@ -242,4 +203,32 @@ class DLCMultiOracleExactNumericExecutionTest extends BitcoinSDualWalletTest { aggregateSignature == statusB.oracleSig } } + + /** Builds an attestment for the given numeric oracle outcome */ + private def buildAttestments( + outcome: NumericOracleOutcome): Vector[OracleAttestmentTLV] = { + privateKeys.zip(kValues).flatMap { case (priv, kValues) => + val outcomeOpt = + outcome.oraclesAndOutcomes.find(_._1.publicKey == priv.schnorrPublicKey) + + outcomeOpt.map { case (oracleInfo, outcome) => + val neededPadding = numDigits - outcome.digits.length + val digitsPadded = outcome.digits ++ Vector.fill(neededPadding)(0) + val sigs = digitsPadded.zip(kValues).map { case (num, kValue) => + val hash = CryptoUtil.sha256DLCAttestation(num.toString).bytes + priv.schnorrSignWithNonce(hash, kValue) + } + val eventId = oracleInfo.announcement.eventTLV match { + case v0: OracleEventV0TLV => v0.eventId + } + + require(kValues.length == sigs.length, + s"kValues.length=${kValues.length} sigs.length=${sigs.length}") + OracleAttestmentV0TLV(eventId, + priv.schnorrPublicKey, + sigs, + digitsPadded.map(_.toString)) + } + } + } } diff --git a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCMultiOracleNumericExecutionTest.scala b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCMultiOracleNumericExecutionTest.scala index 8ea7093886..7ed5915526 100644 --- a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCMultiOracleNumericExecutionTest.scala +++ b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCMultiOracleNumericExecutionTest.scala @@ -81,26 +81,7 @@ class DLCMultiOracleNumericExecutionTest initiatorWinVec, Some(params)) - val initiatorWinSigs = - privateKeys.zip(kValues).flatMap { case (priv, kValues) => - val outcomeOpt = initWinOutcomes.oraclesAndOutcomes.find( - _._1.publicKey == priv.schnorrPublicKey) - - outcomeOpt.map { case (oracleInfo, outcome) => - val sigs = outcome.digits.zip(kValues).map { case (num, kValue) => - val hash = CryptoUtil.sha256DLCAttestation(num.toString).bytes - priv.schnorrSignWithNonce(hash, kValue) - } - val eventId = oracleInfo.announcement.eventTLV match { - case v0: OracleEventV0TLV => v0.eventId - } - - OracleAttestmentV0TLV(eventId, - priv.schnorrPublicKey, - sigs, - outcome.digits.map(_.toString)) - } - } + val initiatorWinSigs = buildAttestments(initWinOutcomes) val recipientChosenOracles = Random.shuffle(oracleIndices).take(oracleInfo.threshold).sorted @@ -121,26 +102,8 @@ class DLCMultiOracleNumericExecutionTest recipientWinVec, Some(params)) - val recipientWinSigs = - privateKeys.zip(kValues).flatMap { case (priv, kValues) => - val outcomeOpt = recipientWinOutcomes.oraclesAndOutcomes.find( - _._1.publicKey == priv.schnorrPublicKey) - - outcomeOpt.map { case (oracleInfo, outcome) => - val sigs = outcome.digits.zip(kValues).map { case (num, kValue) => - val hash = CryptoUtil.sha256DLCAttestation(num.toString).bytes - priv.schnorrSignWithNonce(hash, kValue) - } - val eventId = oracleInfo.announcement.eventTLV match { - case v0: OracleEventV0TLV => v0.eventId - } - - OracleAttestmentV0TLV(eventId, - priv.schnorrPublicKey, - sigs, - outcome.digits.map(_.toString)) - } - } + val recipientWinSigs: Vector[OracleAttestmentTLV] = buildAttestments( + recipientWinOutcomes) // Shuffle to make sure ordering doesn't matter (Random.shuffle(initiatorWinSigs), Random.shuffle(recipientWinSigs)) @@ -249,4 +212,32 @@ class DLCMultiOracleNumericExecutionTest aggregateSignature == statusB.oracleSig } } + + /** Builds an attestment for the given numeric oracle outcome */ + private def buildAttestments( + outcome: NumericOracleOutcome): Vector[OracleAttestmentTLV] = { + privateKeys.zip(kValues).flatMap { case (priv, kValues) => + val outcomeOpt = + outcome.oraclesAndOutcomes.find(_._1.publicKey == priv.schnorrPublicKey) + + outcomeOpt.map { case (oracleInfo, outcome) => + val neededPadding = numDigits - outcome.digits.length + val digitsPadded = outcome.digits ++ Vector.fill(neededPadding)(0) + val sigs = digitsPadded.zip(kValues).map { case (num, kValue) => + val hash = CryptoUtil.sha256DLCAttestation(num.toString).bytes + priv.schnorrSignWithNonce(hash, kValue) + } + val eventId = oracleInfo.announcement.eventTLV match { + case v0: OracleEventV0TLV => v0.eventId + } + + require(kValues.length == sigs.length, + s"kValues.length=${kValues.length} sigs.length=${sigs.length}") + OracleAttestmentV0TLV(eventId, + priv.schnorrPublicKey, + sigs, + digitsPadded.map(_.toString)) + } + } + } } diff --git a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCNumericExecutionTest.scala b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCNumericExecutionTest.scala index 3134141742..6e32e2db45 100644 --- a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCNumericExecutionTest.scala +++ b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCNumericExecutionTest.scala @@ -164,6 +164,34 @@ class DLCNumericExecutionTest extends BitcoinSDualWalletTest { } } + it must "throw an exception for a numeric contract when do not have all the oracle signatures/outcomes" in { + wallets => + val resultF = for { + contractId <- getContractId(wallets._1.wallet) + status <- getDLCStatus(wallets._2.wallet) + (_, goodAttestment) = getSigs(status.contractInfo) + //purposefully drop these + //we cannot drop just a sig, or just an outcome because + //of invariants in OracleAttestmentV0TLV + badSigs = goodAttestment.sigs.dropRight(1) + badOutcomes = goodAttestment.outcomes.dropRight(1) + badAttestment = OracleAttestmentV0TLV(eventId = goodAttestment.eventId, + publicKey = + goodAttestment.publicKey, + sigs = badSigs, + outcomes = badOutcomes) + func = (wallet: DLCWallet) => + wallet.executeDLC(contractId, badAttestment) + + result <- dlcExecutionTest(wallets = wallets, + asInitiator = false, + func = func, + expectedOutputs = 1) + } yield assert(result) + + recoverToSucceededIf[IllegalArgumentException](resultF) + } + private def verifyingMatchingOracleSigs( statusA: Claimed, statusB: RemoteClaimed): Boolean = { 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 c3f982ae04..62d595c8ac 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 @@ -1355,21 +1355,9 @@ abstract class DLCWallet announcementData, nonceDbs) - oracleSigs = - sigs.foldLeft(Vector.empty[OracleSignatures]) { (acc, sig) => - // Nonces should be unique so searching for the first nonce should be safe - val firstNonce = sig.sigs.head.rx - announcementTLVs - .find( - _.eventTLV.nonces.headOption - .contains(firstNonce)) match { - case Some(announcement) => - acc :+ OracleSignatures(SingleOracleInfo(announcement), sig.sigs) - case None => - throw new RuntimeException( - s"Cannot find announcement for associated public key, ${sig.publicKey.hex}") - } - } + oracleSigs = DLCUtil.buildOracleSignatures(announcements = + announcementTLVs, + attestments = sigs.toVector) tx <- executeDLC(contractId, oracleSigs) } yield tx diff --git a/testkit-core/src/main/scala/org/bitcoins/testkitcore/dlc/DLCTest.scala b/testkit-core/src/main/scala/org/bitcoins/testkitcore/dlc/DLCTest.scala index 8467963688..19c1451e16 100644 --- a/testkit-core/src/main/scala/org/bitcoins/testkitcore/dlc/DLCTest.scala +++ b/testkit-core/src/main/scala/org/bitcoins/testkitcore/dlc/DLCTest.scala @@ -880,8 +880,11 @@ trait DLCTest { .get ._2 + val neededPadding = + singleOracleInfo.announcement.eventTLV.eventDescriptor.noncesNeeded - digitsToSign.digits.length + val paddedDigits = digitsToSign.digits ++ Vector.fill(neededPadding)(0) val sigs = - computeNumericOracleSignatures(digitsToSign.digits, + computeNumericOracleSignatures(paddedDigits, oraclePrivKeys(index), preCommittedKsPerOracle(index)) @@ -1044,7 +1047,8 @@ trait DLCTest { outcomeIndices: Vector[Long], contractParams: ContractParams)(implicit ec: ExecutionContext): Future[Assertion] = { - executeForCasesInUnion(outcomeIndices.map((0, _)), contractParams) + executeForCasesInUnion(outcomeIndices = outcomeIndices.map((0, _)), + contractParams = contractParams) } def executeForCasesInUnion( @@ -1110,12 +1114,14 @@ trait DLCTest { (numDigits, true, paramsOpt) } - val oracleSigs = genOracleSignatures(numOutcomes, - isMultiDigit, - singleContractInfo, - possibleOutcomesForContract, - outcomeIndex, - paramsOpt) + val oracleSigs = genOracleSignatures( + numOutcomesOrDigits = numOutcomes, + isNumeric = isMultiDigit, + contractInfo = singleContractInfo, + outcomes = possibleOutcomesForContract, + outcomeIndex = outcomeIndex, + paramsOpt = paramsOpt + ) for { offerOutcome <-