From ac3bae403bb34152c1372211847026bfad67da1c Mon Sep 17 00:00:00 2001 From: Nadav Kohen Date: Tue, 18 May 2021 05:29:46 -0600 Subject: [PATCH] Pulled down all remaining non-wallet non-gui code on adaptor-dlc (#3101) --- .../commons/serializers/JsonWriters.scala | 2 +- .../scala/org/bitcoins/cli/ConsoleCli.scala | 61 +++++++--- .../gui/dlc/dialog/AcceptDLCDialog.scala | 2 +- .../gui/dlc/dialog/SignDLCDialog.scala | 2 +- .../core/protocol/dlc/DLCMessageTest.scala | 44 ++++++- .../protocol/dlc/build/DLCTxBuilder.scala | 61 ++++++---- .../dlc/compute/DLCAdaptorPointComputer.scala | 110 ++++++++++++++++++ .../core/protocol/dlc/compute/DLCUtil.scala | 48 ++++---- .../protocol/dlc/execution/DLCExecutor.scala | 9 +- .../protocol/dlc/execution/SetupDLC.scala | 11 +- .../protocol/dlc/models/ContractInfo.scala | 23 +++- ...clePair.scala => ContractOraclePair.scala} | 0 .../core/protocol/dlc/models/DLCMessage.scala | 12 +- .../protocol/dlc/models/DLCSignatures.scala | 16 ++- .../protocol/dlc/models/OracleOutcome.scala | 11 +- .../core/protocol/dlc/sign/DLCTxSigner.scala | 100 +++++++++------- .../dlc/verify/DLCSignatureVerifier.scala | 25 ++-- .../org/bitcoins/core/util/FutureUtil.scala | 16 ++- .../scala/org/bitcoins/crypto/ECKey.scala | 4 + .../bitcoins/db/DbCommonsColumnMappers.scala | 14 ++- .../org/bitcoins/dlc/DLCClientTest.scala | 106 +++++++++++++---- .../scala/org/bitcoins/dlc/SetupDLCTest.scala | 7 +- .../org/bitcoins/dlc/testgen/DLCTLVGen.scala | 2 +- .../bitcoins/dlc/testgen/DLCTestVector.scala | 6 +- .../org/bitcoins/dlc/testgen/DLCTxGen.scala | 10 +- .../testkitcore/dlc/TestDLCClient.scala | 2 +- .../testkit/BitcoinSTestAppConfig.scala | 8 +- 27 files changed, 507 insertions(+), 205 deletions(-) create mode 100644 core/src/main/scala/org/bitcoins/core/protocol/dlc/compute/DLCAdaptorPointComputer.scala rename core/src/main/scala/org/bitcoins/core/protocol/dlc/models/{ContractDescriptorOraclePair.scala => ContractOraclePair.scala} (100%) diff --git a/app-commons/src/main/scala/org/bitcoins/commons/serializers/JsonWriters.scala b/app-commons/src/main/scala/org/bitcoins/commons/serializers/JsonWriters.scala index 8cd656c406..b4c7395ea1 100644 --- a/app-commons/src/main/scala/org/bitcoins/commons/serializers/JsonWriters.scala +++ b/app-commons/src/main/scala/org/bitcoins/commons/serializers/JsonWriters.scala @@ -5,7 +5,7 @@ import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.{ WalletCreateFundedPsbtOptions } import org.bitcoins.core.currency.Bitcoins -import org.bitcoins.core.number.{UInt32, UInt64} +import org.bitcoins.core.number._ import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.protocol.ln.currency.MilliSatoshis import org.bitcoins.core.protocol.script.{ScriptPubKey, WitnessScriptPubKey} diff --git a/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala index e82e74b0f8..3b98d75927 100644 --- a/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala +++ b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala @@ -22,12 +22,7 @@ import org.bitcoins.core.psbt.PSBT import org.bitcoins.core.util.EnvUtil import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte import org.bitcoins.core.wallet.utxo.AddressLabelTag -import org.bitcoins.crypto.{ - AesPassword, - DoubleSha256DigestBE, - ECPublicKey, - Sha256DigestBE -} +import org.bitcoins.crypto._ import scodec.bits.ByteVector import scopt.OParser import ujson._ @@ -238,7 +233,8 @@ object ConsoleCli { ), cmd("acceptdlcofferfromfile") .action((_, conf) => - conf.copy(command = AcceptDLCOfferFromFile(new File("").toPath))) + conf.copy(command = + AcceptDLCOfferFromFile(new File("").toPath, None))) .text("Accepts a DLC offer given from another party") .children( arg[Path]("path") @@ -248,6 +244,14 @@ object ConsoleCli { case accept: AcceptDLCOfferFromFile => accept.copy(path = path) case other => other + })), + arg[Path]("destination") + .optional() + .action((dest, conf) => + conf.copy(command = conf.command match { + case accept: AcceptDLCOfferFromFile => + accept.copy(destination = Some(dest)) + case other => other })) ), cmd("signdlc") @@ -265,7 +269,7 @@ object ConsoleCli { ), cmd("signdlcfromfile") .action((_, conf) => - conf.copy(command = SignDLCFromFile(new File("").toPath))) + conf.copy(command = SignDLCFromFile(new File("").toPath, None))) .text("Signs a DLC") .children( arg[Path]("path") @@ -275,6 +279,14 @@ object ConsoleCli { case signDLC: SignDLCFromFile => signDLC.copy(path = path) case other => other + })), + arg[Path]("destination") + .optional() + .action((dest, conf) => + conf.copy(command = conf.command match { + case accept: SignDLCFromFile => + accept.copy(destination = Some(dest)) + case other => other })) ), cmd("adddlcsigs") @@ -383,6 +395,20 @@ object ConsoleCli { case other => other })) ), + cmd("canceldlc") + .action((_, conf) => + conf.copy(command = CancelDLC(Sha256DigestBE.empty))) + .text("Cancels a DLC and unreserves used utxos") + .children( + arg[Sha256DigestBE]("paramhash") + .required() + .action((paramHash, conf) => + conf.copy(command = conf.command match { + case cancelDLC: CancelDLC => + cancelDLC.copy(paramHash = paramHash) + case other => other + })) + ), cmd("getdlcs") .action((_, conf) => conf.copy(command = GetDLCs)) .text("Returns all dlcs in the wallet"), @@ -1440,12 +1466,13 @@ object ConsoleCli { ) case AcceptDLCOffer(offer) => RequestParam("acceptdlcoffer", Seq(up.writeJs(offer))) - case AcceptDLCOfferFromFile(path) => - RequestParam("acceptdlcofferfromfile", Seq(up.writeJs(path))) + case AcceptDLCOfferFromFile(path, dest) => + RequestParam("acceptdlcofferfromfile", + Seq(up.writeJs(path), up.writeJs(dest))) case SignDLC(accept) => RequestParam("signdlc", Seq(up.writeJs(accept))) - case SignDLCFromFile(path) => - RequestParam("signdlcfromfile", Seq(up.writeJs(path))) + case SignDLCFromFile(path, dest) => + RequestParam("signdlcfromfile", Seq(up.writeJs(path), up.writeJs(dest))) case AddDLCSigs(sigs) => RequestParam("adddlcsigs", Seq(up.writeJs(sigs))) case AddDLCSigsFromFile(path) => @@ -1462,6 +1489,8 @@ object ConsoleCli { case ExecuteDLCRefund(contractId, noBroadcast) => RequestParam("executedlcrefund", Seq(up.writeJs(contractId), up.writeJs(noBroadcast))) + case CancelDLC(paramHash) => + RequestParam("canceldlc", Seq(up.writeJs(paramHash))) // Wallet case GetBalance(isSats) => RequestParam("getbalance", Seq(up.writeJs(isSats))) @@ -1800,13 +1829,15 @@ object CliCommand { case class AcceptDLCOffer(offer: LnMessage[DLCOfferTLV]) extends AcceptDLCCliCommand - case class AcceptDLCOfferFromFile(path: Path) extends AcceptDLCCliCommand + case class AcceptDLCOfferFromFile(path: Path, destination: Option[Path]) + extends AcceptDLCCliCommand sealed trait SignDLCCliCommand extends AppServerCliCommand case class SignDLC(accept: LnMessage[DLCAcceptTLV]) extends SignDLCCliCommand - case class SignDLCFromFile(path: Path) extends SignDLCCliCommand + case class SignDLCFromFile(path: Path, destination: Option[Path]) + extends SignDLCCliCommand sealed trait AddDLCSigsCliCommand extends AppServerCliCommand @@ -1831,6 +1862,8 @@ object CliCommand { extends AppServerCliCommand with Broadcastable + case class CancelDLC(paramHash: Sha256DigestBE) extends AppServerCliCommand + case object GetDLCs extends AppServerCliCommand case class GetDLC(paramHash: Sha256DigestBE) extends AppServerCliCommand diff --git a/app/gui/src/main/scala/org/bitcoins/gui/dlc/dialog/AcceptDLCDialog.scala b/app/gui/src/main/scala/org/bitcoins/gui/dlc/dialog/AcceptDLCDialog.scala index df9e629ca2..77ed220d28 100644 --- a/app/gui/src/main/scala/org/bitcoins/gui/dlc/dialog/AcceptDLCDialog.scala +++ b/app/gui/src/main/scala/org/bitcoins/gui/dlc/dialog/AcceptDLCDialog.scala @@ -43,7 +43,7 @@ class AcceptDLCDialog // TODO figure how to validate when using a file offerDLCFile = None // reset offerFileChosenLabel.text = "" // reset - AcceptDLCOfferFromFile(file.toPath) + AcceptDLCOfferFromFile(file.toPath, None) case None => val offerHex = readStringFromNode(inputs(dlcOfferStr)) val offer = LnMessageFactory(DLCOfferTLV).fromHex(offerHex) diff --git a/app/gui/src/main/scala/org/bitcoins/gui/dlc/dialog/SignDLCDialog.scala b/app/gui/src/main/scala/org/bitcoins/gui/dlc/dialog/SignDLCDialog.scala index d0bb0948f5..5bf96dbd5b 100644 --- a/app/gui/src/main/scala/org/bitcoins/gui/dlc/dialog/SignDLCDialog.scala +++ b/app/gui/src/main/scala/org/bitcoins/gui/dlc/dialog/SignDLCDialog.scala @@ -29,7 +29,7 @@ class SignDLCDialog case Some(file) => acceptDLCFile = None // reset acceptFileChosenLabel.text = "" // reset - SignDLCFromFile(file.toPath) + SignDLCFromFile(file.toPath, None) case None => val acceptHex = readStringFromNode(inputs(dlcAcceptStr)) diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/dlc/DLCMessageTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/dlc/DLCMessageTest.scala index 9d72fd1fa4..3c720f82f8 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/dlc/DLCMessageTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/dlc/DLCMessageTest.scala @@ -4,12 +4,17 @@ import org.bitcoins.core.currency.Satoshis import org.bitcoins.core.number.{UInt32, UInt64} import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.protocol.BlockStamp.{BlockHeight, BlockTime} -import org.bitcoins.core.protocol.dlc.models.DLCMessage.{DLCAccept, DLCOffer} +import org.bitcoins.core.protocol.dlc.models.DLCMessage.{ + DLCAccept, + DLCOffer, + DLCSign +} import org.bitcoins.core.protocol.dlc.models._ import org.bitcoins.core.protocol.tlv.EnumOutcome import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte import org.bitcoins.crypto._ +import org.bitcoins.testkitcore.gen.{LnMessageGen, TLVGen} import org.bitcoins.testkitcore.util.BitcoinSJvmTest class DLCMessageTest extends BitcoinSJvmTest { @@ -80,14 +85,41 @@ class DLCMessageTest extends BitcoinSJvmTest { dummyAddress, payoutSerialId = UInt64.zero, changeSerialId = UInt64.one, - CETSignatures(Vector( - EnumOracleOutcome( - Vector(dummyOracle), - EnumOutcome(dummyStr)) -> ECAdaptorSignature.dummy), - dummySig), + CETSignatures( + Vector( + EnumOracleOutcome( + Vector(dummyOracle), + EnumOutcome(dummyStr)).sigPoint -> ECAdaptorSignature.dummy), + dummySig), DLCAccept.NoNegotiationFields, Sha256Digest.empty ) ) } + + it must "be able to go back and forth between TLV and deserialized" in { + forAll(TLVGen.dlcOfferTLVAcceptTLVSignTLV) { + case (offerTLV, acceptTLV, signTLV) => + val offer = DLCOffer.fromTLV(offerTLV) + val accept = DLCAccept.fromTLV(acceptTLV, offer) + val sign = DLCSign.fromTLV(signTLV, offer) + + assert(offer.toTLV == offerTLV) + assert(accept.toTLV == acceptTLV) + assert(sign.toTLV == signTLV) + } + } + + it must "be able to go back and forth between LN Message and deserialized" in { + forAll(LnMessageGen.dlcOfferMessageAcceptMessageSignMessage) { + case (offerMsg, acceptMsg, signMsg) => + val offer = DLCOffer.fromMessage(offerMsg) + val accept = DLCAccept.fromMessage(acceptMsg, offer) + val sign = DLCSign.fromMessage(signMsg, offer) + + assert(offer.toMessage == offerMsg) + assert(accept.toMessage == acceptMsg) + assert(sign.toMessage == signMsg) + } + } } diff --git a/core/src/main/scala/org/bitcoins/core/protocol/dlc/build/DLCTxBuilder.scala b/core/src/main/scala/org/bitcoins/core/protocol/dlc/build/DLCTxBuilder.scala index 21838c2ebf..fa2e1e1dc0 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/dlc/build/DLCTxBuilder.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/dlc/build/DLCTxBuilder.scala @@ -14,6 +14,7 @@ import org.bitcoins.core.protocol.dlc.models._ import org.bitcoins.core.protocol.script._ import org.bitcoins.core.protocol.transaction._ import org.bitcoins.core.protocol.{BitcoinAddress, BlockTimeStamp} +import org.bitcoins.core.util.Indexed import org.bitcoins.core.wallet.builder.DualFundingTxFinalizer import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte import org.bitcoins.core.wallet.utxo.{ @@ -172,14 +173,19 @@ case class DLCTxBuilder(offer: DLCOffer, accept: DLCAcceptWithoutSigs) { /** Constructs the unsigned Contract Execution Transaction (CET) * for a given outcome hash */ - def buildCET(msg: OracleOutcome): WitnessTransaction = { - buildCETs(Vector(msg)).head + def buildCET(adaptorPoint: Indexed[ECPublicKey]): WitnessTransaction = { + buildCETs(Vector(adaptorPoint)).head } - def buildCETsMap(msgs: Vector[OracleOutcome]): Vector[OutcomeCETPair] = { + def buildCET(adaptorPoint: ECPublicKey, index: Int): WitnessTransaction = { + buildCET(Indexed(adaptorPoint, index)) + } + + def buildCETsMap(adaptorPoints: Vector[Indexed[ECPublicKey]]): Vector[ + AdaptorPointCETPair] = { DLCTxBuilder .buildCETs( - msgs, + adaptorPoints, contractInfo, offerFundingKey, offerFinalAddress.scriptPubKey, @@ -193,8 +199,9 @@ case class DLCTxBuilder(offer: DLCOffer, accept: DLCAcceptWithoutSigs) { ) } - def buildCETs(msgs: Vector[OracleOutcome]): Vector[WitnessTransaction] = { - buildCETsMap(msgs).map(_.wtx) + def buildCETs(adaptorPoints: Vector[Indexed[ECPublicKey]]): Vector[ + WitnessTransaction] = { + buildCETsMap(adaptorPoints).map(_.wtx) } /** Constructs the unsigned refund transaction */ @@ -318,7 +325,7 @@ object DLCTxBuilder { } def buildCET( - outcome: OracleOutcome, + adaptorPoint: Indexed[ECPublicKey], contractInfo: ContractInfo, offerFundingKey: ECPublicKey, offerFinalSPK: ScriptPubKey, @@ -328,22 +335,24 @@ object DLCTxBuilder { acceptSerialId: UInt64, timeouts: DLCTimeouts, fundingOutputRef: OutputReference): WitnessTransaction = { - val Vector(OutcomeCETPair(_, cet)) = buildCETs(Vector(outcome), - contractInfo, - offerFundingKey, - offerFinalSPK, - offerSerialId, - acceptFundingKey, - acceptFinalSPK, - acceptSerialId, - timeouts, - fundingOutputRef) + val Vector(AdaptorPointCETPair(_, cet)) = buildCETs( + Vector(adaptorPoint), + contractInfo, + offerFundingKey, + offerFinalSPK, + offerSerialId, + acceptFundingKey, + acceptFinalSPK, + acceptSerialId, + timeouts, + fundingOutputRef + ) cet } def buildCETs( - outcomes: Vector[OracleOutcome], + adaptorPoints: Vector[Indexed[ECPublicKey]], contractInfo: ContractInfo, offerFundingKey: ECPublicKey, offerFinalSPK: ScriptPubKey, @@ -352,7 +361,7 @@ object DLCTxBuilder { acceptFinalSPK: ScriptPubKey, acceptSerialId: UInt64, timeouts: DLCTimeouts, - fundingOutputRef: OutputReference): Vector[OutcomeCETPair] = { + fundingOutputRef: OutputReference): Vector[AdaptorPointCETPair] = { val builder = DLCCETBuilder(contractInfo, offerFundingKey, @@ -364,13 +373,17 @@ object DLCTxBuilder { timeouts, fundingOutputRef) - outcomes.map { outcome => - OutcomeCETPair(outcome, builder.buildCET(outcome)) + val outcomes = adaptorPoints.map { case Indexed(_, index) => + contractInfo.allOutcomes(index) + } + + adaptorPoints.zip(outcomes).map { case (Indexed(sigPoint, _), outcome) => + AdaptorPointCETPair(sigPoint, builder.buildCET(outcome)) } } def buildCETs( - outcomes: Vector[OracleOutcome], + adaptorPoints: Vector[Indexed[ECPublicKey]], contractInfo: ContractInfo, offerFundingKey: ECPublicKey, offerFinalSPK: ScriptPubKey, @@ -380,13 +393,13 @@ object DLCTxBuilder { acceptSerialId: UInt64, timeouts: DLCTimeouts, fundingTx: Transaction, - fundOutputIndex: Int): Vector[OutcomeCETPair] = { + fundOutputIndex: Int): Vector[AdaptorPointCETPair] = { val fundingOutPoint = TransactionOutPoint(fundingTx.txId, UInt32(fundOutputIndex)) val fundingOutputRef = OutputReference(fundingOutPoint, fundingTx.outputs(fundOutputIndex)) - buildCETs(outcomes, + buildCETs(adaptorPoints, contractInfo, offerFundingKey, offerFinalSPK, diff --git a/core/src/main/scala/org/bitcoins/core/protocol/dlc/compute/DLCAdaptorPointComputer.scala b/core/src/main/scala/org/bitcoins/core/protocol/dlc/compute/DLCAdaptorPointComputer.scala new file mode 100644 index 0000000000..b8a41e22fa --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/dlc/compute/DLCAdaptorPointComputer.scala @@ -0,0 +1,110 @@ +package org.bitcoins.core.protocol.dlc.compute + +import org.bitcoins.core.protocol.dlc.models.{ + ContractInfo, + EnumContractDescriptor, + NumericContractDescriptor +} +import org.bitcoins.core.protocol.tlv.{ + EnumOutcome, + SignedNumericOutcome, + UnsignedNumericOutcome +} +import org.bitcoins.crypto.{ + CryptoUtil, + ECPublicKey, + FieldElement, + SchnorrPublicKey +} +import scodec.bits.ByteVector + +/** Responsible for optimized computation of DLC adaptor point batches. */ +object DLCAdaptorPointComputer { + + private val base: Int = 2 + + private lazy val numericPossibleOutcomes: Vector[ByteVector] = { + 0 + .until(base) + .toVector + .map(_.toString) + .map(CryptoUtil.serializeForHash) + } + + /** Computes: + * nonce + outcomeHash*pubKey + * where outcomeHash is as specified in the DLC spec. + * @see https://github.com/discreetlogcontracts/dlcspecs/blob/master/Oracle.md#signing-algorithm + */ + def computePoint( + pubKey: SchnorrPublicKey, + nonce: ECPublicKey, + outcome: ByteVector): ECPublicKey = { + val hash = CryptoUtil + .sha256SchnorrChallenge( + nonce.schnorrNonce.bytes ++ pubKey.bytes ++ CryptoUtil + .sha256DLCAttestation(outcome) + .bytes) + .bytes + + nonce.add(pubKey.publicKey.tweakMultiply(FieldElement(hash))) + } + + /** Efficiently computes all adaptor points, in order, for a given ContractInfo. + * @see https://medium.com/crypto-garage/optimizing-numeric-outcome-dlc-creation-6d6091ac0e47 + */ + def computeAdaptorPoints(contractInfo: ContractInfo): Vector[ECPublicKey] = { + // The possible messages a single nonce may be used to sign + val possibleOutcomes: Vector[ByteVector] = + contractInfo.contractDescriptor match { + case enum: EnumContractDescriptor => + enum.keys.map(_.outcome).map(CryptoUtil.serializeForHash) + case _: NumericContractDescriptor => numericPossibleOutcomes + } + + // Oracle -> Nonce -> Outcome -> SubSigPoint + // These are the points that are then combined to construct aggregate points. + val preComputeTable: Vector[Vector[Vector[ECPublicKey]]] = + contractInfo.oracleInfo.singleOracleInfos.map { info => + val announcement = info.announcement + val pubKey = announcement.publicKey + val nonces = announcement.eventTLV.nonces.map(_.publicKey) + + nonces.map { nonce => + possibleOutcomes.map { outcome => + computePoint(pubKey, nonce, outcome) + } + } + } + + val oraclesAndOutcomes = contractInfo.allOutcomes.map(_.oraclesAndOutcomes) + + oraclesAndOutcomes.map { oracleAndOutcome => + // For the given oracleAndOutcome, look up the point in the preComputeTable + val subSigPoints = oracleAndOutcome.flatMap { case (info, outcome) => + val oracleIndex = + contractInfo.oracleInfo.singleOracleInfos.indexOf(info) + val outcomeIndices = outcome match { + case outcome: EnumOutcome => + Vector( + contractInfo.contractDescriptor + .asInstanceOf[EnumContractDescriptor] + .keys + .indexOf(outcome) + ) + case UnsignedNumericOutcome(digits) => digits + case _: SignedNumericOutcome => + throw new UnsupportedOperationException( + "Signed numeric outcomes not supported!") + } + + outcomeIndices.zipWithIndex.map { case (outcomeIndex, nonceIndex) => + preComputeTable(oracleIndex)(nonceIndex)(outcomeIndex) + } + } + + // TODO: Memoization of sub-combinations for further optimization! + CryptoUtil.combinePubKeys(subSigPoints) + } + } +} 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 cbdb1bba07..8bfac7604a 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 @@ -24,43 +24,38 @@ object DLCUtil { * This method is used to search through possible cetSigs until the correct * one is found by validating the returned signature. * - * @param outcome A potential outcome that could have been executed for + * @param adaptorPoint A potential adaptor point that could have been executed for * @param adaptorSig The adaptor signature corresponding to outcome * @param cetSig The actual signature for local's key found on-chain on a CET */ private def sigFromOutcomeAndSigs( - outcome: OracleOutcome, + adaptorPoint: ECPublicKey, adaptorSig: ECAdaptorSignature, - cetSig: ECDigitalSignature): Try[SchnorrDigitalSignature] = { - val sigPubKey = outcome.sigPoint - + cetSig: ECDigitalSignature): Try[FieldElement] = { // This value is either the oracle signature S value or it is // useless garbage, but we don't know in this scope, the caller // must do further work to check this. - val possibleOracleST = Try { - sigPubKey + Try { + adaptorPoint .extractAdaptorSecret(adaptorSig, ECDigitalSignature(cetSig.bytes.init)) .fieldElement } - - possibleOracleST.map { possibleOracleS => - SchnorrDigitalSignature(outcome.aggregateNonce, possibleOracleS) - } } def computeOutcome( completedSig: ECDigitalSignature, - possibleAdaptorSigs: Vector[(OracleOutcome, ECAdaptorSignature)]): Option[ - (SchnorrDigitalSignature, OracleOutcome)] = { - val sigOpt = possibleAdaptorSigs.find { case (outcome, adaptorSig) => + possibleAdaptorSigs: Vector[(ECPublicKey, ECAdaptorSignature)]): Option[ + (FieldElement, ECPublicKey)] = { + val sigOpt = possibleAdaptorSigs.find { case (adaptorPoint, adaptorSig) => val possibleOracleSigT = - sigFromOutcomeAndSigs(outcome, adaptorSig, completedSig) + sigFromOutcomeAndSigs(adaptorPoint, adaptorSig, completedSig) - possibleOracleSigT.isSuccess && possibleOracleSigT.get.sig.getPublicKey == outcome.sigPoint + possibleOracleSigT.isSuccess && possibleOracleSigT.get.getPublicKey == adaptorPoint } - sigOpt.map { case (outcome, adaptorSig) => - (sigFromOutcomeAndSigs(outcome, adaptorSig, completedSig).get, outcome) + sigOpt.map { case (adaptorPoint, adaptorSig) => + (sigFromOutcomeAndSigs(adaptorPoint, adaptorSig, completedSig).get, + adaptorPoint) } } @@ -69,9 +64,11 @@ object DLCUtil { offerFundingKey: ECPublicKey, acceptFundingKey: ECPublicKey, contractInfo: ContractInfo, - localAdaptorSigs: Vector[(OracleOutcome, ECAdaptorSignature)], + localAdaptorSigs: Vector[(ECPublicKey, ECAdaptorSignature)], cet: WitnessTransaction): Option[ (SchnorrDigitalSignature, OracleOutcome)] = { + val allAdaptorPoints = contractInfo.adaptorPoints + val cetSigs = cet.witness.head .asInstanceOf[P2WSHWitnessV0] .signatures @@ -82,8 +79,8 @@ object DLCUtil { val outcomeValues = cet.outputs.map(_.value).sorted val totalCollateral = contractInfo.totalCollateral - val possibleOutcomes = contractInfo.allOutcomesAndPayouts - .filter { case (_, amt) => + val possibleOutcomes = contractInfo.allOutcomesAndPayouts.zipWithIndex + .filter { case ((_, amt), _) => val amts = Vector(amt, totalCollateral - amt) .filter(_ >= Policy.dustThreshold) .sorted @@ -95,7 +92,7 @@ object DLCUtil { Math.abs((amts.head - outcomeValues.head).satoshis.toLong) <= 1 && Math .abs((amts.last - outcomeValues.last).satoshis.toLong) <= 1 } - .map(_._1) + .map { case (_, index) => allAdaptorPoints(index) } val (offerCETSig, acceptCETSig) = if (offerFundingKey.hex.compareTo(acceptFundingKey.hex) > 0) { @@ -114,6 +111,11 @@ object DLCUtil { offerCETSig } - computeOutcome(cetSig, outcomeSigs) + computeOutcome(cetSig, outcomeSigs).map { case (s, adaptorPoint) => + val index = allAdaptorPoints.indexOf(adaptorPoint) + val outcome: OracleOutcome = contractInfo.allOutcomes(index) + + (SchnorrDigitalSignature(outcome.aggregateNonce, s), outcome) + } } } 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 f7b422e2a0..799affe8b2 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 @@ -7,6 +7,7 @@ import org.bitcoins.core.protocol.dlc.models._ import org.bitcoins.core.protocol.dlc.sign.DLCTxSigner import org.bitcoins.core.protocol.transaction.{Transaction, WitnessTransaction} import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature +import org.bitcoins.core.util.Indexed import org.bitcoins.crypto.{AdaptorSign, ECPublicKey} import scala.util.{Success, Try} @@ -51,7 +52,7 @@ case class DLCExecutor(signer: DLCTxSigner) { } val CETSignatures(outcomeSigs, refundSig) = cetSigs - val msgs = outcomeSigs.map(_._1) + val msgs = Indexed(outcomeSigs.map(_._1)) val cets = cetsOpt match { case Some(cets) => cets case None => builder.buildCETs(msgs) @@ -123,7 +124,7 @@ object DLCExecutor { * a valid set of expected oracle signatures as per the oracle announcements in the ContractInfo. */ def executeDLC( - remoteCETInfos: Vector[(OracleOutcome, CETInfo)], + remoteCETInfos: Vector[(ECPublicKey, CETInfo)], oracleSigs: Vector[OracleSignatures], fundingKey: AdaptorSign, remoteFundingPubKey: ECPublicKey, @@ -141,7 +142,9 @@ object DLCExecutor { } val msgAndCETInfoOpt = msgOpt.flatMap { msg => - remoteCETInfos.find(_._1 == msg) + remoteCETInfos + .find(_._1 == msg.sigPoint) + .map { case (_, info) => (msg, info) } } val (msg, ucet, remoteAdaptorSig) = msgAndCETInfoOpt match { diff --git a/core/src/main/scala/org/bitcoins/core/protocol/dlc/execution/SetupDLC.scala b/core/src/main/scala/org/bitcoins/core/protocol/dlc/execution/SetupDLC.scala index 3771aa679a..c18f65c498 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/dlc/execution/SetupDLC.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/dlc/execution/SetupDLC.scala @@ -1,12 +1,11 @@ package org.bitcoins.core.protocol.dlc.execution -import org.bitcoins.core.protocol.dlc.models.OracleOutcome import org.bitcoins.core.protocol.transaction.{Transaction, WitnessTransaction} -import org.bitcoins.crypto.ECAdaptorSignature +import org.bitcoins.crypto.{ECAdaptorSignature, ECPublicKey} case class SetupDLC( fundingTx: Transaction, - cets: Vector[(OracleOutcome, CETInfo)], + cets: Vector[(ECPublicKey, CETInfo)], refundTx: WitnessTransaction) { cets.foreach { case (msg, cetInfo) => require( @@ -25,12 +24,12 @@ case class SetupDLC( s"RefundTx is not spending the funding tx, ${refundTx.inputs.head}" ) - def getCETInfo(outcome: OracleOutcome): CETInfo = { - cets.find(_._1 == outcome) match { + def getCETInfo(adaptorPoint: ECPublicKey): CETInfo = { + cets.find(_._1 == adaptorPoint) match { case Some((_, info)) => info case None => throw new IllegalArgumentException( - s"No CET found for the given outcome $outcome") + s"No CET found for the given adaptor point $adaptorPoint") } } } diff --git a/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/ContractInfo.scala b/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/ContractInfo.scala index efaa065ce9..8c7ffa261c 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/ContractInfo.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/ContractInfo.scala @@ -5,7 +5,10 @@ import org.bitcoins.core.protocol.dlc.compute.CETCalculator.{ CETOutcome, MultiOracleOutcome } -import org.bitcoins.core.protocol.dlc.compute.CETCalculator +import org.bitcoins.core.protocol.dlc.compute.{ + CETCalculator, + DLCAdaptorPointComputer +} import org.bitcoins.core.protocol.dlc.models.DLCMessage.DLCAccept import org.bitcoins.core.protocol.tlv.{ ContractInfoV0TLV, @@ -13,6 +16,7 @@ import org.bitcoins.core.protocol.tlv.{ TLVSerializable, UnsignedNumericOutcome } +import org.bitcoins.core.util.Indexed import org.bitcoins.crypto.ECPublicKey import scala.collection.immutable.HashMap @@ -134,23 +138,30 @@ case class ContractInfo( /** Maps adpator points to their corresponding OracleOutcomes (which correspond to CETs) */ lazy val sigPointMap: Map[ECPublicKey, OracleOutcome] = - allOutcomes.map(outcome => outcome.sigPoint -> outcome).toMap + adaptorPoints.zip(allOutcomes).toMap /** Map OracleOutcomes (which correspond to CETs) to their adpator point and payouts */ lazy val outcomeMap: Map[OracleOutcome, (ECPublicKey, Satoshis, Satoshis)] = { val builder = HashMap.newBuilder[OracleOutcome, (ECPublicKey, Satoshis, Satoshis)] - allOutcomesAndPayouts.foreach { case (outcome, offerPayout) => - val acceptPayout = (totalCollateral - offerPayout).satoshis - val adaptorPoint = outcome.sigPoint + allOutcomesAndPayouts.zip(adaptorPoints).foreach { + case ((outcome, offerPayout), adaptorPoint) => + val acceptPayout = (totalCollateral - offerPayout).satoshis - builder.+=((outcome, (adaptorPoint, offerPayout, acceptPayout))) + builder.+=((outcome, (adaptorPoint, offerPayout, acceptPayout))) } builder.result() } + lazy val adaptorPoints: Vector[ECPublicKey] = { + DLCAdaptorPointComputer.computeAdaptorPoints(this) + } + + lazy val adaptorPointsIndexed: Vector[Indexed[ECPublicKey]] = Indexed( + adaptorPoints) + /** Checks if the given OracleSignatures exactly match the given OracleOutcome. * * Warning: This will return false if too many OracleSignatures are given. diff --git a/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/ContractDescriptorOraclePair.scala b/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/ContractOraclePair.scala similarity index 100% rename from core/src/main/scala/org/bitcoins/core/protocol/dlc/models/ContractDescriptorOraclePair.scala rename to core/src/main/scala/org/bitcoins/core/protocol/dlc/models/ContractOraclePair.scala diff --git a/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/DLCMessage.scala b/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/DLCMessage.scala index b5ed1c8cbf..8467802ec2 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/DLCMessage.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/DLCMessage.scala @@ -275,10 +275,10 @@ object DLCMessage { def fromTLV( accept: DLCAcceptTLV, network: NetworkParameters, - outcomes: Vector[OracleOutcome]): DLCAccept = { + adaptorPoints: Vector[ECPublicKey]): DLCAccept = { val outcomeSigs = accept.cetSignatures match { case CETSignaturesV0TLV(sigs) => - outcomes.zip(sigs) + adaptorPoints.zip(sigs) } DLCAccept( @@ -308,7 +308,7 @@ object DLCMessage { accept: DLCAcceptTLV, network: NetworkParameters, contractInfo: ContractInfo): DLCAccept = { - fromTLV(accept, network, contractInfo.allOutcomes) + fromTLV(accept, network, contractInfo.adaptorPoints) } def fromTLV(accept: DLCAcceptTLV, offer: DLCOffer): DLCAccept = { @@ -348,11 +348,11 @@ object DLCMessage { def fromTLV( sign: DLCSignTLV, fundingPubKey: ECPublicKey, - outcomes: Vector[OracleOutcome], + adaptorPoints: Vector[ECPublicKey], fundingOutPoints: Vector[TransactionOutPoint]): DLCSign = { val outcomeSigs = sign.cetSignatures match { case CETSignaturesV0TLV(sigs) => - outcomes.zip(sigs) + adaptorPoints.zip(sigs) } val sigs = sign.fundingSignatures match { @@ -376,7 +376,7 @@ object DLCMessage { def fromTLV(sign: DLCSignTLV, offer: DLCOffer): DLCSign = { fromTLV(sign, offer.pubKeys.fundingKey, - offer.contractInfo.allOutcomes, + offer.contractInfo.adaptorPoints, offer.fundingInputs.map(_.outPoint)) } diff --git a/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/DLCSignatures.scala b/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/DLCSignatures.scala index 7072d0c57e..509aacfcc3 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/DLCSignatures.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/DLCSignatures.scala @@ -4,8 +4,8 @@ import org.bitcoins.core.protocol.script.ScriptWitnessV0 import org.bitcoins.core.protocol.tlv.FundingSignaturesV0TLV import org.bitcoins.core.protocol.transaction.TransactionOutPoint import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature -import org.bitcoins.core.util.SeqWrapper -import org.bitcoins.crypto.ECAdaptorSignature +import org.bitcoins.core.util.{Indexed, SeqWrapper} +import org.bitcoins.crypto.{ECAdaptorSignature, ECPublicKey} sealed trait DLCSignatures @@ -35,13 +35,19 @@ case class FundingSignatures( } case class CETSignatures( - outcomeSigs: Vector[(OracleOutcome, ECAdaptorSignature)], + outcomeSigs: Vector[(ECPublicKey, ECAdaptorSignature)], refundSig: PartialSignature) extends DLCSignatures { - lazy val keys: Vector[OracleOutcome] = outcomeSigs.map(_._1) + lazy val keys: Vector[ECPublicKey] = outcomeSigs.map(_._1) lazy val adaptorSigs: Vector[ECAdaptorSignature] = outcomeSigs.map(_._2) - def apply(key: OracleOutcome): ECAdaptorSignature = { + def indexedOutcomeSigs: Vector[(Indexed[ECPublicKey], ECAdaptorSignature)] = { + outcomeSigs.zipWithIndex.map { case ((adaptorPoint, sig), index) => + (Indexed(adaptorPoint, index), sig) + } + } + + def apply(key: ECPublicKey): ECAdaptorSignature = { outcomeSigs .find(_._1 == key) .map(_._2) diff --git a/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/OracleOutcome.scala b/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/OracleOutcome.scala index d789585322..a8aedb12ac 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/OracleOutcome.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/OracleOutcome.scala @@ -25,6 +25,8 @@ sealed trait OracleOutcome { */ def outcome: DLCOutcomeType + def oraclesAndOutcomes: Vector[(SingleOracleInfo, DLCOutcomeType)] + protected def computeSigPoint: ECPublicKey /** The adaptor point used to encrypt the signatures for this corresponding CET. */ @@ -48,6 +50,9 @@ case class EnumOracleOutcome( oracles.map(_.sigPoint(outcome)).reduce(_.add(_)) } + override val oraclesAndOutcomes: Vector[(EnumSingleOracleInfo, EnumOutcome)] = + oracles.map((_, outcome)) + override lazy val aggregateNonce: SchnorrNonce = { oracles .map(_.aggregateNonce(outcome)) @@ -60,7 +65,7 @@ case class EnumOracleOutcome( /** Corresponds to a CET in an Numeric Outcome DLC where some set of `threshold` * oracles have each signed some NumericOutcome. */ -case class NumericOracleOutcome(oraclesAndOutcomes: Vector[ +case class NumericOracleOutcome(override val oraclesAndOutcomes: Vector[ (NumericSingleOracleInfo, UnsignedNumericOutcome)]) extends OracleOutcome { @@ -103,5 +108,5 @@ object NumericOracleOutcome { } } -/** An oracle outcome and it's corresponding CET */ -case class OutcomeCETPair(outcome: OracleOutcome, wtx: WitnessTransaction) +/** An adaptor point and it's corresponding CET */ +case class AdaptorPointCETPair(sigPoint: ECPublicKey, wtx: WitnessTransaction) diff --git a/core/src/main/scala/org/bitcoins/core/protocol/dlc/sign/DLCTxSigner.scala b/core/src/main/scala/org/bitcoins/core/protocol/dlc/sign/DLCTxSigner.scala index 4ebf0339bb..fcd9ab3751 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/dlc/sign/DLCTxSigner.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/dlc/sign/DLCTxSigner.scala @@ -15,7 +15,7 @@ import org.bitcoins.core.protocol.{Bech32Address, BitcoinAddress} import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature import org.bitcoins.core.psbt.PSBT import org.bitcoins.core.script.crypto.HashType -import org.bitcoins.core.util.FutureUtil +import org.bitcoins.core.util.{FutureUtil, Indexed} import org.bitcoins.core.wallet.signer.BitcoinSigner import org.bitcoins.core.wallet.utxo._ import org.bitcoins.crypto._ @@ -125,28 +125,28 @@ case class DLCTxSigner( } /** Signs remote's Contract Execution Transaction (CET) for a given outcome */ - def signCET(outcome: OracleOutcome): ECAdaptorSignature = { - signCETs(Vector(outcome)).head._2 + def signCET(adaptorPoint: ECPublicKey, index: Int): ECAdaptorSignature = { + signCETs(Vector(Indexed(adaptorPoint, index))).head._2 } /** Signs remote's Contract Execution Transaction (CET) for a given outcomes */ - def buildAndSignCETs(outcomes: Vector[OracleOutcome]): Vector[ - (OracleOutcome, WitnessTransaction, ECAdaptorSignature)] = { - val outcomesAndCETs = builder.buildCETsMap(outcomes) + def buildAndSignCETs(adaptorPoints: Vector[Indexed[ECPublicKey]]): Vector[ + (ECPublicKey, WitnessTransaction, ECAdaptorSignature)] = { + val outcomesAndCETs = builder.buildCETsMap(adaptorPoints) DLCTxSigner.buildAndSignCETs(outcomesAndCETs, cetSigningInfo, fundingKey) } /** Signs remote's Contract Execution Transaction (CET) for a given outcomes */ - def signCETs(outcomes: Vector[OracleOutcome]): Vector[ - (OracleOutcome, ECAdaptorSignature)] = { - buildAndSignCETs(outcomes).map { case (outcome, _, sig) => + def signCETs(adaptorPoints: Vector[Indexed[ECPublicKey]]): Vector[ + (ECPublicKey, ECAdaptorSignature)] = { + buildAndSignCETs(adaptorPoints).map { case (outcome, _, sig) => outcome -> sig } } /** Signs remote's Contract Execution Transaction (CET) for a given outcomes and their corresponding CETs */ - def signGivenCETs(outcomesAndCETs: Vector[OutcomeCETPair]): Vector[ - (OracleOutcome, ECAdaptorSignature)] = { + def signGivenCETs(outcomesAndCETs: Vector[AdaptorPointCETPair]): Vector[ + (ECPublicKey, ECAdaptorSignature)] = { DLCTxSigner.signCETs(outcomesAndCETs, cetSigningInfo, fundingKey) } @@ -154,12 +154,14 @@ case class DLCTxSigner( outcome: OracleOutcome, remoteAdaptorSig: ECAdaptorSignature, oracleSigs: Vector[OracleSignatures]): WitnessTransaction = { + val index = builder.contractInfo.allOutcomes.indexOf(outcome) + DLCTxSigner.completeCET( outcome, cetSigningInfo, builder.fundingMultiSig, builder.buildFundingTx, - builder.buildCET(outcome), + builder.buildCET(outcome.sigPoint, index), remoteAdaptorSig, remoteFundingPubKey, oracleSigs @@ -184,7 +186,8 @@ case class DLCTxSigner( /** Creates all of this party's CETSignatures */ def createCETSigs(): CETSignatures = { - val cetSigs = signCETs(builder.contractInfo.allOutcomes) + val adaptorPoints = builder.contractInfo.adaptorPointsIndexed + val cetSigs = signCETs(adaptorPoints) val refundSig = signRefundTx CETSignatures(cetSigs, refundSig) @@ -193,19 +196,26 @@ case class DLCTxSigner( /** Creates CET signatures async */ def createCETSigsAsync()(implicit ec: ExecutionContext): Future[CETSignatures] = { - val outcomes = builder.contractInfo.allOutcomes + val adaptorPoints = builder.contractInfo.adaptorPointsIndexed + //divide and conquer - val computeBatchFn: Vector[OracleOutcome] => Future[ - Vector[(OracleOutcome, ECAdaptorSignature)]] = { - case outcomes: Vector[OracleOutcome] => + //we want a batch size of at least 1 + val size = + Math.max(adaptorPoints.length / Runtime.getRuntime.availableProcessors(), + 1) + + val computeBatchFn: Vector[Indexed[ECPublicKey]] => Future[ + Vector[(ECPublicKey, ECAdaptorSignature)]] = { + adaptorPoints: Vector[Indexed[ECPublicKey]] => Future { - signCETs(outcomes) + signCETs(adaptorPoints) } } - val cetSigsF: Future[Vector[(OracleOutcome, ECAdaptorSignature)]] = { - FutureUtil.batchAndParallelExecute(elements = outcomes, - f = computeBatchFn) + val cetSigsF: Future[Vector[(ECPublicKey, ECAdaptorSignature)]] = { + FutureUtil.batchAndParallelExecute(elements = adaptorPoints, + f = computeBatchFn, + batchSize = size) }.map(_.flatten) for { @@ -216,30 +226,32 @@ case class DLCTxSigner( /** Creates all of this party's CETSignatures */ def createCETsAndCETSigs(): (CETSignatures, Vector[WitnessTransaction]) = { - val cetsAndSigs = buildAndSignCETs(builder.contractInfo.allOutcomes) + val adaptorPoints = builder.contractInfo.adaptorPointsIndexed + val cetsAndSigs = buildAndSignCETs(adaptorPoints) val (msgs, cets, sigs) = cetsAndSigs.unzip3 val refundSig = signRefundTx + (CETSignatures(msgs.zip(sigs), refundSig), cets) } /** The equivalent of [[createCETsAndCETSigs()]] but async */ def createCETsAndCETSigsAsync()(implicit ec: ExecutionContext): Future[(CETSignatures, Vector[WitnessTransaction])] = { - val outcomes = builder.contractInfo.allOutcomes - val fn = { outcomes: Vector[OracleOutcome] => + val adaptorPoints = builder.contractInfo.adaptorPointsIndexed + val fn = { adaptorPoints: Vector[Indexed[ECPublicKey]] => Future { - buildAndSignCETs(outcomes) + buildAndSignCETs(adaptorPoints) } } - val cetsAndSigsF: Future[Vector[ - Vector[(OracleOutcome, WitnessTransaction, ECAdaptorSignature)]]] = { - FutureUtil.batchAndParallelExecute[OracleOutcome, + val cetsAndSigsF: Future[ + Vector[Vector[(ECPublicKey, WitnessTransaction, ECAdaptorSignature)]]] = { + FutureUtil.batchAndParallelExecute[Indexed[ECPublicKey], Vector[( - OracleOutcome, + ECPublicKey, WitnessTransaction, - ECAdaptorSignature)]](elements = - outcomes, - f = fn) + ECAdaptorSignature)]]( + elements = adaptorPoints, + f = fn) } val refundSig = signRefundTx @@ -252,7 +264,8 @@ case class DLCTxSigner( } /** Creates this party's CETSignatures given the outcomes and their unsigned CETs */ - def createCETSigs(outcomesAndCETs: Vector[OutcomeCETPair]): CETSignatures = { + def createCETSigs( + outcomesAndCETs: Vector[AdaptorPointCETPair]): CETSignatures = { val cetSigs = signGivenCETs(outcomesAndCETs) val refundSig = signRefundTx @@ -296,38 +309,37 @@ object DLCTxSigner { } def signCET( - outcome: OracleOutcome, + sigPoint: ECPublicKey, cet: WitnessTransaction, cetSigningInfo: ECSignatureParams[P2WSHV0InputInfo], fundingKey: AdaptorSign): ECAdaptorSignature = { - signCETs(Vector(OutcomeCETPair(outcome, cet)), + signCETs(Vector(AdaptorPointCETPair(sigPoint, cet)), cetSigningInfo, fundingKey).head._2 } def signCETs( - outcomesAndCETs: Vector[OutcomeCETPair], + outcomesAndCETs: Vector[AdaptorPointCETPair], cetSigningInfo: ECSignatureParams[P2WSHV0InputInfo], - fundingKey: AdaptorSign): Vector[(OracleOutcome, ECAdaptorSignature)] = { + fundingKey: AdaptorSign): Vector[(ECPublicKey, ECAdaptorSignature)] = { buildAndSignCETs(outcomesAndCETs, cetSigningInfo, fundingKey).map { case (outcome, _, sig) => outcome -> sig } } def buildAndSignCETs( - outcomesAndCETs: Vector[OutcomeCETPair], + outcomesAndCETs: Vector[AdaptorPointCETPair], cetSigningInfo: ECSignatureParams[P2WSHV0InputInfo], fundingKey: AdaptorSign): Vector[ - (OracleOutcome, WitnessTransaction, ECAdaptorSignature)] = { - outcomesAndCETs.map { case OutcomeCETPair(outcome, cet) => - val adaptorPoint = outcome.sigPoint + (ECPublicKey, WitnessTransaction, ECAdaptorSignature)] = { + outcomesAndCETs.map { case AdaptorPointCETPair(sigPoint, cet) => val hashToSign = TransactionSignatureSerializer.hashForSignature(cet, cetSigningInfo, HashType.sigHashAll) - val adaptorSig = fundingKey.adaptorSign(adaptorPoint, hashToSign.bytes) - (outcome, cet, adaptorSig) + val adaptorSig = fundingKey.adaptorSign(sigPoint, hashToSign.bytes) + (sigPoint, cet, adaptorSig) } } @@ -371,7 +383,7 @@ object DLCTxSigner { .map(_.asInstanceOf[WitnessTransaction]) cetT match { - case Success(cet) => cet.asInstanceOf[WitnessTransaction] + case Success(cet) => cet case Failure(err) => throw err } } diff --git a/core/src/main/scala/org/bitcoins/core/protocol/dlc/verify/DLCSignatureVerifier.scala b/core/src/main/scala/org/bitcoins/core/protocol/dlc/verify/DLCSignatureVerifier.scala index 4b79399416..d19bdd497a 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/dlc/verify/DLCSignatureVerifier.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/dlc/verify/DLCSignatureVerifier.scala @@ -10,14 +10,13 @@ import org.bitcoins.core.policy.Policy import org.bitcoins.core.protocol.dlc.build.DLCTxBuilder import org.bitcoins.core.protocol.dlc.models.{ DLCFundingInput, - FundingSignatures, - OracleOutcome + FundingSignatures } import org.bitcoins.core.protocol.transaction.{Transaction, WitnessTransaction} import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature import org.bitcoins.core.psbt.PSBT import org.bitcoins.core.script.crypto.HashType -import org.bitcoins.core.util.FutureUtil +import org.bitcoins.core.util.{FutureUtil, Indexed} import org.bitcoins.crypto.{ECAdaptorSignature, ECPublicKey} import scodec.bits.ByteVector @@ -37,15 +36,17 @@ case class DLCSignatureVerifier(builder: DLCTxBuilder, isInitiator: Boolean) { } /** Verifies remote's CET signature for a given outcome hash */ - def verifyCETSig(outcome: OracleOutcome, sig: ECAdaptorSignature): Boolean = { + def verifyCETSig( + adaptorPoint: Indexed[ECPublicKey], + sig: ECAdaptorSignature): Boolean = { val remoteFundingPubKey = if (isInitiator) { builder.acceptFundingKey } else { builder.offerFundingKey } - val cet = builder.buildCET(outcome) + val cet = builder.buildCET(adaptorPoint) - DLCSignatureVerifier.validateCETSignature(outcome, + DLCSignatureVerifier.validateCETSignature(adaptorPoint.element, sig, remoteFundingPubKey, fundingTx, @@ -53,14 +54,14 @@ case class DLCSignatureVerifier(builder: DLCTxBuilder, isInitiator: Boolean) { cet) } - def verifyCETSigs(sigs: Vector[(OracleOutcome, ECAdaptorSignature)])(implicit - ec: ExecutionContext): Future[Boolean] = { + def verifyCETSigs(sigs: Vector[(Indexed[ECPublicKey], ECAdaptorSignature)])( + implicit ec: ExecutionContext): Future[Boolean] = { val correctNumberOfSigs = sigs.size >= builder.contractInfo.allOutcomes.length def runVerify( - outcomeSigs: Vector[(OracleOutcome, ECAdaptorSignature)]): Future[ - Boolean] = { + outcomeSigs: Vector[ + (Indexed[ECPublicKey], ECAdaptorSignature)]): Future[Boolean] = { Future { outcomeSigs.foldLeft(true) { case (ret, (outcome, sig)) => ret && verifyCETSig(outcome, sig) @@ -89,15 +90,13 @@ case class DLCSignatureVerifier(builder: DLCTxBuilder, isInitiator: Boolean) { object DLCSignatureVerifier { def validateCETSignature( - outcome: OracleOutcome, + adaptorPoint: ECPublicKey, sig: ECAdaptorSignature, remoteFundingPubKey: ECPublicKey, fundingTx: Transaction, fundOutputIndex: Int, cet: WitnessTransaction ): Boolean = { - val adaptorPoint = outcome.sigPoint - val sigComponent = WitnessTxSigComponentRaw( transaction = cet, inputIndex = UInt32.zero, diff --git a/core/src/main/scala/org/bitcoins/core/util/FutureUtil.scala b/core/src/main/scala/org/bitcoins/core/util/FutureUtil.scala index ce28977959..a9e0ac830d 100644 --- a/core/src/main/scala/org/bitcoins/core/util/FutureUtil.scala +++ b/core/src/main/scala/org/bitcoins/core/util/FutureUtil.scala @@ -103,11 +103,17 @@ object FutureUtil { elements: Vector[T], f: Vector[T] => Future[U], batchSize: Int)(implicit ec: ExecutionContext): Future[Vector[U]] = { - require(batchSize > 0, s"Cannot have batch size 0 or less, got=$batchSize") - val batches = elements.grouped(batchSize).toVector - val execute: Vector[Future[U]] = batches.map(b => f(b)) - val doneF = Future.sequence(execute) - doneF + require( + batchSize > 0, + s"Cannot have batch size less than or equal to zero, got=$batchSize") + if (elements.isEmpty) { + Future.successful(Vector.empty) + } else { + val batches = elements.grouped(batchSize).toVector + val execute: Vector[Future[U]] = batches.map(b => f(b)) + val doneF = Future.sequence(execute) + doneF + } } /** Same as [[batchAndParallelExecute()]], but computes the batchSize based on the diff --git a/crypto/src/main/scala/org/bitcoins/crypto/ECKey.scala b/crypto/src/main/scala/org/bitcoins/crypto/ECKey.scala index 32df9f92a5..9097c77722 100644 --- a/crypto/src/main/scala/org/bitcoins/crypto/ECKey.scala +++ b/crypto/src/main/scala/org/bitcoins/crypto/ECKey.scala @@ -102,6 +102,10 @@ sealed trait PublicKey extends NetworkElement { fromBytes(x.+:(leadByte)) } } + + override def hashCode: Int = { + bytes.hashCode + } } /** Wraps raw ECPublicKey bytes without doing any validation or deserialization (may be invalid). */ diff --git a/db-commons/src/main/scala/org/bitcoins/db/DbCommonsColumnMappers.scala b/db-commons/src/main/scala/org/bitcoins/db/DbCommonsColumnMappers.scala index a3eda70f31..cb58350e60 100644 --- a/db-commons/src/main/scala/org/bitcoins/db/DbCommonsColumnMappers.scala +++ b/db-commons/src/main/scala/org/bitcoins/db/DbCommonsColumnMappers.scala @@ -153,14 +153,16 @@ class DbCommonsColumnMappers(val profile: JdbcProfile) { } implicit val uint64Mapper: BaseColumnType[UInt64] = { - MappedColumnType.base[UInt64, BigDecimal]( + MappedColumnType.base[UInt64, String]( { u64: UInt64 => - BigDecimal(u64.toBigInt.bigInteger) + val bytes = u64.bytes + val padded = if (bytes.length <= 8) { + bytes.padLeft(8) + } else bytes + + padded.toHex }, - //this has the potential to throw - { bigDec: BigDecimal => - UInt64(bigDec.toBigIntExact.get) - } + UInt64.fromHex ) } diff --git a/dlc-test/src/test/scala/org/bitcoins/dlc/DLCClientTest.scala b/dlc-test/src/test/scala/org/bitcoins/dlc/DLCClientTest.scala index a32eb0a79f..75e85eba40 100644 --- a/dlc-test/src/test/scala/org/bitcoins/dlc/DLCClientTest.scala +++ b/dlc-test/src/test/scala/org/bitcoins/dlc/DLCClientTest.scala @@ -20,7 +20,7 @@ import org.bitcoins.core.protocol.tlv.{ } import org.bitcoins.core.protocol.transaction._ import org.bitcoins.core.script.crypto.HashType -import org.bitcoins.core.util.{BitcoinScriptUtil, NumberUtil} +import org.bitcoins.core.util.{BitcoinScriptUtil, Indexed, NumberUtil} import org.bitcoins.core.wallet.utxo._ import org.bitcoins.crypto._ import org.bitcoins.testkitcore.dlc.{DLCFeeTestUtil, DLCTest, TestDLCClient} @@ -334,6 +334,39 @@ class DLCClientTest extends BitcoinSJvmTest with DLCTest { assert(!acceptVerifier.verifyRemoteFundingSigs(acceptFundingSigs)) } + it should "succeed on valid CET signatures" in { + val (offerClient, acceptClient, outcomes) = + constructDLCClients(numOutcomesOrDigits = 2, + isNumeric = false, + oracleThreshold = 1, + numOracles = 1, + paramsOpt = None) + val builder = offerClient.dlcTxBuilder + val offerVerifier = DLCSignatureVerifier(builder, isInitiator = true) + val acceptVerifier = DLCSignatureVerifier(builder, isInitiator = false) + + val offerCETSigs = offerClient.dlcTxSigner.createCETSigs() + val acceptCETSigs = acceptClient.dlcTxSigner.createCETSigs() + + outcomes.zipWithIndex.foreach { case (outcomeUncast, index) => + val outcome = EnumOracleOutcome( + Vector(offerClient.offer.oracleInfo.asInstanceOf[EnumSingleOracleInfo]), + outcomeUncast.asInstanceOf[EnumOutcome]) + + assert( + offerVerifier.verifyCETSig(Indexed(outcome.sigPoint, index), + acceptCETSigs(outcome.sigPoint))) + assert( + acceptVerifier.verifyCETSig(Indexed(outcome.sigPoint, index), + offerCETSigs(outcome.sigPoint))) + } + + assert(offerVerifier.verifyRefundSig(acceptCETSigs.refundSig)) + assert(offerVerifier.verifyRefundSig(offerCETSigs.refundSig)) + assert(acceptVerifier.verifyRefundSig(offerCETSigs.refundSig)) + assert(acceptVerifier.verifyRefundSig(acceptCETSigs.refundSig)) + } + it should "fail on invalid CET signatures" in { val (offerClient, acceptClient, outcomes) = constructDLCClients(numOutcomesOrDigits = 3, @@ -360,15 +393,16 @@ class DLCClientTest extends BitcoinSJvmTest with DLCTest { val oracleSig = genEnumOracleSignature(oracleInfo, outcome.outcome) assertThrows[RuntimeException] { - offerClient.dlcTxSigner.completeCET(oracleOutcome, - badAcceptCETSigs(oracleOutcome), - Vector(oracleSig)) + offerClient.dlcTxSigner.completeCET( + oracleOutcome, + badAcceptCETSigs(oracleOutcome.sigPoint), + Vector(oracleSig)) } assertThrows[RuntimeException] { acceptClient.dlcTxSigner .completeCET(oracleOutcome, - badOfferCETSigs(oracleOutcome), + badOfferCETSigs(oracleOutcome.sigPoint), Vector(oracleSig)) } } @@ -381,29 +415,25 @@ class DLCClientTest extends BitcoinSJvmTest with DLCTest { acceptClient.dlcTxSigner.completeRefundTx(badOfferCETSigs.refundSig) } - outcomes.foreach { outcomeUncast => + outcomes.zipWithIndex.foreach { case (outcomeUncast, index) => val outcome = EnumOracleOutcome( Vector(offerClient.offer.oracleInfo.asInstanceOf[EnumSingleOracleInfo]), outcomeUncast.asInstanceOf[EnumOutcome]) + val adaptorPoint = Indexed(outcome.sigPoint, index) - assert(offerVerifier.verifyCETSig(outcome, acceptCETSigs(outcome))) - assert(acceptVerifier.verifyCETSig(outcome, offerCETSigs(outcome))) - } - assert(offerVerifier.verifyRefundSig(acceptCETSigs.refundSig)) - assert(offerVerifier.verifyRefundSig(offerCETSigs.refundSig)) - assert(acceptVerifier.verifyRefundSig(offerCETSigs.refundSig)) - assert(acceptVerifier.verifyRefundSig(acceptCETSigs.refundSig)) + assert( + !offerVerifier.verifyCETSig(adaptorPoint, + badAcceptCETSigs(outcome.sigPoint))) + assert( + !acceptVerifier.verifyCETSig(adaptorPoint, + badOfferCETSigs(outcome.sigPoint))) - outcomes.foreach { outcomeUncast => - val outcome = EnumOracleOutcome( - Vector(offerClient.offer.oracleInfo.asInstanceOf[EnumSingleOracleInfo]), - outcomeUncast.asInstanceOf[EnumOutcome]) - - assert(!offerVerifier.verifyCETSig(outcome, badAcceptCETSigs(outcome))) - assert(!acceptVerifier.verifyCETSig(outcome, badOfferCETSigs(outcome))) - - assert(!offerVerifier.verifyCETSig(outcome, offerCETSigs(outcome))) - assert(!acceptVerifier.verifyCETSig(outcome, acceptCETSigs(outcome))) + assert( + !offerVerifier.verifyCETSig(adaptorPoint, + offerCETSigs(outcome.sigPoint))) + assert( + !acceptVerifier.verifyCETSig(adaptorPoint, + acceptCETSigs(outcome.sigPoint))) } assert(!offerVerifier.verifyRefundSig(badAcceptCETSigs.refundSig)) assert(!offerVerifier.verifyRefundSig(badOfferCETSigs.refundSig)) @@ -411,6 +441,36 @@ class DLCClientTest extends BitcoinSJvmTest with DLCTest { assert(!acceptVerifier.verifyRefundSig(badAcceptCETSigs.refundSig)) } + it should "compute sigpoints correctly" in { + runTestsForParam(Vector(4, 6, 8)) { numDigitsOrOutcomes => + runTestsForParam(Vector(true, false)) { isNumeric => + runTestsForParam(Vector((1, 1), (2, 3), (3, 5))) { + case (threshold, numOracles) => + runTestsForParam( + Vector(None, + Some( + OracleParamsV0TLV(numDigitsOrOutcomes / 2 + 1, + numDigitsOrOutcomes / 2, + maximizeCoverage = true)))) { + oracleParams => + val (client, _, _) = constructDLCClients(numDigitsOrOutcomes, + isNumeric, + threshold, + numOracles, + oracleParams) + val contract = client.offer.contractInfo + val outcomes = contract.allOutcomes + + val adaptorPoints = contract.adaptorPoints + val expectedAdaptorPoints = outcomes.map(_.sigPoint) + + assert(adaptorPoints == expectedAdaptorPoints) + } + } + } + } + } + def assertCorrectSigDerivation( offerSetup: SetupDLC, dlcOffer: TestDLCClient, diff --git a/dlc-test/src/test/scala/org/bitcoins/dlc/SetupDLCTest.scala b/dlc-test/src/test/scala/org/bitcoins/dlc/SetupDLCTest.scala index 43f7ae7eb8..1c0efadce3 100644 --- a/dlc-test/src/test/scala/org/bitcoins/dlc/SetupDLCTest.scala +++ b/dlc-test/src/test/scala/org/bitcoins/dlc/SetupDLCTest.scala @@ -54,9 +54,10 @@ class SetupDLCTest extends BitcoinSJvmTest { refundTx: WitnessTransaction = validRefundTx): SetupDLC = { SetupDLC( fundingTx = fundingTx, - cets = Vector( - EnumOracleOutcome(Vector(oracleInfo), EnumOutcome("WIN")) -> cet0, - EnumOracleOutcome(Vector(oracleInfo), EnumOutcome("LOSE")) -> cet1), + cets = Vector(EnumOracleOutcome(Vector(oracleInfo), + EnumOutcome("WIN")).sigPoint -> cet0, + EnumOracleOutcome(Vector(oracleInfo), + EnumOutcome("LOSE")).sigPoint -> cet1), refundTx = refundTx ) } diff --git a/dlc-test/src/test/scala/org/bitcoins/dlc/testgen/DLCTLVGen.scala b/dlc-test/src/test/scala/org/bitcoins/dlc/testgen/DLCTLVGen.scala index b89e0f1bf7..36f956c8d5 100644 --- a/dlc-test/src/test/scala/org/bitcoins/dlc/testgen/DLCTLVGen.scala +++ b/dlc-test/src/test/scala/org/bitcoins/dlc/testgen/DLCTLVGen.scala @@ -207,7 +207,7 @@ object DLCTLVGen { ECPublicKey.freshPublicKey): CETSignatures = { CETSignatures( outcomes.map(outcome => - EnumOracleOutcome(Vector(oracleInfo), outcome) -> adaptorSig), + EnumOracleOutcome(Vector(oracleInfo), outcome).sigPoint -> adaptorSig), partialSig(fundingPubKey, sigHashByte = false)) } diff --git a/dlc-test/src/test/scala/org/bitcoins/dlc/testgen/DLCTestVector.scala b/dlc-test/src/test/scala/org/bitcoins/dlc/testgen/DLCTestVector.scala index 5a98841d2d..acff39fbdd 100644 --- a/dlc-test/src/test/scala/org/bitcoins/dlc/testgen/DLCTestVector.scala +++ b/dlc-test/src/test/scala/org/bitcoins/dlc/testgen/DLCTestVector.scala @@ -22,6 +22,7 @@ import org.bitcoins.core.protocol.transaction.{ } import org.bitcoins.core.protocol.{BitcoinAddress, BlockTimeStamp} import org.bitcoins.core.script.crypto.HashType +import org.bitcoins.core.util.Indexed import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte import org.bitcoins.core.wallet.utxo.{ ConditionalPath, @@ -186,12 +187,13 @@ case class ValidTestInputs( def buildTransactions: DLCTransactions = { val builder = this.builder val fundingTx = builder.buildFundingTx - val cets = + val adaptorPoints = params.contractInfo .map(_.preImage) .map(EnumOutcome.apply) .map(outcome => EnumOracleOutcome(Vector(params.oracleInfo), outcome)) - .map(builder.buildCET) + .map(_.sigPoint) + val cets = builder.buildCETs(Indexed(adaptorPoints)) val refundTx = builder.buildRefundTx DLCTransactions(fundingTx, cets, refundTx) diff --git a/dlc-test/src/test/scala/org/bitcoins/dlc/testgen/DLCTxGen.scala b/dlc-test/src/test/scala/org/bitcoins/dlc/testgen/DLCTxGen.scala index edd271a989..701b269739 100644 --- a/dlc-test/src/test/scala/org/bitcoins/dlc/testgen/DLCTxGen.scala +++ b/dlc-test/src/test/scala/org/bitcoins/dlc/testgen/DLCTxGen.scala @@ -246,7 +246,7 @@ object DLCTxGen { EnumOracleOutcome(Vector(inputs.params.oracleInfo), EnumOutcome(outcomeStr)) - val accpetCETSigs = acceptSigner.createCETSigs() + val acceptCETSigs = acceptSigner.createCETSigs() val offerCETSigs = offerSigner.createCETSigs() for { @@ -256,23 +256,23 @@ object DLCTxGen { signedFundingTx <- acceptSigner.completeFundingTx(offerFundingSigs) } yield { - val signedRefundTx = offerSigner.completeRefundTx(accpetCETSigs.refundSig) + val signedRefundTx = offerSigner.completeRefundTx(acceptCETSigs.refundSig) val offerSignedCET = offerSigner.completeCET( outcome, - accpetCETSigs(outcome), + acceptCETSigs(outcome.sigPoint), Vector( EnumOracleSignature(inputs.params.oracleInfo, inputs.params.oracleSignature))) val acceptSignedCET = acceptSigner.completeCET( outcome, - offerCETSigs(outcome), + offerCETSigs(outcome.sigPoint), Vector( EnumOracleSignature(inputs.params.oracleInfo, inputs.params.oracleSignature))) - val accept = acceptWithoutSigs.withSigs(accpetCETSigs) + val accept = acceptWithoutSigs.withSigs(acceptCETSigs) val contractId = fundingTx.txIdBE.bytes.xor(accept.tempContractId.bytes) val sign = DLCSign(offerCETSigs, offerFundingSigs, contractId) diff --git a/testkit-core/src/main/scala/org/bitcoins/testkitcore/dlc/TestDLCClient.scala b/testkit-core/src/main/scala/org/bitcoins/testkitcore/dlc/TestDLCClient.scala index 2183df2b99..c678f0890a 100644 --- a/testkit-core/src/main/scala/org/bitcoins/testkitcore/dlc/TestDLCClient.scala +++ b/testkit-core/src/main/scala/org/bitcoins/testkitcore/dlc/TestDLCClient.scala @@ -98,7 +98,7 @@ case class TestDLCClient( } cetSigs = dlcTxSigner.createCETSigs(setupDLCWithoutFundingTxSigs.cets.map { - case (msg, info) => OutcomeCETPair(msg, info.tx) + case (msg, info) => AdaptorPointCETPair(msg, info.tx) }) localFundingSigs <- Future.fromTry { dlcTxSigner.signFundingTx() diff --git a/testkit/src/main/scala/org/bitcoins/testkit/BitcoinSTestAppConfig.scala b/testkit/src/main/scala/org/bitcoins/testkit/BitcoinSTestAppConfig.scala index e9f25a7c64..b67763487e 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/BitcoinSTestAppConfig.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/BitcoinSTestAppConfig.scala @@ -3,10 +3,10 @@ package org.bitcoins.testkit import com.typesafe.config._ import org.bitcoins.dlc.oracle.config.DLCOracleAppConfig import org.bitcoins.server.BitcoinSAppConfig -import org.bitcoins.testkitcore.Implicits.GeneratorOps -import org.bitcoins.testkitcore.gen.{NumberGenerator, StringGenerators} import org.bitcoins.testkit.keymanager.KeyManagerTestUtil import org.bitcoins.testkit.util.FileUtil +import org.bitcoins.testkitcore.Implicits.GeneratorOps +import org.bitcoins.testkitcore.gen.{NumberGenerator, StringGenerators} import java.nio.file._ import scala.concurrent.ExecutionContext @@ -149,9 +149,10 @@ object BitcoinSTestAppConfig { case object Node extends ProjectType case object Chain extends ProjectType case object Oracle extends ProjectType + case object DLC extends ProjectType case object Test extends ProjectType - val all = List(Wallet, Node, Chain, Oracle, Test) + val all = List(Wallet, Node, Chain, Oracle, DLC, Test) } /** Generates a Typesafe config with DBs set to memory @@ -178,6 +179,7 @@ object BitcoinSTestAppConfig { case ProjectType.Chain => "chain" case ProjectType.Node => "node" case ProjectType.Oracle => "oracle" + case ProjectType.DLC => "dlc" case ProjectType.Test => "test" }