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
2 changes: 0 additions & 2 deletions crates/deadcat-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,3 @@

pub mod market_builder;
pub mod validation;

mod simplicity;
54 changes: 34 additions & 20 deletions crates/deadcat-client/src/market_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ use deadcat_contracts::SimplicityNetwork;
use deadcat_contracts::binary_market::{
AppliedBinaryMarketTransition, BinaryMarketAction, BinaryMarketEconomics, BinaryMarketError,
BinaryMarketSlot, BinaryMarketTransition, BinaryOutcome, CompiledBinaryMarket,
derived_binary_market,
CompiledBinaryMarketError, CompiledBinaryMarketExecutionError, derived_binary_market,
};
#[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 Down Expand Up @@ -565,12 +567,13 @@ impl BinaryMarketTransitionPlan {
output_base: usize,
network: &SimplicityNetwork,
) -> Result<(), MarketBuilderError> {
if pset
if let Some((input_index, _)) = pset
.inputs()
.iter()
.any(|input| input.witness_utxo.is_none())
.enumerate()
.find(|(_, input)| input.witness_utxo.is_none())
{
return Err(MarketBuilderError::MissingWitnessUtxo);
return Err(MarketBuilderError::MissingWitnessUtxo { input_index });
}
self.verify_inputs(compiled, pset, input_base)?;
self.verify_expiry(pset, input_base)?;
Expand Down Expand Up @@ -654,16 +657,17 @@ impl BinaryMarketTransitionPlan {
tokens_burned: self.tokens_burned,
redeem_yes: self.redeem_yes,
};
let stack = compiled
.finalize(slot, pset, &witness.build_witness(), input_index, network)
.map_err(|error| MarketBuilderError::Covenant(error.to_string()))?;
let stack =
crate::simplicity::ensure_budget(stack).map_err(MarketBuilderError::Covenant)?;
finalized.push((input_index, stack));
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() {
let input_index = add_index(input_base, offset)?;
compiled.execute_finalized(slot, pset, input_index, network)?;
}
Ok(())
}

