Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 46 additions & 117 deletions crates/deadcat-client/src/market_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@

use deadcat_contracts::SimplicityNetwork;
use deadcat_contracts::binary_market::{
AppliedBinaryMarketTransition, BinaryMarketAction, BinaryMarketEconomics, BinaryMarketError,
BinaryMarketSlot, BinaryMarketTransition, BinaryOutcome, CompiledBinaryMarket,
CompiledBinaryMarketError, CompiledBinaryMarketExecutionError, derived_binary_market,
AppliedBinaryMarketTransition, BinaryMarketAction, BinaryMarketCoordinatorAction,
BinaryMarketEconomics, BinaryMarketError, BinaryMarketLayout, BinaryMarketLayoutError,
BinaryMarketPath, BinaryMarketResolution, BinaryMarketSlot, BinaryMarketTransition,
BinaryMarketWitness, BinaryOutcome, CompiledBinaryMarket, CompiledBinaryMarketError,
CompiledBinaryMarketExecutionError,
};
#[cfg(test)]
use deadcat_contracts::finalized_spend::FinalizedSimplicitySpend;
use deadcat_contracts::interpret::BinaryMarketPath;
use deadcat_contracts::market_crypto::{
BinaryOutcome as OracleOutcome, derive_issuance_assets, oracle_message,
};
Expand All @@ -38,7 +39,6 @@ use elements::{
};
use rand::SeedableRng as _;
use rand::rngs::StdRng;
use simplex::program::WitnessTrait as _;
use thiserror::Error;

/// Network-known assets needed to verify a compact market recovery hint.
Expand Down Expand Up @@ -264,14 +264,12 @@ pub struct BinaryMarketTransitionPlan {
params: BinaryMarketParams,
before: BinaryMarketState,
applied: AppliedBinaryMarketTransition,
path: BinaryMarketPath,
layout: BinaryMarketLayout,
live: BinaryMarketLiveInputs,
output_templates: Vec<TxOut>,
yes_output_factors: Option<RtFactors>,
no_output_factors: Option<RtFactors>,
oracle_signature: [u8; 64],
tokens_burned: u64,
redeem_yes: bool,
resolution: Option<BinaryMarketResolution>,
}

