2022 02 03 issue 4032 (#4042)

* get all dlcWalletTest/test passing

* Get dlcTest/test working with ignored test cases

* WIP

* Rework match oracleSignatures

* Refactor checkSingleContractInfoOracleSigs to use matchOracleSignatures

* Clean up checkOracleSignaturesAgainstContract for disjoint, still doesn't pass test cases

* Some DRY in numeric test cases

* Add test case for enum contracts

* Fix disjoint union contract bug

* Refactor matching of oracle announcements and oracle signatures into DLCUtil

* Fix compile on on 2.12.x

* Address parts of code review
This commit is contained in:
Chris Stewart 2022-02-05 09:04:04 -06:00 committed by GitHub
parent 142612f034
commit 7a6f0430d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 253 additions and 112 deletions

View file

@ -749,7 +749,7 @@ lazy val dlcWalletTest = project
name := "bitcoin-s-dlc-wallet-test", name := "bitcoin-s-dlc-wallet-test",
libraryDependencies ++= Deps.dlcWalletTest libraryDependencies ++= Deps.dlcWalletTest
) )
.dependsOn(coreJVM % testAndCompile, dlcWallet, testkit, dlcTest) .dependsOn(coreJVM % testAndCompile, dlcWallet, testkit, testkitCoreJVM, dlcTest)
lazy val dlcNode = project lazy val dlcNode = project
.in(file("dlc-node")) .in(file("dlc-node"))

View file

@ -8,9 +8,14 @@ import org.bitcoins.core.protocol.dlc.models.DLCMessage.{
DLCAcceptWithoutSigs, DLCAcceptWithoutSigs,
DLCOffer 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.script.P2WSHWitnessV0
import org.bitcoins.core.protocol.tlv.{
OracleAnnouncementTLV,
OracleAttestmentTLV
}
import org.bitcoins.core.protocol.transaction.{Transaction, WitnessTransaction} import org.bitcoins.core.protocol.transaction.{Transaction, WitnessTransaction}
import org.bitcoins.core.util.sorted.OrderedAnnouncements
import org.bitcoins.crypto._ import org.bitcoins.crypto._
import scodec.bits.ByteVector import scodec.bits.ByteVector
@ -187,4 +192,98 @@ object DLCUtil {
outputIdx = fundingOutputIdx, outputIdx = fundingOutputIdx,
tempContractId = offer.tempContractId) 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
}
} }

View file

@ -2,7 +2,7 @@ package org.bitcoins.core.protocol.dlc.execution
import org.bitcoins.core.currency.CurrencyUnit import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.protocol.dlc.build.DLCTxBuilder 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.models._
import org.bitcoins.core.protocol.dlc.sign.DLCTxSigner import org.bitcoins.core.protocol.dlc.sign.DLCTxSigner
import org.bitcoins.core.protocol.transaction.{Transaction, WitnessTransaction} import org.bitcoins.core.protocol.transaction.{Transaction, WitnessTransaction}
@ -136,6 +136,9 @@ object DLCExecutor {
fundingTx: Transaction, fundingTx: Transaction,
fundOutputIndex: Int fundOutputIndex: Int
): ExecutedDLCOutcome = { ): ExecutedDLCOutcome = {
require(
DLCUtil.checkOracleSignaturesAgainstContract(contractInfo, oracleSigs),
s"Incorrect oracle signatures and contract combination")
val sigOracles = oracleSigs.map(_.oracle) val sigOracles = oracleSigs.map(_.oracle)
val oracleInfoOpt = contractInfo.oracleInfos.find { oracleInfo => val oracleInfoOpt = contractInfo.oracleInfos.find { oracleInfo =>

View file

@ -3,16 +3,16 @@ package org.bitcoins.dlc.wallet
import org.bitcoins.core.currency.Satoshis import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.number.UInt32 import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.dlc.models.DLCMessage.DLCOffer 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.{ import org.bitcoins.core.protocol.dlc.models.DLCStatus.{
Claimed, Claimed,
Refunded, Refunded,
RemoteClaimed RemoteClaimed
} }
import org.bitcoins.core.protocol.dlc.models.{
DLCState,
DisjointUnionContractInfo,
SingleContractInfo
}
import org.bitcoins.core.protocol.tlv._ import org.bitcoins.core.protocol.tlv._
import org.bitcoins.core.script.interpreter.ScriptInterpreter import org.bitcoins.core.script.interpreter.ScriptInterpreter
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
@ -416,4 +416,41 @@ class DLCExecutionTest extends BitcoinSDualWalletTest {
_ <- walletA.listDLCs() _ <- walletA.listDLCs()
} yield succeed } 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)
}
} }

View file

