use std::fmt;
use bitcoin::secp256k1::{schnorr, PublicKey};
use bitcoin::{Amount, OutPoint, Sequence, ScriptBuf, Transaction, TxIn, TxOut, Witness};
use bitcoin::hashes::{sha256, Hash};
use bitcoin::key::TweakedPublicKey;
use bitcoin::sighash;
use bitcoin::taproot::{self, TapLeafHash, LeafVersion, TapTweakHash};
use bitcoin_ext::{fee, BlockDelta, BlockHeight, TaprootSpendInfoExt};
use crate::SECP;
use crate::musig;
use crate::tree::signed::{cosign_taproot, leaf_cosign_taproot, unlock_clause};
use crate::vtxo::MaybePreimage;
pub enum TransitionKind {
Cosigned,
HashLockedCosigned,
Arkoor,
}
impl TransitionKind {
pub fn as_str(&self) -> &'static str {
match self {
Self::Cosigned => "cosigned",
Self::HashLockedCosigned => "hash-locked-cosigned",
Self::Arkoor => "arkoor",
}
}
}
impl fmt::Display for TransitionKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl fmt::Debug for TransitionKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CosignedGenesis {
pub pubkeys: Vec<PublicKey>,
pub signature: Option<schnorr::Signature>,
}
impl CosignedGenesis {
pub fn input_taproot(
&self,
server_pubkey: PublicKey,
expiry_height: BlockHeight,
) -> taproot::TaprootSpendInfo {
let agg_pk = musig::combine_keys(self.pubkeys.iter().copied())
.x_only_public_key().0;
cosign_taproot(agg_pk, server_pubkey, expiry_height)
}
pub fn input_txout(
&self,
amount: Amount,
server_pubkey: PublicKey,
expiry_height: BlockHeight,
) -> TxOut {
TxOut {
value: amount,
script_pubkey: self.input_taproot(server_pubkey, expiry_height).script_pubkey(),
}
}
pub fn witness(&self) -> Witness {
match self.signature {
Some(ref sig) => Witness::from_slice(&[&sig[..]]),
None => Witness::new(),
}
}
pub fn has_all_witnesses(&self) -> bool {
self.signature.is_some()
}
pub fn validate_sigs(
&self,
tx: &Transaction,
input_idx: usize,
prev_txout: &TxOut,
server_pubkey: PublicKey,
expiry_height: BlockHeight,
) -> Result<(), &'static str> {
let signature = match self.signature {
Some(sig) => sig,
None => return Err("missing cosigned signature"),
};
let mut shc = sighash::SighashCache::new(tx);
let tapsighash = shc.taproot_key_spend_signature_hash(
input_idx,
&sighash::Prevouts::All(&[prev_txout]),
sighash::TapSighashType::Default
).expect("correct prevouts");
let pubkey = self.input_taproot(server_pubkey, expiry_height)
.output_key()
.to_x_only_public_key();
SECP.verify_schnorr(&signature, &tapsighash.into(), &pubkey)
.map_err(|_| "invalid signature")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HashLockedCosignedGenesis {
pub user_pubkey: PublicKey,
pub signature: Option<schnorr::Signature>,
pub unlock: MaybePreimage,
}
impl HashLockedCosignedGenesis {
pub fn input_taproot(
&self,
server_pubkey: PublicKey,
expiry_height: BlockHeight,
) -> taproot::TaprootSpendInfo {
leaf_cosign_taproot(self.user_pubkey, server_pubkey, expiry_height, self.unlock.hash())
}
pub fn input_txout(
&self,
amount: Amount,
server_pubkey: PublicKey,
expiry_height: BlockHeight,
) -> TxOut {
TxOut {
value: amount,
script_pubkey: self.input_taproot(server_pubkey, expiry_height).script_pubkey(),
}
}
pub fn witness(
&self,
server_pubkey: PublicKey,
expiry_height: BlockHeight,
) -> Witness {
let preimage = match self.unlock {
MaybePreimage::Preimage(p) => p,
MaybePreimage::Hash(_) => return Witness::new(),
};
let sig = match self.signature {
Some(sig) => sig,
None => return Witness::new(),
};
let unlock_hash = sha256::Hash::hash(&preimage);
let taproot = leaf_cosign_taproot(
self.user_pubkey, server_pubkey, expiry_height, unlock_hash,
);
let clause = unlock_clause(taproot.internal_key(), unlock_hash);
let script_leaf = (clause, LeafVersion::TapScript);
let cb = taproot.control_block(&script_leaf)
.expect("unlock clause not found in hArk taproot");
Witness::from_slice(&[
&sig.serialize()[..],
&preimage[..],
&script_leaf.0.as_bytes(),
&cb.serialize()[..],
])
}
pub fn has_all_witnesses(&self) -> bool {
match self.unlock {
MaybePreimage::Preimage(_) => {},
MaybePreimage::Hash(_) => return false,
};
match self.signature {
Some(_) => true,
None => false,
}
}
pub fn validate_sigs(
&self,
tx: &Transaction,
input_idx: usize,
prev_txout: &TxOut,
server_pubkey: PublicKey,
expiry_height: BlockHeight,
) -> Result<(), &'static str> {
match self.unlock {
MaybePreimage::Preimage(_) => {},
MaybePreimage::Hash(_) => return Err("missing preimage")
};
let mut shc = sighash::SighashCache::new(tx);
let agg_pk = musig::combine_keys([self.user_pubkey, server_pubkey])
.x_only_public_key().0;
let script = unlock_clause(agg_pk, self.unlock.hash());
let leaf = TapLeafHash::from_script(&script, bitcoin::taproot::LeafVersion::TapScript);
let tapsighash = shc.taproot_script_spend_signature_hash(
input_idx, &sighash::Prevouts::All(&[prev_txout]), leaf, sighash::TapSighashType::Default,
).expect("correct prevouts");
let pk = self.input_taproot(server_pubkey, expiry_height)
.internal_key();
match self.signature {
None => return Err("missing signature"),
Some(sig) => {
SECP.verify_schnorr(&sig, &tapsighash.into(), &pk)
.map_err(|_| "invalid signature")
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArkoorGenesis {
pub client_cosigners: Vec<PublicKey>,
pub tap_tweak: taproot::TapTweakHash,
pub signature: Option<schnorr::Signature>,
}
impl ArkoorGenesis {
pub fn client_cosigners(&self) -> impl Iterator<Item = PublicKey> + '_ {
self.client_cosigners.iter().copied()
}
pub fn cosigners<'a>(&'a self, server_pubkey: PublicKey) -> impl Iterator<Item = PublicKey> + 'a {
self.client_cosigners.iter().cloned().chain([server_pubkey])
}
pub fn input_txout(&self, amount: Amount, server_pubkey: PublicKey) -> TxOut {
TxOut {
value: amount,
script_pubkey: ScriptBuf::new_p2tr_tweaked(self.output_key(server_pubkey))
}
}
pub fn output_key(&self, server_pubkey: PublicKey) -> TweakedPublicKey {
let (_, agg_pk) = musig::tweaked_key_agg(self.cosigners(server_pubkey), self.tap_tweak.to_byte_array());
TweakedPublicKey::dangerous_assume_tweaked(agg_pk.x_only_public_key().0)
}
pub fn witness(&self) -> Witness {
match self.signature {
Some(sig) => Witness::from_slice(&[&sig[..]]),
None => Witness::new(),
}
}
pub fn has_all_witnesses(&self) -> bool {
self.signature.is_some()
}
pub fn validate_sigs(
&self,
tx: &Transaction,
input_idx: usize,
prev_txout: &TxOut,
server_pubkey: PublicKey,
) -> Result<(), &'static str> {
let signature = match self.signature {
Some(sig) => sig,
None => return Err("missing signature"),
};
let mut shc = sighash::SighashCache::new(tx);
let tapsighash = shc.taproot_key_spend_signature_hash(
input_idx,
&sighash::Prevouts::All(&[prev_txout]),
sighash::TapSighashType::Default
).expect("correct prevouts");
SECP.verify_schnorr(
&signature,
&tapsighash.into(),
&self.output_key(server_pubkey).to_x_only_public_key(),
).map_err(|_| "invalid signature")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GenesisTransition {
Cosigned(CosignedGenesis),
HashLockedCosigned(HashLockedCosignedGenesis),
Arkoor(ArkoorGenesis),
}
impl GenesisTransition {
pub fn new_cosigned(pubkeys: Vec<PublicKey>, signature: Option<schnorr::Signature>) -> Self {
Self::Cosigned(CosignedGenesis { pubkeys, signature })
}
pub fn new_hash_locked_cosigned(
user_pubkey: PublicKey,
signature: Option<schnorr::Signature>,
unlock: MaybePreimage
) -> Self {
Self::HashLockedCosigned(
HashLockedCosignedGenesis { user_pubkey, signature, unlock }
)
}
pub fn new_arkoor(
cosigners: Vec<PublicKey>,
tap_tweak: TapTweakHash,
signature: Option<schnorr::Signature>
) -> Self {
Self::Arkoor(ArkoorGenesis { client_cosigners: cosigners, tap_tweak, signature })
}
pub fn input_txout(
&self,
amount: Amount,
server_pubkey: PublicKey,
expiry_height: BlockHeight,
_exit_delta: BlockDelta,
) -> TxOut {
match self {
Self::Cosigned(inner) => inner.input_txout(amount, server_pubkey, expiry_height),
Self::HashLockedCosigned(inner) => inner.input_txout(amount, server_pubkey, expiry_height),
Self::Arkoor(inner) => inner.input_txout(amount, server_pubkey),
}
}
pub fn witness(
&self,
server_pubkey: PublicKey,
expiry_height: BlockHeight,
) -> Witness {
match self {
Self::Cosigned(inner) => inner.witness(),
Self::HashLockedCosigned(inner) => inner.witness(server_pubkey, expiry_height),
Self::Arkoor(inner) => inner.witness(),
}
}
pub fn has_all_witnesses(&self) -> bool {
match self {
Self::Cosigned(inner) => inner.has_all_witnesses(),
Self::HashLockedCosigned(inner) => inner.has_all_witnesses(),
Self::Arkoor(inner) => inner.has_all_witnesses(),
}
}
pub fn kind(&self) -> TransitionKind {
match self {
Self::Cosigned { .. } => TransitionKind::Cosigned,
Self::HashLockedCosigned { .. } => TransitionKind::HashLockedCosigned,
Self::Arkoor { .. } => TransitionKind::Arkoor,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GenesisItem {
pub transition: GenesisTransition,
pub output_idx: u8,
pub other_outputs: Vec<TxOut>,
pub fee_amount: Amount,
}
impl GenesisItem {
pub fn fee_anchor(&self) -> TxOut {
fee::fee_anchor_with_amount(self.fee_amount)
}
pub fn other_output_sum(&self) -> Option<Amount> {
let mut result = self.fee_amount;
for o in &self.other_outputs {
result = result.checked_add(o.value)?;
}
Some(result)
}
pub fn tx(&self,
prev: OutPoint,
next: TxOut,
server_pubkey: PublicKey,
expiry_height: BlockHeight,
) -> Transaction {
Transaction {
version: bitcoin::transaction::Version(3),
lock_time: bitcoin::absolute::LockTime::ZERO,
input: vec![TxIn {
previous_output: prev,
script_sig: ScriptBuf::new(),
sequence: Sequence::ZERO,
witness: self.transition.witness(server_pubkey, expiry_height),
}],
output: {
let mut out = Vec::with_capacity(self.other_outputs.len() + 2);
out.extend(self.other_outputs.iter().take(self.output_idx as usize).cloned());
out.push(next);
out.extend(self.other_outputs.iter().skip(self.output_idx as usize).cloned());
out.push(self.fee_anchor());
out
},
}
}
}