bitcoin-s/docs/core/dlc.md
benthecarman e098aba680
Create and implement OrderedAnnouncements type (#3356)
* Create and implement OrderedAnnouncements type

* Fix docs

* Update scaladoc
2021-07-06 07:58:58 -05:00

11 KiB

id title
dlc Discreet Log Contract Data Structures

Bitcoin-S now has support for the basic data structures and algorithms involved in the setup and execution of Discreet Log Contracts (DLCs) in the package org.bitcoins.core.protocol.dlc as well as basic TLV format and LN Message format support at org.bitcoins.core.protocol.tlv which can be useful for many off-chain protocols including DLCs and Lightning.

Please note that this code is experimental as the DLC specification is a work-in-progress and so this code is not stable and is subject to change. This means that the DLC code in the official release may be out-of-date and may conflict with other implementations' results.

Let's now cover the main data structures and interfaces supported.

import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.protocol.BlockStamp
import org.bitcoins.core.protocol.dlc.compute._
import org.bitcoins.core.protocol.dlc.models._
import org.bitcoins.core.protocol.dlc.models.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
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.crypto._
import org.bitcoins.core.util.sorted._

DLCPayoutCurve

DLCPayoutCurve.scala provides an interface for serializing and evaluating payout curves for DLCs as specified in the Payout Curve Specification. This file supports arbitrary polynomial interpolation.

To approximate a payout curve that is not a piecewise polynomial function, one may either propose a new kind of curve to the specification, or use approximation. For example by feeding DLCPayoutCurve a list of OutcomePayoutEndpoints, one receives a linear approximation of their payout curve which takes the sampled points and "connects the dots" with straight lines. Alternatively one can use spline interpolation and sample two midpoints of every spline to get a piecewise cubic interpolation.

// Constructing a forward contract's payout curve (going long) that looks like this:
//         ________________
//        /
//       /
// _____/

// Assume a 15 binary digit oracle
val maxVal = (1L << 15) - 1
val pts = Vector(
    OutcomePayoutEndpoint(0, 0),
    OutcomePayoutEndpoint(1000, 0),
    OutcomePayoutEndpoint(2000, 1000),
    OutcomePayoutEndpoint(maxVal, 1000)
)
val curve = DLCPayoutCurve(pts)

// Let's evalute the curve's values at varios points
curve(500)
curve(1500)
curve(1667)
curve(10000)

// Now with rounding to the nearest 100
val roundTo100 = RoundingIntervals(Vector(IntervalStart(0, 100)))
curve(1667, roundTo100)

// Let's take a look at the pieces in this piece-wise polynomial
curve.functionComponents

// And we can even see which component is used on a given outcome
val Indexed(line1, _) = curve.componentFor(500)
line1(500)

val Indexed(line2, _) = curve.componentFor(1667)
line2(1667)

val Indexed(line3, _) = curve.componentFor(10000)
line3(10000)

While many approaches result in higher fidelity to the original curve than linear approximation, usually it is the case that numeric contracts use Rounding which dominates the error so that linear approximation is adequate.

CETCalculator

CETCalculator.scala provides an interface to all Contract Execution Transaction (CET) set computations as specified in the CET Compression Specification and the Multi-Oracle Specification.

Of particular note, are the functions computeCETs and computeMultiOracleCETsBinary which compute the entire set of outcomes (corresponding to CETs) for a single oracle and multiple numeric oracles with a bounded difference allowed respectively.

Additionally the combinations function allows for the easy and correctly-ordered reduction of any t-of-n scheme to many t-of-t schemes.

val totalCollateral = Satoshis(1000)
val cetsNoRounding = CETCalculator.computeCETs(base = 2, numDigits = 15, curve, totalCollateral, RoundingIntervals.noRounding)
cetsNoRounding.length
val cetsWithRounding = CETCalculator.computeCETs(base = 2, numDigits = 15, curve, totalCollateral, roundTo100)
cetsWithRounding.length
val oraclesStr = Vector("Alice", "Bob", "Carol", "Dave", "Eve")
val combinations = CETCalculator.combinations(oraclesStr, threshold = 3)
val multiOracleCETsNoRounding = CETCalculator.computeMultiOracleCETsBinary(
    numDigits = 15,
    curve,
    totalCollateral,
    RoundingIntervals.noRounding,
    maxErrorExp = 5,
    minFailExp = 3,
    maximizeCoverage = false,
    numOracles = 3
)
multiOracleCETsNoRounding.length
val multiOracleCETsWithRounding = CETCalculator.computeMultiOracleCETsBinary(
    numDigits = 15,
    curve,
    totalCollateral,
    roundTo100,
    maxErrorExp = 5,
    minFailExp = 3,
    maximizeCoverage = false,
    numOracles = 3
)
multiOracleCETsWithRounding.length

OracleOutcome

OracleOutcomes correspond one-to-one with DLC execution paths (and with CETs). An OracleOutcome contains information about which oracles signed what, but storing only the information actually used for DLC execution and not extra unneeded information such as what additional oracles signed.

This trait also exposes an endpoint to the aggregate signature point (aka adaptor point) corresponding to this outcome which is used during CET signing, as well as an endpoint to compute the aggregate nonce.

DLCStatus

DLCStatus.scala contains the DLC state machine comprised of the states [Offered, Accepted, Signed, Broadcasted, Confirmed, Claimed, RemoteClaimed, Refunded] each of which contain all relevant P2P messages and on-chain transactions.

The DLCStatus object also contains many useful utility functions to extract and compute various things from a DLCStatus such as transaction ids and even computing the OracleOutcome and aggregate oracle signature from on-chain information in the case that one's remote counter-party initiates execution.

ContractInfo

ContractInfo wraps a ContractDescriptor and an OracleInfo and provides the complete external-facing interface needed during DLC setup and execution. A ContractInfo fully determines a DLC up to the choice of public keys and funding UTXOs to be used. One of its most important functions is allOutcomesAndPayouts which computes the entire set of OracleOutcomes and their corresponding payouts. Most of its other functions utilize that set to do all sorts of things such as retrieving payouts or finding an outcome given OracleSignatures.

val descriptor = NumericContractDescriptor(curve, numDigits = 15, roundTo100)

val announcements = 0.until(5).toVector.map { _ =>
    val oraclePrivKey = ECPrivateKey.freshPrivateKey
    val nonces = 0.until(15).toVector.map(_ => ECPrivateKey.freshPrivateKey.schnorrNonce)
  	OracleAnnouncementV0TLV.dummyForKeys(oraclePrivKey, nonces)
}
val oracleInfo = NumericMultiOracleInfo(
    threshold = 3,
    announcements = OrderedAnnouncements(announcements),
    maxErrorExp = 5,
    minFailExp = 3,
    maximizeCoverage = false
)

val contractInfo = ContractInfo(totalCollateral, ContractOraclePair.NumericPair(descriptor, oracleInfo))
contractInfo.max
contractInfo.allOutcomes.length

val signingOracles = oracleInfo.singleOracleInfos.take(3)
val outcome = NumericOracleOutcome(signingOracles.map((_, UnsignedNumericOutcome(Vector(0, 0, 0, 0, 0)))))
contractInfo.getPayouts(outcome)

TLV

TLV.scala provides protocol agnostic infrastructure for the Type-Length-Value (TLV) serialization format which is used by both the Lightning Network and DLCs. The file contains a sealed trait of all possible TLVs as well as utilities for defining new types.

This includes NormalizedString a standardized string serialization using varint-prefixed UTF-8 with NFC normalization which has implicit conversions with String so that they can be used interchangeably. This also includes a TLVUtil trait for serialization and a ValueIterator class for deserialization.

Currently, only the most basic Lightning messages are defined (Ping, Pong, Error) but all DLC messages are supported.

LnMessage

LnMessage.scala provides support for the Lightning Network Message serialization format, which is very similar to the TLV format with some minor differences so that an LnMessage simply wraps a TLV and provides the altered serialization format. Likewise one can parse a LnMessage using a TLVFactory and subsequently unwrap to get the nested TLV.

val offerTLV = DLCOfferTLV(
    contractFlags = 0.toByte,
    chainHash = DoubleSha256Digest.empty,
    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)
)

val lnMsgHex = LnMessage(offerTLV).hex
val lnMsg = LnMessageFactory(DLCOfferTLV).fromHex(lnMsgHex)

lnMsg.tlv == offerTLV