@ -74,27 +74,7 @@ class DLCMultiOracleExactNumericExecutionTest extends BitcoinSDualWalletTest {
initiatorWinVec, initiatorWinVec,
None) None)
val initiatorWinSigs = val initiatorWinSigs = buildAttestments(initWinOutcomes)
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 recipientChosenOracles = val recipientChosenOracles =
Random.shuffle(oracleIndices).take(oracleInfo.threshold).sorted Random.shuffle(oracleIndices).take(oracleInfo.threshold).sorted
@ -114,26 +94,7 @@ class DLCMultiOracleExactNumericExecutionTest extends BitcoinSDualWalletTest {
recipientWinVec, recipientWinVec,
None) None)
val recipientWinSigs = val recipientWinSigs = buildAttestments(recipientWinOutcomes)
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))
}
}
// Shuffle to make sure ordering doesn't matter // Shuffle to make sure ordering doesn't matter
(Random.shuffle(initiatorWinSigs), Random.shuffle(recipientWinSigs)) (Random.shuffle(initiatorWinSigs), Random.shuffle(recipientWinSigs))
@ -242,4 +203,32 @@ class DLCMultiOracleExactNumericExecutionTest extends BitcoinSDualWalletTest {
aggregateSignature == statusB.oracleSig 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))
}
}
}
} }

View file

@ -81,26 +81,7 @@ class DLCMultiOracleNumericExecutionTest
initiatorWinVec, initiatorWinVec,
Some(params)) Some(params))
val initiatorWinSigs = val initiatorWinSigs = buildAttestments(initWinOutcomes)
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 recipientChosenOracles = val recipientChosenOracles =
Random.shuffle(oracleIndices).take(oracleInfo.threshold).sorted Random.shuffle(oracleIndices).take(oracleInfo.threshold).sorted
@ -121,26 +102,8 @@ class DLCMultiOracleNumericExecutionTest
recipientWinVec, recipientWinVec,
Some(params)) Some(params))
val recipientWinSigs = val recipientWinSigs: Vector[OracleAttestmentTLV] = buildAttestments(
privateKeys.zip(kValues).flatMap { case (priv, kValues) => recipientWinOutcomes)
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))
}
}
// Shuffle to make sure ordering doesn't matter // Shuffle to make sure ordering doesn't matter
(Random.shuffle(initiatorWinSigs), Random.shuffle(recipientWinSigs)) (Random.shuffle(initiatorWinSigs), Random.shuffle(recipientWinSigs))
@ -249,4 +212,32 @@ class DLCMultiOracleNumericExecutionTest
aggregateSignature == statusB.oracleSig 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))
}
}
}
} }

View file

@ -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( private def verifyingMatchingOracleSigs(
statusA: Claimed, statusA: Claimed,
statusB: RemoteClaimed): Boolean = { statusB: RemoteClaimed): Boolean = {

View file

@ -1355,21 +1355,9 @@ abstract class DLCWallet
announcementData, announcementData,
nonceDbs) nonceDbs)
oracleSigs = oracleSigs = DLCUtil.buildOracleSignatures(announcements =
sigs.foldLeft(Vector.empty[OracleSignatures]) { (acc, sig) => announcementTLVs,
// Nonces should be unique so searching for the first nonce should be safe attestments = sigs.toVector)
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}")
}
}
tx <- executeDLC(contractId, oracleSigs) tx <- executeDLC(contractId, oracleSigs)
} yield tx } yield tx

View file

@ -880,8 +880,11 @@ trait DLCTest {
.get .get
._2 ._2
val neededPadding =
singleOracleInfo.announcement.eventTLV.eventDescriptor.noncesNeeded - digitsToSign.digits.length
val paddedDigits = digitsToSign.digits ++ Vector.fill(neededPadding)(0)
val sigs = val sigs =
computeNumericOracleSignatures(digitsToSign.digits, computeNumericOracleSignatures(paddedDigits,
oraclePrivKeys(index), oraclePrivKeys(index),
preCommittedKsPerOracle(index)) preCommittedKsPerOracle(index))
@ -1044,7 +1047,8 @@ trait DLCTest {
outcomeIndices: Vector[Long], outcomeIndices: Vector[Long],
contractParams: ContractParams)(implicit contractParams: ContractParams)(implicit
ec: ExecutionContext): Future[Assertion] = { ec: ExecutionContext): Future[Assertion] = {
executeForCasesInUnion(outcomeIndices.map((0, _)), contractParams) executeForCasesInUnion(outcomeIndices = outcomeIndices.map((0, _)),
contractParams = contractParams)
} }
def executeForCasesInUnion( def executeForCasesInUnion(
@ -1110,12 +1114,14 @@ trait DLCTest {
(numDigits, true, paramsOpt) (numDigits, true, paramsOpt)
} }
val oracleSigs = genOracleSignatures(numOutcomes, val oracleSigs = genOracleSignatures(
isMultiDigit, numOutcomesOrDigits = numOutcomes,
singleContractInfo, isNumeric = isMultiDigit,
possibleOutcomesForContract, contractInfo = singleContractInfo,
outcomeIndex, outcomes = possibleOutcomesForContract,
paramsOpt) outcomeIndex = outcomeIndex,
paramsOpt = paramsOpt
)
for { for {
offerOutcome <- offerOutcome <-