Expand Down Expand Up @@ -706,7 +710,7 @@ impl BinaryMarketTransitionPlan {
let utxo = input
.witness_utxo
.as_ref()
.ok_or(MarketBuilderError::MissingWitnessUtxo)?;
.ok_or(MarketBuilderError::MissingWitnessUtxo { input_index: index })?;
if utxo.script_pubkey != *compiled.slot(slot).script_pubkey() {
return Err(MarketBuilderError::WrongContractInput);
}
Expand Down Expand Up @@ -1460,8 +1464,7 @@ fn add_index(base: usize, offset: usize) -> Result<usize, MarketBuilderError> {
}

fn compile(params: BinaryMarketParams) -> Result<CompiledBinaryMarket, MarketBuilderError> {
CompiledBinaryMarket::new(params)
.map_err(|error| MarketBuilderError::Compilation(error.to_string()))
Ok(CompiledBinaryMarket::new(params)?)
}

#[derive(Debug, Error)]
Expand All @@ -1473,7 +1476,7 @@ pub enum MarketBuilderError {
#[error("RT commitment error: {0}")]
RtCommitment(#[from] RtCommitmentError),
#[error("contract compilation failed: {0}")]
Compilation(String),
Compilation(#[from] CompiledBinaryMarketError),
#[error("compiled binary-market parameters do not match the transition plan")]
CompiledParamsMismatch,
#[error("market recovery hint disagrees with the supplied parameters")]
Expand Down Expand Up @@ -1524,8 +1527,8 @@ pub enum MarketBuilderError {
InputIndexOutOfBounds,
#[error("PSET output index is out of bounds")]
OutputIndexOutOfBounds,
#[error("PSET is missing witness_utxo evidence")]
MissingWitnessUtxo,
#[error("PSET input {input_index} is missing witness_utxo evidence")]
MissingWitnessUtxo { input_index: usize },
#[error("PSET contract input does not match the plan")]
WrongContractInput,
#[error("PSET reissuance fields do not match the plan")]
Expand All @@ -1543,7 +1546,7 @@ pub enum MarketBuilderError {
#[error("mandatory covenant output at index {index} does not match the plan")]
MandatoryOutputMismatch { index: usize },
#[error("Simplicity covenant finalization failed: {0}")]
Covenant(String),
Covenant(#[from] CompiledBinaryMarketExecutionError),
}

#[cfg(test)]
Expand Down Expand Up @@ -2004,10 +2007,12 @@ mod tests {
.final_script_witness
.as_ref()
.expect("final witness");
let (core, annex) = deadcat_contracts::interpret::strip_taproot_annex(stack);
assert_eq!(core.len(), 4);
let finalized = FinalizedSimplicitySpend::parse_witness_stack(stack)
.expect("typed finalized Simplicity witness");
assert!(
annex.is_none_or(|padding| padding.first() == Some(&0x50)),
finalized
.annex()
.is_none_or(|padding| padding.first() == Some(&0x50)),
"budget padding must be a Taproot annex"
);
}
Expand Down Expand Up @@ -2072,6 +2077,15 @@ mod tests {
policy_asset: params.collateral_asset_id,
};
let mut pset = pset_for_plan(&expiry, 0, 0);
let mut missing_utxo = pset.clone();
missing_utxo.inputs_mut()[1].witness_utxo = None;
let missing_untouched = missing_utxo.clone();
assert!(matches!(
expiry.finalize(&mut missing_utxo, 0, 0, &network),
Err(MarketBuilderError::MissingWitnessUtxo { input_index: 1 })
));
assert_eq!(missing_utxo, missing_untouched);

let untouched = pset.clone();
assert!(matches!(
expiry.finalize(&mut pset, 0, 0, &network),
Expand Down
31 changes: 0 additions & 31 deletions crates/deadcat-client/src/simplicity.rs

This file was deleted.

10 changes: 4 additions & 6 deletions crates/deadcat-client/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::collections::{HashMap, HashSet};
use deadcat_contracts::SimplicityNetwork;
use deadcat_contracts::binary_market::{
BinaryMarketEconomics, BinaryMarketSlot, BinaryMarketTransition, BinaryOutcome,
CompiledBinaryMarket,
CompiledBinaryMarket, CompiledBinaryMarketError,
};
use deadcat_contracts::interpret::{
BinaryMarketLiveOutputs, BinaryMarketPath, TrackedContractOutput, interpret_binary_market_spend,
Expand Down Expand Up @@ -92,8 +92,7 @@ pub fn validate_contract_view(

let ContractParametersView::BinaryMarket { params } = &view.parameters;
let ContractStateView::BinaryMarket { state } = view.state;
CompiledBinaryMarket::new(*params)
.map_err(|error| ValidationError::Compilation(error.to_string()))?;
CompiledBinaryMarket::new(*params)?;
BinaryMarketEconomics::new(params.base_payout)
.and_then(|economics| economics.validate_state(state))
.map_err(|error| ValidationError::Economics(error.to_string()))?;
Expand Down Expand Up @@ -223,8 +222,7 @@ fn replay_market(
creation: &TransactionEvidence,
transitions: &[TransactionEvidence],
) -> Result<(), ValidationError> {
let compiled = CompiledBinaryMarket::new(params)
.map_err(|error| ValidationError::Compilation(error.to_string()))?;
let compiled = CompiledBinaryMarket::new(params)?;
let yes_input = unique_defining_input(
&creation.transaction,
params.yes_token_asset_id,
Expand Down Expand Up @@ -675,7 +673,7 @@ pub enum ValidationError {
#[error("invalid contract shape: {0}")]
ContractShape(&'static str),
#[error("contract compilation failed: {0}")]
Compilation(String),
Compilation(#[from] CompiledBinaryMarketError),
#[error("invalid contract economics: {0}")]
Economics(String),
#[error("duplicate live role or invalid live-output shape: {0}")]
Expand Down
22 changes: 6 additions & 16 deletions crates/deadcat-client/tests/market_regtest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ use elements::{
};
use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json};
use simplex::program::{ProgramTrait as _, WitnessTrait as _};
use simplex::program::WitnessTrait as _;
use simplex::provider::ElementsRpc;
use simplex::signer::{Signer, SignerTrait as _};
use simplex::transaction::{FinalTransaction, PartialOutput};
Expand Down Expand Up @@ -1174,21 +1174,11 @@ fn rebuild_pruned_market_followers_from_divergent_witnesses(
tokens_burned,
redeem_yes,
};
compiled
.program(slot)
.as_ref()
.execute(pset, &witness.build_witness(), input_index, network)
.unwrap_or_else(|error| panic!("divergent {slot:?} follower: {error}"));
let mut rebuilt = compiled
.program(slot)
.as_ref()
.finalize(pset, &witness.build_witness(), input_index, network)
.expect("finalize divergent follower witness");
match canonical.len() {
4 => {}
5 => rebuilt.push(canonical[4].clone()),
length => panic!("unexpected canonical follower stack length {length}"),
}
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!(
Expand Down
84 changes: 38 additions & 46 deletions crates/deadcat-client/tests/simplicity_budget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use deadcat_contracts::binary_market::{
BinaryMarketAction, BinaryMarketEconomics, BinaryMarketSlot, BinaryOutcome,
CompiledBinaryMarket, derived_binary_market,
};
use deadcat_contracts::interpret::strip_taproot_annex;
use deadcat_contracts::finalized_spend::FinalizedSimplicitySpend;
use deadcat_contracts::interpret::{
BinaryMarketLiveOutputs, TrackedContractOutput, interpret_binary_market_spend_with_compiled,
};
Expand All @@ -22,9 +22,8 @@ use elements::pset::{Input as PsetInput, Output as PsetOutput, PartiallySignedTr
use elements::secp256k1_zkp::{Keypair, Message, Secp256k1, Tweak};
use elements::{AssetId, LockTime, OutPoint, Script, Sequence, TxOut, TxOutWitness, Txid};
use serde::Serialize;
use simplex::program::{ProgramTrait as _, WitnessTrait as _};
use simplex::simplicityhl::simplicity::jet::Elements;
use simplex::simplicityhl::simplicity::{BitIter, Cost, RedeemNode};
use simplex::program::WitnessTrait as _;
use simplex::simplicityhl::simplicity::Cost;

// Rounded CI ceilings with headroom above the reviewed maxima of 4,557,857 mw,
// 70,903 cells, 62 frames, 6,223 stack bytes, 15,334 transaction bytes,
Expand Down Expand Up @@ -149,34 +148,15 @@ fn assert_canonical_padding(label: &str, annex: &[u8]) {
fn record_budget(label: impl Into<String>, stack: &[Vec<u8>]) -> CovenantMetrics {
let label = label.into();
let stack = stack.to_vec();
let (core_stack, annex) = strip_taproot_annex(&stack);
assert_eq!(
core_stack.len(),
4,
"{label}: finalized Simplicity stack must have four core elements"
);
let redeem = RedeemNode::decode::<_, _, Elements>(
BitIter::from(core_stack[1].iter().copied()),
BitIter::from(core_stack[0].iter().copied()),
)
.expect("decode finalized Simplicity program");
let bounds = redeem.bounds();
let finalized = FinalizedSimplicitySpend::from_witness_stack(stack.clone())
.unwrap_or_else(|error| panic!("{label}: {error}"));
let bounds = finalized.bounds();
let cost = bounds.cost;
assert!(
cost.is_budget_valid(&stack),
"{label}: finalized stack is underbudget for execution cost {cost}mw"
);

let canonical_padding = cost.get_padding(&core_stack.to_vec());
assert_eq!(
annex,
canonical_padding.as_deref(),
"{label}: finalized stack must use exactly the canonical budget padding"
);
let annex = finalized.annex();
if let Some(annex) = annex {
assert_canonical_padding(&label, annex);
if annex.len() > 1 {
let mut shortened = stack.clone();
let mut shortened = stack;
assert_eq!(
shortened.last_mut().expect("annex").pop(),
Some(0),
Expand All @@ -189,14 +169,15 @@ fn record_budget(label: impl Into<String>, stack: &[Vec<u8>]) -> CovenantMetrics
}
}

let sizes = finalized.encoded_sizes();
CovenantMetrics {
cost_milliweight: cost_milliweight(cost),
max_extra_cells: bounds.extra_cells,
max_extra_frames: bounds.extra_frames,
program_bytes: core_stack[1].len(),
witness_bytes: core_stack[0].len(),
stack_bytes: elements::encode::serialize(&stack).len(),
padding_bytes: annex.map_or(0, <[u8]>::len),
program_bytes: sizes.program_bytes,
witness_bytes: sizes.witness_bytes,
stack_bytes: sizes.stack_bytes,
padding_bytes: sizes.annex_bytes,
}
}

Expand Down Expand Up @@ -656,7 +637,7 @@ fn finalized_market_fixture(
}

#[test]
fn reusable_compiled_market_execution_matches_program_api() {
fn typed_finalized_spend_round_trips_and_reexecutes_from_the_installed_stack() {
let params = market_params();
let compiled = CompiledBinaryMarket::new(params).expect("compile canonical market");
let before = BinaryMarketState::Trading {
Expand All @@ -678,17 +659,29 @@ fn reusable_compiled_market_execution_matches_program_api() {
let network = SimplicityNetwork::ElementsRegtest {
policy_asset: params.collateral_asset_id,
};
let installed = pset.inputs()[input_base]
.final_script_witness
.as_ref()
.expect("installed finalized witness");

let reused = compiled
let finalized = compiled
.finalize(slot, &pset, &witness.build_witness(), input_base, &network)
.expect("finalize retained compiled program");
let program_api = compiled
.program(slot)
.as_ref()
.finalize(&pset, &witness.build_witness(), input_base, &network)
.expect("finalize SDK program");
.expect("build typed finalized spend");
assert_eq!(finalized.witness_stack(), installed);
assert_eq!(finalized.cmr(), compiled.cmr());
assert_eq!(
finalized.control_block(),
compiled.slot(slot).control_block()
);
assert_eq!(
finalized.encoded_sizes().stack_bytes,
elements::encode::serialize(installed).len()
);
assert_eq!(finalized.into_witness_stack(), *installed);

assert_eq!(reused, program_api);
compiled
.execute_finalized(slot, &pset, input_base, &network)
.expect("re-execute installed finalized witness");
}

#[test]
Expand Down Expand Up @@ -1812,18 +1805,17 @@ fn market_followers_ignore_transition_witnesses_but_require_the_exact_coordinato
.as_ref()
.expect("canonical follower stack")
.clone();
let mut mixed_stack = compiled
let mixed_stack = compiled
.finalize(
slot,
&mixed_witness_pset,
&witness.build_witness(),
input_index,
&network,
)
.expect("finalize mixed follower");
if canonical_stack.len() == 5 {
mixed_stack.push(canonical_stack[4].clone());
}
.expect("finalize mixed follower")
.into_witness_stack();
assert!((4..=5).contains(&canonical_stack.len()));
record_budget(format!("mixed-follower-{slot:?}"), &mixed_stack);
mixed_witness_pset.inputs_mut()[input_index].final_script_witness = Some(mixed_stack);
}
Expand Down
Loading
Loading