use super::{
utxo_collection::*,
utxo_error::{UtxoAlgebraError, UtxoResult},
};
use crate::tx::{TransactionOutpoint, UtxoEntry, VerifiableTransaction};
use kaspa_utils::mem_size::MemSizeEstimator;
use serde::{Deserialize, Serialize};
use std::collections::hash_map::Entry::Vacant;
pub trait ImmutableUtxoDiff {
fn added(&self) -> &UtxoCollection;
fn removed(&self) -> &UtxoCollection;
}
#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct UtxoDiff {
pub add: UtxoCollection,
pub remove: UtxoCollection,
}
impl MemSizeEstimator for UtxoDiff {
fn estimate_mem_bytes(&self) -> usize {
size_of::<Self>() + (self.add.len() + self.remove.len()) * (size_of::<TransactionOutpoint>() + size_of::<UtxoEntry>())
}
}
impl<T: ImmutableUtxoDiff> ImmutableUtxoDiff for &T {
fn added(&self) -> &UtxoCollection {
(*self).added()
}
fn removed(&self) -> &UtxoCollection {
(*self).removed()
}
}
impl ImmutableUtxoDiff for UtxoDiff {
fn added(&self) -> &UtxoCollection {
&self.add
}
fn removed(&self) -> &UtxoCollection {
&self.remove
}
}
pub struct ReversedUtxoDiff<'a> {
inner: &'a UtxoDiff,
}
impl<'a> ReversedUtxoDiff<'a> {
pub fn new(inner: &'a UtxoDiff) -> Self {
Self { inner }
}
}
impl ImmutableUtxoDiff for ReversedUtxoDiff<'_> {
fn added(&self) -> &UtxoCollection {
&self.inner.remove }
fn removed(&self) -> &UtxoCollection {
&self.inner.add }
}
impl UtxoDiff {
pub fn new(add: UtxoCollection, remove: UtxoCollection) -> Self {
Self { add, remove }
}
pub fn as_reversed(&self) -> impl ImmutableUtxoDiff + '_ {
ReversedUtxoDiff::new(self)
}
pub fn to_reversed(self) -> Self {
Self::new(self.remove, self.add)
}
pub fn with_diff(&self, other: &impl ImmutableUtxoDiff) -> UtxoResult<UtxoDiff> {
let mut clone = self.clone();
clone.with_diff_in_place(other)?;
Ok(clone)
}
pub fn with_diff_in_place(&mut self, other: &impl ImmutableUtxoDiff) -> UtxoResult<()> {
if let Some(offending_outpoint) =
other.removed().intersects_with_rule(&self.remove, |outpoint, entry_to_add, _existing_entry| {
!self.add.contains_with_daa_score(outpoint, entry_to_add.block_daa_score)
})
{
return Err(UtxoAlgebraError::DuplicateRemovePoint(offending_outpoint));
}
if let Some(offending_outpoint) = other.added().intersects_with_rule(&self.add, |outpoint, _entry_to_add, existing_entry| {
!other.removed().contains_with_daa_score(outpoint, existing_entry.block_daa_score)
}) {
return Err(UtxoAlgebraError::DuplicateAddPoint(offending_outpoint));
}
let mut intersection = UtxoCollection::new();
intersection_with_remainder_having_daa_score_in_place(other.removed(), &self.add, &mut intersection, &mut self.remove);
self.add.remove_collection(&intersection);
intersection.clear();
intersection_with_remainder_having_daa_score_in_place(other.added(), &self.remove, &mut intersection, &mut self.add);
self.remove.remove_collection(&intersection);
Ok(())
}
pub fn diff_from(&self, other: &impl ImmutableUtxoDiff) -> UtxoResult<UtxoDiff> {
let rule_not_added_output_removed_with_daa_score =
|outpoint: &TransactionOutpoint, this_entry: &UtxoEntry, other_entry: &UtxoEntry| {
!(other_entry.block_daa_score != this_entry.block_daa_score
&& (self.add.contains_with_daa_score(outpoint, other_entry.block_daa_score)
|| other.removed().contains_with_daa_score(outpoint, this_entry.block_daa_score)))
};
if let Some(offending_outpoint) = self.remove.intersects_with_rule(other.added(), rule_not_added_output_removed_with_daa_score)
{
return Err(UtxoAlgebraError::DiffIntersectionPoint(offending_outpoint, "both in self.add and in other.remove"));
}
let rule_not_removed_output_added_with_daa_score =
|outpoint: &TransactionOutpoint, this_entry: &UtxoEntry, other_entry: &UtxoEntry| {
!(other_entry.block_daa_score != this_entry.block_daa_score
&& (self.remove.contains_with_daa_score(outpoint, other_entry.block_daa_score)
|| other.added().contains_with_daa_score(outpoint, this_entry.block_daa_score)))
};
if let Some(offending_outpoint) = self.add.intersects_with_rule(other.removed(), rule_not_removed_output_added_with_daa_score)
{
return Err(UtxoAlgebraError::DiffIntersectionPoint(offending_outpoint, "both in self.remove and in other.add"));
}
if let Some(offending_outpoint) = self.remove.intersects_with_rule(other.removed(), |_outpoint, this_entry, other_entry| {
other_entry.block_daa_score != this_entry.block_daa_score
}) {
return Err(UtxoAlgebraError::DiffIntersectionPoint(
offending_outpoint,
"both in self.remove and other.remove with different DAA scores, with no corresponding entry in self.add",
));
}
let mut result = UtxoDiff::default();
let mut in_both_to_add = UtxoCollection::new();
subtraction_with_remainder_having_daa_score_in_place(&self.add, other.added(), &mut result.remove, &mut in_both_to_add);
if in_both_to_add.intersects(&self.remove) != in_both_to_add.intersects(other.removed()) {
return Err(UtxoAlgebraError::General(
"diff_from: outpoint both in self.add, other.add, and only one of self.remove and other.remove",
));
}
subtraction_having_daa_score_in_place(other.removed(), &self.remove, &mut result.remove);
subtraction_having_daa_score_in_place(&self.remove, other.removed(), &mut result.add);
subtraction_having_daa_score_in_place(other.added(), &self.add, &mut result.add);
Ok(result)
}
pub fn add_transaction(&mut self, transaction: &impl VerifiableTransaction, block_daa_score: u64) -> UtxoResult<()> {
for (input, entry) in transaction.populated_inputs() {
self.remove_entry(&input.previous_outpoint, entry)?;
}
let is_coinbase = transaction.is_coinbase();
let tx_id = transaction.id();
for (i, output) in transaction.outputs().iter().enumerate() {
let outpoint = TransactionOutpoint::new(tx_id, i as u32);
let entry = UtxoEntry::new(output.value, output.script_public_key.clone(), block_daa_score, is_coinbase);
self.add_entry(outpoint, entry)?;
}
Ok(())
}
fn remove_entry(&mut self, outpoint: &TransactionOutpoint, entry: &UtxoEntry) -> UtxoResult<()> {
if self.add.contains_with_daa_score(outpoint, entry.block_daa_score) {
self.add.remove(outpoint);
} else if let Vacant(e) = self.remove.entry(*outpoint) {
e.insert(entry.clone());
} else {
return Err(UtxoAlgebraError::DoubleRemoveCall(*outpoint));
}
Ok(())
}
fn add_entry(&mut self, outpoint: TransactionOutpoint, entry: UtxoEntry) -> UtxoResult<()> {
if self.remove.contains_with_daa_score(&outpoint, entry.block_daa_score) {
self.remove.remove(&outpoint);
} else if let Vacant(e) = self.add.entry(outpoint) {
e.insert(entry);
} else {
return Err(UtxoAlgebraError::DoubleAddCall(outpoint));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tx::{ScriptPublicKey, TransactionId};
use std::str::FromStr;
#[test]
fn test_utxo_diff_rules() {
let tx_id0 = TransactionId::from_str("0".repeat(64).as_str()).unwrap();
let outpoint0 = TransactionOutpoint::new(tx_id0, 0);
let utxo_entry1 = UtxoEntry::new(10, ScriptPublicKey::default(), 0, true);
let utxo_entry2 = UtxoEntry::new(20, ScriptPublicKey::default(), 1, true);
struct Test {
name: &'static str,
this: UtxoDiff,
other: UtxoDiff,
expected_diff_from_result: UtxoResult<UtxoDiff>,
expected_with_diff_result: UtxoResult<UtxoDiff>,
}
struct UtxoDiffBuilder {
diff: UtxoDiff,
}
impl UtxoDiffBuilder {
fn new() -> Self {
Self { diff: UtxoDiff::default() }
}
fn insert_add_point(mut self, outpoint: TransactionOutpoint, entry: UtxoEntry) -> Self {
assert!(self.diff.add.insert(outpoint, entry).is_none());
self
}
fn insert_remove_point(mut self, outpoint: TransactionOutpoint, entry: UtxoEntry) -> Self {
assert!(self.diff.remove.insert(outpoint, entry).is_none());
self
}
fn build(self) -> UtxoDiff {
self.diff
}
}
let tests = [
Test {
name: "first add in this, first add in other",
this: UtxoDiffBuilder::new().insert_add_point(outpoint0, utxo_entry1.clone()).build(),
other: UtxoDiffBuilder::new().insert_add_point(outpoint0, utxo_entry1.clone()).build(),
expected_diff_from_result: Ok(UtxoDiffBuilder::new().build()),
expected_with_diff_result: Err(UtxoAlgebraError::DuplicateAddPoint(outpoint0)),
},
Test {
name: "first add in this, second add in other",
this: UtxoDiffBuilder::new().insert_add_point(outpoint0, utxo_entry1.clone()).build(),
other: UtxoDiffBuilder::new().insert_add_point(outpoint0, utxo_entry2.clone()).build(),
expected_diff_from_result: Ok(UtxoDiffBuilder::new()
.insert_add_point(outpoint0, utxo_entry2.clone())
.insert_remove_point(outpoint0, utxo_entry1.clone())
.build()),
expected_with_diff_result: Err(UtxoAlgebraError::DuplicateAddPoint(outpoint0)),
},
Test {
name: "first add in this, second remove in other",
this: UtxoDiffBuilder::new().insert_add_point(outpoint0, utxo_entry1.clone()).build(),
other: UtxoDiffBuilder::new().insert_remove_point(outpoint0, utxo_entry1.clone()).build(),
expected_diff_from_result: Err(UtxoAlgebraError::DiffIntersectionPoint(outpoint0, "")),
expected_with_diff_result: Ok(UtxoDiffBuilder::new().build()),
},
Test {
name: "first add in this and other, second remove in other",
this: UtxoDiffBuilder::new().insert_add_point(outpoint0, utxo_entry1.clone()).build(),
other: UtxoDiffBuilder::new()
.insert_add_point(outpoint0, utxo_entry1.clone())
.insert_remove_point(outpoint0, utxo_entry2.clone())
.build(),
expected_diff_from_result: Err(UtxoAlgebraError::General("")),
expected_with_diff_result: Err(UtxoAlgebraError::DuplicateAddPoint(outpoint0)),
},
Test {
name: "first add in this and remove in other, second add in other",
this: UtxoDiffBuilder::new().insert_add_point(outpoint0, utxo_entry1.clone()).build(),
other: UtxoDiffBuilder::new()
.insert_add_point(outpoint0, utxo_entry2.clone())
.insert_remove_point(outpoint0, utxo_entry1.clone())
.build(),
expected_diff_from_result: Err(UtxoAlgebraError::DiffIntersectionPoint(outpoint0, "")),
expected_with_diff_result: Ok(UtxoDiffBuilder::new().insert_add_point(outpoint0, utxo_entry2.clone()).build()),
},
Test {
name: "first add in this, empty other",
this: UtxoDiffBuilder::new().insert_add_point(outpoint0, utxo_entry1.clone()).build(),
other: UtxoDiffBuilder::new().build(),
expected_diff_from_result: Ok(UtxoDiffBuilder::new().insert_remove_point(outpoint0, utxo_entry1.clone()).build()),
expected_with_diff_result: Ok(UtxoDiffBuilder::new().insert_add_point(outpoint0, utxo_entry1.clone()).build()),
},
Test {
name: "first remove in this and add in other",
this: UtxoDiffBuilder::new().insert_remove_point(outpoint0, utxo_entry1.clone()).build(),
other: UtxoDiffBuilder::new().insert_add_point(outpoint0, utxo_entry1.clone()).build(),
expected_diff_from_result: Err(UtxoAlgebraError::DiffIntersectionPoint(outpoint0, "")),
expected_with_diff_result: Ok(UtxoDiffBuilder::new().build()),
},
Test {
name: "first remove in this, second add in other",
this: UtxoDiffBuilder::new().insert_remove_point(outpoint0, utxo_entry1.clone()).build(),
other: UtxoDiffBuilder::new().insert_add_point(outpoint0, utxo_entry2.clone()).build(),
expected_diff_from_result: Err(UtxoAlgebraError::DiffIntersectionPoint(outpoint0, "")),
expected_with_diff_result: Ok(UtxoDiffBuilder::new()
.insert_add_point(outpoint0, utxo_entry2.clone())
.insert_remove_point(outpoint0, utxo_entry1.clone())
.build()),
},
Test {
name: "first remove in this and other",
this: UtxoDiffBuilder::new().insert_remove_point(outpoint0, utxo_entry1.clone()).build(),
other: UtxoDiffBuilder::new().insert_remove_point(outpoint0, utxo_entry1.clone()).build(),
expected_diff_from_result: Ok(UtxoDiffBuilder::new().build()),
expected_with_diff_result: Err(UtxoAlgebraError::DuplicateRemovePoint(outpoint0)),
},
Test {
name: "first remove in this, second remove in other",
this: UtxoDiffBuilder::new().insert_remove_point(outpoint0, utxo_entry1.clone()).build(),
other: UtxoDiffBuilder::new().insert_remove_point(outpoint0, utxo_entry2.clone()).build(),
expected_diff_from_result: Err(UtxoAlgebraError::DiffIntersectionPoint(outpoint0, "")),
expected_with_diff_result: Err(UtxoAlgebraError::DuplicateRemovePoint(outpoint0)),
},
Test {
name: "first remove in this and add in other, second remove in other",
this: UtxoDiffBuilder::new().insert_remove_point(outpoint0, utxo_entry1.clone()).build(),
other: UtxoDiffBuilder::new()
.insert_add_point(outpoint0, utxo_entry1.clone())
.insert_remove_point(outpoint0, utxo_entry2.clone())
.build(),
expected_diff_from_result: Err(UtxoAlgebraError::DiffIntersectionPoint(outpoint0, "")),
expected_with_diff_result: Err(UtxoAlgebraError::DuplicateRemovePoint(outpoint0)),
},
Test {
name: "first remove in this and other, second add in other",
this: UtxoDiffBuilder::new().insert_remove_point(outpoint0, utxo_entry1.clone()).build(),
other: UtxoDiffBuilder::new()
.insert_add_point(outpoint0, utxo_entry2.clone())
.insert_remove_point(outpoint0, utxo_entry1.clone())
.build(),
expected_diff_from_result: Ok(UtxoDiffBuilder::new().insert_add_point(outpoint0, utxo_entry2.clone()).build()),
expected_with_diff_result: Err(UtxoAlgebraError::DuplicateRemovePoint(outpoint0)),
},
Test {
name: "first remove in this, empty other",
this: UtxoDiffBuilder::new().insert_remove_point(outpoint0, utxo_entry1.clone()).build(),
other: UtxoDiffBuilder::new().build(),
expected_diff_from_result: Ok(UtxoDiffBuilder::new().insert_add_point(outpoint0, utxo_entry1.clone()).build()),
expected_with_diff_result: Ok(UtxoDiffBuilder::new().insert_remove_point(outpoint0, utxo_entry1.clone()).build()),
},
Test {
name: "first add in this and other, second remove in this",
this: UtxoDiffBuilder::new()
.insert_add_point(outpoint0, utxo_entry1.clone())
.insert_remove_point(outpoint0, utxo_entry2.clone())
.build(),
other: UtxoDiffBuilder::new().insert_add_point(outpoint0, utxo_entry1.clone()).build(),
expected_diff_from_result: Err(UtxoAlgebraError::General("")),
expected_with_diff_result: Err(UtxoAlgebraError::DuplicateAddPoint(outpoint0)),
},
Test {
name: "first add in this, second remove in this and add in other",
this: UtxoDiffBuilder::new()
.insert_add_point(outpoint0, utxo_entry1.clone())
.insert_remove_point(outpoint0, utxo_entry2.clone())
.build(),
other: UtxoDiffBuilder::new().insert_add_point(outpoint0, utxo_entry2.clone()).build(),
expected_diff_from_result: Err(UtxoAlgebraError::DiffIntersectionPoint(outpoint0, "")),
expected_with_diff_result: Err(UtxoAlgebraError::DuplicateAddPoint(outpoint0)),
},
Test {
name: "first add in this and remove in other, second remove in this",
this: UtxoDiffBuilder::new()
.insert_add_point(outpoint0, utxo_entry1.clone())
.insert_remove_point(outpoint0, utxo_entry2.clone())
.build(),
other: UtxoDiffBuilder::new().insert_remove_point(outpoint0, utxo_entry1.clone()).build(),
expected_diff_from_result: Err(UtxoAlgebraError::DiffIntersectionPoint(outpoint0, "")),
expected_with_diff_result: Ok(UtxoDiffBuilder::new().insert_remove_point(outpoint0, utxo_entry2.clone()).build()),
},
Test {
name: "first add in this, second remove in this and in other",
this: UtxoDiffBuilder::new()
.insert_add_point(outpoint0, utxo_entry1.clone())
.insert_remove_point(outpoint0, utxo_entry2.clone())
.build(),
other: UtxoDiffBuilder::new().insert_remove_point(outpoint0, utxo_entry2.clone()).build(),
expected_diff_from_result: Ok(UtxoDiffBuilder::new().insert_remove_point(outpoint0, utxo_entry1.clone()).build()),
expected_with_diff_result: Err(UtxoAlgebraError::DuplicateRemovePoint(outpoint0)),
},
Test {
name: "first add and second remove in both this and other",
this: UtxoDiffBuilder::new()
.insert_add_point(outpoint0, utxo_entry1.clone())
.insert_remove_point(outpoint0, utxo_entry2.clone())
.build(),
other: UtxoDiffBuilder::new()
.insert_add_point(outpoint0, utxo_entry1.clone())
.insert_remove_point(outpoint0, utxo_entry2.clone())
.build(),
expected_diff_from_result: Ok(UtxoDiffBuilder::new().build()),
expected_with_diff_result: Err(UtxoAlgebraError::DuplicateRemovePoint(outpoint0)),
},
Test {
name: "first add in this and remove in other, second remove in this and add in other",
this: UtxoDiffBuilder::new()
.insert_add_point(outpoint0, utxo_entry1.clone())
.insert_remove_point(outpoint0, utxo_entry2.clone())
.build(),
other: UtxoDiffBuilder::new()
.insert_add_point(outpoint0, utxo_entry2.clone())
.insert_remove_point(outpoint0, utxo_entry1.clone())
.build(),
expected_diff_from_result: Err(UtxoAlgebraError::DiffIntersectionPoint(outpoint0, "")),
expected_with_diff_result: Ok(UtxoDiffBuilder::new().build()),
},
Test {
name: "first add and second remove in this, empty other",
this: UtxoDiffBuilder::new()
.insert_add_point(outpoint0, utxo_entry1.clone())
.insert_remove_point(outpoint0, utxo_entry2.clone())
.build(),
other: UtxoDiffBuilder::new().build(),
expected_diff_from_result: Ok(UtxoDiffBuilder::new()
.insert_add_point(outpoint0, utxo_entry2.clone())
.insert_remove_point(outpoint0, utxo_entry1.clone())
.build()),
expected_with_diff_result: Ok(UtxoDiffBuilder::new()
.insert_add_point(outpoint0, utxo_entry1.clone())
.insert_remove_point(outpoint0, utxo_entry2.clone())
.build()),
},
Test {
name: "empty this, first add in other",
this: UtxoDiffBuilder::new().build(),
other: UtxoDiffBuilder::new().insert_add_point(outpoint0, utxo_entry1.clone()).build(),
expected_diff_from_result: Ok(UtxoDiffBuilder::new().insert_add_point(outpoint0, utxo_entry1.clone()).build()),
expected_with_diff_result: Ok(UtxoDiffBuilder::new().insert_add_point(outpoint0, utxo_entry1.clone()).build()),
},
Test {
name: "empty this, first remove in other",
this: UtxoDiffBuilder::new().build(),
other: UtxoDiffBuilder::new().insert_remove_point(outpoint0, utxo_entry1.clone()).build(),
expected_diff_from_result: Ok(UtxoDiffBuilder::new().insert_remove_point(outpoint0, utxo_entry1.clone()).build()),
expected_with_diff_result: Ok(UtxoDiffBuilder::new().insert_remove_point(outpoint0, utxo_entry1.clone()).build()),
},
Test {
name: "empty this, first add and second remove in other",
this: UtxoDiffBuilder::new().build(),
other: UtxoDiffBuilder::new()
.insert_add_point(outpoint0, utxo_entry1.clone())
.insert_remove_point(outpoint0, utxo_entry2.clone())
.build(),
expected_diff_from_result: Ok(UtxoDiffBuilder::new()
.insert_add_point(outpoint0, utxo_entry1.clone())
.insert_remove_point(outpoint0, utxo_entry2.clone())
.build()),
expected_with_diff_result: Ok(UtxoDiffBuilder::new()
.insert_add_point(outpoint0, utxo_entry1.clone())
.insert_remove_point(outpoint0, utxo_entry2.clone())
.build()),
},
Test {
name: "empty this, empty other",
this: UtxoDiffBuilder::new().build(),
other: UtxoDiffBuilder::new().build(),
expected_diff_from_result: Ok(UtxoDiffBuilder::new().build()),
expected_with_diff_result: Ok(UtxoDiffBuilder::new().build()),
},
];
for test in tests {
let diff_from_result = test.this.diff_from(&test.other);
assert_eq!(diff_from_result, test.expected_diff_from_result, "diff_from failed for test \"{}\"", test.name);
if let Ok(diff_from_inner) = diff_from_result {
assert_eq!(
test.this.with_diff(&diff_from_inner).unwrap(),
test.other,
"reverse diff_from failed for test \"{}\"",
test.name
);
}
let with_diff_result = test.this.with_diff(&test.other);
assert_eq!(with_diff_result, test.expected_with_diff_result, "with_diff failed for test \"{}\"", test.name);
if let Ok(with_diff_inner) = with_diff_result {
assert_eq!(
test.this.diff_from(&with_diff_inner).unwrap(),
test.other,
"reverse with_diff failed for test \"{}\"",
test.name
);
}
}
drop(utxo_entry1);
drop(utxo_entry2);
}
}