use bitcoin::{
key::constants::SCHNORR_SIGNATURE_SIZE,
opcodes::all::*,
sighash::{Prevouts, SighashCache},
taproot::{
LeafVersion, TaprootSpendInfo, TAPROOT_CONTROL_BASE_SIZE, TAPROOT_CONTROL_NODE_SIZE,
},
transaction::InputWeightPrediction,
Amount, ScriptBuf, TapLeafHash, TapSighash, TapSighashType, Transaction, TxOut, Witness,
};
use musig2::{CompactSignature, KeyAggContext};
use secp::{Point, Scalar};
use crate::{
contract::PlayerIndex,
errors::Error,
hashlock::{Preimage, PREIMAGE_SIZE},
parties::{MarketMaker, Player},
};
use std::{borrow::Borrow, collections::BTreeMap};
#[derive(Clone)]
pub(crate) struct OutcomeSpendInfo {
untweaked_ctx: KeyAggContext,
tweaked_ctx: KeyAggContext,
outcome_value: Amount,
spend_info: TaprootSpendInfo,
winner_split_scripts: BTreeMap<PlayerIndex, ScriptBuf>,
reclaim_script: ScriptBuf,
}
impl OutcomeSpendInfo {
pub(crate) fn new(
all_players: &[impl Borrow<Player>],
winner_indexes: impl IntoIterator<Item = PlayerIndex>,
market_maker: &MarketMaker,
outcome_value: Amount,
block_delta: u16,
) -> Result<Self, Error> {
let winners: BTreeMap<PlayerIndex, &Player> = winner_indexes
.into_iter()
.map(|i| {
let player = all_players.get(i).ok_or(Error)?;
Ok((i, player.borrow()))
})
.collect::<Result<_, Error>>()?;
let mut pubkeys: Vec<Point> = [market_maker.pubkey]
.into_iter()
.chain(winners.values().map(|w| w.pubkey))
.collect();
pubkeys.sort();
let untweaked_ctx = KeyAggContext::new(pubkeys)?;
let joint_outcome_pubkey: Point = untweaked_ctx.aggregated_pubkey();
let winner_split_scripts: BTreeMap<PlayerIndex, ScriptBuf> = winners
.into_iter()
.map(|(player_index, winner)| {
let script = bitcoin::script::Builder::new()
.push_opcode(OP_SHA256)
.push_slice(winner.ticket_hash)
.push_opcode(OP_EQUALVERIFY)
.push_slice(joint_outcome_pubkey.serialize_xonly())
.push_opcode(OP_CHECKSIG)
.into_script();
(player_index, script)
})
.collect();
let reclaim_script = bitcoin::script::Builder::new()
.push_int(2 * block_delta as i64)
.push_opcode(OP_CSV)
.push_opcode(OP_DROP)
.push_slice(market_maker.pubkey.serialize_xonly())
.push_opcode(OP_CHECKSIG)
.into_script();
let weighted_script_leaves = winner_split_scripts
.values()
.cloned()
.map(|script| (1, script))
.chain([(u32::MAX, reclaim_script.clone())]);
let tr_spend_info = TaprootSpendInfo::with_huffman_tree(
secp256k1::SECP256K1,
joint_outcome_pubkey.into(),
weighted_script_leaves,
)?;
let tweaked_ctx = untweaked_ctx.clone().with_taproot_tweak(
tr_spend_info
.merkle_root()
.expect("should always have merkle root")
.as_ref(),
)?;
let outcome_spend_info = OutcomeSpendInfo {
untweaked_ctx,
tweaked_ctx,
outcome_value,
spend_info: tr_spend_info,
winner_split_scripts,
reclaim_script,
};
Ok(outcome_spend_info)
}
pub(crate) fn key_agg_ctx_untweaked(&self) -> &KeyAggContext {
&self.untweaked_ctx
}
pub(crate) fn script_pubkey(&self) -> ScriptBuf {
ScriptBuf::new_p2tr_tweaked(self.spend_info.output_key())
}
pub(crate) fn outcome_value(&self) -> Amount {
self.outcome_value
}
pub(crate) fn input_weight_for_split_tx(&self) -> InputWeightPrediction {
let outcome_script_len = self
.winner_split_scripts
.values()
.nth(0)
.expect("always at least one winner")
.len();
let max_taptree_depth = self
.spend_info
.script_map()
.values()
.flatten()
.map(|proof| proof.len())
.max()
.expect("always has at least one node");
InputWeightPrediction::new(
0,
[
SCHNORR_SIGNATURE_SIZE, PREIMAGE_SIZE, outcome_script_len, TAPROOT_CONTROL_BASE_SIZE + TAPROOT_CONTROL_NODE_SIZE * max_taptree_depth, ],
)
}
pub(crate) fn input_weight_for_reclaim_tx(&self) -> InputWeightPrediction {
let reclaim_control_block = self
.spend_info
.control_block(&(self.reclaim_script.clone(), LeafVersion::TapScript))
.expect("reclaim script cannot be missing");
InputWeightPrediction::new(
0,
[
SCHNORR_SIGNATURE_SIZE, self.reclaim_script.len(), reclaim_control_block.size(), ],
)
}
pub(crate) fn sighash_tx_split(
&self,
split_tx: &Transaction,
player_index: &PlayerIndex,
) -> Result<TapSighash, Error> {
let outcome_prevouts = [TxOut {
script_pubkey: self.script_pubkey(),
value: self.outcome_value,
}];
let split_script = self.winner_split_scripts.get(player_index).ok_or(Error)?;
let leaf_hash = TapLeafHash::from_script(split_script, LeafVersion::TapScript);
let sighash = SighashCache::new(split_tx).taproot_script_spend_signature_hash(
0,
&Prevouts::All(&outcome_prevouts),
leaf_hash,
TapSighashType::Default,
)?;
Ok(sighash)
}
pub(crate) fn witness_tx_split(
&self,
signature: &CompactSignature,
ticket_preimage: Preimage,
player_index: &PlayerIndex,
) -> Result<Witness, Error> {
let split_script = self
.winner_split_scripts
.get(player_index)
.ok_or(Error)?
.clone();
let control_block = self
.spend_info
.control_block(&(split_script.clone(), LeafVersion::TapScript))
.ok_or(Error)?;
let mut witness = Witness::new();
witness.push(signature.serialize());
witness.push(ticket_preimage);
witness.push(split_script);
witness.push(control_block.serialize());
Ok(witness)
}
pub(crate) fn witness_tx_reclaim<T: Borrow<TxOut>>(
&self,
reclaim_tx: &Transaction,
input_index: usize,
prevouts: &Prevouts<T>,
market_maker_secret_key: Scalar,
) -> Result<Witness, Error> {
let leaf_hash = TapLeafHash::from_script(&self.reclaim_script, LeafVersion::TapScript);
let sighash = SighashCache::new(reclaim_tx).taproot_script_spend_signature_hash(
input_index,
prevouts,
leaf_hash,
TapSighashType::Default,
)?;
let signature: CompactSignature =
musig2::deterministic::sign_solo(market_maker_secret_key, sighash);
let reclaim_control_block = self
.spend_info
.control_block(&(self.reclaim_script.clone(), LeafVersion::TapScript))
.expect("reclaim script cannot be missing");
let mut witness = Witness::new();
witness.push(signature.serialize());
witness.push(&self.reclaim_script);
witness.push(reclaim_control_block.serialize());
Ok(witness)
}
pub(crate) fn witness_tx_close<T: Borrow<TxOut>>(
&self,
close_tx: &Transaction,
input_index: usize,
prevouts: &Prevouts<T>,
market_maker_secret_key: Scalar,
player_secret_keys: &BTreeMap<Point, Scalar>,
) -> Result<Witness, Error> {
let mm_pubkey = market_maker_secret_key.base_point_mul();
let sighash = SighashCache::new(close_tx).taproot_key_spend_signature_hash(
input_index,
prevouts,
TapSighashType::Default,
)?;
let ordered_seckeys: Vec<Scalar> = self
.tweaked_ctx
.pubkeys()
.into_iter()
.map(|&pubkey| {
if pubkey == mm_pubkey {
Ok(market_maker_secret_key)
} else {
player_secret_keys.get(&pubkey).ok_or(Error).copied()
}
})
.collect::<Result<_, Error>>()?;
let group_seckey: Scalar = self.tweaked_ctx.aggregated_seckey(ordered_seckeys)?;
let signature: CompactSignature = musig2::deterministic::sign_solo(group_seckey, sighash);
let witness = Witness::from_slice(&[signature.serialize()]);
Ok(witness)
}
}