pub mod package;
use std::marker::PhantomData;
use bitcoin::hashes::Hash;
use bitcoin::sighash::{self, SighashCache};
use bitcoin::{
Amount, OutPoint, ScriptBuf, Sequence, TapSighash, TapSighashType, Transaction, TxIn, TxOut, Txid, Witness
};
use bitcoin::taproot::TapTweakHash;
use bitcoin::secp256k1::{schnorr, Keypair, PublicKey};
use bitcoin_ext::{fee, P2TR_DUST, TxOutExt};
use secp256k1_musig::musig::PublicNonce;
use crate::{musig, scripts, Vtxo, VtxoId, ServerVtxo};
use crate::vtxo::{VtxoPolicy, ServerVtxoPolicy};
use crate::vtxo::genesis::{GenesisItem, GenesisTransition};
pub use package::ArkoorPackageBuilder;
#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
pub enum ArkoorConstructionError {
#[error("Input amount of {input} does not match output amount of {output}")]
Unbalanced {
input: Amount,
output: Amount,
},
#[error("An output is below the dust threshold")]
Dust,
#[error("At least one output is required")]
NoOutputs,
#[error("Too many inputs provided")]
TooManyInputs,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
pub enum ArkoorSigningError {
#[error("An error occurred while building arkoor: {0}")]
ArkoorConstructionError(ArkoorConstructionError),
#[error("Wrong number of user nonces provided. Expected {expected}, got {got}")]
InvalidNbUserNonces {
expected: usize,
got: usize,
},
#[error("Wrong number of server nonces provided. Expected {expected}, got {got}")]
InvalidNbServerNonces {
expected: usize,
got: usize,
},
#[error("Incorrect signing key provided. Expected {expected}, got {got}")]
IncorrectKey {
expected: PublicKey,
got: PublicKey,
},
#[error("Wrong number of server partial sigs. Expected {expected}, got {got}")]
InvalidNbServerPartialSigs {
expected: usize,
got: usize
},
#[error("Invalid partial signature at index {index}")]
InvalidPartialSignature {
index: usize,
},
#[error("Wrong number of packages. Expected {expected}, got {got}")]
InvalidNbPackages {
expected: usize,
got: usize,
},
#[error("Wrong number of keypairs. Expected {expected}, got {got}")]
InvalidNbKeypairs {
expected: usize,
got: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
pub struct ArkoorDestination {
pub total_amount: Amount,
#[serde(with = "crate::encode::serde")]
pub policy: VtxoPolicy,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArkoorCosignResponse {
pub server_pub_nonces: Vec<musig::PublicNonce>,
pub server_partial_sigs: Vec<musig::PartialSignature>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArkoorCosignRequest<V> {
pub user_pub_nonces: Vec<musig::PublicNonce>,
pub input: V,
pub outputs: Vec<ArkoorDestination>,
pub isolated_outputs: Vec<ArkoorDestination>,
pub use_checkpoint: bool,
}
impl<V> ArkoorCosignRequest<V> {
pub fn new(
user_pub_nonces: Vec<musig::PublicNonce>,
input: V,
outputs: Vec<ArkoorDestination>,
isolated_outputs: Vec<ArkoorDestination>,
use_checkpoint: bool,
) -> Self {
Self {
user_pub_nonces,
input,
outputs,
isolated_outputs,
use_checkpoint,
}
}
pub fn all_outputs(&self) -> impl Iterator<Item = &ArkoorDestination> + Clone {
self.outputs.iter().chain(&self.isolated_outputs)
}
}
impl ArkoorCosignRequest<VtxoId> {
pub fn with_vtxo(self, vtxo: Vtxo) -> Result<ArkoorCosignRequest<Vtxo>, &'static str> {
if self.input != vtxo.id() {
return Err("Input vtxo id does not match the provided vtxo id")
}
Ok(ArkoorCosignRequest::new(
self.user_pub_nonces,
vtxo,
self.outputs,
self.isolated_outputs,
self.use_checkpoint,
))
}
}
pub mod state {
mod sealed {
pub trait Sealed {}
impl Sealed for super::Initial {}
impl Sealed for super::UserGeneratedNonces {}
impl Sealed for super::UserSigned {}
impl Sealed for super::ServerCanCosign {}
impl Sealed for super::ServerSigned {}
}
pub trait BuilderState: sealed::Sealed {}
pub struct Initial;
impl BuilderState for Initial {}
pub struct UserGeneratedNonces;
impl BuilderState for UserGeneratedNonces {}
pub struct UserSigned;
impl BuilderState for UserSigned {}
pub struct ServerCanCosign;
impl BuilderState for ServerCanCosign {}
pub struct ServerSigned;
impl BuilderState for ServerSigned {}
}
pub struct ArkoorBuilder<S: state::BuilderState> {
input: Vtxo,
outputs: Vec<ArkoorDestination>,
isolated_outputs: Vec<ArkoorDestination>,
checkpoint_data: Option<(Transaction, Txid)>,
unsigned_arkoor_txs: Vec<Transaction>,
unsigned_isolation_fanout_tx: Option<Transaction>,
sighashes: Vec<TapSighash>,
input_tweak: TapTweakHash,
checkpoint_policy_tweak: TapTweakHash,
new_vtxo_ids: Vec<VtxoId>,
user_pub_nonces: Option<Vec<musig::PublicNonce>>,
user_sec_nonces: Option<Vec<musig::SecretNonce>>,
server_pub_nonces: Option<Vec<musig::PublicNonce>>,
server_partial_sigs: Option<Vec<musig::PartialSignature>>,
full_signatures: Option<Vec<schnorr::Signature>>,
_state: PhantomData<S>,
}
impl<S: state::BuilderState> ArkoorBuilder<S> {
pub fn input(&self) -> &Vtxo {
&self.input
}
pub fn normal_outputs(&self) -> &[ArkoorDestination] {
&self.outputs
}
pub fn isolated_outputs(&self) -> &[ArkoorDestination] {
&self.isolated_outputs
}
pub fn all_outputs(
&self,
) -> impl Iterator<Item = &ArkoorDestination> + Clone {
self.outputs.iter().chain(&self.isolated_outputs)
}
fn build_checkpoint_vtxo_at(
&self,
output_idx: usize,
checkpoint_sig: Option<schnorr::Signature>
) -> ServerVtxo {
let output = &self.outputs[output_idx];
let (checkpoint_tx, checkpoint_txid) = self.checkpoint_data.as_ref()
.expect("called checkpoint_vtxo_at in context without checkpoints");
Vtxo {
amount: output.total_amount,
policy: ServerVtxoPolicy::new_checkpoint(self.input.user_pubkey()),
expiry_height: self.input.expiry_height,
server_pubkey: self.input.server_pubkey,
exit_delta: self.input.exit_delta,
point: OutPoint::new(*checkpoint_txid, output_idx as u32),
anchor_point: self.input.anchor_point,
genesis: self.input.genesis.clone().into_iter().chain([
GenesisItem {
transition: GenesisTransition::new_arkoor(
vec![self.input.user_pubkey()],
self.input.policy().taproot(
self.input.server_pubkey,
self.input.exit_delta,
self.input.expiry_height,
).tap_tweak(),
checkpoint_sig,
),
output_idx: output_idx as u8,
other_outputs: checkpoint_tx.output
.iter().enumerate()
.filter_map(|(i, txout)| {
if i == (output_idx as usize) || txout.is_p2a_fee_anchor() {
None
} else {
Some(txout.clone())
}
})
.collect(),
},
]).collect(),
}
}
fn build_vtxo_at(
&self,
output_idx: usize,
checkpoint_sig: Option<schnorr::Signature>,
arkoor_sig: Option<schnorr::Signature>,
) -> Vtxo {
let output = &self.outputs[output_idx];
if let Some((checkpoint_tx, _txid)) = &self.checkpoint_data {
let checkpoint_policy = ServerVtxoPolicy::new_checkpoint(self.input.user_pubkey());
Vtxo {
amount: output.total_amount,
policy: output.policy.clone(),
expiry_height: self.input.expiry_height,
server_pubkey: self.input.server_pubkey,
exit_delta: self.input.exit_delta,
point: self.new_vtxo_ids[output_idx].utxo(),
anchor_point: self.input.anchor_point,
genesis: self.input.genesis.iter().cloned().chain([
GenesisItem {
transition: GenesisTransition::new_arkoor(
vec![self.input.user_pubkey()],
self.input.policy.taproot(
self.input.server_pubkey,
self.input.exit_delta,
self.input.expiry_height,
).tap_tweak(),
checkpoint_sig,
),
output_idx: output_idx as u8,
other_outputs: checkpoint_tx.output
.iter().enumerate()
.filter_map(|(i, txout)| {
if i == (output_idx as usize) || txout.is_p2a_fee_anchor() {
None
} else {
Some(txout.clone())
}
})
.collect(),
},
GenesisItem {
transition: GenesisTransition::new_arkoor(
vec![self.input.user_pubkey()],
checkpoint_policy.taproot(
self.input.server_pubkey,
self.input.exit_delta,
self.input.expiry_height,
).tap_tweak(),
arkoor_sig,
),
output_idx: 0,
other_outputs: vec![]
}
]).collect(),
}
} else {
let arkoor_tx = &self.unsigned_arkoor_txs[0];
Vtxo {
amount: output.total_amount,
policy: output.policy.clone(),
expiry_height: self.input.expiry_height,
server_pubkey: self.input.server_pubkey,
exit_delta: self.input.exit_delta,
point: OutPoint::new(arkoor_tx.compute_txid(), output_idx as u32),
anchor_point: self.input.anchor_point,
genesis: self.input.genesis.iter().cloned().chain([
GenesisItem {
transition: GenesisTransition::new_arkoor(
vec![self.input.user_pubkey()],
self.input.policy.taproot(
self.input.server_pubkey,
self.input.exit_delta,
self.input.expiry_height,
).tap_tweak(),
arkoor_sig,
),
output_idx: output_idx as u8,
other_outputs: arkoor_tx.output
.iter().enumerate()
.filter_map(|(idx, txout)| {
if idx == output_idx || txout.is_p2a_fee_anchor() {
None
} else {
Some(txout.clone())
}
})
.collect(),
}
]).collect(),
}
}
}
fn build_isolated_vtxo_at(
&self,
isolated_idx: usize,
pre_fanout_tx_sig: Option<schnorr::Signature>,
isolation_fanout_tx_sig: Option<schnorr::Signature>,
) -> Vtxo {
let output = &self.isolated_outputs[isolated_idx];
let checkpoint_policy = ServerVtxoPolicy::new_checkpoint(self.input.user_pubkey());
let fanout_tx = self.unsigned_isolation_fanout_tx.as_ref()
.expect("construct_dust_vtxo_at called without dust isolation");
let dust_isolation_output_idx = self.outputs.len();
if let Some((checkpoint_tx, _txid)) = &self.checkpoint_data {
Vtxo {
amount: output.total_amount,
policy: output.policy.clone(),
expiry_height: self.input.expiry_height,
server_pubkey: self.input.server_pubkey,
exit_delta: self.input.exit_delta,
point: OutPoint::new(fanout_tx.compute_txid(), isolated_idx as u32),
anchor_point: self.input.anchor_point,
genesis: self.input.genesis.iter().cloned().chain([
GenesisItem {
transition: GenesisTransition::new_arkoor(
vec![self.input.user_pubkey()],
self.input.policy.taproot(
self.input.server_pubkey,
self.input.exit_delta,
self.input.expiry_height,
).tap_tweak(),
pre_fanout_tx_sig,
),
output_idx: dust_isolation_output_idx as u8,
other_outputs: checkpoint_tx.output
.iter().enumerate()
.filter_map(|(idx, txout)| {
if idx == dust_isolation_output_idx || txout.is_p2a_fee_anchor() {
None
} else {
Some(txout.clone())
}
})
.collect(),
},
GenesisItem {
transition: GenesisTransition::new_arkoor(
vec![self.input.user_pubkey()],
checkpoint_policy.taproot(
self.input.server_pubkey,
self.input.exit_delta,
self.input.expiry_height,
).tap_tweak(),
isolation_fanout_tx_sig,
),
output_idx: isolated_idx as u8,
other_outputs: fanout_tx.output
.iter().enumerate()
.filter_map(|(idx, txout)| {
if idx == isolated_idx || txout.is_p2a_fee_anchor() {
None
} else {
Some(txout.clone())
}
})
.collect(),
},
]).collect(),
}
} else {
let arkoor_tx = &self.unsigned_arkoor_txs[0];
Vtxo {
amount: output.total_amount,
policy: output.policy.clone(),
expiry_height: self.input.expiry_height,
server_pubkey: self.input.server_pubkey,
exit_delta: self.input.exit_delta,
point: OutPoint::new(fanout_tx.compute_txid(), isolated_idx as u32),
anchor_point: self.input.anchor_point,
genesis: self.input.genesis.iter().cloned().chain([
GenesisItem {
transition: GenesisTransition::new_arkoor(
vec![self.input.user_pubkey()],
self.input.policy.taproot(
self.input.server_pubkey,
self.input.exit_delta,
self.input.expiry_height,
).tap_tweak(),
pre_fanout_tx_sig,
),
output_idx: dust_isolation_output_idx as u8,
other_outputs: arkoor_tx.output
.iter().enumerate()
.filter_map(|(idx, txout)| {
if idx == dust_isolation_output_idx || txout.is_p2a_fee_anchor() {
None
} else {
Some(txout.clone())
}
})
.collect(),
},
GenesisItem {
transition: GenesisTransition::new_arkoor(
vec![self.input.user_pubkey()],
checkpoint_policy.taproot(
self.input.server_pubkey,
self.input.exit_delta,
self.input.expiry_height,
).tap_tweak(),
isolation_fanout_tx_sig,
),
output_idx: isolated_idx as u8,
other_outputs: fanout_tx.output
.iter().enumerate()
.filter_map(|(idx, txout)| {
if idx == isolated_idx || txout.is_p2a_fee_anchor() {
None
} else {
Some(txout.clone())
}
})
.collect(),
},
]).collect(),
}
}
}
fn nb_sigs(&self) -> usize {
let base = if self.checkpoint_data.is_some() {
1 + self.outputs.len() } else {
1 };
if self.unsigned_isolation_fanout_tx.is_some() {
base + 1 } else {
base
}
}
pub fn build_unsigned_vtxos<'a>(&'a self) -> impl Iterator<Item = Vtxo> + 'a {
let regular = (0..self.outputs.len()).map(|i| self.build_vtxo_at(i, None, None));
let isolated = (0..self.isolated_outputs.len())
.map(|i| self.build_isolated_vtxo_at(i, None, None));
regular.chain(isolated)
}
pub fn build_unsigned_internal_vtxos<'a>(&'a self) -> impl Iterator<Item = ServerVtxo> + 'a {
let checkpoint_vtxos = {
let range = if self.checkpoint_data.is_some() {
0..self.outputs.len()
} else {
0..0
};
range.map(|i| self.build_checkpoint_vtxo_at(i, None))
};
let isolation_vtxo = if !self.isolated_outputs.is_empty() {
let output_idx = self.outputs.len();
let (int_tx, int_txid) = if let Some((tx, txid)) = &self.checkpoint_data {
(tx, *txid)
} else {
let arkoor_tx = &self.unsigned_arkoor_txs[0];
(arkoor_tx, arkoor_tx.compute_txid())
};
Some(Vtxo {
amount: self.isolated_outputs.iter().map(|o| o.total_amount).sum(),
policy: ServerVtxoPolicy::new_checkpoint(self.input.user_pubkey()),
expiry_height: self.input.expiry_height,
server_pubkey: self.input.server_pubkey,
exit_delta: self.input.exit_delta,
point: OutPoint::new(int_txid, output_idx as u32),
anchor_point: self.input.anchor_point,
genesis: self.input.genesis.clone().into_iter().chain([
GenesisItem {
transition: GenesisTransition::new_arkoor(
vec![self.input.user_pubkey()],
self.input_tweak,
None,
),
output_idx: output_idx as u8,
other_outputs: int_tx.output.iter().enumerate()
.filter_map(|(i, txout)| {
if i == output_idx || txout.is_p2a_fee_anchor() {
None
} else {
Some(txout.clone())
}
})
.collect(),
},
]).collect(),
})
} else {
None
};
checkpoint_vtxos.chain(isolation_vtxo)
}
pub fn spend_info(&self) -> Vec<(VtxoId, Txid)> {
let mut ret = Vec::with_capacity(1 + self.outputs.len());
if let Some((_tx, checkpoint_txid)) = &self.checkpoint_data {
ret.push((self.input.id(), *checkpoint_txid));
for idx in 0..self.outputs.len() {
ret.push((
VtxoId::from(OutPoint::new(*checkpoint_txid, idx as u32)),
self.unsigned_arkoor_txs[idx].compute_txid()
));
}
if let Some(fanout_tx) = &self.unsigned_isolation_fanout_tx {
let fanout_txid = fanout_tx.compute_txid();
let isolated_output_idx = self.outputs.len() as u32;
ret.push((
VtxoId::from(OutPoint::new(*checkpoint_txid, isolated_output_idx)),
fanout_txid,
));
}
} else {
let arkoor_txid = self.unsigned_arkoor_txs[0].compute_txid();
ret.push((self.input.id(), arkoor_txid));
if let Some(fanout_tx) = &self.unsigned_isolation_fanout_tx {
let fanout_txid = fanout_tx.compute_txid();
let isolation_output_idx = self.outputs.len() as u32;
ret.push((
VtxoId::from(OutPoint::new(arkoor_txid, isolation_output_idx)),
fanout_txid,
));
}
}
ret
}
pub fn virtual_transactions(&self) -> Vec<Txid> {
let mut ret = Vec::new();
if let Some((_, txid)) = &self.checkpoint_data {
ret.push(*txid);
}
ret.extend(self.unsigned_arkoor_txs.iter().map(|tx| tx.compute_txid()));
if let Some(tx) = &self.unsigned_isolation_fanout_tx {
ret.push(tx.compute_txid());
}
ret
}
fn taptweak_at(&self, idx: usize) -> TapTweakHash {
if idx == 0 { self.input_tweak } else { self.checkpoint_policy_tweak }
}
fn user_pubkey(&self) -> PublicKey {
self.input.user_pubkey()
}
fn server_pubkey(&self) -> PublicKey {
self.input.server_pubkey()
}
fn construct_unsigned_checkpoint_tx(
input: &Vtxo,
outputs: &[ArkoorDestination],
dust_isolation_amount: Option<Amount>,
) -> Transaction {
let output_policy = ServerVtxoPolicy::new_checkpoint(input.user_pubkey());
let checkpoint_spk = output_policy
.script_pubkey(input.server_pubkey(), input.exit_delta(), input.expiry_height());
Transaction {
version: bitcoin::transaction::Version(3),
lock_time: bitcoin::absolute::LockTime::ZERO,
input: vec![TxIn {
previous_output: input.point(),
script_sig: ScriptBuf::new(),
sequence: Sequence::ZERO,
witness: Witness::new(),
}],
output: outputs.iter().map(|o| {
TxOut {
value: o.total_amount,
script_pubkey: checkpoint_spk.clone(),
}
})
.chain(dust_isolation_amount.map(|amt| {
TxOut {
value: amt,
script_pubkey: checkpoint_spk.clone(),
}
}))
.chain([fee::fee_anchor()]).collect()
}
}
fn construct_unsigned_arkoor_txs(
input: &Vtxo,
outputs: &[ArkoorDestination],
checkpoint_txid: Option<Txid>,
dust_isolation_amount: Option<Amount>,
) -> Vec<Transaction> {
if let Some(checkpoint_txid) = checkpoint_txid {
let mut arkoor_txs = Vec::with_capacity(outputs.len());
for (vout, output) in outputs.iter().enumerate() {
let transaction = Transaction {
version: bitcoin::transaction::Version(3),
lock_time: bitcoin::absolute::LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint::new(checkpoint_txid, vout as u32),
script_sig: ScriptBuf::new(),
sequence: Sequence::ZERO,
witness: Witness::new(),
}],
output: vec![
output.policy.txout(
output.total_amount,
input.server_pubkey(),
input.exit_delta(),
input.expiry_height(),
),
fee::fee_anchor(),
]
};
arkoor_txs.push(transaction);
}
arkoor_txs
} else {
let checkpoint_policy = ServerVtxoPolicy::new_checkpoint(input.user_pubkey());
let checkpoint_spk = checkpoint_policy.script_pubkey(
input.server_pubkey(),
input.exit_delta(),
input.expiry_height()
);
let transaction = Transaction {
version: bitcoin::transaction::Version(3),
lock_time: bitcoin::absolute::LockTime::ZERO,
input: vec![TxIn {
previous_output: input.point(),
script_sig: ScriptBuf::new(),
sequence: Sequence::ZERO,
witness: Witness::new(),
}],
output: outputs.iter()
.map(|o| o.policy.txout(
o.total_amount,
input.server_pubkey(),
input.exit_delta(),
input.expiry_height(),
))
.chain(dust_isolation_amount.map(|amt| TxOut {
value: amt,
script_pubkey: checkpoint_spk.clone(),
}))
.chain([fee::fee_anchor()])
.collect()
};
vec![transaction]
}
}
fn construct_unsigned_isolation_fanout_tx(
input: &Vtxo,
isolated_outputs: &[ArkoorDestination],
parent_txid: Txid, dust_isolation_output_vout: u32, ) -> Transaction {
let mut tx_outputs: Vec<TxOut> = isolated_outputs.iter().map(|o| {
TxOut {
value: o.total_amount,
script_pubkey: o.policy.script_pubkey(
input.server_pubkey(),
input.exit_delta(),
input.expiry_height(),
),
}
}).collect();
tx_outputs.push(fee::fee_anchor());
Transaction {
version: bitcoin::transaction::Version(3),
lock_time: bitcoin::absolute::LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint::new(parent_txid, dust_isolation_output_vout),
script_sig: ScriptBuf::new(),
sequence: Sequence::ZERO,
witness: Witness::new(),
}],
output: tx_outputs,
}
}
fn validate_amounts(
input: &Vtxo,
outputs: &[ArkoorDestination],
isolation_outputs: &[ArkoorDestination],
) -> Result<(), ArkoorConstructionError> {
let input_amount = input.amount();
let output_amount = outputs.iter().chain(isolation_outputs.iter())
.map(|o| o.total_amount).sum::<Amount>();
if input_amount != output_amount {
return Err(ArkoorConstructionError::Unbalanced {
input: input_amount,
output: output_amount,
})
}
if outputs.is_empty() {
return Err(ArkoorConstructionError::NoOutputs)
}
if !isolation_outputs.is_empty() {
let isolation_sum: Amount = isolation_outputs.iter()
.map(|o| o.total_amount).sum();
if isolation_sum < P2TR_DUST {
return Err(ArkoorConstructionError::Dust)
}
}
Ok(())
}
fn to_state<S2: state::BuilderState>(self) -> ArkoorBuilder<S2> {
ArkoorBuilder {
input: self.input,
outputs: self.outputs,
isolated_outputs: self.isolated_outputs,
checkpoint_data: self.checkpoint_data,
unsigned_arkoor_txs: self.unsigned_arkoor_txs,
unsigned_isolation_fanout_tx: self.unsigned_isolation_fanout_tx,
new_vtxo_ids: self.new_vtxo_ids,
sighashes: self.sighashes,
input_tweak: self.input_tweak,
checkpoint_policy_tweak: self.checkpoint_policy_tweak,
user_pub_nonces: self.user_pub_nonces,
user_sec_nonces: self.user_sec_nonces,
server_pub_nonces: self.server_pub_nonces,
server_partial_sigs: self.server_partial_sigs,
full_signatures: self.full_signatures,
_state: PhantomData,
}
}
}
impl ArkoorBuilder<state::Initial> {
pub fn new_with_checkpoint(
input: Vtxo,
outputs: Vec<ArkoorDestination>,
isolated_outputs: Vec<ArkoorDestination>,
) -> Result<Self, ArkoorConstructionError> {
Self::new(input, outputs, isolated_outputs, true)
}
pub fn new_without_checkpoint(
input: Vtxo,
outputs: Vec<ArkoorDestination>,
isolated_outputs: Vec<ArkoorDestination>,
) -> Result<Self, ArkoorConstructionError> {
Self::new(input, outputs, isolated_outputs, false)
}
pub fn new_with_checkpoint_isolate_dust(
input: Vtxo,
outputs: Vec<ArkoorDestination>,
) -> Result<Self, ArkoorConstructionError> {
Self::new_isolate_dust(input, outputs, true)
}
pub(crate) fn new_isolate_dust(
input: Vtxo,
outputs: Vec<ArkoorDestination>,
use_checkpoints: bool,
) -> Result<Self, ArkoorConstructionError> {
if outputs.iter().all(|v| v.total_amount >= P2TR_DUST)
|| outputs.iter().all(|v| v.total_amount < P2TR_DUST)
{
return Self::new(input, outputs, vec![], use_checkpoints);
}
let (mut dust, mut non_dust) = outputs.iter().cloned()
.partition::<Vec<_>, _>(|v| v.total_amount < P2TR_DUST);
let dust_sum = dust.iter().map(|o| o.total_amount).sum::<Amount>();
if dust_sum >= P2TR_DUST {
return Self::new(input, non_dust, dust, use_checkpoints);
}
let non_dust_sum = non_dust.iter().map(|o| o.total_amount).sum::<Amount>();
if non_dust_sum < P2TR_DUST * 2 {
return Self::new(input, outputs, vec![], use_checkpoints);
}
let deficit = P2TR_DUST - dust_sum;
let split_idx = non_dust.iter()
.position(|o| o.total_amount - deficit >= P2TR_DUST);
if let Some(idx) = split_idx {
let output_to_split = non_dust[idx].clone();
let dust_piece = ArkoorDestination {
total_amount: deficit,
policy: output_to_split.policy.clone(),
};
let leftover = ArkoorDestination {
total_amount: output_to_split.total_amount - deficit,
policy: output_to_split.policy,
};
non_dust[idx] = leftover;
dust.insert(0, dust_piece);
return Self::new(input, non_dust, dust, use_checkpoints);
} else {
let all_outputs = non_dust.into_iter().chain(dust).collect();
return Self::new(input, all_outputs, vec![], use_checkpoints);
}
}
pub(crate) fn new(
input: Vtxo,
outputs: Vec<ArkoorDestination>,
isolated_outputs: Vec<ArkoorDestination>,
use_checkpoint: bool,
) -> Result<Self, ArkoorConstructionError> {
Self::validate_amounts(&input, &outputs, &isolated_outputs)?;
let combined_dust_amount = if !isolated_outputs.is_empty() {
Some(isolated_outputs.iter().map(|o| o.total_amount).sum())
} else {
None
};
let unsigned_checkpoint_tx = if use_checkpoint {
let tx = Self::construct_unsigned_checkpoint_tx(
&input,
&outputs,
combined_dust_amount,
);
let txid = tx.compute_txid();
Some((tx, txid))
} else {
None
};
let unsigned_arkoor_txs = Self::construct_unsigned_arkoor_txs(
&input,
&outputs,
unsigned_checkpoint_tx.as_ref().map(|t| t.1),
combined_dust_amount,
);
let unsigned_isolation_fanout_tx = if !isolated_outputs.is_empty() {
let dust_isolation_output_vout = outputs.len() as u32;
let parent_txid = if let Some((_tx, txid)) = &unsigned_checkpoint_tx {
*txid
} else {
unsigned_arkoor_txs[0].compute_txid()
};
Some(Self::construct_unsigned_isolation_fanout_tx(
&input,
&isolated_outputs,
parent_txid,
dust_isolation_output_vout,
))
} else {
None
};
let new_vtxo_ids = unsigned_arkoor_txs.iter()
.map(|tx| OutPoint::new(tx.compute_txid(), 0))
.map(|outpoint| VtxoId::from(outpoint))
.collect();
let mut sighashes = Vec::new();
if let Some((checkpoint_tx, _txid)) = &unsigned_checkpoint_tx {
sighashes.push(arkoor_sighash(&input.txout(), checkpoint_tx));
for vout in 0..outputs.len() {
let prevout = checkpoint_tx.output[vout].clone();
sighashes.push(arkoor_sighash(&prevout, &unsigned_arkoor_txs[vout]));
}
} else {
sighashes.push(arkoor_sighash(&input.txout(), &unsigned_arkoor_txs[0]));
}
if let Some(ref tx) = unsigned_isolation_fanout_tx {
let dust_output_vout = outputs.len(); let prevout = if let Some((checkpoint_tx, _txid)) = &unsigned_checkpoint_tx {
checkpoint_tx.output[dust_output_vout].clone()
} else {
unsigned_arkoor_txs[0].output[dust_output_vout].clone()
};
sighashes.push(arkoor_sighash(&prevout, tx));
}
let policy = ServerVtxoPolicy::new_checkpoint(input.user_pubkey());
let input_tweak = input.output_taproot().tap_tweak();
let checkpoint_policy_tweak = policy.taproot(
input.server_pubkey(),
input.exit_delta(),
input.expiry_height(),
).tap_tweak();
Ok(Self {
input: input,
outputs: outputs,
isolated_outputs,
sighashes: sighashes,
input_tweak,
checkpoint_policy_tweak,
checkpoint_data: unsigned_checkpoint_tx,
unsigned_arkoor_txs: unsigned_arkoor_txs,
unsigned_isolation_fanout_tx,
new_vtxo_ids: new_vtxo_ids,
user_pub_nonces: None,
user_sec_nonces: None,
server_pub_nonces: None,
server_partial_sigs: None,
full_signatures: None,
_state: PhantomData,
})
}
pub fn generate_user_nonces(
mut self,
user_keypair: Keypair,
) -> ArkoorBuilder<state::UserGeneratedNonces> {
let mut user_pub_nonces = Vec::with_capacity(self.nb_sigs());
let mut user_sec_nonces = Vec::with_capacity(self.nb_sigs());
for idx in 0..self.nb_sigs() {
let sighash = &self.sighashes[idx].to_byte_array();
let (sec_nonce, pub_nonce) = musig::nonce_pair_with_msg(&user_keypair, sighash);
user_pub_nonces.push(pub_nonce);
user_sec_nonces.push(sec_nonce);
}
self.user_pub_nonces = Some(user_pub_nonces);
self.user_sec_nonces = Some(user_sec_nonces);
self.to_state::<state::UserGeneratedNonces>()
}
fn set_user_pub_nonces(
mut self,
user_pub_nonces: Vec<musig::PublicNonce>,
) -> Result<ArkoorBuilder<state::ServerCanCosign>, ArkoorSigningError> {
if user_pub_nonces.len() != self.nb_sigs() {
return Err(ArkoorSigningError::InvalidNbUserNonces {
expected: self.nb_sigs(),
got: user_pub_nonces.len()
})
}
self.user_pub_nonces = Some(user_pub_nonces);
Ok(self.to_state::<state::ServerCanCosign>())
}
}
impl<'a> ArkoorBuilder<state::ServerCanCosign> {
pub fn from_cosign_request(
cosign_request: ArkoorCosignRequest<Vtxo>,
) -> Result<ArkoorBuilder<state::ServerCanCosign>, ArkoorSigningError> {
let ret = ArkoorBuilder::new(
cosign_request.input,
cosign_request.outputs,
cosign_request.isolated_outputs,
cosign_request.use_checkpoint,
)
.map_err(ArkoorSigningError::ArkoorConstructionError)?
.set_user_pub_nonces(cosign_request.user_pub_nonces.clone())?;
Ok(ret)
}
pub fn server_cosign(
mut self,
server_keypair: &Keypair,
) -> Result<ArkoorBuilder<state::ServerSigned>, ArkoorSigningError> {
if server_keypair.public_key() != self.input.server_pubkey() {
return Err(ArkoorSigningError::IncorrectKey {
expected: self.input.server_pubkey(),
got: server_keypair.public_key(),
});
}
let mut server_pub_nonces = Vec::with_capacity(self.outputs.len() + 1);
let mut server_partial_sigs = Vec::with_capacity(self.outputs.len() + 1);
for idx in 0..self.nb_sigs() {
let (server_pub_nonce, server_partial_sig) = musig::deterministic_partial_sign(
&server_keypair,
[self.input.user_pubkey()],
&[&self.user_pub_nonces.as_ref().expect("state-invariant")[idx]],
self.sighashes[idx].to_byte_array(),
Some(self.taptweak_at(idx).to_byte_array()),
);
server_pub_nonces.push(server_pub_nonce);
server_partial_sigs.push(server_partial_sig);
};
self.server_pub_nonces = Some(server_pub_nonces);
self.server_partial_sigs = Some(server_partial_sigs);
Ok(self.to_state::<state::ServerSigned>())
}
}
impl ArkoorBuilder<state::ServerSigned> {
pub fn user_pub_nonces(&self) -> Vec<musig::PublicNonce> {
self.user_pub_nonces.as_ref().expect("state invariant").clone()
}
pub fn server_partial_signatures(&self) -> Vec<musig::PartialSignature> {
self.server_partial_sigs.as_ref().expect("state invariant").clone()
}
pub fn cosign_response(&self) -> ArkoorCosignResponse {
ArkoorCosignResponse {
server_pub_nonces: self.server_pub_nonces.as_ref()
.expect("state invariant").clone(),
server_partial_sigs: self.server_partial_sigs.as_ref()
.expect("state invariant").clone(),
}
}
}
impl ArkoorBuilder<state::UserGeneratedNonces> {
pub fn user_pub_nonces(&self) -> &[PublicNonce] {
self.user_pub_nonces.as_ref().expect("State invariant")
}
pub fn cosign_request(&self) -> ArkoorCosignRequest<Vtxo> {
ArkoorCosignRequest {
user_pub_nonces: self.user_pub_nonces().to_vec(),
input: self.input.clone(),
outputs: self.outputs.clone(),
isolated_outputs: self.isolated_outputs.clone(),
use_checkpoint: self.checkpoint_data.is_some(),
}
}
fn validate_server_cosign_response(
&self,
data: &ArkoorCosignResponse,
) -> Result<(), ArkoorSigningError> {
if data.server_pub_nonces.len() != self.nb_sigs() {
return Err(ArkoorSigningError::InvalidNbServerNonces {
expected: self.nb_sigs(),
got: data.server_pub_nonces.len(),
});
}
if data.server_partial_sigs.len() != self.nb_sigs() {
return Err(ArkoorSigningError::InvalidNbServerPartialSigs {
expected: self.nb_sigs(),
got: data.server_partial_sigs.len(),
})
}
for idx in 0..self.nb_sigs() {
let is_valid_sig = scripts::verify_partial_sig(
self.sighashes[idx],
self.taptweak_at(idx),
(self.input.server_pubkey(), &data.server_pub_nonces[idx]),
(self.input.user_pubkey(), &self.user_pub_nonces()[idx]),
&data.server_partial_sigs[idx]
);
if !is_valid_sig {
return Err(ArkoorSigningError::InvalidPartialSignature {
index: idx,
});
}
}
Ok(())
}
pub fn user_cosign(
mut self,
user_keypair: &Keypair,
server_cosign_data: &ArkoorCosignResponse,
) -> Result<ArkoorBuilder<state::UserSigned>, ArkoorSigningError> {
if user_keypair.public_key() != self.input.user_pubkey() {
return Err(ArkoorSigningError::IncorrectKey {
expected: self.input.user_pubkey(),
got: user_keypair.public_key(),
});
}
self.validate_server_cosign_response(&server_cosign_data)?;
let mut sigs = Vec::with_capacity(self.nb_sigs());
let user_sec_nonces = self.user_sec_nonces.take().expect("state invariant");
for (idx, user_sec_nonce) in user_sec_nonces.into_iter().enumerate() {
let user_pub_nonce = self.user_pub_nonces()[idx];
let server_pub_nonce = server_cosign_data.server_pub_nonces[idx];
let agg_nonce = musig::nonce_agg(&[&user_pub_nonce, &server_pub_nonce]);
let (_partial, maybe_sig) = musig::partial_sign(
[self.user_pubkey(), self.server_pubkey()],
agg_nonce,
&user_keypair,
user_sec_nonce,
self.sighashes[idx].to_byte_array(),
Some(self.taptweak_at(idx).to_byte_array()),
Some(&[&server_cosign_data.server_partial_sigs[idx]])
);
let sig = maybe_sig.expect("The full signature exists. The server did sign first");
sigs.push(sig);
}
self.full_signatures = Some(sigs);
Ok(self.to_state::<state::UserSigned>())
}
}
impl<'a> ArkoorBuilder<state::UserSigned> {
pub fn build_signed_vtxos(&self) -> Vec<Vtxo> {
let sigs = self.full_signatures.as_ref().expect("state invariant");
let mut ret = Vec::with_capacity(self.outputs.len() + self.isolated_outputs.len());
if self.checkpoint_data.is_some() {
let checkpoint_sig = sigs[0];
for i in 0..self.outputs.len() {
let arkoor_sig = sigs[1 + i];
ret.push(self.build_vtxo_at(i, Some(checkpoint_sig), Some(arkoor_sig)));
}
if self.unsigned_isolation_fanout_tx.is_some() {
let m = self.outputs.len();
let fanout_tx_sig = sigs[1 + m];
for i in 0..self.isolated_outputs.len() {
ret.push(self.build_isolated_vtxo_at(
i,
Some(checkpoint_sig),
Some(fanout_tx_sig),
));
}
}
} else {
let arkoor_sig = sigs[0];
for i in 0..self.outputs.len() {
ret.push(self.build_vtxo_at(i, None, Some(arkoor_sig)));
}
if self.unsigned_isolation_fanout_tx.is_some() {
let fanout_tx_sig = sigs[1];
for i in 0..self.isolated_outputs.len() {
ret.push(self.build_isolated_vtxo_at(
i,
Some(arkoor_sig), Some(fanout_tx_sig),
));
}
}
}
ret
}
}
fn arkoor_sighash(prevout: &TxOut, arkoor_tx: &Transaction) -> TapSighash {
let mut shc = SighashCache::new(arkoor_tx);
shc.taproot_key_spend_signature_hash(
0, &sighash::Prevouts::All(&[prevout]), TapSighashType::Default,
).expect("sighash error")
}
#[cfg(test)]
mod test {
use super::*;
use std::collections::HashSet;
use bitcoin::Amount;
use bitcoin::secp256k1::Keypair;
use bitcoin::secp256k1::rand;
use crate::SECP;
use crate::test_util::dummy::DummyTestVtxoSpec;
use crate::vtxo::VtxoId;
fn verify_builder<S: state::BuilderState>(
builder: &ArkoorBuilder<S>,
input: &Vtxo,
outputs: &[ArkoorDestination],
isolated_outputs: &[ArkoorDestination],
) {
let has_isolation = !isolated_outputs.is_empty();
let spend_info = builder.spend_info();
let spend_vtxo_ids: HashSet<VtxoId> = spend_info.iter().map(|(id, _)| *id).collect();
assert_eq!(spend_info[0].0, input.id());
assert_eq!(spend_vtxo_ids.len(), spend_info.len());
let internal_vtxos: Vec<ServerVtxo> = builder.build_unsigned_internal_vtxos().collect();
let internal_vtxo_ids: HashSet<VtxoId> = internal_vtxos.iter().map(|v| v.id()).collect();
for internal_vtxo in &internal_vtxos {
assert!(spend_vtxo_ids.contains(&internal_vtxo.id()));
assert!(matches!(internal_vtxo.policy(), ServerVtxoPolicy::Checkpoint(_)));
}
for (vtxo_id, _) in &spend_info[1..] {
assert!(internal_vtxo_ids.contains(vtxo_id));
}
if has_isolation {
let isolation_vtxo = internal_vtxos.last().unwrap();
let expected_isolation_amount: Amount = isolated_outputs.iter()
.map(|o| o.total_amount)
.sum();
assert_eq!(isolation_vtxo.amount(), expected_isolation_amount);
}
let final_vtxos: Vec<Vtxo> = builder.build_unsigned_vtxos().collect();
for final_vtxo in &final_vtxos {
assert!(!spend_vtxo_ids.contains(&final_vtxo.id()));
}
let all_destinations: Vec<&ArkoorDestination> = outputs.iter()
.chain(isolated_outputs.iter())
.collect();
for (vtxo, dest) in final_vtxos.iter().zip(all_destinations.iter()) {
assert_eq!(vtxo.amount(), dest.total_amount);
assert_eq!(vtxo.policy, dest.policy);
}
let total_output_amount: Amount = final_vtxos.iter().map(|v| v.amount()).sum();
assert_eq!(total_output_amount, input.amount());
}
#[test]
fn build_checkpointed_arkoor() {
let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
println!("Alice keypair: {}", alice_keypair.public_key());
println!("Bob keypair: {}", bob_keypair.public_key());
println!("Server keypair: {}", server_keypair.public_key());
println!("-----------------------------------------------");
let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
amount: Amount::from_sat(100_000),
expiry_height: 1000,
exit_delta : 128,
user_keypair: alice_keypair.clone(),
server_keypair: server_keypair.clone()
}.build();
alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
let dest = vec![
ArkoorDestination {
total_amount: Amount::from_sat(96_000),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(4_000),
policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
}
];
let user_builder = ArkoorBuilder::new_with_checkpoint(
alice_vtxo.clone(),
dest.clone(),
vec![], ).expect("Valid arkoor request");
verify_builder(&user_builder, &alice_vtxo, &dest, &[]);
let user_builder = user_builder.generate_user_nonces(alice_keypair);
let cosign_request = user_builder.cosign_request();
let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
.expect("Invalid cosign request")
.server_cosign(&server_keypair)
.expect("Incorrect key");
let cosign_data = server_builder.cosign_response();
let vtxos = user_builder
.user_cosign(&alice_keypair, &cosign_data)
.expect("Valid cosign data and correct key")
.build_signed_vtxos();
for vtxo in vtxos.into_iter() {
vtxo.validate(&funding_tx).expect("Invalid VTXO");
let mut prev_tx = funding_tx.clone();
for tx in vtxo.transactions().map(|item| item.tx) {
let prev_outpoint: OutPoint = tx.input[0].previous_output;
let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
prev_tx = tx;
}
}
}
#[test]
fn build_checkpointed_arkoor_with_dust_isolation() {
let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let charlie_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
amount: Amount::from_sat(100_000),
expiry_height: 1000,
exit_delta : 128,
user_keypair: alice_keypair.clone(),
server_keypair: server_keypair.clone()
}.build();
alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
let outputs = vec![
ArkoorDestination {
total_amount: Amount::from_sat(99_600),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
},
];
let dust_outputs = vec![
ArkoorDestination {
total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(charlie_keypair.public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
}
];
let user_builder = ArkoorBuilder::new_with_checkpoint(
alice_vtxo.clone(),
outputs.clone(),
dust_outputs.clone(),
).expect("Valid arkoor request with dust isolation");
verify_builder(&user_builder, &alice_vtxo, &outputs, &dust_outputs);
assert!(
user_builder.unsigned_isolation_fanout_tx.is_some(),
"Dust isolation should be active",
);
assert_eq!(user_builder.nb_sigs(), 3);
let user_builder = user_builder.generate_user_nonces(alice_keypair);
let cosign_request = user_builder.cosign_request();
let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
.expect("Invalid cosign request")
.server_cosign(&server_keypair)
.expect("Incorrect key");
let cosign_data = server_builder.cosign_response();
let vtxos = user_builder
.user_cosign(&alice_keypair, &cosign_data)
.expect("Valid cosign data and correct key")
.build_signed_vtxos();
assert_eq!(vtxos.len(), 3);
for vtxo in vtxos.into_iter() {
vtxo.validate(&funding_tx).expect("Invalid VTXO");
let mut prev_tx = funding_tx.clone();
for tx in vtxo.transactions().map(|item| item.tx) {
let prev_outpoint: OutPoint = tx.input[0].previous_output;
let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
prev_tx = tx;
}
}
}
#[test]
fn build_no_checkpoint_arkoor() {
let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
println!("Alice keypair: {}", alice_keypair.public_key());
println!("Bob keypair: {}", bob_keypair.public_key());
println!("Server keypair: {}", server_keypair.public_key());
println!("-----------------------------------------------");
let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
amount: Amount::from_sat(100_000),
expiry_height: 1000,
exit_delta : 128,
user_keypair: alice_keypair.clone(),
server_keypair: server_keypair.clone()
}.build();
alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
let dest = vec![
ArkoorDestination {
total_amount: Amount::from_sat(96_000),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(4_000),
policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
}
];
let user_builder = ArkoorBuilder::new_without_checkpoint(
alice_vtxo.clone(),
dest.clone(),
vec![], ).expect("Valid arkoor request");
verify_builder(&user_builder, &alice_vtxo, &dest, &[]);
let user_builder = user_builder.generate_user_nonces(alice_keypair);
let cosign_request = user_builder.cosign_request();
let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
.expect("Invalid cosign request")
.server_cosign(&server_keypair)
.expect("Incorrect key");
let cosign_data = server_builder.cosign_response();
let vtxos = user_builder
.user_cosign(&alice_keypair, &cosign_data)
.expect("Valid cosign data and correct key")
.build_signed_vtxos();
for vtxo in vtxos.into_iter() {
vtxo.validate(&funding_tx).expect("Invalid VTXO");
let mut prev_tx = funding_tx.clone();
for tx in vtxo.transactions().map(|item| item.tx) {
let prev_outpoint: OutPoint = tx.input[0].previous_output;
let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
prev_tx = tx;
}
}
}
#[test]
fn build_no_checkpoint_arkoor_with_dust_isolation() {
let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let charlie_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
amount: Amount::from_sat(100_000),
expiry_height: 1000,
exit_delta : 128,
user_keypair: alice_keypair.clone(),
server_keypair: server_keypair.clone()
}.build();
alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
let outputs = vec![
ArkoorDestination {
total_amount: Amount::from_sat(99_600),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
},
];
let dust_outputs = vec![
ArkoorDestination {
total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(charlie_keypair.public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
}
];
let user_builder = ArkoorBuilder::new_without_checkpoint(
alice_vtxo.clone(),
outputs.clone(),
dust_outputs.clone(),
).expect("Valid arkoor request with dust isolation");
verify_builder(&user_builder, &alice_vtxo, &outputs, &dust_outputs);
assert!(
user_builder.unsigned_isolation_fanout_tx.is_some(),
"Dust isolation should be active",
);
assert_eq!(user_builder.nb_sigs(), 2);
let user_builder = user_builder.generate_user_nonces(alice_keypair);
let cosign_request = user_builder.cosign_request();
let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
.expect("Invalid cosign request")
.server_cosign(&server_keypair)
.expect("Incorrect key");
let cosign_data = server_builder.cosign_response();
let vtxos = user_builder
.user_cosign(&alice_keypair, &cosign_data)
.expect("Valid cosign data and correct key")
.build_signed_vtxos();
assert_eq!(vtxos.len(), 3);
for vtxo in vtxos.into_iter() {
vtxo.validate(&funding_tx).expect("Invalid VTXO");
let mut prev_tx = funding_tx.clone();
for tx in vtxo.transactions().map(|item| item.tx) {
let prev_outpoint: OutPoint = tx.input[0].previous_output;
let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
prev_tx = tx;
}
}
}
#[test]
fn build_checkpointed_arkoor_outputs_must_be_above_dust_if_mixed() {
let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
amount: Amount::from_sat(1000),
expiry_height: 1000,
exit_delta : 128,
user_keypair: alice_keypair.clone(),
server_keypair: server_keypair.clone()
}.build();
alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
ArkoorBuilder::new_with_checkpoint(
alice_vtxo.clone(),
vec![
ArkoorDestination {
total_amount: Amount::from_sat(100), policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
}; 10
],
vec![],
).unwrap();
let res_empty = ArkoorBuilder::new_with_checkpoint(
alice_vtxo.clone(),
vec![],
vec![
ArkoorDestination {
total_amount: Amount::from_sat(100), policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
}; 10
],
);
match res_empty {
Err(ArkoorConstructionError::NoOutputs) => {},
_ => panic!("Expected NoOutputs error for empty outputs"),
}
ArkoorBuilder::new_with_checkpoint(
alice_vtxo.clone(),
vec![
ArkoorDestination {
total_amount: Amount::from_sat(330), policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
}; 2
],
vec![
ArkoorDestination {
total_amount: Amount::from_sat(170),
policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
}; 2
],
).unwrap();
let res_mixed_small = ArkoorBuilder::new_with_checkpoint(
alice_vtxo.clone(),
vec![
ArkoorDestination {
total_amount: Amount::from_sat(500),
policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(300),
policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
}
],
vec![
ArkoorDestination {
total_amount: Amount::from_sat(100),
policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
}; 2 ],
);
match res_mixed_small {
Err(ArkoorConstructionError::Dust) => {},
_ => panic!("Expected Dust error for isolation sum < 330"),
}
}
#[test]
fn build_checkpointed_arkoor_dust_sum_too_small() {
let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
amount: Amount::from_sat(100_000),
expiry_height: 1000,
exit_delta : 128,
user_keypair: alice_keypair.clone(),
server_keypair: server_keypair.clone()
}.build();
alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
let outputs = vec![
ArkoorDestination {
total_amount: Amount::from_sat(99_900),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
},
];
let dust_outputs = vec![
ArkoorDestination {
total_amount: Amount::from_sat(50),
policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(50),
policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
}
];
let result = ArkoorBuilder::new_with_checkpoint(
alice_vtxo.clone(),
outputs.clone(),
dust_outputs.clone(),
);
match result {
Err(ArkoorConstructionError::Dust) => {},
_ => panic!("Expected Dust error for isolation sum < 330"),
}
}
#[test]
fn spend_dust_vtxo() {
let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
amount: Amount::from_sat(200),
expiry_height: 1000,
exit_delta: 128,
user_keypair: alice_keypair.clone(),
server_keypair: server_keypair.clone()
}.build();
alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
let dust_outputs = vec![
ArkoorDestination {
total_amount: Amount::from_sat(100),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(100),
policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
}
];
let user_builder = ArkoorBuilder::new_with_checkpoint(
alice_vtxo.clone(),
dust_outputs,
vec![],
).expect("Valid arkoor request for all-dust case");
assert!(
user_builder.unsigned_isolation_fanout_tx.is_none(),
"Dust isolation should NOT be active",
);
assert_eq!(user_builder.outputs.len(), 2);
assert_eq!(user_builder.nb_sigs(), 3);
let user_builder = user_builder.generate_user_nonces(alice_keypair);
let cosign_request = user_builder.cosign_request();
let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
.expect("Invalid cosign request")
.server_cosign(&server_keypair)
.expect("Incorrect key");
let cosign_data = server_builder.cosign_response();
let vtxos = user_builder
.user_cosign(&alice_keypair, &cosign_data)
.expect("Valid cosign data and correct key")
.build_signed_vtxos();
assert_eq!(vtxos.len(), 2);
for vtxo in vtxos.into_iter() {
vtxo.validate(&funding_tx).expect("Invalid VTXO");
assert_eq!(vtxo.amount(), Amount::from_sat(100));
let mut prev_tx = funding_tx.clone();
for tx in vtxo.transactions().map(|item| item.tx) {
let prev_outpoint: OutPoint = tx.input[0].previous_output;
let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
prev_tx = tx;
}
}
}
#[test]
fn spend_nondust_vtxo_to_dust() {
let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
amount: Amount::from_sat(500),
expiry_height: 1000,
exit_delta: 128,
user_keypair: alice_keypair.clone(),
server_keypair: server_keypair.clone()
}.build();
alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
let dust_outputs = vec![
ArkoorDestination {
total_amount: Amount::from_sat(250),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(250),
policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
}
];
let user_builder = ArkoorBuilder::new_with_checkpoint(
alice_vtxo.clone(),
dust_outputs,
vec![],
).expect("Valid arkoor request for non-dust to dust case");
assert!(
user_builder.unsigned_isolation_fanout_tx.is_none(),
"Dust isolation should NOT be active",
);
assert_eq!(user_builder.outputs.len(), 2);
assert_eq!(user_builder.nb_sigs(), 3);
let user_builder = user_builder.generate_user_nonces(alice_keypair);
let cosign_request = user_builder.cosign_request();
let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
.expect("Invalid cosign request")
.server_cosign(&server_keypair)
.expect("Incorrect key");
let cosign_data = server_builder.cosign_response();
let vtxos = user_builder
.user_cosign(&alice_keypair, &cosign_data)
.expect("Valid cosign data and correct key")
.build_signed_vtxos();
assert_eq!(vtxos.len(), 2);
for vtxo in vtxos.into_iter() {
vtxo.validate(&funding_tx).expect("Invalid VTXO");
assert_eq!(vtxo.amount(), Amount::from_sat(250));
let mut prev_tx = funding_tx.clone();
for tx in vtxo.transactions().map(|item| item.tx) {
let prev_outpoint: OutPoint = tx.input[0].previous_output;
let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
prev_tx = tx;
}
}
}
#[test]
fn isolate_dust_all_nondust() {
let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
amount: Amount::from_sat(1000),
expiry_height: 1000,
exit_delta: 128,
user_keypair: alice_keypair.clone(),
server_keypair: server_keypair.clone()
}.build();
alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
alice_vtxo,
vec![
ArkoorDestination {
total_amount: Amount::from_sat(500),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(500),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
}
],
).unwrap();
assert!(builder.unsigned_isolation_fanout_tx.is_none());
assert_eq!(builder.outputs.len(), 2);
assert_eq!(builder.isolated_outputs.len(), 0);
}
#[test]
fn isolate_dust_all_dust() {
let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
amount: Amount::from_sat(400),
expiry_height: 1000,
exit_delta: 128,
user_keypair: alice_keypair.clone(),
server_keypair: server_keypair.clone()
}.build();
alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
alice_vtxo,
vec![
ArkoorDestination {
total_amount: Amount::from_sat(200),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(200),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
}
],
).unwrap();
assert!(builder.unsigned_isolation_fanout_tx.is_none());
assert_eq!(builder.outputs.len(), 2);
assert_eq!(builder.isolated_outputs.len(), 0);
}
#[test]
fn isolate_dust_sufficient_dust() {
let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
amount: Amount::from_sat(1000),
expiry_height: 1000,
exit_delta: 128,
user_keypair: alice_keypair.clone(),
server_keypair: server_keypair.clone()
}.build();
alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
alice_vtxo,
vec![
ArkoorDestination {
total_amount: Amount::from_sat(600),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(200),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(200),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
}
],
).unwrap();
assert!(builder.unsigned_isolation_fanout_tx.is_some());
assert_eq!(builder.outputs.len(), 1);
assert_eq!(builder.isolated_outputs.len(), 2);
}
#[test]
fn isolate_dust_split_successful() {
let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
amount: Amount::from_sat(1000),
expiry_height: 1000,
exit_delta: 128,
user_keypair: alice_keypair.clone(),
server_keypair: server_keypair.clone()
}.build();
alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
alice_vtxo,
vec![
ArkoorDestination {
total_amount: Amount::from_sat(800),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(100),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(100),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
}
],
).unwrap();
assert!(builder.unsigned_isolation_fanout_tx.is_some());
assert_eq!(builder.outputs.len(), 1);
assert_eq!(builder.isolated_outputs.len(), 3);
assert_eq!(builder.outputs[0].total_amount, Amount::from_sat(670));
let isolated_sum: Amount = builder.isolated_outputs.iter().map(|o| o.total_amount).sum();
assert_eq!(isolated_sum, P2TR_DUST);
}
#[test]
fn isolate_dust_split_impossible() {
let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
amount: Amount::from_sat(600),
expiry_height: 1000,
exit_delta: 128,
user_keypair: alice_keypair.clone(),
server_keypair: server_keypair.clone()
}.build();
alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
alice_vtxo,
vec![
ArkoorDestination {
total_amount: Amount::from_sat(400),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(100),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(100),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
}
],
).unwrap();
assert!(builder.unsigned_isolation_fanout_tx.is_none());
assert_eq!(builder.outputs.len(), 3);
assert_eq!(builder.isolated_outputs.len(), 0);
}
#[test]
fn isolate_dust_exactly_boundary() {
let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
amount: Amount::from_sat(1000),
expiry_height: 1000,
exit_delta: 128,
user_keypair: alice_keypair.clone(),
server_keypair: server_keypair.clone()
}.build();
alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
alice_vtxo,
vec![
ArkoorDestination {
total_amount: Amount::from_sat(660),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(170),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(170),
policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
}
],
).unwrap();
assert!(builder.unsigned_isolation_fanout_tx.is_some());
assert_eq!(builder.outputs.len(), 1);
assert_eq!(builder.isolated_outputs.len(), 2);
assert_eq!(builder.outputs[0].total_amount, Amount::from_sat(660));
assert_eq!(builder.isolated_outputs[0].total_amount, Amount::from_sat(170));
assert_eq!(builder.isolated_outputs[1].total_amount, Amount::from_sat(170));
}
}