use std::error::Error;
use bitcoin::{Transaction, TxIn, TxOut, Script, OutPoint, Witness, ScriptBuf, Sequence, Amount, WitnessProgram, WitnessVersion};
use bitcoin::secp256k1::{Secp256k1, SecretKey, PublicKey, Message};
use bitcoin::secp256k1::ecdsa::Signature;
use bitcoin::hashes::{Hash, sha256};
use bitcoin::psbt::Psbt as PartiallySignedTransaction;
use std::collections::HashMap;
use bitcoin::sighash::{Prevouts, SighashCache, TapSighashType};
use bitcoin::blockdata::opcodes::all;
use crate::bitcoin::LockTime;
use crate::bitcoin::Version;
use bitcoin::taproot::{TapLeafHash, ScriptPath};
use anyhow::{Result, anyhow};
use crate::bitcoin::error::BitcoinError;
use crate::bitcoin::error::BitcoinResult;
#[derive(Debug, Clone)]
pub struct DLCContract {
pub id: [u8; 32],
pub oracle_pubkey: PublicKey,
pub outcomes: HashMap<String, u64>,
pub collateral_amount: u64,
pub timelock: u32,
pub funding_tx: Option<Transaction>,
pub execution_txs: HashMap<String, Transaction>,
pub funding_output_index: Option<usize>,
pub party_a_pubkey: PublicKey,
pub party_b_pubkey: PublicKey,
}
#[derive(Debug)]
pub struct DLCOracle {
pub name: String,
pub pubkey: PublicKey,
pub secret_key: Option<SecretKey>,
pub announcement_signature: Option<Signature>,
}
#[derive(Debug, Clone)]
pub struct AdaptorSignature {
pub signature: Vec<u8>,
pub adaptor_point: PublicKey,
}
pub fn create_contract(
oracle_pubkey: &PublicKey,
outcomes: &[(String, u64)],
collateral_amount: u64,
timelock: u32,
) -> Result<DLCContract, &'static str> {
if outcomes.is_empty() {
return Err("No outcomes specified");
}
if collateral_amount == 0 {
return Err("Collateral amount must be greater than zero");
}
let mut contract_params = Vec::new();
contract_params.extend_from_slice(&oracle_pubkey.serialize());
for (outcome, payout) in outcomes {
contract_params.extend_from_slice(outcome.as_bytes());
contract_params.extend_from_slice(&payout.to_le_bytes());
}
contract_params.extend_from_slice(&collateral_amount.to_le_bytes());
contract_params.extend_from_slice(&timelock.to_le_bytes());
let contract_id = sha256::Hash::hash(&contract_params).to_byte_array();
let mut contract = DLCContract {
id: contract_id,
oracle_pubkey: *oracle_pubkey,
outcomes: outcomes.iter().map(|(k, v)| (k.clone(), *v)).collect(),
collateral_amount,
timelock,
funding_tx: None,
execution_txs: HashMap::new(),
funding_output_index: None,
party_a_pubkey: PublicKey::from_slice(&[2; 33]).unwrap_or_else(|_| panic!("Invalid public key")),
party_b_pubkey: PublicKey::from_slice(&[3; 33]).unwrap_or_else(|_| panic!("Invalid public key")),
};
Ok(contract)
}
pub fn create_oracle(name: &str) -> Result<DLCOracle, &'static str> {
let secp = Secp256k1::new();
let secret_key = SecretKey::new(&mut rand::thread_rng());
let pubkey = PublicKey::from_secret_key(&secp, &secret_key);
Ok(DLCOracle {
name: name.to_string(),
pubkey,
secret_key: Some(secret_key),
announcement_signature: None,
})
}
pub fn create_funding_transaction(
contract: &mut DLCContract,
party_a_inputs: Vec<(OutPoint, TxOut, SecretKey)>,
party_b_inputs: Vec<(OutPoint, TxOut, SecretKey)>,
) -> Result<Transaction, &'static str> {
let secp = Secp256k1::new();
let party_a_input_amount: u64 = party_a_inputs.iter().map(|(_, txout, _)| txout.value.to_sat()).sum();
let party_b_input_amount: u64 = party_b_inputs.iter().map(|(_, txout, _)| txout.value.to_sat()).sum();
if party_a_input_amount < contract.collateral_amount {
return Err("Party A has insufficient funds");
}
if party_b_input_amount < contract.collateral_amount {
return Err("Party B has insufficient funds");
}
let mut inputs = Vec::new();
for (outpoint, _, _) in &party_a_inputs {
inputs.push(TxIn {
previous_output: *outpoint,
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::new(),
});
}
for (outpoint, _, _) in &party_b_inputs {
inputs.push(TxIn {
previous_output: *outpoint,
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::new(),
});
}
let party_a_pubkey = PublicKey::from_secret_key(&secp, &party_a_inputs[0].2);
let party_b_pubkey = PublicKey::from_secret_key(&secp, &party_b_inputs[0].2);
let script = bitcoin::blockdata::script::Builder::new()
.push_opcode(all::OP_PUSHNUM_2)
.push_key(&bitcoin::PublicKey::new(party_a_pubkey))
.push_key(&bitcoin::PublicKey::new(party_b_pubkey))
.push_opcode(all::OP_PUSHNUM_2)
.push_opcode(all::OP_CHECKMULTISIG)
.into_script();
let script_hash = bitcoin::hashes::sha256::Hash::hash(script.as_bytes());
let witness_program = WitnessProgram::new(
WitnessVersion::V0,
script_hash.as_byte_array(),
)?;
let contract_script = ScriptBuf::new_witness_program(&witness_program);
let contract_output = TxOut {
value: Amount::from_sat(contract.collateral_amount * 2), script_pubkey: contract_script,
};
let party_a_change = party_a_input_amount - contract.collateral_amount - 1000; let party_b_change = party_b_input_amount - contract.collateral_amount - 1000;
let party_a_pubkey_obj = bitcoin::PublicKey::from_slice(&party_a_pubkey.serialize())?;
let party_a_script = ScriptBuf::new_p2wpkh(&party_a_pubkey_obj.wpubkey_hash()?);
let party_a_change_output = TxOut {
value: Amount::from_sat(party_a_change),
script_pubkey: party_a_script,
};
let party_b_pubkey_obj = bitcoin::PublicKey::from_slice(&party_b_pubkey.serialize())?;
let party_b_script = ScriptBuf::new_p2wpkh(&party_b_pubkey_obj.wpubkey_hash()?);
let party_b_change_output = TxOut {
value: Amount::from_sat(party_b_change),
script_pubkey: party_b_script,
};
let mut outputs = vec![contract_output, party_a_change_output, party_b_change_output];
let funding_tx = Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: inputs,
output: outputs,
};
let contract_output_index = funding_tx.output
.iter()
.position(|output| output.value == Amount::from_sat(contract.collateral_amount * 2))
.ok_or("Could not find funding output with correct amount")?;
contract.funding_tx = Some(funding_tx.clone());
contract.funding_output_index = Some(contract_output_index);
Ok(funding_tx)
}
pub fn create_execution_transactions(
contract: &mut DLCContract,
party_a_pubkey: &PublicKey,
party_b_pubkey: &PublicKey,
) -> Result<HashMap<String, Transaction>, &'static str> {
let funding_tx = contract.funding_tx.as_ref().ok_or("Funding transaction not created")?;
let contract_output_index = funding_tx.output.iter()
.position(|output| output.value == Amount::from_sat(contract.collateral_amount * 2))
.ok_or("Contract output not found in funding transaction")?;
let mut execution_txs = HashMap::new();
for (outcome, payout_ratio) in &contract.outcomes {
let total_collateral = contract.collateral_amount * 2;
let party_a_payout = (total_collateral as f64 * (*payout_ratio as f64 / 100.0)) as u64;
let party_b_payout = total_collateral - party_a_payout;
let input = TxIn {
previous_output: OutPoint {
txid: funding_tx.compute_txid(),
vout: contract_output_index as u32,
},
script_sig: Script::new().into(),
sequence: bitcoin::Sequence(0xFFFFFFFF),
witness: Witness::new(),
};
let mut outputs = Vec::new();
if party_a_payout > 0 {
let party_a_pubkey_obj = bitcoin::PublicKey::from_slice(&party_a_pubkey.serialize())?;
let party_a_script = ScriptBuf::new_p2wpkh(&party_a_pubkey_obj.wpubkey_hash()?);
outputs.push(TxOut {
value: Amount::from_sat(party_a_payout),
script_pubkey: party_a_script,
});
}
if party_b_payout > 0 {
let party_b_pubkey_obj = bitcoin::PublicKey::from_slice(&party_b_pubkey.serialize())?;
let party_b_script = ScriptBuf::new_p2wpkh(&party_b_pubkey_obj.wpubkey_hash()?);
outputs.push(TxOut {
value: Amount::from_sat(party_b_payout),
script_pubkey: party_b_script,
});
}
let execution_tx = Transaction {
version: Version::TWO,
lock_time: LockTime::from_consensus(contract.timelock),
input: vec![input],
output: outputs,
};
execution_txs.insert(outcome.clone(), execution_tx);
}
contract.execution_txs = execution_txs.clone();
Ok(execution_txs)
}
pub fn create_adaptor_signatures(
contract: &DLCContract,
party_secret_key: &SecretKey,
) -> Result<HashMap<String, AdaptorSignature>, &'static str> {
let secp = Secp256k1::new();
let mut adaptor_signatures = HashMap::new();
for (outcome, _) in &contract.outcomes {
let execution_tx = contract.execution_txs.get(outcome)
.ok_or("Execution transaction not found for outcome")?;
let outcome_hash = sha256::Hash::hash(outcome.as_bytes());
let message = Message::from_digest_slice(&outcome_hash[..])
.map_err(|_| "Failed to create message from outcome hash")?;
let signature = secp.sign_ecdsa(&message, party_secret_key);
let adaptor_signature = AdaptorSignature {
signature: signature.serialize_der().to_vec(),
adaptor_point: contract.oracle_pubkey,
};
adaptor_signatures.insert(outcome.clone(), adaptor_signature);
}
Ok(adaptor_signatures)
}
pub fn sign_oracle_outcome(
outcome: &str,
private_key: &SecretKey,
oracle_r_point: &PublicKey,
) -> Result<Signature, &'static str> {
let outcome_hash = sha256::Hash::hash(outcome.as_bytes());
let message = Message::from_digest_slice(&outcome_hash[..]).map_err(|_| "Failed to create message")?;
let secp = Secp256k1::new();
let signature = secp.sign_ecdsa(&message, private_key);
Ok(signature)
}
pub fn execute_contract_with_signatures(
contract: &DLCContract,
outcome: &str,
party_a_sig: &Signature,
party_b_sig: &Signature,
oracle_signature: &Signature,
funding_tx: &Transaction,
execution_tx: &mut Transaction,
) -> Result<(), &'static str> {
let contract_script = create_contract_script(
&contract.party_a_pubkey,
&contract.party_b_pubkey,
&contract.oracle_pubkey,
).into_script();
let mut witness_stack = Vec::new();
witness_stack.push(party_a_sig.serialize_der().to_vec());
witness_stack.push(party_b_sig.serialize_der().to_vec());
witness_stack.push(oracle_signature.serialize_der().to_vec());
witness_stack.push(contract_script.as_bytes().to_vec());
let contract_output_index = 0; execution_tx.input[contract_output_index].witness = Witness::from(witness_stack);
Ok(())
}
pub fn create_execution_transaction(
contract: &DLCContract,
oracle_signature: &Signature,
outcome: &str,
) -> Result<Transaction, &'static str> {
let funding_tx = contract.funding_tx.as_ref().ok_or("Funding transaction not found")?;
let secp = Secp256k1::new();
let (party_a_payout, party_b_payout) = match contract.outcomes.get(outcome) {
Some(payout) => {
let party_a_payout = *payout;
let party_b_payout = contract.collateral_amount * 2 - party_a_payout;
(party_a_payout, party_b_payout)
},
None => return Err("Outcome not found in contract"),
};
let input = TxIn {
previous_output: OutPoint {
txid: funding_tx.compute_txid(),
vout: contract.funding_output_index? as u32,
},
script_sig: Script::new().into(),
sequence: bitcoin::Sequence(0xFFFFFFFF),
witness: Witness::new(),
};
let mut outputs = Vec::new();
if party_a_payout > 0 {
let party_a_pubkey = &contract.party_a_pubkey;
let party_a_pubkey_obj = bitcoin::PublicKey::from_slice(&party_a_pubkey.serialize())?;
let party_a_script = ScriptBuf::new_p2wpkh(&party_a_pubkey_obj.wpubkey_hash()?);
outputs.push(TxOut {
value: Amount::from_sat(party_a_payout),
script_pubkey: party_a_script,
});
}
if party_b_payout > 0 {
let party_b_pubkey = &contract.party_b_pubkey;
let party_b_pubkey_obj = bitcoin::PublicKey::from_slice(&party_b_pubkey.serialize())?;
let party_b_script = ScriptBuf::new_p2wpkh(&party_b_pubkey_obj.wpubkey_hash()?);
outputs.push(TxOut {
value: Amount::from_sat(party_b_payout),
script_pubkey: party_b_script,
});
}
let execution_tx = Transaction {
version: Version::TWO,
lock_time: LockTime::from_consensus(contract.timelock),
input: vec![input],
output: outputs,
};
Ok(execution_tx)
}
pub fn sign_execution_transaction(
contract: &DLCContract,
execution_tx: &mut Transaction,
party_a_key: &SecretKey,
party_b_key: &SecretKey,
oracle_signature: &Signature,
) -> Result<(), &'static str> {
let secp = Secp256k1::new();
let funding_tx = contract.funding_tx.as_ref().ok_or("Funding transaction not found")?;
let contract_output_index = execution_tx.input
.iter()
.position(|input| input.previous_output.txid == funding_tx.compute_txid() &&
input.previous_output.vout == contract.funding_output_index? as u32)
.ok_or("Contract input not found in execution transaction")?;
let contract_script = &funding_tx.output[contract.funding_output_index?].script_pubkey;
let mut sighash_cache = bitcoin::sighash::SighashCache::new(&*execution_tx);
let script_path = ScriptPath::with_defaults(&contract_script);
let sighash = sighash_cache.taproot_script_spend_signature_hash(
0,
&Prevouts::All(&[]),
TapLeafHash::from(script_path.clone()), TapSighashType::Default,
).map_err(|e| anyhow!("Failed to calculate sighash: {}", e))?;
let party_a_sig = secp.sign_ecdsa(&Message::from_digest_slice(&sighash[..])?, party_a_key);
let party_b_sig = secp.sign_ecdsa(&Message::from_digest_slice(&sighash[..])?, party_b_key);
let mut witness_stack = Vec::new();
witness_stack.push(party_a_sig.serialize_der().to_vec());
witness_stack.push(party_b_sig.serialize_der().to_vec());
witness_stack.push(oracle_signature.serialize_der().to_vec());
witness_stack.push(contract_script.as_bytes().to_vec());
execution_tx.input[contract_output_index].witness = Witness::from(witness_stack);
Ok(())
}
pub fn verify_oracle_signature(
outcome: &str,
oracle_signature: &Signature,
oracle_public_key: &PublicKey,
) -> BitcoinResult<bool> {
let outcome_hash = sha256::Hash::hash(outcome.as_bytes());
let message = Message::from_digest_slice(&outcome_hash[..])
.map_err(|_| BitcoinError::InvalidSignature("Failed to create message".to_string()))?;
let secp = Secp256k1::new();
if !secp.verify_ecdsa(&message, oracle_signature, oracle_public_key).is_ok() {
return Ok(false);
}
Ok(true)
}
fn create_contract_script(
party_a_pubkey: &PublicKey,
party_b_pubkey: &PublicKey,
oracle_pubkey: &PublicKey,
) -> bitcoin::blockdata::script::Builder {
bitcoin::blockdata::script::Builder::new()
.push_opcode(all::OP_PUSHNUM_2)
.push_key(&bitcoin::PublicKey::new(*party_a_pubkey))
.push_key(&bitcoin::PublicKey::new(*party_b_pubkey))
.push_opcode(all::OP_PUSHNUM_2)
.push_opcode(all::OP_CHECKMULTISIG)
}
impl DLC {
fn calculate_sighash(&self, script_path: &ScriptPath) -> Result<Vec<u8>> {
let sighash = sighash_cache
.taproot_script_spend_signature_hash(
0,
&Prevouts::All(&[]),
TapLeafHash::from(script_path.clone()), TapSighashType::Default,
)
.map_err(|e| anyhow!("Failed to calculate sighash: {}", e))?;
Ok(sighash.to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_dlc_contract() {
let secp = Secp256k1::new();
let oracle_key = SecretKey::from_slice(&[1; 32])?;
let oracle_pubkey = PublicKey::from_secret_key(&secp, &oracle_key);
let outcomes = vec![
("win".to_string(), 15000000),
("lose".to_string(), 5000000),
];
let outcomes_map: HashMap<String, u64> = outcomes.iter()
.map(|(k, v)| (k.clone(), *v))
.collect();
let collateral_amount = 10000000;
let timelock = 144 * 7;
let contract = create_contract(
&oracle_pubkey,
&outcomes,
collateral_amount,
timelock,
)?;
assert_eq!(contract.oracle_pubkey, oracle_pubkey);
assert_eq!(contract.outcomes, outcomes_map);
assert_eq!(contract.collateral_amount, collateral_amount);
assert_eq!(contract.timelock, timelock);
}
#[test]
fn test_create_oracle() {
let oracle = create_oracle("Sports Oracle")?;
assert_eq!(oracle.name, "Sports Oracle");
assert!(oracle.secret_key.is_some());
}
}