use std::convert::Infallible;
use bitcoin::Txid;
use bitcoin::secp256k1::Keypair;
use crate::{Vtxo, VtxoId, VtxoPolicy, ServerVtxo, Amount};
use crate::arkoor::ArkoorDestination;
use crate::arkoor::{
ArkoorBuilder, ArkoorConstructionError, state, ArkoorCosignResponse,
ArkoorSigningError, ArkoorCosignRequest,
};
pub struct ArkoorPackageBuilder<S: state::BuilderState> {
pub builders: Vec<ArkoorBuilder<S>>,
}
#[derive(Debug, Clone)]
pub struct ArkoorPackageCosignRequest<V> {
pub requests: Vec<ArkoorCosignRequest<V>>
}
impl<V> ArkoorPackageCosignRequest<V> {
pub fn convert_vtxo<F, O>(self, mut f: F) -> ArkoorPackageCosignRequest<O>
where F: FnMut(V) -> O
{
ArkoorPackageCosignRequest {
requests: self.requests.into_iter().map(|r| {
ArkoorCosignRequest {
user_pub_nonces: r.user_pub_nonces,
input: f(r.input),
outputs: r.outputs,
isolated_outputs: r.isolated_outputs,
use_checkpoint: r.use_checkpoint,
}
}).collect::<Vec<_>>()
}
}
pub fn inputs(&self) -> impl Iterator<Item=&V> {
self.requests.iter()
.map(|r| Some(&r.input))
.flatten()
}
pub fn all_outputs(
&self,
) -> impl Iterator<Item = &ArkoorDestination> + Clone {
self.requests.iter()
.map(|r| r.all_outputs())
.flatten()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
#[error("VTXO id mismatch. Expected {expected}, got {got}")]
pub struct InputMismatchError {
expected: VtxoId,
got: VtxoId,
}
impl ArkoorPackageCosignRequest<VtxoId> {
pub fn set_vtxos(
self,
vtxos: impl IntoIterator<Item = Vtxo>,
) -> Result<ArkoorPackageCosignRequest<Vtxo>, InputMismatchError> {
let package = ArkoorPackageCosignRequest {
requests: self.requests.into_iter().zip(vtxos).map(|(r, vtxo)| {
if r.input != vtxo.id() {
return Err(InputMismatchError {
expected: r.input,
got: vtxo.id(),
})
}
Ok(ArkoorCosignRequest {
input: vtxo,
user_pub_nonces: r.user_pub_nonces,
outputs: r.outputs,
isolated_outputs: r.isolated_outputs,
use_checkpoint: r.use_checkpoint,
})
}).collect::<Result<Vec<_>, _>>()?,
};
Ok(package)
}
}
#[derive(Debug, Clone)]
pub struct ArkoorPackageCosignResponse {
pub responses: Vec<ArkoorCosignResponse>
}
impl ArkoorPackageBuilder<state::Initial> {
fn allocate_outputs_to_inputs(
inputs: impl IntoIterator<Item = Vtxo>,
outputs: Vec<ArkoorDestination>,
) -> Result<Vec<(Vtxo, Vec<ArkoorDestination>)>, ArkoorConstructionError> {
let total_output = outputs.iter().map(|r| r.total_amount).sum::<Amount>();
if outputs.is_empty() || total_output == Amount::ZERO {
return Err(ArkoorConstructionError::NoOutputs);
}
let mut allocations: Vec<(Vtxo, Vec<ArkoorDestination>)> = Vec::new();
let mut output_iter = outputs.into_iter();
let mut current_output = output_iter.next();
let mut current_output_remaining = current_output.as_ref()
.map(|o| o.total_amount).unwrap_or_default();
let mut total_input = Amount::ZERO;
'inputs:
for input in inputs {
total_input += input.amount();
let mut input_remaining = input.amount();
let mut input_allocation: Vec<ArkoorDestination> = Vec::new();
'outputs:
while let Some(ref output) = current_output {
let _: Infallible = if input_remaining == current_output_remaining {
input_allocation.push(ArkoorDestination {
total_amount: current_output_remaining,
policy: output.policy.clone(),
});
current_output = output_iter.next();
current_output_remaining = current_output.as_ref()
.map(|o| o.total_amount).unwrap_or_default();
allocations.push((input, input_allocation));
continue 'inputs;
} else if input_remaining > current_output_remaining {
input_allocation.push(ArkoorDestination {
total_amount: current_output_remaining,
policy: output.policy.clone(),
});
input_remaining -= current_output_remaining;
current_output = output_iter.next();
current_output_remaining = current_output.as_ref()
.map(|o| o.total_amount).unwrap_or_default();
continue 'outputs;
} else {
input_allocation.push(ArkoorDestination {
total_amount: input_remaining,
policy: output.policy.clone(),
});
current_output_remaining -= input_remaining;
allocations.push((input, input_allocation));
continue 'inputs;
};
}
}
if total_input != total_output {
return Err(ArkoorConstructionError::Unbalanced {
input: total_input,
output: total_output,
});
}
Ok(allocations)
}
pub fn new_with_checkpoints(
inputs: impl IntoIterator<Item = Vtxo>,
outputs: Vec<ArkoorDestination>,
) -> Result<Self, ArkoorConstructionError> {
Self::new(inputs, outputs, true)
}
pub fn new_without_checkpoints(
inputs: impl IntoIterator<Item = Vtxo>,
outputs: Vec<ArkoorDestination>,
) -> Result<Self, ArkoorConstructionError> {
Self::new(inputs, outputs, false)
}
pub fn new_single_output_with_checkpoints(
inputs: impl IntoIterator<Item = Vtxo>,
output: ArkoorDestination,
change_policy: VtxoPolicy,
) -> Result<Self, ArkoorConstructionError> {
let inputs = inputs.into_iter().collect::<Vec<_>>();
let total_input = inputs.iter().map(|v| v.amount()).sum::<Amount>();
let change_amount = total_input.checked_sub(output.total_amount)
.ok_or(ArkoorConstructionError::Unbalanced {
input: total_input,
output: output.total_amount,
})?;
let outputs = if change_amount == Amount::ZERO {
vec![output]
} else {
vec![
output,
ArkoorDestination {
total_amount: change_amount,
policy: change_policy,
},
]
};
Self::new_with_checkpoints(inputs, outputs)
}
pub fn new_claim_all_with_checkpoints(
inputs: impl IntoIterator<Item = Vtxo>,
output_policy: VtxoPolicy,
) -> Result<Self, ArkoorConstructionError> {
let inputs = inputs.into_iter().collect::<Vec<_>>();
let total_input = inputs.iter().map(|v| v.amount()).sum::<Amount>();
let output = ArkoorDestination {
total_amount: total_input,
policy: output_policy,
};
Self::new_with_checkpoints(inputs, vec![output])
}
pub fn new_claim_all_without_checkpoints(
inputs: impl IntoIterator<Item = Vtxo>,
output_policy: VtxoPolicy,
) -> Result<Self, ArkoorConstructionError> {
let inputs = inputs.into_iter().collect::<Vec<_>>();
let total_input = inputs.iter().map(|v| v.amount()).sum::<Amount>();
let output = ArkoorDestination {
total_amount: total_input,
policy: output_policy,
};
Self::new_without_checkpoints(inputs, vec![output])
}
fn new(
inputs: impl IntoIterator<Item = Vtxo>,
outputs: Vec<ArkoorDestination>,
use_checkpoint: bool,
) -> Result<Self, ArkoorConstructionError> {
let allocations = Self::allocate_outputs_to_inputs(inputs, outputs)?;
let mut builders = Vec::with_capacity(allocations.len());
for (input, allocated_outputs) in allocations {
let builder = ArkoorBuilder::new_isolate_dust(
input,
allocated_outputs,
use_checkpoint,
)?;
builders.push(builder);
}
Ok(Self { builders })
}
pub fn generate_user_nonces(
self,
user_keypairs: &[Keypair],
) -> Result<ArkoorPackageBuilder<state::UserGeneratedNonces>, ArkoorSigningError> {
if user_keypairs.len() != self.builders.len() {
return Err(ArkoorSigningError::InvalidNbKeypairs {
expected: self.builders.len(),
got: user_keypairs.len(),
})
}
let mut builder = Vec::with_capacity(self.builders.len());
for (idx, package) in self.builders.into_iter().enumerate() {
builder.push(package.generate_user_nonces(user_keypairs[idx]));
}
Ok(ArkoorPackageBuilder { builders: builder })
}
}
impl ArkoorPackageBuilder<state::UserGeneratedNonces> {
pub fn user_cosign(
self,
user_keypairs: &[Keypair],
server_cosign_response: ArkoorPackageCosignResponse,
) -> Result<ArkoorPackageBuilder<state::UserSigned>, ArkoorSigningError> {
if server_cosign_response.responses.len() != self.builders.len() {
return Err(ArkoorSigningError::InvalidNbPackages {
expected: self.builders.len(),
got: server_cosign_response.responses.len()
})
}
if user_keypairs.len() != self.builders.len() {
return Err(ArkoorSigningError::InvalidNbKeypairs {
expected: self.builders.len(),
got: user_keypairs.len(),
})
}
let mut packages = Vec::with_capacity(self.builders.len());
for (idx, pkg) in self.builders.into_iter().enumerate() {
packages.push(pkg.user_cosign(
&user_keypairs[idx],
&server_cosign_response.responses[idx],
)?,);
}
Ok(ArkoorPackageBuilder { builders: packages })
}
pub fn cosign_request(&self) -> ArkoorPackageCosignRequest<Vtxo> {
let requests = self.builders.iter()
.map(|package| package.cosign_request())
.collect::<Vec<_>>();
ArkoorPackageCosignRequest { requests }
}
}
impl ArkoorPackageBuilder<state::UserSigned> {
pub fn build_signed_vtxos(self) -> Vec<Vtxo> {
self.builders.into_iter()
.map(|b| b.build_signed_vtxos())
.flatten()
.collect::<Vec<_>>()
}
}
impl ArkoorPackageBuilder<state::ServerCanCosign> {
pub fn from_cosign_request(
cosign_request: ArkoorPackageCosignRequest<Vtxo>,
) -> Result<Self, ArkoorSigningError> {
let request_iter = cosign_request.requests.into_iter();
let mut packages = Vec::with_capacity(request_iter.size_hint().0);
for request in request_iter {
packages.push(ArkoorBuilder::from_cosign_request(request)?);
}
Ok(Self { builders: packages })
}
pub fn server_cosign(
self,
server_keypair: &Keypair,
) -> Result<ArkoorPackageBuilder<state::ServerSigned>, ArkoorSigningError> {
let mut packages = Vec::with_capacity(self.builders.len());
for package in self.builders.into_iter() {
packages.push(package.server_cosign(&server_keypair)?);
}
Ok(ArkoorPackageBuilder { builders: packages })
}
}
impl ArkoorPackageBuilder<state::ServerSigned> {
pub fn cosign_response(&self) -> ArkoorPackageCosignResponse {
let responses = self.builders.iter()
.map(|package| package.cosign_response())
.collect::<Vec<_>>();
ArkoorPackageCosignResponse { responses }
}
}
impl<S: state::BuilderState> ArkoorPackageBuilder<S> {
pub fn input_ids<'a>(&'a self) -> impl Iterator<Item = VtxoId> + Clone + 'a {
self.builders.iter().map(|b| b.input().id())
}
pub fn build_unsigned_vtxos<'a>(&'a self) -> impl Iterator<Item = Vtxo> + 'a {
self.builders.iter()
.map(|b| b.build_unsigned_vtxos())
.flatten()
}
pub fn build_unsigned_internal_vtxos<'a>(&'a self) -> impl Iterator<Item = ServerVtxo> + 'a {
self.builders.iter()
.map(|b| b.build_unsigned_internal_vtxos())
.flatten()
}
pub fn spend_info<'a>(&'a self) -> impl Iterator<Item = (VtxoId, Txid)> + 'a {
self.builders.iter()
.map(|b| b.spend_info())
.flatten()
}
pub fn virtual_transactions<'a>(&'a self) -> impl Iterator<Item = Txid> + 'a {
self.builders.iter()
.flat_map(|b| b.virtual_transactions())
}
}
#[cfg(test)]
mod test {
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use bitcoin::{Transaction, Txid};
use bitcoin::secp256k1::Keypair;
use super::*;
use crate::test_util::dummy::DummyTestVtxoSpec;
use crate::PublicKey;
fn server_keypair() -> Keypair {
Keypair::from_str("f7a2a5d150afb575e98fff9caeebf6fbebbaeacfdfa7433307b208b39f1155f2").expect("Invalid key")
}
fn alice_keypair() -> Keypair {
Keypair::from_str("9b4382c8985f12e4bd8d1b51e63615bf0187843630829f4c5e9c45ef2cf994a4").expect("Invalid key")
}
fn bob_keypair() -> Keypair {
Keypair::from_str("c86435ba7e30d7afd7c5df9f3263ce2eb86b3ff9866a16ccd22a0260496ddf0f").expect("Invalid key")
}
fn alice_public_key() -> PublicKey {
alice_keypair().public_key()
}
fn bob_public_key() -> PublicKey {
bob_keypair().public_key()
}
fn dummy_vtxo_for_amount(amt: Amount) -> (Transaction, Vtxo) {
DummyTestVtxoSpec {
amount: amt,
expiry_height: 1000,
exit_delta: 128,
user_keypair: alice_keypair(),
server_keypair: server_keypair()
}.build()
}
fn verify_package_builder(
builder: ArkoorPackageBuilder<state::Initial>,
keypairs: &[Keypair],
funding_tx_map: HashMap<Txid, Transaction>,
) {
let vtxs: Vec<Txid> = builder.virtual_transactions().collect();
let vtx_set: HashSet<Txid> = vtxs.iter().copied().collect();
let spend_txids: HashSet<Txid> = builder.spend_info().map(|(_, txid)| txid).collect();
assert_eq!(vtxs.len(), vtx_set.len(), "virtual_transactions() contains duplicates");
for txid in &vtx_set {
assert!(spend_txids.contains(txid), "virtual_transaction {} not in spend_info", txid);
}
for txid in &spend_txids {
assert!(vtx_set.contains(txid), "spend_info txid {} not in virtual_transactions", txid);
}
let user_builder = builder.generate_user_nonces(keypairs).expect("Valid nb of keypairs");
let cosign_requests = user_builder.cosign_request();
let cosign_responses = ArkoorPackageBuilder::from_cosign_request(cosign_requests)
.expect("Invalid cosign requests")
.server_cosign(&server_keypair())
.expect("Wrong server key")
.cosign_response();
let vtxos = user_builder.user_cosign(keypairs, cosign_responses)
.expect("Invalid cosign responses")
.build_signed_vtxos();
for vtxo in vtxos {
let funding_txid = vtxo.chain_anchor().txid;
let funding_tx = funding_tx_map.get(&funding_txid).expect("Funding tx not found");
vtxo.validate(&funding_tx).expect("Invalid vtxo");
let mut prev_tx = funding_tx.clone();
for tx in vtxo.transactions().map(|item| item.tx) {
crate::test_util::verify_tx(
&[prev_tx.output[vtxo.chain_anchor().vout as usize].clone()],
0,
&tx).expect("Invalid transaction");
prev_tx = tx;
}
}
}
#[test]
fn send_full_vtxo() {
let (funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(100_000));
let package_builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
[alice_vtxo],
ArkoorDestination {
total_amount: Amount::from_sat(100_000),
policy: VtxoPolicy::new_pubkey(bob_public_key()),
},
VtxoPolicy::new_pubkey(alice_public_key())
).expect("Valid package");
let funding_map = HashMap::from([(funding_tx.compute_txid(), funding_tx)]);
verify_package_builder(package_builder, &[alice_keypair()], funding_map);
}
#[test]
fn arkoor_dust_change() {
let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
let package_builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
[alice_vtxo],
ArkoorDestination {
total_amount: Amount::from_sat(900),
policy: VtxoPolicy::new_pubkey(bob_public_key()),
},
VtxoPolicy::new_pubkey(alice_public_key())
).expect("Valid package");
let vtxos: Vec<Vtxo> = package_builder.build_unsigned_vtxos().collect();
assert_eq!(vtxos.len(), 3);
assert_eq!(vtxos[0].amount(), Amount::from_sat(670));
assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
assert_eq!(vtxos[1].amount(), Amount::from_sat(230));
assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
assert_eq!(vtxos[2].amount(), Amount::from_sat(100));
assert_eq!(vtxos[2].policy().user_pubkey(), alice_public_key());
}
#[test]
fn can_send_multiple_inputs() {
let (funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
let (funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
let (funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
[alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
ArkoorDestination {
total_amount: Amount::from_sat(17_000),
policy: VtxoPolicy::new_pubkey(bob_public_key()),
},
VtxoPolicy::new_pubkey(alice_public_key())
).expect("Valid package");
let vtxos: Vec<Vtxo> = package.build_unsigned_vtxos().collect();
assert_eq!(vtxos.len(), 3);
assert_eq!(vtxos[0].amount(), Amount::from_sat(10_000));
assert_eq!(vtxos[1].amount(), Amount::from_sat(5_000));
assert_eq!(vtxos[2].amount(), Amount::from_sat(2_000));
assert_eq!(
vtxos.iter().map(|v| v.policy().user_pubkey()).collect::<Vec<_>>(),
vec![bob_public_key(); 3],
);
let funding_map = HashMap::from([
(funding_tx_1.compute_txid(), funding_tx_1),
(funding_tx_2.compute_txid(), funding_tx_2),
(funding_tx_3.compute_txid(), funding_tx_3),
]);
verify_package_builder(
package, &[alice_keypair(), alice_keypair(), alice_keypair()], funding_map,
);
}
#[test]
fn can_send_multiple_inputs_with_change() {
let (funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
let (funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
let (funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
[alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
ArkoorDestination {
total_amount: Amount::from_sat(16_000),
policy: VtxoPolicy::new_pubkey(bob_public_key()),
},
VtxoPolicy::new_pubkey(alice_public_key())
).expect("Valid package");
let vtxos: Vec<Vtxo> = package.build_unsigned_vtxos().collect();
assert_eq!(vtxos.len(), 4);
assert_eq!(vtxos[0].amount(), Amount::from_sat(10_000));
assert_eq!(vtxos[1].amount(), Amount::from_sat(5_000));
assert_eq!(vtxos[2].amount(), Amount::from_sat(1_000));
assert_eq!(vtxos[3].amount(), Amount::from_sat(1_000),
"Alice should receive a 1000 sats as change",
);
assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
assert_eq!(vtxos[2].policy().user_pubkey(), bob_public_key());
assert_eq!(vtxos[3].policy().user_pubkey(), alice_public_key());
let funding_map = HashMap::from([
(funding_tx_1.compute_txid(), funding_tx_1),
(funding_tx_2.compute_txid(), funding_tx_2),
(funding_tx_3.compute_txid(), funding_tx_3),
]);
verify_package_builder(
package, &[alice_keypair(), alice_keypair(), alice_keypair()], funding_map,
);
}
#[test]
fn can_send_multiple_vtxos_with_dust_change() {
let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(1_000));
let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
[alice_vtxo_1, alice_vtxo_2],
ArkoorDestination {
total_amount: Amount::from_sat(5_700),
policy: VtxoPolicy::new_pubkey(bob_public_key()),
},
VtxoPolicy::new_pubkey(alice_public_key())
).expect("Valid package");
let vtxos: Vec<Vtxo> = package.build_unsigned_vtxos().collect();
assert_eq!(vtxos.len(), 4);
assert_eq!(vtxos[0].amount(), Amount::from_sat(5_000));
assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
assert_eq!(vtxos[1].amount(), Amount::from_sat(670));
assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
assert_eq!(vtxos[2].amount(), Amount::from_sat(30));
assert_eq!(vtxos[2].policy().user_pubkey(), bob_public_key());
assert_eq!(vtxos[3].amount(), Amount::from_sat(300));
assert_eq!(vtxos[3].policy().user_pubkey(), alice_public_key());
}
#[test]
fn not_enough_money() {
let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(900));
let result = ArkoorPackageBuilder::new_single_output_with_checkpoints(
[alice_vtxo],
ArkoorDestination {
total_amount: Amount::from_sat(1000),
policy: VtxoPolicy::new_pubkey(bob_public_key()),
},
VtxoPolicy::new_pubkey(alice_public_key())
);
match result {
Ok(_) => panic!("Package should be invalid"),
Err(ArkoorConstructionError::Unbalanced { input, output }) => {
assert_eq!(input, Amount::from_sat(900));
assert_eq!(output, Amount::from_sat(1000));
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
#[test]
fn not_enough_money_with_multiple_inputs() {
let (_funding_tx, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
let (_funding_tx, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
let (_funding_tx, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
[alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
ArkoorDestination {
total_amount: Amount::from_sat(20_000),
policy: VtxoPolicy::new_pubkey(bob_public_key()),
},
VtxoPolicy::new_pubkey(alice_public_key())
);
match package {
Ok(_) => panic!("Package should be invalid"),
Err(ArkoorConstructionError::Unbalanced { input, output }) => {
assert_eq!(input, Amount::from_sat(17_000));
assert_eq!(output, Amount::from_sat(20_000));
}
Err(e) => panic!("Unexpected error: {:?}", e)
}
}
#[test]
fn can_use_all_provided_inputs_with_change() {
let (_funding_tx, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(1000));
let (_funding_tx, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(1000));
let (_funding_tx, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(1000));
let (_funding_tx, alice_vtxo_4) = dummy_vtxo_for_amount(Amount::from_sat(1000));
let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
[alice_vtxo_1, alice_vtxo_2, alice_vtxo_3, alice_vtxo_4],
ArkoorDestination {
total_amount: Amount::from_sat(2000),
policy: VtxoPolicy::new_pubkey(bob_public_key()),
},
VtxoPolicy::new_pubkey(alice_public_key())
).expect("Package should be valid");
let vtxos = package.build_unsigned_vtxos().collect::<Vec<_>>();
let total_output = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
assert_eq!(total_output, Amount::from_sat(4000));
}
#[test]
fn single_input_multiple_outputs() {
let (funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
let outputs = vec![
ArkoorDestination {
total_amount: Amount::from_sat(4_000),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(3_000),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(3_000),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
];
let package = ArkoorPackageBuilder::new_with_checkpoints(
[alice_vtxo.clone()],
outputs,
).expect("Valid package");
let vtxos: Vec<Vtxo> = package.build_unsigned_vtxos().collect();
assert_eq!(vtxos.len(), 3);
assert_eq!(vtxos[0].amount(), Amount::from_sat(4_000));
assert_eq!(vtxos[1].amount(), Amount::from_sat(3_000));
assert_eq!(vtxos[2].amount(), Amount::from_sat(3_000));
let user_keypair = alice_keypair();
let user_builder = package.generate_user_nonces(&[user_keypair])
.expect("Valid nb of keypairs");
let cosign_requests = user_builder.cosign_request();
let cosign_responses = ArkoorPackageBuilder::from_cosign_request(cosign_requests)
.expect("Invalid cosign requests")
.server_cosign(&server_keypair())
.expect("Wrong server key")
.cosign_response();
let signed_vtxos = user_builder.user_cosign(&[user_keypair], cosign_responses)
.expect("Invalid cosign responses")
.build_signed_vtxos();
assert_eq!(signed_vtxos.len(), 3, "Should create 3 signed vtxos");
signed_vtxos[0].validate(&funding_tx).expect("First vtxo should be valid");
}
#[test]
fn output_split_across_inputs() {
let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(600));
let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(500));
let outputs = vec![
ArkoorDestination {
total_amount: Amount::from_sat(800),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(300),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
];
let package = ArkoorPackageBuilder::new_with_checkpoints(
[alice_vtxo_1, alice_vtxo_2],
outputs,
).expect("Valid package");
let vtxos: Vec<Vtxo> = package.build_unsigned_vtxos().collect();
assert_eq!(vtxos.len(), 3);
assert_eq!(vtxos[0].amount(), Amount::from_sat(600));
assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
assert_eq!(vtxos[1].amount(), Amount::from_sat(200));
assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
assert_eq!(vtxos[2].amount(), Amount::from_sat(300));
assert_eq!(vtxos[2].policy().user_pubkey(), bob_public_key());
}
#[test]
fn dust_splits_allowed() {
let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(500));
let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(500));
let outputs = vec![
ArkoorDestination {
total_amount: Amount::from_sat(750),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(250),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
];
let package = ArkoorPackageBuilder::new_with_checkpoints(
[alice_vtxo_1, alice_vtxo_2],
outputs,
).expect("Valid package");
let vtxos: Vec<Vtxo> = package.build_unsigned_vtxos().collect();
assert_eq!(vtxos.len(), 3);
assert_eq!(vtxos[0].amount(), Amount::from_sat(500));
assert_eq!(vtxos[1].amount(), Amount::from_sat(250)); assert_eq!(vtxos[2].amount(), Amount::from_sat(250));
}
#[test]
fn unbalanced_amounts_rejected() {
let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
let outputs = vec![
ArkoorDestination {
total_amount: Amount::from_sat(600),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(600),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
];
let result = ArkoorPackageBuilder::new_with_checkpoints(
[alice_vtxo],
outputs,
);
match result {
Err(ArkoorConstructionError::Unbalanced { input, output }) => {
assert_eq!(input, Amount::from_sat(1000));
assert_eq!(output, Amount::from_sat(1200));
}
_ => panic!("Expected Unbalanced error"),
}
}
#[test]
fn empty_outputs_rejected() {
let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
let result = ArkoorPackageBuilder::new_with_checkpoints(
[alice_vtxo],
vec![],
);
match result {
Err(ArkoorConstructionError::NoOutputs) => {}
Err(e) => panic!("Expected NoOutputs error, got: {:?}", e),
Ok(_) => panic!("Expected NoOutputs error, got Ok"),
}
}
#[test]
fn multiple_inputs_multiple_outputs_exact_balance() {
let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(1000));
let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(2000));
let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(1500));
let outputs = vec![
ArkoorDestination {
total_amount: Amount::from_sat(2500),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(2000),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
];
let package = ArkoorPackageBuilder::new_with_checkpoints(
[alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
outputs,
).expect("Valid package");
let vtxos: Vec<Vtxo> = package.build_unsigned_vtxos().collect();
assert_eq!(vtxos.len(), 4);
assert_eq!(vtxos[0].amount(), Amount::from_sat(1000));
assert_eq!(vtxos[1].amount(), Amount::from_sat(1500));
assert_eq!(vtxos[2].amount(), Amount::from_sat(500));
assert_eq!(vtxos[3].amount(), Amount::from_sat(1500));
}
#[test]
fn single_output_across_many_inputs() {
let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(100));
let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(100));
let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(100));
let (_funding_tx_4, alice_vtxo_4) = dummy_vtxo_for_amount(Amount::from_sat(100));
let outputs = vec![
ArkoorDestination {
total_amount: Amount::from_sat(400),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
];
let package = ArkoorPackageBuilder::new_with_checkpoints(
[alice_vtxo_1, alice_vtxo_2, alice_vtxo_3, alice_vtxo_4],
outputs,
).expect("Valid package");
let vtxos: Vec<Vtxo> = package.build_unsigned_vtxos().collect();
assert_eq!(vtxos.len(), 4);
assert_eq!(vtxos[0].amount(), Amount::from_sat(100));
assert_eq!(vtxos[1].amount(), Amount::from_sat(100));
assert_eq!(vtxos[2].amount(), Amount::from_sat(100));
assert_eq!(vtxos[3].amount(), Amount::from_sat(100));
let total: Amount = vtxos.iter().map(|v| v.amount()).sum();
assert_eq!(total, Amount::from_sat(400));
}
#[test]
fn many_outputs_from_single_input() {
let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
let outputs = vec![
ArkoorDestination {
total_amount: Amount::from_sat(100),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(200),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(150),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(250),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(300),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
];
let package = ArkoorPackageBuilder::new_with_checkpoints(
[alice_vtxo],
outputs,
).expect("Valid package");
let vtxos: Vec<Vtxo> = package.build_unsigned_vtxos().collect();
assert_eq!(vtxos.len(), 5);
assert_eq!(vtxos[0].amount(), Amount::from_sat(100));
assert_eq!(vtxos[1].amount(), Amount::from_sat(200));
assert_eq!(vtxos[2].amount(), Amount::from_sat(150));
assert_eq!(vtxos[3].amount(), Amount::from_sat(250));
assert_eq!(vtxos[4].amount(), Amount::from_sat(300));
}
#[test]
fn first_input_exactly_matches_first_output() {
let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(1000));
let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(500));
let outputs = vec![
ArkoorDestination {
total_amount: Amount::from_sat(1000),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(500),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
];
let package = ArkoorPackageBuilder::new_with_checkpoints(
[alice_vtxo_1, alice_vtxo_2],
outputs,
).expect("Valid package");
let vtxos: Vec<Vtxo> = package.build_unsigned_vtxos().collect();
assert_eq!(vtxos.len(), 2);
assert_eq!(vtxos[0].amount(), Amount::from_sat(1000));
assert_eq!(vtxos[1].amount(), Amount::from_sat(500));
}
#[test]
fn empty_inputs_rejected() {
let outputs = vec![
ArkoorDestination {
total_amount: Amount::from_sat(1000),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
];
let result = ArkoorPackageBuilder::new_with_checkpoints(
Vec::<Vtxo>::new(),
outputs,
);
match result {
Ok(_) => panic!("Should reject empty inputs"),
Err(ArkoorConstructionError::Unbalanced { input, output }) => {
assert_eq!(input, Amount::ZERO);
assert_eq!(output, Amount::from_sat(1000));
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
#[test]
fn alternating_split_pattern() {
let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(300));
let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(700));
let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(500));
let outputs = vec![
ArkoorDestination {
total_amount: Amount::from_sat(500),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(400),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
ArkoorDestination {
total_amount: Amount::from_sat(600),
policy: VtxoPolicy::new_pubkey(bob_public_key())
},
];
let package = ArkoorPackageBuilder::new_with_checkpoints(
[alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
outputs,
).expect("Valid package");
let vtxos: Vec<Vtxo> = package.build_unsigned_vtxos().collect();
assert_eq!(vtxos.len(), 5);
assert_eq!(vtxos[0].amount(), Amount::from_sat(300));
assert_eq!(vtxos[1].amount(), Amount::from_sat(200));
assert_eq!(vtxos[2].amount(), Amount::from_sat(400));
assert_eq!(vtxos[3].amount(), Amount::from_sat(100));
assert_eq!(vtxos[4].amount(), Amount::from_sat(500));
let total: Amount = vtxos.iter().map(|v| v.amount()).sum();
assert_eq!(total, Amount::from_sat(1500));
}
#[test]
fn spend_info_correctness_simple_checkpoint() {
let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(
Amount::from_sat(100_000)
);
let input_id = alice_vtxo.id();
let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
[alice_vtxo],
ArkoorDestination {
total_amount: Amount::from_sat(100_000),
policy: VtxoPolicy::new_pubkey(bob_public_key()),
},
VtxoPolicy::new_pubkey(alice_public_key())
).expect("Valid package");
let internal_vtxos: Vec<VtxoId> = package
.build_unsigned_internal_vtxos()
.map(|v| v.id())
.collect();
let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
let mut expected_vtxo_ids = vec![input_id];
expected_vtxo_ids.extend(internal_vtxos.iter());
let actual_vtxo_ids: Vec<VtxoId> = spend_info
.iter()
.map(|(id, _)| *id)
.collect();
for id in &expected_vtxo_ids {
assert!(
actual_vtxo_ids.contains(id),
"Expected VTXO ID {} not found in spend_info",
id
);
}
assert_eq!(
actual_vtxo_ids.len(),
expected_vtxo_ids.len(),
"spend_info contains unexpected entries"
);
}
#[test]
fn spend_info_correctness_with_dust_isolation() {
let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(
Amount::from_sat(1000)
);
let input_id = alice_vtxo.id();
let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
[alice_vtxo],
ArkoorDestination {
total_amount: Amount::from_sat(900),
policy: VtxoPolicy::new_pubkey(bob_public_key()),
},
VtxoPolicy::new_pubkey(alice_public_key())
).expect("Valid package");
let internal_vtxos: Vec<VtxoId> = package
.build_unsigned_internal_vtxos()
.map(|v| v.id())
.collect();
let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
let mut expected_vtxo_ids = vec![input_id];
expected_vtxo_ids.extend(internal_vtxos.iter());
let actual_vtxo_ids: Vec<VtxoId> = spend_info
.iter()
.map(|(id, _)| *id)
.collect();
for id in &expected_vtxo_ids {
assert!(
actual_vtxo_ids.contains(id),
"Expected VTXO ID {} not found in spend_info",
id
);
}
assert_eq!(
actual_vtxo_ids.len(),
expected_vtxo_ids.len(),
"spend_info contains unexpected entries"
);
}
#[test]
fn spend_info_correctness_without_checkpoints() {
let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(
Amount::from_sat(100_000)
);
let input_id = alice_vtxo.id();
let package = ArkoorPackageBuilder::new_without_checkpoints(
[alice_vtxo],
vec![
ArkoorDestination {
total_amount: Amount::from_sat(100_000),
policy: VtxoPolicy::new_pubkey(bob_public_key()),
}
]
).expect("Valid package");
let internal_vtxos: Vec<VtxoId> = package
.build_unsigned_internal_vtxos()
.map(|v| v.id())
.collect();
let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
let mut expected_vtxo_ids = vec![input_id];
expected_vtxo_ids.extend(internal_vtxos.iter());
let actual_vtxo_ids: Vec<VtxoId> = spend_info
.iter()
.map(|(id, _)| *id)
.collect();
for id in &expected_vtxo_ids {
assert!(
actual_vtxo_ids.contains(id),
"Expected VTXO ID {} not found in spend_info",
id
);
}
assert_eq!(
actual_vtxo_ids.len(),
expected_vtxo_ids.len(),
"spend_info contains unexpected entries"
);
}
#[test]
fn spend_info_correctness_multiple_inputs() {
let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(
Amount::from_sat(10_000)
);
let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(
Amount::from_sat(5_000)
);
let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(
Amount::from_sat(2_000)
);
let input_ids = vec![
alice_vtxo_1.id(),
alice_vtxo_2.id(),
alice_vtxo_3.id(),
];
let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
[alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
ArkoorDestination {
total_amount: Amount::from_sat(16_000),
policy: VtxoPolicy::new_pubkey(bob_public_key()),
},
VtxoPolicy::new_pubkey(alice_public_key())
).expect("Valid package");
let internal_vtxos: Vec<VtxoId> = package
.build_unsigned_internal_vtxos()
.map(|v| v.id())
.collect();
let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
let mut expected_vtxo_ids = input_ids.clone();
expected_vtxo_ids.extend(internal_vtxos.iter());
let actual_vtxo_ids: Vec<VtxoId> = spend_info
.iter()
.map(|(id, _)| *id)
.collect();
for id in &expected_vtxo_ids {
assert!(
actual_vtxo_ids.contains(id),
"Expected VTXO ID {} not found in spend_info",
id
);
}
assert_eq!(
actual_vtxo_ids.len(),
expected_vtxo_ids.len(),
"spend_info contains unexpected entries"
);
}
}