From bce58ba33dc90281e77390dc052475ce8fc3f903 Mon Sep 17 00:00:00 2001 From: rorp Date: Fri, 11 Feb 2022 04:50:45 -0800 Subject: [PATCH] Validate announcement signatures on create/accept offer (#4071) * Validate announcement signatures on create/accept offer * unit tests --- .../dlc/wallet/WalletDLCSetupTest.scala | 47 +++++++++++++++++++ .../org/bitcoins/dlc/wallet/DLCWallet.scala | 20 ++++++++ .../testkit/wallet/DLCWalletUtil.scala | 30 ++++++++++++ 3 files changed, 97 insertions(+) diff --git a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/WalletDLCSetupTest.scala b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/WalletDLCSetupTest.scala index 72fdf1fc90..b72abd95f0 100644 --- a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/WalletDLCSetupTest.scala +++ b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/WalletDLCSetupTest.scala @@ -9,6 +9,7 @@ import org.bitcoins.core.protocol.tlv._ import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte import org.bitcoins.core.wallet.utxo.TxoState import org.bitcoins.crypto._ +import org.bitcoins.dlc.wallet.DLCWallet.InvalidAnnouncementSignature import org.bitcoins.dlc.wallet.internal.DLCDataManagement import org.bitcoins.testkit.wallet.DLCWalletUtil._ import org.bitcoins.testkit.wallet.FundWalletUtil.FundedDLCWallet @@ -856,4 +857,50 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { } yield res } + it must "fail to create an offer with an invalid announcement signature" in { + wallets => + val walletA = wallets._1.wallet + + val offerData: DLCOffer = DLCWalletUtil.invalidDLCOffer + + for { + res <- recoverToSucceededIf[InvalidAnnouncementSignature]( + walletA.createDLCOffer( + offerData.contractInfo, + offerData.totalCollateral, + Some(offerData.feeRate), + offerData.timeouts.contractMaturity.toUInt32, + UInt32.max + )) + } yield { + res + } + } + + it must "fail to accept an offer with an invalid announcement signature" in { + wallets => + val walletA = wallets._1.wallet + val walletB = wallets._2.wallet + + //https://test.oracle.suredbits.com/contract/enum/75b08299654dca23b80cf359db6afb6cfd6e55bc898b5397d3c0fe796dfc13f0/12fb3e5f091086329ed0d2a12c3fcfa80111a36ef3fc1ac9c2567076a57d6a73 + val contractInfo = ContractInfoV0TLV.fromHex( + "fdd82eeb00000000000186a0fda71026030359455300000000000186a0024e4f0000000000000000056f746865720000000000000000fda712b5fdd824b1596ec40d0dae3fdf54d9795ad51ec069970c6863a02d244663d39fd6bedadc0070349e1ba2e17583ee2d1cb3ae6fffaaa1c45039b61c5c4f1d0d864221c461745d1bcfab252c6dd9edd7aea4c5eeeef138f7ff7346061ea40143a9f5ae80baa9fdd8224d0001fa5b84283852400b21a840d5d5ca1cc31867c37326ad521aa50bebf3df4eea1a60b03280fdd8060f000303594553024e4f056f74686572135465746865722d52657365727665732d363342") + val feeRateOpt = Some(SatoshisPerVirtualByte(Satoshis.one)) + val totalCollateral = Satoshis(5000) + + for { + offer <- walletA.createDLCOffer(contractInfoTLV = contractInfo, + collateral = totalCollateral, + feeRateOpt = feeRateOpt, + locktime = UInt32.zero, + refundLT = UInt32.one) + invalidOffer = offer.copy(contractInfo = invalidContractInfo) + res <- recoverToSucceededIf[InvalidAnnouncementSignature]( + walletB.acceptDLCOffer(invalidOffer)) + } yield { + res + } + + } + } 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 19ea337a8d..1bc8d54eae 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 @@ -30,6 +30,7 @@ import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte import org.bitcoins.core.wallet.utxo._ import org.bitcoins.crypto._ import org.bitcoins.db.SafeDatabase +import org.bitcoins.dlc.wallet.DLCWallet.InvalidAnnouncementSignature import org.bitcoins.dlc.wallet.internal._ import org.bitcoins.dlc.wallet.models._ import org.bitcoins.dlc.wallet.util.{ @@ -269,6 +270,12 @@ abstract class DLCWallet locktime: UInt32, refundLocktime: UInt32): Future[DLCOffer] = { logger.info("Creating DLC Offer") + if (!validateAnnouncementSignatures(contractInfo.oracleInfos)) { + return Future.failed( + InvalidAnnouncementSignature( + s"Contract info contains invalid announcement signature(s)")) + } + val announcements = contractInfo.oracleInfos.head.singleOracleInfos.map(_.announcement) @@ -552,6 +559,10 @@ abstract class DLCWallet */ override def acceptDLCOffer(offer: DLCOffer): Future[DLCAccept] = { logger.debug("Calculating relevant wallet data for DLC Accept") + if (!validateAnnouncementSignatures(offer.oracleInfos)) { + return Future.failed(InvalidAnnouncementSignature( + s"Offer ${offer.tempContractId.hex} contains invalid announcement signature(s)")) + } val dlcId = calcDLCId(offer.fundingInputs.map(_.outPoint)) @@ -575,6 +586,12 @@ abstract class DLCWallet } yield dlcAccept } + private def validateAnnouncementSignatures( + oracleInfos: Vector[OracleInfo]): Boolean = { + oracleInfos.forall(infos => + infos.singleOracleInfos.forall(_.announcement.validateSignature)) + } + private def fundDLCAcceptMsg( offer: DLCOffer, collateral: CurrencyUnit, @@ -1726,6 +1743,9 @@ abstract class DLCWallet object DLCWallet extends WalletLogger { + case class InvalidAnnouncementSignature(message: String) + extends RuntimeException(message) + private case class DLCWalletImpl( nodeApi: NodeApi, chainQueryApi: ChainQueryApi, diff --git a/testkit/src/main/scala/org/bitcoins/testkit/wallet/DLCWalletUtil.scala b/testkit/src/main/scala/org/bitcoins/testkit/wallet/DLCWalletUtil.scala index 6f16211a2d..9c358fc0ec 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/wallet/DLCWalletUtil.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/wallet/DLCWalletUtil.scala @@ -67,12 +67,28 @@ object DLCWalletUtil extends Logging { rValue, sampleOutcomes.map(_._1)) + lazy val invalidOracleInfo: EnumSingleOracleInfo = { + val info = EnumSingleOracleInfo.dummyForKeys(oraclePrivKey, + rValue, + sampleOutcomes.map(_._1)) + val announcement = info.announcement.asInstanceOf[OracleAnnouncementV0TLV] + val invalidAnnouncement = + announcement.copy(announcementSignature = SchnorrDigitalSignature.dummy) + info.copy(announcement = invalidAnnouncement) + } + lazy val sampleContractOraclePair: ContractOraclePair.EnumPair = ContractOraclePair.EnumPair(sampleContractDescriptor, sampleOracleInfo) + lazy val invalidContractOraclePair: ContractOraclePair.EnumPair = + ContractOraclePair.EnumPair(sampleContractDescriptor, invalidOracleInfo) + lazy val sampleContractInfo: ContractInfo = SingleContractInfo(half, sampleContractOraclePair) + lazy val invalidContractInfo: ContractInfo = + SingleContractInfo(half, invalidContractOraclePair) + lazy val sampleOracleWinSig: SchnorrDigitalSignature = oraclePrivKey.schnorrSignWithNonce(winHash.bytes, kValue) @@ -164,6 +180,20 @@ object DLCWalletUtil extends Logging { timeouts = dummyTimeouts ) + lazy val invalidDLCOffer: DLCOffer = DLCOffer( + protocolVersionOpt = DLCOfferTLV.currentVersionOpt, + contractInfo = invalidContractInfo, + pubKeys = dummyDLCKeys, + totalCollateral = half, + fundingInputs = Vector(dummyFundingInputs.head), + changeAddress = dummyAddress, + payoutSerialId = sampleOfferPayoutSerialId, + changeSerialId = sampleOfferChangeSerialId, + fundOutputSerialId = sampleFundOutputSerialId, + feeRate = SatoshisPerVirtualByte(Satoshis(3)), + timeouts = dummyTimeouts + ) + lazy val sampleMultiNonceDLCOffer: DLCOffer = sampleDLCOffer.copy(contractInfo = multiNonceContractInfo)