impl BinaryMarketTransitionPlan {
Expand Down Expand Up @@ -301,9 +299,10 @@ impl BinaryMarketTransitionPlan {
let params = compiled.params();
let economics = BinaryMarketEconomics::new(params.base_payout)?;
let applied = economics.apply(before, action)?;
let path = select_path(before, action, applied)?;
let layout = BinaryMarketLayout::for_transition(before, action, applied)?;
let path = layout.path();
validate_live_shape(compiled, params, before, path, &live)?;
let oracle_signature = validate_attestation(params, action, attestation)?;
let resolution = validate_attestation(params, action, attestation)?;
let (tokens_burned, redeem_yes) = match action {
BinaryMarketAction::Redeem { outcome, tokens } => {
(tokens, outcome == BinaryOutcome::Yes)
Expand Down Expand Up @@ -397,20 +396,23 @@ impl BinaryMarketTransitionPlan {
params,
before,
applied,
path,
layout,
live,
output_templates,
yes_output_factors,
no_output_factors,
oracle_signature,
tokens_burned,
redeem_yes,
resolution,
})
}

#[must_use]
pub const fn path(&self) -> BinaryMarketPath {
self.path
self.layout.path()
}

#[must_use]
pub const fn layout(&self) -> BinaryMarketLayout {
self.layout
}

#[must_use]
Expand Down Expand Up @@ -507,7 +509,7 @@ impl BinaryMarketTransitionPlan {
input_base: usize,
) -> Result<(), MarketBuilderError> {
if !matches!(
self.path,
self.path(),
BinaryMarketPath::ActiveExpiry | BinaryMarketPath::DormantExpiry
) {
return Err(MarketBuilderError::NotExpiryPath);
Expand Down Expand Up @@ -588,7 +590,7 @@ impl BinaryMarketTransitionPlan {
}
}

if path_consumes_rt(self.path) {
if path_consumes_rt(self.path()) {
let yes = self
.live
.yes_rt
Expand Down Expand Up @@ -636,37 +638,27 @@ impl BinaryMarketTransitionPlan {

let output_base_u32 =
u32::try_from(output_base).map_err(|_| MarketBuilderError::IndexOverflow)?;
let input_slots = self.input_slots();
let mut finalized = Vec::with_capacity(input_slots.len());
for (offset, slot) in input_slots.iter().copied().enumerate() {
let action = BinaryMarketCoordinatorAction::for_layout(
self.layout,
output_base_u32,
self.resolution,
)?;
let input_roles = self.layout.input_roles();
let mut finalized = Vec::with_capacity(input_roles.len());
for (offset, role) in input_roles.iter().copied().enumerate() {
let input_index = add_index(input_base, offset)?;
let oracle_outcome_yes = self.redeem_yes
|| matches!(
self.applied.transition,
BinaryMarketTransition::Resolved {
outcome: BinaryOutcome::Yes,
..
}
);
let witness = derived_binary_market::BinaryMarketWitness {
path: self.path as u8,
slot: slot as u8,
output_base: output_base_u32,
oracle_outcome_yes,
oracle_signature: self.oracle_signature,
tokens_burned: self.tokens_burned,
redeem_yes: self.redeem_yes,
};
let slot = role.slot();
let witness = BinaryMarketWitness::new(self.layout, role, action)?;
let spend =
compiled.finalize(slot, pset, &witness.build_witness(), input_index, network)?;
finalized.push((input_index, spend.into_witness_stack()));
}
for (input_index, stack) in finalized {
pset.inputs_mut()[input_index].final_script_witness = Some(stack);
}
for (offset, slot) in input_slots.iter().copied().enumerate() {
for (offset, role) in input_roles.iter().copied().enumerate() {
let input_index = add_index(input_base, offset)?;
compiled.execute_finalized(slot, pset, input_index, network)?;
compiled.execute_finalized(role.slot(), pset, input_index, network)?;
}
Ok(())
}
Expand Down Expand Up @@ -808,7 +800,7 @@ impl BinaryMarketTransitionPlan {
input_base: usize,
) -> Result<(), MarketBuilderError> {
if !matches!(
self.path,
self.path(),
BinaryMarketPath::ActiveExpiry | BinaryMarketPath::DormantExpiry
) {
return Ok(());
Expand Down Expand Up @@ -836,29 +828,11 @@ impl BinaryMarketTransitionPlan {
}

fn input_slots(&self) -> Vec<BinaryMarketSlot> {
match self.path {
BinaryMarketPath::InitialIssuance
| BinaryMarketPath::DormantResolution
| BinaryMarketPath::DormantExpiry => vec![
BinaryMarketSlot::DormantYesRt,
BinaryMarketSlot::DormantNoRt,
],
BinaryMarketPath::SubsequentIssuance
| BinaryMarketPath::PartialCancellation
| BinaryMarketPath::FullCancellation
| BinaryMarketPath::ActiveResolution
| BinaryMarketPath::ActiveExpiry => vec![
BinaryMarketSlot::UnresolvedYesRt,
BinaryMarketSlot::UnresolvedNoRt,
BinaryMarketSlot::UnresolvedCollateral,
],
BinaryMarketPath::ResolvedRedemption => vec![match self.before {
BinaryMarketState::ResolvedYes { .. } => BinaryMarketSlot::ResolvedYesCollateral,
BinaryMarketState::ResolvedNo { .. } => BinaryMarketSlot::ResolvedNoCollateral,
_ => unreachable!("path selection validates resolved state"),
}],
BinaryMarketPath::ExpiryRedemption => vec![BinaryMarketSlot::ExpiredCollateral],
}
self.layout
.input_roles()
.iter()
.map(|role| role.slot())
.collect()
}

fn contract_input_indices(&self, input_base: usize) -> Result<Vec<usize>, MarketBuilderError> {
Expand Down Expand Up @@ -1096,54 +1070,6 @@ fn append_non_rt_outputs(
}
}

fn select_path(
before: BinaryMarketState,
action: BinaryMarketAction,
applied: AppliedBinaryMarketTransition,
) -> Result<BinaryMarketPath, MarketBuilderError> {
Ok(match action {
BinaryMarketAction::Issue { .. } => match before {
BinaryMarketState::Trading {
outstanding_pairs: 0,
} => BinaryMarketPath::InitialIssuance,
BinaryMarketState::Trading { .. } => BinaryMarketPath::SubsequentIssuance,
_ => return Err(MarketBuilderError::UnsupportedTransition),
},
BinaryMarketAction::Cancel { .. } => match applied.transition {
BinaryMarketTransition::Cancelled { full: true, .. } => {
BinaryMarketPath::FullCancellation
}
BinaryMarketTransition::Cancelled { full: false, .. } => {
BinaryMarketPath::PartialCancellation
}
_ => return Err(MarketBuilderError::UnsupportedTransition),
},
BinaryMarketAction::Resolve { .. } => match before {
BinaryMarketState::Trading {
outstanding_pairs: 0,
} => BinaryMarketPath::DormantResolution,
BinaryMarketState::Trading { .. } => BinaryMarketPath::ActiveResolution,
_ => return Err(MarketBuilderError::UnsupportedTransition),
},
BinaryMarketAction::Expire => match before {
BinaryMarketState::Trading {
outstanding_pairs: 0,
} => BinaryMarketPath::DormantExpiry,
BinaryMarketState::Trading { .. } => BinaryMarketPath::ActiveExpiry,
_ => return Err(MarketBuilderError::UnsupportedTransition),
},
BinaryMarketAction::Redeem { .. } => match before {
BinaryMarketState::ResolvedYes { .. } | BinaryMarketState::ResolvedNo { .. } => {
BinaryMarketPath::ResolvedRedemption
}
BinaryMarketState::Expired { .. } => BinaryMarketPath::ExpiryRedemption,
BinaryMarketState::Trading { .. } => {
return Err(MarketBuilderError::UnsupportedTransition);
}
},
})
}

fn validate_live_shape(
compiled: &CompiledBinaryMarket,
params: BinaryMarketParams,
Expand Down Expand Up @@ -1255,12 +1181,12 @@ fn validate_attestation(
params: BinaryMarketParams,
action: BinaryMarketAction,
attestation: Option<OracleAttestation>,
) -> Result<[u8; 64], MarketBuilderError> {
) -> Result<Option<BinaryMarketResolution>, MarketBuilderError> {
let BinaryMarketAction::Resolve { outcome } = action else {
if attestation.is_some() {
return Err(MarketBuilderError::UnexpectedOracleAttestation);
}
return Ok([0; 64]);
return Ok(None);
};
let attestation = attestation.ok_or(MarketBuilderError::MissingOracleAttestation)?;
if attestation.outcome != outcome {
Expand All @@ -1282,7 +1208,10 @@ fn validate_attestation(
Secp256k1::verification_only()
.verify_schnorr(&signature, &message, &public_key)
.map_err(|_| MarketBuilderError::InvalidOracleAttestation)?;
Ok(attestation.signature)
Ok(Some(BinaryMarketResolution::new(
attestation.outcome,
attestation.signature,
)))
}

fn validate_market_hint(
Expand Down Expand Up @@ -1471,6 +1400,8 @@ fn compile(params: BinaryMarketParams) -> Result<CompiledBinaryMarket, MarketBui
pub enum MarketBuilderError {
#[error("binary-market economics error: {0}")]
Economics(#[from] BinaryMarketError),
#[error("binary-market layout error: {0}")]
Layout(#[from] BinaryMarketLayoutError),
#[error("recovery encoding error: {0}")]
Recovery(#[from] RecoveryError),
#[error("RT commitment error: {0}")]
Expand All @@ -1497,8 +1428,6 @@ pub enum MarketBuilderError {
InvalidSiblingGroup,
#[error("the live YES and NO RTs are on different A/B sides")]
MismatchedRtSides,
#[error("unsupported state/action transition")]
UnsupportedTransition,
#[error("resolution requires an oracle attestation")]
MissingOracleAttestation,
#[error("an oracle attestation was supplied for a non-resolution path")]
Expand Down
6 changes: 3 additions & 3 deletions crates/deadcat-client/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ use std::collections::{HashMap, HashSet};

use deadcat_contracts::SimplicityNetwork;
use deadcat_contracts::binary_market::{
BinaryMarketEconomics, BinaryMarketSlot, BinaryMarketTransition, BinaryOutcome,
CompiledBinaryMarket, CompiledBinaryMarketError,
BinaryMarketEconomics, BinaryMarketPath, BinaryMarketSlot, BinaryMarketTransition,
BinaryOutcome, CompiledBinaryMarket, CompiledBinaryMarketError,
};
use deadcat_contracts::interpret::{
BinaryMarketLiveOutputs, BinaryMarketPath, TrackedContractOutput, interpret_binary_market_spend,
BinaryMarketLiveOutputs, TrackedContractOutput, interpret_binary_market_spend,
};
use deadcat_contracts::rt::{RtLeg, RtSide, commitments, factors};
use deadcat_rpc::{
Expand Down
46 changes: 11 additions & 35 deletions crates/deadcat-client/tests/market_regtest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ use deadcat_client::market_builder::{
use deadcat_client::validation::replay_contract_history;
use deadcat_contracts::SimplicityNetwork;
use deadcat_contracts::binary_market::{
BinaryMarketAction, BinaryMarketEconomics, BinaryMarketSlot, BinaryMarketTransition,
BinaryOutcome, CompiledBinaryMarket, derived_binary_market,
BinaryMarketAction, BinaryMarketCoordinatorAction, BinaryMarketEconomics, BinaryMarketPath,
BinaryMarketSlot, BinaryMarketTransition, BinaryMarketWitness, BinaryOutcome,
CompiledBinaryMarket,
};
use deadcat_contracts::market_crypto::{
BinaryOutcome as OracleOutcome, derive_issuance_assets, oracle_message,
Expand Down Expand Up @@ -62,7 +63,6 @@ use elements::{
};
use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json};
use simplex::program::WitnessTrait as _;
use simplex::provider::ElementsRpc;
use simplex::signer::{Signer, SignerTrait as _};
use simplex::transaction::{FinalTransaction, PartialOutput};
Expand Down Expand Up @@ -1140,51 +1140,27 @@ fn rebuild_pruned_market_followers_from_divergent_witnesses(
network: &SimplicityNetwork,
) {
let compiled = CompiledBinaryMarket::new(params).expect("compile canonical market");
for (input_index, slot, path, output_base, signature, tokens_burned, redeem_yes) in [
(
1,
BinaryMarketSlot::UnresolvedNoRt,
u8::MAX,
u32::MAX,
[0xa5; 64],
u64::MAX,
true,
),
(
2,
BinaryMarketSlot::UnresolvedCollateral,
9,
u32::MAX - 1,
[0x5a; 64],
u64::MAX - 1,
false,
),
let layout = plan.layout();
for (input_index, slot, output_base) in [
(1, BinaryMarketSlot::UnresolvedNoRt, u32::MAX),
(2, BinaryMarketSlot::UnresolvedCollateral, u32::MAX - 1),
] {
let canonical = pset.inputs()[input_index]
.final_script_witness
.as_ref()
.expect("canonical follower witness")
.clone();
let witness = derived_binary_market::BinaryMarketWitness {
path,
slot: slot as u8,
output_base,
oracle_outcome_yes: input_index == 1,
oracle_signature: signature,
tokens_burned,
redeem_yes,
};
let action = BinaryMarketCoordinatorAction::Cancel { output_base };
let witness = BinaryMarketWitness::for_slot(layout, slot, action)
.expect("follower slot belongs to cancellation layout");
let rebuilt = compiled
.finalize(slot, pset, &witness.build_witness(), input_index, network)
.unwrap_or_else(|error| panic!("finalize divergent {slot:?} follower: {error}"))
.into_witness_stack();
assert!((4..=5).contains(&canonical.len()));
pset.inputs_mut()[input_index].final_script_witness = Some(rebuilt);
}
assert_eq!(
plan.path(),
deadcat_contracts::interpret::BinaryMarketPath::PartialCancellation
);
assert_eq!(plan.path(), BinaryMarketPath::PartialCancellation);
}

#[allow(clippy::too_many_arguments)]
Expand Down
Loading
Loading