abstract event announcement away as an array of locking points

To support digit decomposition events, dlctix will represent
the oracle event data as an array of locking points, each
representing a different outcome in the DLC. Enum events can
be easily converted into a set of locking points. Digit decomp
events can also be likewise converted, by enumerating the set
of relevant digit ranges, and aggregating sets of locking points
together. As a bonus, this also adds support for multi-oracle
DLCs, as oracle locking points from different oracles can also
be safely aggregated.
This commit is contained in:
conduition
2024-06-26 01:16:59 +00:00
parent 8712a099d6
commit 1cdef68a0a
7 changed files with 164 additions and 158 deletions

View File

@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
use crate::{
consts::{P2TR_DUST_VALUE, P2TR_SCRIPT_PUBKEY_SIZE},
errors::Error,
oracles::EventAnnouncement,
oracles::EventLockingConditions,
parties::{MarketMaker, Player},
spend_info::FundingSpendInfo,
};
@@ -53,10 +53,10 @@ pub struct ContractParameters {
pub players: Vec<Player>,
/// The event whose outcome determines the payouts.
pub event: EventAnnouncement,
pub event: EventLockingConditions,
/// A mapping of payout weights under different outcomes. Attestation indexes should
/// align with [`self.event.outcome_messages`][EventAnnouncement::outcome_messages].
/// align with [`self.event.locking_points`][EventLockingConditions::locking_points].
///
// The outcome payouts map describes how payouts are allocated based on the Outcome
// which has been attested to by the oracle. If the oracle doesn't attest to any

View File

@@ -136,7 +136,8 @@ pub(crate) fn partial_sign_outcome_txs(
// All outcome TX signatures should be locked by the oracle's outcome point.
let attestation_lock_point = params
.event
.attestation_lock_point(outcome_index)
.locking_points
.get(outcome_index)
.ok_or(Error)?;
// sign under an attestation lock point
@@ -145,7 +146,7 @@ pub(crate) fn partial_sign_outcome_txs(
seckey,
secnonce,
aggnonce,
attestation_lock_point,
*attestation_lock_point,
sighash,
)?
}
@@ -189,14 +190,15 @@ pub(crate) fn verify_outcome_tx_partial_signatures(
// All outcome TX signatures should be locked by the oracle's outcome point.
let attestation_lock_point = params
.event
.attestation_lock_point(outcome_index)
.locking_points
.get(outcome_index)
.ok_or(Error)?;
musig2::adaptor::verify_partial(
funding_spend_info.key_agg_ctx(),
partial_sig,
aggnonce,
attestation_lock_point,
*attestation_lock_point,
signer_pubkey,
pubnonce,
sighash,
@@ -273,13 +275,14 @@ where
Outcome::Attestation(outcome_index) => {
let attestation_lock_point = params
.event
.attestation_lock_point(outcome_index)
.locking_points
.get(outcome_index)
.ok_or(Error)?;
let adaptor_sig = musig2::adaptor::aggregate_partial_signatures(
funding_spend_info.key_agg_ctx(),
aggnonce,
attestation_lock_point,
*attestation_lock_point,
partial_sigs,
sighash,
)?;
@@ -342,7 +345,8 @@ pub(crate) fn verify_outcome_tx_aggregated_signatures(
Outcome::Attestation(outcome_index) => {
let adaptor_point = params
.event
.attestation_lock_point(outcome_index)
.locking_points
.get(outcome_index)
.ok_or(Error)?;
let &signature = outcome_tx_signatures.get(&outcome_index).ok_or(Error)?;
@@ -350,7 +354,7 @@ pub(crate) fn verify_outcome_tx_aggregated_signatures(
joint_pubkey,
sighash,
signature,
adaptor_point,
*adaptor_point,
)
}

View File

@@ -42,7 +42,7 @@ use std::{
pub use contract::{
ContractParameters, Outcome, OutcomeIndex, PayoutWeights, PlayerIndex, SigMap, WinCondition,
};
pub use oracles::EventAnnouncement;
pub use oracles::{attestation_locking_point, attestation_secret, EventLockingConditions};
pub use parties::{MarketMaker, Player};
/// Represents the combined output of building all transactions and precomputing
@@ -547,9 +547,9 @@ pub struct ContractSignatures {
pub expiry_tx_signature: Option<CompactSignature>,
/// A mapping of outcome attestation indexes to adaptor signatures on outcome transactions.
/// The index of each entry corresponds to the outcomes in
/// [`EventAnnouncement::outcome_messages`]. Each adaptor signature can be decrypted
/// by the [`EventAnnouncement`]'s oracle producing an attestation signature using
/// [`EventAnnouncement::attestation_secret`].
/// [`EventLockingConditions::outcome_messages`]. Each adaptor signature can be decrypted
/// by the [`EventLockingConditions`]'s oracle producing an attestation signature using
/// [`EventLockingConditions::attestation_secret`].
pub outcome_tx_signatures: BTreeMap<OutcomeIndex, AdaptorSignature>,
/// A set of signatures needed for broadcasting split transactions. Each signature
/// is specific to a certain combination of player and outcome.
@@ -653,11 +653,12 @@ impl SignedContract {
.dlc
.params
.event
.attestation_lock_point(outcome_index)
.locking_points
.get(outcome_index)
.ok_or(Error)?;
// Invalid attestation.
if attestation.base_point_mul() != locking_point {
if &attestation.base_point_mul() != locking_point {
return Err(Error)?;
}

View File

@@ -1,83 +1,96 @@
use secp::{MaybePoint, MaybeScalar, Point, Scalar};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::{serialization, Outcome, OutcomeIndex};
use crate::Outcome;
/// An oracle's announcement of a future event.
/// The locking points derived from the oracle's announcement of a future event.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EventAnnouncement {
/// The signing oracle's pubkey
pub oracle_pubkey: Point,
/// The `R` point with which the oracle promises to attest to this event.
pub nonce_point: Point,
/// Naive but easy.
#[serde(with = "serialization::vec_of_byte_vecs")]
pub outcome_messages: Vec<Vec<u8>>,
pub struct EventLockingConditions {
/// An array of locking points which represent distinct outcomes. Each locking point
/// should have its discrete log revealed by the oracle when and if that outcome occurs.
pub locking_points: Vec<MaybePoint>,
/// The unix timestamp beyond which the oracle is considered to have gone AWOL.
/// If set to `None`, the event has no expected expiry.
pub expiry: Option<u32>,
}
impl EventAnnouncement {
/// Computes the oracle's locking point for the given outcome index.
pub fn attestation_lock_point(&self, index: OutcomeIndex) -> Option<MaybePoint> {
let msg = &self.outcome_messages.get(index)?;
let e: MaybeScalar = musig2::compute_challenge_hash_tweak(
&self.nonce_point.serialize_xonly(),
&self.oracle_pubkey,
msg,
);
// S = R + eD
Some(self.nonce_point.to_even_y() + e * self.oracle_pubkey.to_even_y())
}
/// Computes the oracle's attestation secret scalar - the discrete log of the
/// locking point - for the given outcome index.
pub fn attestation_secret(
&self,
index: usize,
oracle_seckey: impl Into<Scalar>,
nonce: impl Into<Scalar>,
) -> Option<MaybeScalar> {
let oracle_seckey = oracle_seckey.into();
let nonce = nonce.into();
if oracle_seckey.base_point_mul() != self.oracle_pubkey
|| nonce.base_point_mul() != self.nonce_point
{
return None;
}
let d = oracle_seckey.negate_if(self.oracle_pubkey.parity());
let k = nonce.negate_if(self.nonce_point.parity());
let msg = &self.outcome_messages.get(index)?;
let e: MaybeScalar = musig2::compute_challenge_hash_tweak(
&self.nonce_point.serialize_xonly(),
&self.oracle_pubkey,
msg,
);
Some(k + e * d)
}
impl EventLockingConditions {
/// Returns true if the given outcome is a valid outcome to wager on
/// for this event.
pub fn is_valid_outcome(&self, outcome: &Outcome) -> bool {
match outcome {
&Outcome::Attestation(i) => i < self.outcome_messages.len(),
&Outcome::Attestation(i) => i < self.locking_points.len(),
Outcome::Expiry => self.expiry.is_some(),
}
}
/// Returns an iterator over all possible outcomes in the event.
pub fn all_outcomes(&self) -> impl IntoIterator<Item = Outcome> {
(0..self.outcome_messages.len())
(0..self.locking_points.len())
.map(|i| Outcome::Attestation(i))
.chain(self.expiry.map(|_| Outcome::Expiry))
}
}
fn tagged_hash(tag: &str) -> Sha256 {
let tag_hash = Sha256::new().chain_update(tag).finalize();
Sha256::new()
.chain_update(&tag_hash)
.chain_update(&tag_hash)
}
pub(crate) fn outcome_message_hash(msg: impl AsRef<[u8]>) -> [u8; 32] {
tagged_hash("DLC/oracle/attestation/v0")
.chain_update(msg)
.finalize()
.into()
}
/// Computes the attestation locking point given an oracle pubkey, nonce, and message.
#[allow(non_snake_case)]
pub fn attestation_locking_point(
oracle_pubkey: impl Into<Point>,
nonce: impl Into<Point>,
message: impl AsRef<[u8]>,
) -> MaybePoint {
let oracle_pubkey = oracle_pubkey.into();
let nonce = nonce.into();
let R = nonce.to_even_y();
let D = oracle_pubkey.to_even_y();
let e: MaybeScalar = musig2::compute_challenge_hash_tweak(
&nonce.serialize_xonly(),
&oracle_pubkey,
outcome_message_hash(message),
);
// S = R + eD
R + e * D
}
/// Computes the oracle's attestation secret scalar - the discrete log of the
/// locking point - for the given outcome message.
pub fn attestation_secret(
oracle_seckey: impl Into<Scalar>,
nonce: impl Into<Scalar>,
message: impl AsRef<[u8]>,
) -> MaybeScalar {
let oracle_seckey = oracle_seckey.into();
let nonce = nonce.into();
let oracle_pubkey = oracle_seckey.base_point_mul();
let nonce_point = nonce.base_point_mul();
let d = oracle_seckey.negate_if(oracle_pubkey.parity());
let k = nonce.negate_if(nonce_point.parity());
let e: MaybeScalar = musig2::compute_challenge_hash_tweak(
&nonce_point.serialize_xonly(),
&oracle_pubkey,
outcome_message_hash(message),
);
k + e * d
}

View File

@@ -12,7 +12,7 @@ use bitcoin::{
};
use musig2::{CompactSignature, LiftedSignature, PartialSignature, PubNonce};
use rand::{CryptoRng, Rng, RngCore, SeedableRng};
use secp::{MaybeScalar, Point, Scalar};
use secp::{MaybePoint, MaybeScalar, Point, Scalar};
use bitcoincore_rpc::{jsonrpc::serde_json, Auth, Client as BitcoinClient, RpcApi};
use once_cell::sync::Lazy;
@@ -412,6 +412,7 @@ struct SimulationManager {
market_maker_seckey: Scalar,
oracle_seckey: Scalar,
oracle_secnonce: Scalar,
outcome_messages: Vec<Vec<u8>>,
contract: SignedContract,
rpc: BitcoinClient,
@@ -429,6 +430,17 @@ impl SimulationManager {
// Oracle
let oracle_seckey = Scalar::random(&mut rng);
let oracle_secnonce = Scalar::random(&mut rng);
let oracle_pubkey = oracle_seckey.base_point_mul();
let nonce_point = oracle_secnonce.base_point_mul();
let outcome_messages = vec![
Vec::from(b"alice, bob, and carol win"),
Vec::from(b"bob and carol win"),
Vec::from(b"alice wins"),
];
let locking_points: Vec<MaybePoint> = outcome_messages
.iter()
.map(|msg| attestation_locking_point(oracle_pubkey, nonce_point, msg))
.collect();
// Market maker
let market_maker_seckey = Scalar::random(&mut rng);
@@ -493,14 +505,8 @@ impl SimulationManager {
let contract_params = ContractParameters {
market_maker,
players,
event: EventAnnouncement {
oracle_pubkey: oracle_seckey.base_point_mul(),
nonce_point: oracle_secnonce.base_point_mul(),
outcome_messages: vec![
Vec::from(b"alice, bob, and carol win"),
Vec::from(b"bob and carol win"),
Vec::from(b"alice wins"),
],
event: EventLockingConditions {
locking_points,
expiry: u32::try_from(initial_block_height + 100).ok(),
},
outcome_payouts,
@@ -554,6 +560,7 @@ impl SimulationManager {
market_maker_seckey,
oracle_seckey,
oracle_secnonce,
outcome_messages,
contract: signed_contract,
rpc,
@@ -562,13 +569,16 @@ impl SimulationManager {
}
}
fn event(&self) -> &EventAnnouncement {
fn event(&self) -> &EventLockingConditions {
&self.contract.params().event
}
fn oracle_attestation(&self, outcome_index: OutcomeIndex) -> Option<MaybeScalar> {
self.event()
.attestation_secret(outcome_index, self.oracle_seckey, self.oracle_secnonce)
Some(attestation_secret(
self.oracle_seckey,
self.oracle_secnonce,
self.outcome_messages.get(outcome_index)?,
))
}
fn mine_delta_blocks(&self) -> Result<(), bitcoincore_rpc::Error> {
@@ -614,11 +624,11 @@ fn with_on_chain_resolutions() {
// The attestation should be a valid BIP340 signature by the oracle's pubkey.
{
let oracle_signature =
LiftedSignature::new(manager.event().nonce_point, oracle_attestation);
LiftedSignature::new(manager.oracle_secnonce.base_point_mul(), oracle_attestation);
musig2::verify_single(
manager.event().oracle_pubkey,
manager.oracle_seckey.base_point_mul(),
oracle_signature,
&manager.event().outcome_messages[outcome_index],
crate::oracles::outcome_message_hash(&manager.outcome_messages[outcome_index]),
)
.expect("invalid oracle signature");
}
@@ -1313,6 +1323,8 @@ fn stress_test() {
// Oracle
let oracle_seckey = Scalar::random(&mut rng);
let oracle_secnonce = Scalar::random(&mut rng);
let oracle_pubkey = oracle_seckey.base_point_mul();
let nonce_point = oracle_secnonce.base_point_mul();
// Market maker
let market_maker_seckey = Scalar::random(&mut rng);
@@ -1326,6 +1338,11 @@ fn stress_test() {
.map(|i| Vec::from((i as u32).to_be_bytes()))
.collect();
let locking_points: Vec<MaybePoint> = outcome_messages
.iter()
.map(|msg| attestation_locking_point(oracle_pubkey, nonce_point, msg))
.collect();
// Generate random payouts with 4 winners per outcome
let outcome_payouts: BTreeMap<Outcome, PayoutWeights> = (0..n_outcomes)
.map(|i| {
@@ -1344,10 +1361,8 @@ fn stress_test() {
let contract_params = ContractParameters {
market_maker,
players,
event: EventAnnouncement {
oracle_pubkey: oracle_seckey.base_point_mul(),
nonce_point: oracle_secnonce.base_point_mul(),
outcome_messages,
event: EventLockingConditions {
locking_points,
expiry: None,
},
outcome_payouts,

View File

@@ -157,38 +157,10 @@ pub(crate) mod byte_array {
}
}
pub(crate) mod vec_of_byte_vecs {
use serde::{ser::SerializeSeq, Deserialize, Deserializer, Serialize, Serializer};
use serdect::slice::HexOrBin;
pub(crate) fn serialize<S: Serializer>(vecs: &Vec<Vec<u8>>, ser: S) -> Result<S::Ok, S::Error> {
if !ser.is_human_readable() {
return vecs.serialize(ser);
}
let mut seq = ser.serialize_seq(Some(vecs.len()))?;
for vec in vecs {
let slice: &[u8] = vec.as_ref();
seq.serialize_element(&hex::encode(slice))?;
}
seq.end()
}
pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<Vec<u8>>, D::Error> {
Ok(
Vec::<serdect::slice::HexOrBin<false>>::deserialize(deserializer)?
.into_iter()
.map(|HexOrBin(vec)| vec)
.collect(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{EventAnnouncement, MarketMaker, PayoutWeights, Player};
use crate::{EventLockingConditions, MarketMaker, PayoutWeights, Player};
use bitcoin::{Amount, FeeRate};
use hex::ToHex;
@@ -241,17 +213,14 @@ mod tests {
payout_hash: [40; 32],
},
],
event: EventAnnouncement {
oracle_pubkey: "03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7"
.parse()
.unwrap(),
nonce_point: "0317aec4eea8a2b02c38e6b67c26015d16c82a3a44abc28d1def124c1f79786fc5"
.parse()
.unwrap(),
outcome_messages: vec![
Vec::from(b"option 1"),
Vec::from(b"option 2"),
Vec::from(b"option 3"),
event: EventLockingConditions {
locking_points: vec![
"036b382e40647af612900fe6ad0aa0003790fef503ffa910c06d04811863d0791f"
.parse()
.unwrap(),
"038f7cd041cdf74616b9ce4837dbbc8a316e7f0150ab96419bd9a24db9b36e261d"
.parse()
.unwrap(),
],
expiry: Some(u32::MAX),
},
@@ -289,12 +258,9 @@ mod tests {
}
],
"event": {
"oracle_pubkey": "03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7",
"nonce_point": "0317aec4eea8a2b02c38e6b67c26015d16c82a3a44abc28d1def124c1f79786fc5",
"outcome_messages": [
"6f7074696f6e2031",
"6f7074696f6e2032",
"6f7074696f6e2033"
"locking_points": [
"036b382e40647af612900fe6ad0aa0003790fef503ffa910c06d04811863d0791f",
"038f7cd041cdf74616b9ce4837dbbc8a316e7f0150ab96419bd9a24db9b36e261d"
],
"expiry": 4294967295
},

View File

@@ -1,8 +1,8 @@
use dlctix::bitcoin;
use dlctix::musig2;
use dlctix::secp::{Point, Scalar};
use dlctix::secp::{MaybePoint, Point, Scalar};
use dlctix::{
hashlock, ContractParameters, ContributorPartialSignatureSharingRound, EventAnnouncement,
hashlock, ContractParameters, ContributorPartialSignatureSharingRound, EventLockingConditions,
MarketMaker, NonceSharingRound, Outcome, PayoutWeights, Player, SigMap, SignedContract,
SigningSession, TicketedDLC, WinCondition,
};
@@ -59,22 +59,29 @@ fn two_player_example() -> Result<(), Box<dyn std::error::Error>> {
// Oracles usually publish their announcements and attestations over
// public mediums like a website, or Twitter, or Nostr.
let oracle_seckey = Scalar::random(&mut rng);
let oracle_pubkey = oracle_seckey.base_point_mul();
// Each event has an associated nonce which the oracle commits to
// ahead of time.
let oracle_secnonce = Scalar::random(&mut rng);
let nonce_point = oracle_secnonce.base_point_mul();
// An announcement describes the different messages an oracle might sign.
let event = EventAnnouncement {
oracle_pubkey: oracle_seckey.base_point_mul(),
nonce_point: oracle_secnonce.base_point_mul(),
// We enumerate the different outcome messages the oracle could sign...
let outcome_messages = vec![
Vec::from(b"alice wins"),
Vec::from(b"bob wins"),
Vec::from(b"tie"),
];
// We enumerate the different outcome messages the oracle could sign.
outcome_messages: vec![
Vec::from(b"alice wins"),
Vec::from(b"bob wins"),
Vec::from(b"tie"),
],
// ...and then precompute the locking points needed for each possible outcome.
let locking_points: Vec<MaybePoint> = outcome_messages
.iter()
.map(|msg| dlctix::attestation_locking_point(oracle_pubkey, nonce_point, msg))
.collect();
// This struct describes the different possible outcomes an oracle might sign.
let event = EventLockingConditions {
locking_points,
// The expiry time is the time after which the Expiry outcome transaction should be
// triggered. This can either be a unix seconds timestamp, or a bitcoin block height,
@@ -96,7 +103,7 @@ fn two_player_example() -> Result<(), Box<dyn std::error::Error>> {
let tie_payout = PayoutWeights::from([(0, 1), (1, 1)]); // split the pot evenly
// An Outcome is a compact representation of which of the messages in the
// `EventAnnouncement::outcome_messages` field (if any) an oracle might attest
// `EventLockingConditions::outcome_messages` field (if any) an oracle might attest
// to.
let alice_wins_outcome = Outcome::Attestation(0);
let bob_wins_outcome = Outcome::Attestation(1);
@@ -239,11 +246,11 @@ fn two_player_example() -> Result<(), Box<dyn std::error::Error>> {
//
// However, before _any_ outcome can be enforced, the oracle must publish their attestation.
let outcome_index = 0;
let oracle_attestation = signed_contract
.params()
.event
.attestation_secret(outcome_index, oracle_seckey, oracle_secnonce)
.unwrap();
let oracle_attestation = dlctix::attestation_secret(
oracle_seckey,
oracle_secnonce,
&outcome_messages[outcome_index],
);
// A win condition describes an outcome and a particular player
// who is paid out under that outcome.