Pulled down core diff from adaptor-dlc (#3038)

This commit is contained in:
Nadav Kohen 2021-05-05 09:26:09 -05:00 committed by GitHub
parent 1cda5cbf1e
commit aacba1c077
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 552 additions and 194 deletions

View file

@ -0,0 +1,97 @@
package org.bitcoins.core.protocol.dlc
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.protocol.tlv.{EnumOutcome, OracleAnnouncementV0TLV}
import org.bitcoins.crypto.ECPrivateKey
import org.bitcoins.testkitcore.util.BitcoinSUnitTest
class ContractOraclePairTest extends BitcoinSUnitTest {
behavior of "ContractOraclePair"
it should "not be able to construct an invalid enum contract oracle pair" in {
val contractDescriptor = EnumContractDescriptor(
Vector("Never", "gonna", "give", "you", "up").map(
EnumOutcome(_) -> Satoshis.zero))
def enumOracleInfo(outcomes: Vector[String]): EnumSingleOracleInfo = {
EnumSingleOracleInfo(
OracleAnnouncementV0TLV.dummyForEventsAndKeys(
ECPrivateKey.freshPrivateKey,
ECPrivateKey.freshPrivateKey.schnorrNonce,
outcomes.map(EnumOutcome.apply)))
}
val oracleInfo1 =
enumOracleInfo(Vector("Never", "gonna", "let", "you", "down"))
val oracleInfo2 = enumOracleInfo(
Vector("Never", "gonna", "run", "around", "and", "desert", "you"))
val oracleInfo3 =
enumOracleInfo(Vector("Never", "gonna", "make", "you", "cry"))
val multiOracleInfo = EnumMultiOracleInfo(
2,
Vector(oracleInfo1, oracleInfo2, oracleInfo3).map(_.announcement))
assertThrows[IllegalArgumentException](
ContractOraclePair.EnumPair(contractDescriptor, oracleInfo1)
)
assertThrows[IllegalArgumentException](
ContractOraclePair.EnumPair(contractDescriptor, oracleInfo2)
)
assertThrows[IllegalArgumentException](
ContractOraclePair.EnumPair(contractDescriptor, oracleInfo3)
)
assertThrows[IllegalArgumentException](
ContractOraclePair.EnumPair(contractDescriptor, multiOracleInfo)
)
}
it should "not be able to construct an invalid numeric contract oracle pair" in {
val contractDescriptor =
NumericContractDescriptor(
DLCPayoutCurve(
Vector(OutcomePayoutEndpoint(0, 0),
OutcomePayoutEndpoint((1L << 7) - 1, 1))),
numDigits = 7,
RoundingIntervals.noRounding)
def numericOracleInfo(numDigits: Int): NumericSingleOracleInfo = {
NumericSingleOracleInfo(
OracleAnnouncementV0TLV.dummyForKeys(
ECPrivateKey.freshPrivateKey,
Vector.fill(numDigits)(ECPrivateKey.freshPrivateKey.schnorrNonce)))
}
val oracleInfo1 = numericOracleInfo(1)
val oracleInfo2 = numericOracleInfo(2)
val oracleInfo3 = numericOracleInfo(3)
val announcements =
Vector(oracleInfo1, oracleInfo2, oracleInfo3).map(_.announcement)
val multiOracleInfoExact = NumericExactMultiOracleInfo(2, announcements)
val multiOracleInfo = NumericMultiOracleInfo(2,
announcements,
maxErrorExp = 2,
minFailExp = 1,
maximizeCoverage = true)
assertThrows[IllegalArgumentException](
ContractOraclePair.NumericPair(contractDescriptor, oracleInfo1)
)
assertThrows[IllegalArgumentException](
ContractOraclePair.NumericPair(contractDescriptor, oracleInfo2)
)
assertThrows[IllegalArgumentException](
ContractOraclePair.NumericPair(contractDescriptor, oracleInfo3)
)
assertThrows[IllegalArgumentException](
ContractOraclePair.NumericPair(contractDescriptor, multiOracleInfoExact)
)
assertThrows[IllegalArgumentException](
ContractOraclePair.NumericPair(contractDescriptor, multiOracleInfo)
)
}
}

View file

@ -1,7 +1,7 @@
package org.bitcoins.core.protocol.dlc
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.number.UInt32
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.DLCMessage._
@ -41,13 +41,32 @@ class DLCMessageTest extends BitcoinSJvmTest {
it must "not allow a negative collateral for a DLCOffer" in {
assertThrows[IllegalArgumentException](
DLCOffer(
ContractInfo.dummy,
DLCPublicKeys(dummyPubKey, dummyAddress),
Satoshis(-1),
Vector.empty,
dummyAddress,
SatoshisPerVirtualByte.one,
DLCTimeouts(BlockHeight(1), BlockHeight(2))
contractInfo = ContractInfo.dummy,
pubKeys = DLCPublicKeys(dummyPubKey, dummyAddress),
totalCollateral = Satoshis(-1),
fundingInputs = Vector.empty,
changeAddress = dummyAddress,
payoutSerialId = UInt64.zero,
changeSerialId = UInt64.one,
fundOutputSerialId = UInt64.max,
feeRate = SatoshisPerVirtualByte.one,
timeouts = DLCTimeouts(BlockHeight(1), BlockHeight(2))
))
}
it must "not allow same change and fund output serial id for a DLCOffer" in {
assertThrows[IllegalArgumentException](
DLCOffer(
contractInfo = ContractInfo.dummy,
pubKeys = DLCPublicKeys(dummyPubKey, dummyAddress),
totalCollateral = Satoshis(-1),
fundingInputs = Vector.empty,
changeAddress = dummyAddress,
payoutSerialId = UInt64.zero,
changeSerialId = UInt64.one,
fundOutputSerialId = UInt64.one,
feeRate = SatoshisPerVirtualByte.one,
timeouts = DLCTimeouts(BlockHeight(1), BlockHeight(2))
))
}
@ -58,6 +77,8 @@ class DLCMessageTest extends BitcoinSJvmTest {
DLCPublicKeys(dummyPubKey, dummyAddress),
Vector.empty,
dummyAddress,
payoutSerialId = UInt64.zero,
changeSerialId = UInt64.one,
CETSignatures(Vector(
EnumOracleOutcome(
Vector(dummyOracle),

View file

@ -112,7 +112,7 @@ class TLVTest extends BitcoinSUnitTest {
}
"FundingInputV0TLV" must "have serialization symmetry" in {
forAll(TLVGen.fundingInputV0TLV) { fundingInput =>
forAll(TLVGen.fundingInputV0TLV()) { fundingInput =>
assert(FundingInputV0TLV(fundingInput.bytes) == fundingInput)
assert(TLV(fundingInput.bytes) == fundingInput)
}

View file

@ -1,5 +1,11 @@
package org.bitcoins.core.protocol.dlc
import org.bitcoins.core.protocol.tlv.{
EnumEventDescriptorV0TLV,
EnumOutcome,
NumericEventDescriptorTLV
}
/** A pair of [[ContractDescriptor]] and [[OracleInfo]]
* This type is meant to ensure consistentcy between various
* [[ContractDescriptor]] and [[OracleInfo]] so that you cannot
@ -15,12 +21,39 @@ object ContractOraclePair {
case class EnumPair(
contractDescriptor: EnumContractDescriptor,
oracleInfo: EnumOracleInfo)
extends ContractOraclePair
extends ContractOraclePair {
private val descriptorOutcomes =
contractDescriptor.map(_._1).sortBy(_.outcome)
private val isValid = oracleInfo.singleOracleInfos.forall { singleInfo =>
val announcementOutcomes =
singleInfo.announcement.eventTLV.eventDescriptor
.asInstanceOf[EnumEventDescriptorV0TLV]
.outcomes
.map(EnumOutcome(_))
.sortBy(_.outcome)
announcementOutcomes == descriptorOutcomes
}
require(isValid, s"OracleInfo did not match ContractDescriptor: $this")
}
case class NumericPair(
contractDescriptor: NumericContractDescriptor,
oracleInfo: NumericOracleInfo)
extends ContractOraclePair
extends ContractOraclePair {
private val isValid = oracleInfo.singleOracleInfos.forall { singleInfo =>
val announcementDescriptor =
singleInfo.announcement.eventTLV.eventDescriptor
.asInstanceOf[NumericEventDescriptorTLV]
announcementDescriptor.base.toInt == 2 && announcementDescriptor.noncesNeeded == contractDescriptor.numDigits
}
require(isValid, s"OracleInfo did not match ContractDescriptor: $this")
}
/** Returns a valid [[ContractOraclePair]] if the
* [[ContractDescriptor]] and [[OracleInfo]] are of the same type
@ -50,7 +83,7 @@ object ContractOraclePair {
case Some(pair) => pair
case None =>
sys.error(
s"You passed in an incompatible contract/oracle pair, contract=$descriptor, oracle=${oracleInfo}")
s"You passed in an incompatible contract/oracle pair, contract=$descriptor, oracle=$oracleInfo")
}
}
}

View file

@ -281,4 +281,13 @@ object ContractInfo
ContractInfo(totalCollateral = enumDescriptor.values.maxBy(_.toLong),
enumPair)
}
def apply(
totalCollateral: Satoshis,
contractDescriptor: ContractDescriptor,
oracleInfo: OracleInfo): ContractInfo = {
ContractInfo(
totalCollateral,
ContractOraclePair.fromDescriptorOracle(contractDescriptor, oracleInfo))
}
}

View file

@ -1,6 +1,6 @@
package org.bitcoins.core.protocol.dlc
import org.bitcoins.core.number.{UInt16, UInt32}
import org.bitcoins.core.number.{UInt16, UInt32, UInt64}
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.tlv.FundingInputV0TLV
import org.bitcoins.core.protocol.transaction._
@ -8,6 +8,7 @@ import org.bitcoins.core.wallet.builder.DualFundingInput
import org.bitcoins.core.wallet.utxo.{InputInfo, ScriptSignatureParams}
sealed trait DLCFundingInput {
def inputSerialId: UInt64
def prevTx: Transaction
def prevTxVout: UInt32
def sequence: UInt32
@ -39,6 +40,7 @@ sealed trait DLCFundingInput {
lazy val toTLV: FundingInputV0TLV = {
FundingInputV0TLV(
inputSerialId,
prevTx,
prevTxVout,
sequence,
@ -54,6 +56,7 @@ sealed trait DLCFundingInput {
object DLCFundingInput {
def apply(
inputSerialId: UInt64,
prevTx: Transaction,
prevTxVout: UInt32,
sequence: UInt32,
@ -63,7 +66,19 @@ object DLCFundingInput {
case _: P2SHScriptPubKey =>
redeemScriptOpt match {
case Some(redeemScript) =>
DLCFundingInputP2SHSegwit(prevTx,
redeemScript match {
case _: P2WPKHWitnessSPKV0 =>
require(
maxWitnessLen == UInt16(107) || maxWitnessLen == UInt16(108),
s"P2WPKH max witness length must be 107 or 108, got $maxWitnessLen")
case _: P2WSHWitnessSPKV0 => ()
case spk: UnassignedWitnessScriptPubKey =>
throw new IllegalArgumentException(
s"Unknown segwit version: $spk")
}
DLCFundingInputP2SHSegwit(inputSerialId,
prevTx,
prevTxVout,
sequence,
maxWitnessLen,
@ -76,9 +91,13 @@ object DLCFundingInput {
require(
maxWitnessLen == UInt16(107) || maxWitnessLen == UInt16(108),
s"P2WPKH max witness length must be 107 or 108, got $maxWitnessLen")
DLCFundingInputP2WPKHV0(prevTx, prevTxVout, sequence)
DLCFundingInputP2WPKHV0(inputSerialId, prevTx, prevTxVout, sequence)
case _: P2WSHWitnessSPKV0 =>
DLCFundingInputP2WSHV0(prevTx, prevTxVout, sequence, maxWitnessLen)
DLCFundingInputP2WSHV0(inputSerialId,
prevTx,
prevTxVout,
sequence,
maxWitnessLen)
case spk: UnassignedWitnessScriptPubKey =>
throw new IllegalArgumentException(s"Unknown segwit version: $spk")
case spk: RawScriptPubKey =>
@ -88,6 +107,7 @@ object DLCFundingInput {
def fromTLV(fundingInput: FundingInputV0TLV): DLCFundingInput = {
DLCFundingInput(
fundingInput.inputSerialId,
fundingInput.prevTx,
fundingInput.prevTxVout,
fundingInput.sequence,
@ -98,8 +118,10 @@ object DLCFundingInput {
def fromInputSigningInfo(
info: ScriptSignatureParams[InputInfo],
inputSerialId: UInt64,
sequence: UInt32 = TransactionConstants.sequence): DLCFundingInput = {
DLCFundingInput(
inputSerialId,
info.prevTransaction,
info.outPoint.vout,
sequence,
@ -112,6 +134,7 @@ object DLCFundingInput {
}
case class DLCFundingInputP2WPKHV0(
inputSerialId: UInt64,
prevTx: Transaction,
prevTxVout: UInt32,
sequence: UInt32)
@ -124,6 +147,7 @@ case class DLCFundingInputP2WPKHV0(
}
case class DLCFundingInputP2WSHV0(
inputSerialId: UInt64,
prevTx: Transaction,
prevTxVout: UInt32,
sequence: UInt32,
@ -136,6 +160,7 @@ case class DLCFundingInputP2WSHV0(
}
case class DLCFundingInputP2SHSegwit(
inputSerialId: UInt64,
prevTx: Transaction,
prevTxVout: UInt32,
sequence: UInt32,
@ -149,3 +174,11 @@ case class DLCFundingInputP2SHSegwit(
override val redeemScriptOpt: Option[WitnessScriptPubKey] = Some(redeemScript)
}
case class SpendingInfoWithSerialId(
spendingInfo: ScriptSignatureParams[InputInfo],
serialId: UInt64) {
def toDLCFundingInput: DLCFundingInput =
DLCFundingInput.fromInputSigningInfo(spendingInfo, serialId)
}

View file

@ -2,6 +2,7 @@ package org.bitcoins.core.protocol.dlc
import org.bitcoins.core.config.{NetworkParameters, Networks}
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.number.UInt64
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.core.protocol.transaction.TransactionOutPoint
@ -11,6 +12,9 @@ import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.crypto._
import scodec.bits.ByteVector
import scala.annotation.tailrec
import scala.util.Random
sealed trait DLCMessage
object DLCMessage {
@ -23,6 +27,32 @@ object DLCMessage {
.flip
}
@tailrec
def genSerialId(notEqualTo: Vector[UInt64] = Vector.empty): UInt64 = {
val rand = {
// Copy of Random.nextBytes(Int)
// Not available for older versions
val bytes = new Array[Byte](0 max 8)
Random.nextBytes(bytes)
bytes
}
val res = UInt64(ByteVector(rand))
if (notEqualTo.contains(res)) genSerialId(notEqualTo)
else res
}
@tailrec
def genSerialIds(
size: Int,
notEqualTo: Vector[UInt64] = Vector.empty): Vector[UInt64] = {
val ids = 0.until(size).toVector.map(_ => genSerialId(notEqualTo))
if (ids.distinct.size != size)
genSerialIds(size, notEqualTo)
else ids
}
sealed trait DLCSetupMessage extends DLCMessage {
def pubKeys: DLCPublicKeys
@ -55,10 +85,21 @@ object DLCMessage {
totalCollateral: Satoshis,
fundingInputs: Vector[DLCFundingInput],
changeAddress: BitcoinAddress,
payoutSerialId: UInt64,
changeSerialId: UInt64,
fundOutputSerialId: UInt64,
feeRate: SatoshisPerVirtualByte,
timeouts: DLCTimeouts)
extends DLCSetupMessage {
require(
fundingInputs.map(_.inputSerialId).distinct.size == fundingInputs.size,
"All funding input serial ids must be unique")
require(
changeSerialId != fundOutputSerialId,
s"changeSerialId ($changeSerialId) cannot be equal to fundOutputSerialId ($fundOutputSerialId)")
val oracleInfo: OracleInfo = contractInfo.oracleInfo
val contractDescriptor: ContractDescriptor = contractInfo.contractDescriptor
@ -77,9 +118,12 @@ object DLCMessage {
contractInfo.toTLV,
fundingPubKey = pubKeys.fundingKey,
payoutSPK = pubKeys.payoutAddress.scriptPubKey,
payoutSerialId = payoutSerialId,
totalCollateralSatoshis = totalCollateral,
fundingInputs = fundingInputs.map(_.toTLV),
changeSPK = changeAddress.scriptPubKey,
changeSerialId = changeSerialId,
fundOutputSerialId = fundOutputSerialId,
feeRate = feeRate,
contractMaturityBound = timeouts.contractMaturity,
contractTimeout = timeouts.contractTimeout
@ -109,6 +153,9 @@ object DLCMessage {
},
changeAddress =
BitcoinAddress.fromScriptPubKey(offer.changeSPK, network),
payoutSerialId = offer.payoutSerialId,
changeSerialId = offer.changeSerialId,
fundOutputSerialId = offer.fundOutputSerialId,
feeRate = offer.feeRate,
timeouts =
DLCTimeouts(offer.contractMaturityBound, offer.contractTimeout)
@ -125,6 +172,8 @@ object DLCMessage {
pubKeys: DLCPublicKeys,
fundingInputs: Vector[DLCFundingInput],
changeAddress: BitcoinAddress,
payoutSerialId: UInt64,
changeSerialId: UInt64,
negotiationFields: DLCAccept.NegotiationFields,
tempContractId: Sha256Digest) {
@ -134,6 +183,8 @@ object DLCMessage {
pubKeys = pubKeys,
fundingInputs = fundingInputs,
changeAddress = changeAddress,
payoutSerialId = payoutSerialId,
changeSerialId = changeSerialId,
cetSigs = cetSigs,
negotiationFields = negotiationFields,
tempContractId = tempContractId
@ -146,19 +197,27 @@ object DLCMessage {
pubKeys: DLCPublicKeys,
fundingInputs: Vector[DLCFundingInput],
changeAddress: BitcoinAddress,
payoutSerialId: UInt64,
changeSerialId: UInt64,
cetSigs: CETSignatures,
negotiationFields: DLCAccept.NegotiationFields,
tempContractId: Sha256Digest)
extends DLCSetupMessage {
require(
fundingInputs.map(_.inputSerialId).distinct.size == fundingInputs.size,
"All funding input serial ids must be unique")
def toTLV: DLCAcceptTLV = {
DLCAcceptTLV(
tempContractId = tempContractId,
totalCollateralSatoshis = totalCollateral,
fundingPubKey = pubKeys.fundingKey,
payoutSPK = pubKeys.payoutAddress.scriptPubKey,
payoutSerialId = payoutSerialId,
fundingInputs = fundingInputs.map(_.toTLV),
changeSPK = changeAddress.scriptPubKey,
changeSerialId = changeSerialId,
cetSignatures = CETSignaturesV0TLV(cetSigs.adaptorSigs),
refundSignature = ECDigitalSignature.fromFrontOfBytes(
cetSigs.refundSig.signature.bytes),
@ -171,12 +230,16 @@ object DLCMessage {
}
def withoutSigs: DLCAcceptWithoutSigs = {
DLCAcceptWithoutSigs(totalCollateral,
pubKeys,
fundingInputs,
changeAddress,
negotiationFields,
tempContractId)
DLCAcceptWithoutSigs(
totalCollateral = totalCollateral,
pubKeys = pubKeys,
fundingInputs = fundingInputs,
changeAddress = changeAddress,
payoutSerialId = payoutSerialId,
changeSerialId = changeSerialId,
negotiationFields = negotiationFields,
tempContractId = tempContractId
)
}
}
@ -228,6 +291,8 @@ object DLCMessage {
},
changeAddress =
BitcoinAddress.fromScriptPubKey(accept.changeSPK, network),
payoutSerialId = accept.payoutSerialId,
changeSerialId = accept.changeSerialId,
cetSigs = CETSignatures(
outcomeSigs,
PartialSignature(

View file

@ -1,14 +1,8 @@
package org.bitcoins.core.protocol.dlc
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.policy.Policy
import org.bitcoins.core.protocol.dlc.DLCMessage._
import org.bitcoins.core.protocol.script.P2WSHWitnessV0
import org.bitcoins.core.protocol.transaction.{
NonWitnessTransaction,
Transaction,
WitnessTransaction
}
import org.bitcoins.core.protocol.transaction.WitnessTransaction
import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.crypto._
import scodec.bits.ByteVector
@ -217,100 +211,19 @@ object DLCStatus {
offer: DLCOffer,
accept: DLCAccept,
sign: DLCSign,
cet: Transaction): (SchnorrDigitalSignature, OracleOutcome) = {
val wCET = cet match {
case wtx: WitnessTransaction => wtx
case _: NonWitnessTransaction =>
throw new IllegalArgumentException(s"Expected Witness CET: $cet")
}
val cetSigs = wCET.witness.head
.asInstanceOf[P2WSHWitnessV0]
.signatures
require(cetSigs.size == 2,
s"There must be only 2 signatures, got ${cetSigs.size}")
/** Extracts an adaptor secret from cetSig assuming it is the completion
* adaptorSig (which it may not be) and returns the oracle signature if
* and only if adaptorSig does correspond to cetSig.
*
* 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 adaptorSig The adaptor signature corresponding to outcome
* @param cetSig The actual signature for local's key found on-chain on a CET
*/
def sigFromOutcomeAndSigs(
outcome: OracleOutcome,
adaptorSig: ECAdaptorSignature,
cetSig: ECDigitalSignature): SchnorrDigitalSignature = {
val sigPubKey = outcome.sigPoint
// 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 possibleOracleS =
sigPubKey
.extractAdaptorSecret(adaptorSig,
ECDigitalSignature(cetSig.bytes.init))
.fieldElement
SchnorrDigitalSignature(outcome.aggregateNonce, possibleOracleS)
}
val outcomeValues = wCET.outputs.map(_.value).sorted
val totalCollateral = offer.contractInfo.totalCollateral
val possibleOutcomes = offer.contractInfo.allOutcomesAndPayouts
.filter { case (_, amt) =>
val amts = Vector(amt, totalCollateral - amt)
.filter(_ >= Policy.dustThreshold)
.sorted
// Only messages within 1 satoshi of the on-chain CET's value
// should be considered.
// Off-by-one is okay because both parties round to the nearest
// Satoshi for fees and if both round up they could be off-by-one.
Math.abs((amts.head - outcomeValues.head).satoshis.toLong) <= 1 && Math
.abs((amts.last - outcomeValues.last).satoshis.toLong) <= 1
}
.map(_._1)
val (offerCETSig, acceptCETSig) =
if (
offer.pubKeys.fundingKey.hex.compareTo(
accept.pubKeys.fundingKey.hex) > 0
) {
(cetSigs.last, cetSigs.head)
} else {
(cetSigs.head, cetSigs.last)
}
val (cetSig, outcomeSigs) = if (isInitiator) {
val possibleOutcomeSigs = sign.cetSigs.outcomeSigs.filter {
case (outcome, _) => possibleOutcomes.contains(outcome)
}
(acceptCETSig, possibleOutcomeSigs)
cet: WitnessTransaction): Option[
(SchnorrDigitalSignature, OracleOutcome)] = {
val localAdaptorSigs = if (isInitiator) {
sign.cetSigs.outcomeSigs
} else {
val possibleOutcomeSigs = accept.cetSigs.outcomeSigs.filter {
case (outcome, _) => possibleOutcomes.contains(outcome)
}
(offerCETSig, possibleOutcomeSigs)
accept.cetSigs.outcomeSigs
}
val sigOpt = outcomeSigs.find { case (outcome, adaptorSig) =>
val possibleOracleSig =
sigFromOutcomeAndSigs(outcome, adaptorSig, cetSig)
possibleOracleSig.sig.getPublicKey == outcome.sigPoint
}
sigOpt match {
case Some((outcome, adaptorSig)) =>
(sigFromOutcomeAndSigs(outcome, adaptorSig, cetSig), outcome)
case None =>
throw new IllegalArgumentException("No Oracle Signature found from CET")
}
DLCUtil.computeOutcome(isInitiator,
offer.pubKeys.fundingKey,
accept.pubKeys.fundingKey,
offer.contractInfo,
localAdaptorSigs,
cet)
}
}

View file

@ -0,0 +1,124 @@
package org.bitcoins.core.protocol.dlc
import org.bitcoins.core.policy.Policy
import org.bitcoins.core.protocol.script.P2WSHWitnessV0
import org.bitcoins.core.protocol.transaction.{Transaction, WitnessTransaction}
import org.bitcoins.crypto.{
ECAdaptorSignature,
ECDigitalSignature,
ECPublicKey,
SchnorrDigitalSignature,
Sha256Digest
}
import scodec.bits.ByteVector
import scala.util.Try
object DLCUtil {
def computeContractId(
fundingTx: Transaction,
tempContractId: Sha256Digest): ByteVector = {
fundingTx.txIdBE.bytes.xor(tempContractId.bytes)
}
/** Extracts an adaptor secret from cetSig assuming it is the completion
* adaptorSig (which it may not be) and returns the oracle signature if
* and only if adaptorSig does correspond to cetSig.
*
* 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 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,
adaptorSig: ECAdaptorSignature,
cetSig: ECDigitalSignature): Try[SchnorrDigitalSignature] = {
val sigPubKey = outcome.sigPoint
// 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
.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) =>
val possibleOracleSigT =
sigFromOutcomeAndSigs(outcome, adaptorSig, completedSig)
possibleOracleSigT.isSuccess && possibleOracleSigT.get.sig.getPublicKey == outcome.sigPoint
}
sigOpt.map { case (outcome, adaptorSig) =>
(sigFromOutcomeAndSigs(outcome, adaptorSig, completedSig).get, outcome)
}
}
def computeOutcome(
isInitiator: Boolean,
offerFundingKey: ECPublicKey,
acceptFundingKey: ECPublicKey,
contractInfo: ContractInfo,
localAdaptorSigs: Vector[(OracleOutcome, ECAdaptorSignature)],
cet: WitnessTransaction): Option[
(SchnorrDigitalSignature, OracleOutcome)] = {
val cetSigs = cet.witness.head
.asInstanceOf[P2WSHWitnessV0]
.signatures
require(cetSigs.size == 2,
s"There must be only 2 signatures, got ${cetSigs.size}")
val outcomeValues = cet.outputs.map(_.value).sorted
val totalCollateral = contractInfo.totalCollateral
val possibleOutcomes = contractInfo.allOutcomesAndPayouts
.filter { case (_, amt) =>
val amts = Vector(amt, totalCollateral - amt)
.filter(_ >= Policy.dustThreshold)
.sorted
// Only messages within 1 satoshi of the on-chain CET's value
// should be considered.
// Off-by-one is okay because both parties round to the nearest
// Satoshi for fees and if both round up they could be off-by-one.
Math.abs((amts.head - outcomeValues.head).satoshis.toLong) <= 1 && Math
.abs((amts.last - outcomeValues.last).satoshis.toLong) <= 1
}
.map(_._1)
val (offerCETSig, acceptCETSig) =
if (offerFundingKey.hex.compareTo(acceptFundingKey.hex) > 0) {
(cetSigs.last, cetSigs.head)
} else {
(cetSigs.head, cetSigs.last)
}
val outcomeSigs = localAdaptorSigs.filter { case (outcome, _) =>
possibleOutcomes.contains(outcome)
}
val cetSig = if (isInitiator) {
acceptCETSig
} else {
offerCETSig
}
computeOutcome(cetSig, outcomeSigs)
}
}

View file

@ -704,6 +704,7 @@ object DigitDecompositionEventDescriptorV0TLV
sealed trait OracleEventTLV extends TLV {
def eventDescriptor: EventDescriptorTLV
def nonces: Vector[SchnorrNonce]
def eventId: NormalizedString
}
case class OracleEventV0TLV(
@ -1253,9 +1254,12 @@ object ContractInfoV0TLV extends TLVFactory[ContractInfoV0TLV] {
override val typeName: String = "ContractInfoV0TLV"
}
sealed trait FundingInputTLV extends TLV
sealed trait FundingInputTLV extends TLV {
def inputSerialId: UInt64
}
case class FundingInputV0TLV(
inputSerialId: UInt64,
prevTx: Transaction,
prevTxVout: UInt32,
sequence: UInt32,
@ -1284,7 +1288,8 @@ case class FundingInputV0TLV(
val redeemScript =
redeemScriptOpt.getOrElse(EmptyScriptPubKey)
u16Prefix(prevTx.bytes) ++
inputSerialId.bytes ++
u16Prefix(prevTx.bytes) ++
prevTxVout.bytes ++
sequence.bytes ++
maxWitnessLen.bytes ++
@ -1298,6 +1303,7 @@ object FundingInputV0TLV extends TLVFactory[FundingInputV0TLV] {
override def fromTLVValue(value: ByteVector): FundingInputV0TLV = {
val iter = ValueIterator(value)
val serialId = iter.takeU64()
val prevTx = iter.takeU16Prefixed(iter.take(Transaction, _))
val prevTxVout = iter.takeU32()
val sequence = iter.takeU32()
@ -1308,10 +1314,11 @@ object FundingInputV0TLV extends TLVFactory[FundingInputV0TLV] {
case wspk: WitnessScriptPubKey => Some(wspk)
case _: NonWitnessScriptPubKey =>
throw new IllegalArgumentException(
s"Redeem Script must be Segwith SPK: $redeemScript")
s"Redeem Script must be Segwit SPK: $redeemScript")
}
FundingInputV0TLV(prevTx,
FundingInputV0TLV(serialId,
prevTx,
prevTxVout,
sequence,
maxWitnessLen,
@ -1391,13 +1398,20 @@ case class DLCOfferTLV(
contractInfo: ContractInfoV0TLV,
fundingPubKey: ECPublicKey,
payoutSPK: ScriptPubKey,
payoutSerialId: UInt64,
totalCollateralSatoshis: Satoshis,
fundingInputs: Vector[FundingInputTLV],
changeSPK: ScriptPubKey,
changeSerialId: UInt64,
fundOutputSerialId: UInt64,
feeRate: SatoshisPerVirtualByte,
contractMaturityBound: BlockTimeStamp,
contractTimeout: BlockTimeStamp)
extends TLV {
require(
changeSerialId != fundOutputSerialId,
s"changeSerialId ($changeSerialId) cannot be equal to fundOutputSerialId ($fundOutputSerialId)")
override val tpe: BigSizeUInt = DLCOfferTLV.tpe
override val value: ByteVector = {
@ -1406,9 +1420,12 @@ case class DLCOfferTLV(
contractInfo.bytes ++
fundingPubKey.bytes ++
TLV.encodeScript(payoutSPK) ++
payoutSerialId.bytes ++
satBytes(totalCollateralSatoshis) ++
u16PrefixedList(fundingInputs) ++
TLV.encodeScript(changeSPK) ++
changeSerialId.bytes ++
fundOutputSerialId.bytes ++
satBytes(feeRate.currencyUnit.satoshis) ++
contractMaturityBound.toUInt32.bytes ++
contractTimeout.toUInt32.bytes
@ -1426,10 +1443,13 @@ object DLCOfferTLV extends TLVFactory[DLCOfferTLV] {
val contractInfo = iter.take(ContractInfoV0TLV)
val fundingPubKey = iter.take(ECPublicKey, 33)
val payoutSPK = iter.takeSPK()
val payoutSerialId = iter.takeU64()
val totalCollateralSatoshis = iter.takeSats()
val fundingInputs =
iter.takeU16PrefixedList(() => iter.take(FundingInputV0TLV))
val changeSPK = iter.takeSPK()
val changeSerialId = iter.takeU64()
val fundingOutputSerialId = iter.takeU64()
val feeRate = SatoshisPerVirtualByte(iter.takeSats())
val contractMaturityBound = BlockTimeStamp(iter.takeU32())
val contractTimeout = BlockTimeStamp(iter.takeU32())
@ -1440,9 +1460,12 @@ object DLCOfferTLV extends TLVFactory[DLCOfferTLV] {
contractInfo,
fundingPubKey,
payoutSPK,
payoutSerialId,
totalCollateralSatoshis,
fundingInputs,
changeSPK,
changeSerialId,
fundingOutputSerialId,
feeRate,
contractMaturityBound,
contractTimeout
@ -1510,8 +1533,10 @@ case class DLCAcceptTLV(
totalCollateralSatoshis: Satoshis,
fundingPubKey: ECPublicKey,
payoutSPK: ScriptPubKey,
payoutSerialId: UInt64,
fundingInputs: Vector[FundingInputTLV],
changeSPK: ScriptPubKey,
changeSerialId: UInt64,
cetSignatures: CETSignaturesTLV,
refundSignature: ECDigitalSignature,
negotiationFields: NegotiationFieldsTLV)
@ -1523,8 +1548,10 @@ case class DLCAcceptTLV(
satBytes(totalCollateralSatoshis) ++
fundingPubKey.bytes ++
TLV.encodeScript(payoutSPK) ++
payoutSerialId.bytes ++
u16PrefixedList(fundingInputs) ++
TLV.encodeScript(changeSPK) ++
changeSerialId.bytes ++
cetSignatures.bytes ++
refundSignature.toRawRS ++
negotiationFields.bytes
@ -1541,22 +1568,28 @@ object DLCAcceptTLV extends TLVFactory[DLCAcceptTLV] {
val totalCollateralSatoshis = iter.takeSats()
val fundingPubKey = iter.take(ECPublicKey, 33)
val payoutSPK = iter.takeSPK()
val payoutSerialId = iter.takeU64()
val fundingInputs =
iter.takeU16PrefixedList(() => iter.take(FundingInputV0TLV))
val changeSPK = iter.takeSPK()
val changeSerialId = iter.takeU64()
val cetSignatures = iter.take(CETSignaturesV0TLV)
val refundSignature = ECDigitalSignature.fromRS(iter.take(64))
val negotiationFields = iter.take(NegotiationFieldsTLV)
DLCAcceptTLV(tempContractId,
totalCollateralSatoshis,
fundingPubKey,
payoutSPK,
fundingInputs,
changeSPK,
cetSignatures,
refundSignature,
negotiationFields)
DLCAcceptTLV(
tempContractId,
totalCollateralSatoshis,
fundingPubKey,
payoutSPK,
payoutSerialId,
fundingInputs,
changeSPK,
changeSerialId,
cetSignatures,
refundSignature,
negotiationFields
)
}
override val typeName: String = "DLCAcceptTLV"

View file

@ -563,9 +563,13 @@ case class DualFundingTxFinalizer(
lazy val (offerFutureFee, offerFundingFee) =
computeFees(offerInputs, offerPayoutSPK, offerChangeSPK)
lazy val offerFees: CurrencyUnit = offerFutureFee + offerFundingFee
lazy val (acceptFutureFee, acceptFundingFee) =
computeFees(acceptInputs, acceptPayoutSPK, acceptChangeSPK)
lazy val acceptFees: CurrencyUnit = acceptFutureFee + acceptFundingFee
override def buildTx(txBuilderResult: RawTxBuilderResult): Transaction = {
val addOfferFutureFee =
AddFutureFeeFinalizer(fundingSPK, offerFutureFee, Vector(offerChangeSPK))

View file

@ -17,6 +17,7 @@ import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.protocol.BlockStamp
import org.bitcoins.core.protocol.dlc._
import org.bitcoins.core.protocol.dlc.RoundingIntervals.IntervalStart
import org.bitcoins.core.number._
import org.bitcoins.core.protocol.script.EmptyScriptPubKey
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.core.util.Indexed
@ -179,9 +180,12 @@ val offerTLV = DLCOfferTLV(
contractInfo = contractInfo.toTLV,
fundingPubKey = ECPublicKey.freshPublicKey,
payoutSPK = EmptyScriptPubKey,
payoutSerialId = UInt64(1),
totalCollateralSatoshis = Satoshis(500),
fundingInputs = Vector.empty,
changeSPK = EmptyScriptPubKey,
changeSerialId = UInt64(2),
fundOutputSerialId = UInt64(3),
feeRate = SatoshisPerVirtualByte(Satoshis(1)),
contractMaturityBound = BlockStamp.BlockHeight(0),
contractTimeout = BlockStamp.BlockHeight(0)

View file

@ -1,12 +1,13 @@
package org.bitcoins.testkitcore.gen
import org.bitcoins.core.protocol.dlc.DLCMessage.DLCOffer
import org.bitcoins.core.config.Networks
import org.bitcoins.core.currency.{Bitcoins, CurrencyUnit, Satoshis}
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.number.{UInt32, UInt64}
import org.bitcoins.core.protocol.dlc.DLCMessage.DLCOffer
import org.bitcoins.core.protocol.dlc.{
ContractInfo,
DLCFundingInputP2WPKHV0,
DLCMessage,
EnumContractDescriptor
}
import org.bitcoins.core.protocol.tlv._
@ -117,38 +118,6 @@ trait TLVGen {
def contractDescriptorV0TLVWithTotalCollateral: Gen[
(ContractDescriptorV0TLV, Satoshis)] = {
def genValues(size: Int, totalAmount: CurrencyUnit): Vector[Satoshis] = {
val vals = if (size < 2) {
throw new IllegalArgumentException(
s"Size must be at least two, got $size")
} else if (size == 2) {
Vector(totalAmount.satoshis, Satoshis.zero)
} else {
(0 until size - 2).map { _ =>
Satoshis(NumberUtil.randomLong(totalAmount.satoshis.toLong))
}.toVector :+ totalAmount.satoshis :+ Satoshis.zero
}
val valsWithOrder = vals.map(_ -> scala.util.Random.nextDouble())
valsWithOrder.sortBy(_._2).map(_._1)
}
def genContractDescriptors(
outcomes: Vector[String],
totalInput: CurrencyUnit): (
EnumContractDescriptor,
EnumContractDescriptor) = {
val outcomeMap =
outcomes
.map(EnumOutcome.apply)
.zip(genValues(outcomes.length, totalInput))
val info = EnumContractDescriptor(outcomeMap)
val remoteInfo = info.flip(totalInput.satoshis)
(info, remoteInfo)
}
for {
numOutcomes <- Gen.choose(2, 10)
outcomes <- Gen.listOfN(numOutcomes, StringGenerators.genString)
@ -156,8 +125,31 @@ trait TLVGen {
Gen
.choose(numOutcomes + 1, Long.MaxValue / 10000L)
.map(Satoshis.apply)
(contractDescriptor, _) =
genContractDescriptors(outcomes.toVector, totalInput)
contractDescriptor = {
//DLCTestUtil.genContractDescriptors(outcomes.toVector, totalInput)
val satVals = {
val vals = if (outcomes.length < 2) {
throw new IllegalArgumentException(
s"Size must be at least two, got ${outcomes.length}")
} else if (outcomes.length == 2) {
Vector(totalInput, Satoshis.zero)
} else {
(0 until outcomes.length - 2).map { _ =>
Satoshis(NumberUtil.randomLong(totalInput.toLong))
}.toVector :+ totalInput :+ Satoshis.zero
}
val valsWithOrder = vals.map(_ -> scala.util.Random.nextDouble())
valsWithOrder.sortBy(_._2).map(_._1)
}
val outcomeMap =
outcomes.toVector
.map(EnumOutcome.apply)
.zip(satVals)
EnumContractDescriptor(outcomeMap)
}
} yield {
(contractDescriptor.toTLV, totalInput)
}
@ -167,25 +159,30 @@ trait TLVGen {
contractDescriptorV0TLVWithTotalCollateral.map(_._1)
}
def oracleInfoV0TLV: Gen[OracleInfoV0TLV] = {
def oracleInfoV0TLV(outcomes: Vector[String]): Gen[OracleInfoV0TLV] = {
for {
privKey <- CryptoGenerators.privateKey
rValue <- CryptoGenerators.schnorrNonce
outcomes <- Gen.listOf(StringGenerators.genUTF8String)
} yield {
OracleInfoV0TLV(
OracleAnnouncementV0TLV.dummyForEventsAndKeys(
privKey,
rValue,
outcomes.toVector.map(EnumOutcome.apply)))
outcomes.map(EnumOutcome.apply)))
}
}
def oracleInfoV0TLV: Gen[OracleInfoV0TLV] = {
Gen
.listOf(StringGenerators.genUTF8String)
.flatMap(outcomes => oracleInfoV0TLV(outcomes.toVector))
}
def contractInfoV0TLV: Gen[ContractInfoV0TLV] = {
for {
(descriptor, totalCollateral) <-
contractDescriptorV0TLVWithTotalCollateral
oracleInfo <- oracleInfoV0TLV
oracleInfo <- oracleInfoV0TLV(descriptor.outcomes.map(_._1))
} yield {
ContractInfoV0TLV(totalCollateral, descriptor, oracleInfo)
}
@ -208,7 +205,8 @@ trait TLVGen {
}
}
def fundingInputP2WPKHTLV: Gen[FundingInputV0TLV] = {
def fundingInputP2WPKHTLV(ignoreSerialIds: Vector[UInt64] =
Vector.empty): Gen[FundingInputV0TLV] = {
for {
prevTx <- TransactionGenerators.realisticTransactionWitnessOut
prevTxVout <- Gen.choose(0, prevTx.outputs.length - 1)
@ -225,20 +223,26 @@ trait TLVGen {
wtx.copy(outputs = wtx.outputs.updated(prevTxVout, newOutput))
}
} yield {
DLCFundingInputP2WPKHV0(newPrevTx, UInt32(prevTxVout), sequence).toTLV
DLCFundingInputP2WPKHV0(DLCMessage.genSerialId(ignoreSerialIds),
newPrevTx,
UInt32(prevTxVout),
sequence).toTLV
}
}
def fundingInputV0TLV: Gen[FundingInputV0TLV] = {
fundingInputP2WPKHTLV // Soon to be Gen.oneOf
def fundingInputV0TLV(ignoreSerialIds: Vector[UInt64] = Vector.empty): Gen[
FundingInputV0TLV] = {
fundingInputP2WPKHTLV(ignoreSerialIds) // Soon to be Gen.oneOf
}
def fundingInputV0TLVs(
collateralNeeded: CurrencyUnit): Gen[Vector[FundingInputV0TLV]] = {
collateralNeeded: CurrencyUnit,
ignoreSerialIds: Vector[UInt64] = Vector.empty): Gen[
Vector[FundingInputV0TLV]] = {
for {
numInputs <- Gen.choose(0, 5)
inputs <- Gen.listOfN(numInputs, fundingInputV0TLV)
input <- fundingInputV0TLV
inputs <- Gen.listOfN(numInputs, fundingInputV0TLV(ignoreSerialIds))
input <- fundingInputV0TLV(ignoreSerialIds)
} yield {
val totalFunding =
inputs.foldLeft[CurrencyUnit](Satoshis.zero)(_ + _.output.value)
@ -296,9 +300,12 @@ trait TLVGen {
contractInfo <- contractInfoV0TLV
fundingPubKey <- CryptoGenerators.publicKey
payoutAddress <- AddressGenerator.bitcoinAddress
payoutSerialId <- NumberGenerator.uInt64
totalCollateralSatoshis <- CurrencyUnitGenerator.positiveRealistic
fundingInputs <- fundingInputV0TLVs(totalCollateralSatoshis)
changeAddress <- AddressGenerator.bitcoinAddress
changeSerialId <- NumberGenerator.uInt64
fundOutputSerialId <- NumberGenerator.uInt64.suchThat(_ != changeSerialId)
feeRate <- CurrencyUnitGenerator.positiveRealistic.map(
SatoshisPerVirtualByte.apply)
timeout1 <- NumberGenerator.uInt32s
@ -311,17 +318,20 @@ trait TLVGen {
}
DLCOfferTLV(
0.toByte,
chainHash,
contractInfo,
fundingPubKey,
payoutAddress.scriptPubKey,
totalCollateralSatoshis,
fundingInputs,
changeAddress.scriptPubKey,
feeRate,
contractMaturityBound,
contractTimeout
contractFlags = 0.toByte,
chainHash = chainHash,
contractInfo = contractInfo,
fundingPubKey = fundingPubKey,
payoutSPK = payoutAddress.scriptPubKey,
payoutSerialId = payoutSerialId,
totalCollateralSatoshis = totalCollateralSatoshis,
fundingInputs = fundingInputs,
changeSPK = changeAddress.scriptPubKey,
changeSerialId = changeSerialId,
fundOutputSerialId = fundOutputSerialId,
feeRate = feeRate,
contractMaturityBound = contractMaturityBound,
contractTimeout = contractTimeout
)
}
}
@ -345,8 +355,10 @@ trait TLVGen {
totalCollateralSatoshis <- CurrencyUnitGenerator.positiveRealistic
fundingPubKey <- CryptoGenerators.publicKey
payoutAddress <- AddressGenerator.bitcoinAddress
payoutSerialId <- NumberGenerator.uInt64
fundingInputs <- fundingInputV0TLVs(totalCollateralSatoshis)
changeAddress <- AddressGenerator.bitcoinAddress
changeSerialId <- NumberGenerator.uInt64
cetSigs <- cetSignaturesV0TLV
refundSig <- CryptoGenerators.digitalSignature
} yield {
@ -355,8 +367,10 @@ trait TLVGen {
totalCollateralSatoshis,
fundingPubKey,
payoutAddress.scriptPubKey,
payoutSerialId,
fundingInputs,
changeAddress.scriptPubKey,
changeSerialId,
cetSigs,
refundSig,
NoNegotiationFieldsTLV
@ -370,12 +384,18 @@ trait TLVGen {
for {
fundingPubKey <- CryptoGenerators.publicKey
payoutAddress <- AddressGenerator.bitcoinAddress
payoutSerialId <- NumberGenerator.uInt64.suchThat(
_ != offer.payoutSerialId)
totalCollateralSatoshis <- CurrencyUnitGenerator.positiveRealistic
totalCollateral = scala.math.max(
(contractInfo.max - offer.totalCollateralSatoshis).satoshis.toLong,
totalCollateralSatoshis.toLong)
fundingInputs <- fundingInputV0TLVs(Satoshis(totalCollateral))
fundingInputs <- fundingInputV0TLVs(
Satoshis(totalCollateral),
offer.fundingInputs.map(_.inputSerialId))
changeAddress <- AddressGenerator.bitcoinAddress
changeSerialId <- NumberGenerator.uInt64.suchThat(num =>
num != offer.changeSerialId && num != offer.fundOutputSerialId)
cetSigs <- cetSignaturesV0TLV(contractInfo.allOutcomes.length)
refundSig <- CryptoGenerators.digitalSignature
} yield {
@ -384,8 +404,10 @@ trait TLVGen {
Satoshis(totalCollateral),
fundingPubKey,
payoutAddress.scriptPubKey,
payoutSerialId,
fundingInputs,
changeAddress.scriptPubKey,
changeSerialId,
cetSigs,
refundSig,
NoNegotiationFieldsTLV
@ -430,7 +452,7 @@ trait TLVGen {
oracleAnnouncementV0TLV,
contractInfoV0TLV,
oracleInfoV0TLV,
fundingInputV0TLV,
fundingInputV0TLV(),
cetSignaturesV0TLV,
fundingSignaturesV0TLV,
dlcOfferTLV,