use std::collections::BTreeMap;
use crate::{
DbcContent, DbcContentHash, DbcTransaction, Error, Hash, KeyCache, PublicKey, Result, Signature,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Dbc {
pub content: DbcContent,
pub transaction: DbcTransaction,
pub transaction_sigs: BTreeMap<DbcContentHash, (PublicKey, Signature)>,
}
impl Dbc {
pub fn amount(&self) -> u64 {
self.content.amount
}
pub fn name(&self) -> Hash {
self.content.hash()
}
pub fn confirm_valid(&self, key_cache: &KeyCache) -> Result<(), Error> {
for (input, (mint_key, mint_sig)) in self.transaction_sigs.iter() {
if !self.transaction.inputs.contains(input) {
return Err(Error::UnknownInput);
}
key_cache.verify(&self.transaction.hash(), &mint_key, &mint_sig)?;
}
if self.transaction.inputs.is_empty() {
Err(Error::TransactionMustHaveAnInput)
} else if self.transaction_sigs.len() < self.transaction.inputs.len() {
Err(Error::MissingSignatureForInput)
} else if self.transaction.inputs != self.content.parents {
Err(Error::DbcContentParentsDifferentFromTransactionInputs)
} else if !self.transaction.outputs.contains(&self.content.hash()) {
Err(Error::DbcContentNotPresentInTransactionOutput)
} else {
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::{BTreeSet, HashSet};
use quickcheck_macros::quickcheck;
use crate::tests::{NonZeroTinyInt, TinyInt};
use crate::{Mint, MintRequest};
fn divide(amount: u64, n_ways: u8) -> impl Iterator<Item = u64> {
(0..n_ways).into_iter().map(move |i| {
let equal_parts = amount / (n_ways as u64);
let leftover = amount % (n_ways as u64);
let odd_compensation = if (i as u64) < leftover { 1 } else { 0 };
equal_parts + odd_compensation
})
}
fn prepare_even_split(dbc: &Dbc, n_ways: u8) -> MintRequest {
let inputs: HashSet<_> = vec![dbc.clone()].into_iter().collect();
let input_hashes: BTreeSet<_> = inputs.iter().map(|in_dbc| in_dbc.name()).collect();
let outputs = divide(dbc.amount(), n_ways)
.enumerate()
.map(|(i, amount)| DbcContent::new(input_hashes.clone(), amount, i as u8))
.collect();
MintRequest { inputs, outputs }
}
#[test]
fn test_dbc_without_inputs_is_invalid() {
let input_content = DbcContent {
parents: Default::default(),
amount: 100,
output_number: 0,
};
let input_content_hashes: BTreeSet<_> = vec![input_content.hash()].into_iter().collect();
let dbc = Dbc {
content: input_content,
transaction: DbcTransaction {
inputs: Default::default(),
outputs: input_content_hashes,
},
transaction_sigs: Default::default(),
};
assert!(matches!(
dbc.confirm_valid(&KeyCache::default()),
Err(Error::TransactionMustHaveAnInput)
));
}
#[allow(clippy::too_many_arguments)]
#[quickcheck]
fn prop_dbc_validation(
n_inputs: NonZeroTinyInt, n_valid_sigs: TinyInt, n_wrong_signer_sigs: TinyInt, n_wrong_msg_sigs: TinyInt, n_extra_input_sigs: TinyInt, extra_output_amount: TinyInt, n_add_random_parents: TinyInt, n_drop_parents: TinyInt, ) {
let amount = 100;
let (mut genesis, genesis_dbc) = Mint::genesis(amount);
let mint_request = prepare_even_split(&genesis_dbc, n_inputs.coerce());
let (split_transaction, split_transaction_sigs) =
genesis.reissue(mint_request.clone()).unwrap();
assert_eq!(split_transaction, mint_request.to_transaction());
let inputs: HashSet<_> = mint_request
.outputs
.into_iter()
.map(|content| Dbc {
content,
transaction: split_transaction.clone(),
transaction_sigs: split_transaction_sigs.clone(),
})
.collect();
let input_hashes: BTreeSet<DbcContentHash> =
inputs.iter().map(|in_dbc| in_dbc.name()).collect();
let content = DbcContent::new(input_hashes.clone(), amount, 0);
let outputs = vec![content].into_iter().collect();
let mint_request = MintRequest { inputs, outputs };
let (transaction, transaction_sigs) = genesis.reissue(mint_request.clone()).unwrap();
assert_eq!(mint_request.to_transaction(), transaction);
let fuzzed_parents = input_hashes
.into_iter()
.skip(n_drop_parents.coerce()) .chain(
(0..n_add_random_parents.coerce())
.into_iter()
.map(|_| rand::random()),
)
.collect();
let fuzzed_content = DbcContent::new(
fuzzed_parents,
amount + extra_output_amount.coerce::<u64>(),
0,
);
let mut fuzzed_transaction_sigs = BTreeMap::new();
fuzzed_transaction_sigs.extend(
transaction_sigs
.iter()
.take(n_valid_sigs.coerce())
.to_owned(),
);
let mut repeating_inputs = mint_request
.inputs
.iter()
.cycle()
.skip(n_valid_sigs.coerce());
for _ in 0..n_wrong_signer_sigs.coerce() {
use crate::key_manager::{ed25519_keypair, PublicKey, Signature};
use ed25519::Signer;
if let Some(input) = repeating_inputs.next() {
let keypair = ed25519_keypair();
let transaction_sig = keypair.sign(&transaction.hash());
fuzzed_transaction_sigs.insert(
input.name(),
(PublicKey(keypair.public), Signature(transaction_sig)),
);
}
}
for _ in 0..n_wrong_msg_sigs.coerce() {
if let Some(input) = repeating_inputs.next() {
let wrong_msg_sig = genesis.key_mgr.sign(&[0u8; 32]);
fuzzed_transaction_sigs.insert(input.name(), (genesis.public_key(), wrong_msg_sig));
}
}
if let Some((_, key_sig)) = transaction_sigs.iter().next() {
for _ in 0..n_extra_input_sigs.coerce() {
fuzzed_transaction_sigs.insert(rand::random(), key_sig.to_owned());
}
}
let dbc = Dbc {
content: fuzzed_content,
transaction,
transaction_sigs: fuzzed_transaction_sigs,
};
let key_cache = KeyCache::from(vec![genesis.public_key()]);
let validation_res = dbc.confirm_valid(&key_cache);
println!("Validation Result: {:#?}", validation_res);
match validation_res {
Ok(()) => {
assert!(dbc.transaction.outputs.contains(&dbc.content.hash()));
assert!(n_inputs.coerce::<u8>() > 0);
assert!(n_valid_sigs.coerce::<u8>() >= n_inputs.coerce::<u8>());
assert_eq!(dbc.amount(), amount);
assert_eq!(n_extra_input_sigs.coerce::<u8>(), 0);
assert_eq!(n_wrong_signer_sigs.coerce::<u8>(), 0);
assert_eq!(n_wrong_msg_sigs.coerce::<u8>(), 0);
assert_eq!(extra_output_amount.coerce::<u8>(), 0);
assert_eq!(n_add_random_parents.coerce::<u8>(), 0);
assert_eq!(n_drop_parents.coerce::<u8>(), 0);
}
Err(Error::MissingSignatureForInput) => {
assert!(n_valid_sigs.coerce::<u8>() < n_inputs.coerce::<u8>());
}
Err(Error::Ed25519(_)) => {
assert!(n_wrong_msg_sigs.coerce::<u8>() > 0);
}
Err(Error::UnknownInput) => {
assert!(n_extra_input_sigs.coerce::<u8>() > 0);
assert!(
dbc.transaction_sigs
.keys()
.copied()
.collect::<BTreeSet<_>>()
!= dbc.transaction.inputs
);
}
Err(Error::UnrecognisedAuthority) => {
assert!(n_wrong_signer_sigs.coerce::<u8>() > 0);
assert!(dbc
.transaction_sigs
.values()
.any(|(k, _)| key_cache.verify_known_key(k).is_err()));
}
Err(Error::DbcContentParentsDifferentFromTransactionInputs) => {
assert!(
n_add_random_parents.coerce::<u8>() > 0 || n_drop_parents.coerce::<u8>() > 0
);
assert!(dbc.transaction.inputs != dbc.content.parents);
assert!(!dbc.transaction.outputs.contains(&dbc.content.hash()));
}
Err(Error::DbcContentNotPresentInTransactionOutput) => {
assert!(!dbc.transaction.outputs.contains(&dbc.content.hash()));
}
Err(Error::TransactionMustHaveAnInput) => {
assert_eq!(n_inputs.coerce::<u8>(), 0);
}
res => panic!("Unexpected verification result {:?}", res),
}
}
}