Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 69 additions & 33 deletions crates/chain/src/chain_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,45 @@ use crate::Anchor;
))
)]
pub enum ChainPosition<A> {
/// The chain data is seen as confirmed, and in anchored by `A`.
Confirmed(A),
/// The chain data is not confirmed and last seen in the mempool at this timestamp.
Unconfirmed(u64),
/// The chain data is confirmed as it is anchored in the best chain by `A`.
Confirmed {
/// The [`Anchor`].
anchor: A,
/// Whether the chain data is anchored transitively by a child transaction.
///
/// If the value is `Some`, it means we have incomplete data. We can only deduce that the
/// chain data is confirmed at a block equal to or lower than the block referenced by `A`.
transitively: Option<Txid>,
},
/// The chain data is not confirmed.
Unconfirmed {
/// When the chain data is last seen in the mempool.
///
/// This value will be `None` if the chain data was never seen in the mempool and only seen
/// in a conflicting chain.
last_seen: Option<u64>,
},
}

impl<A> ChainPosition<A> {
/// Returns whether [`ChainPosition`] is confirmed or not.
pub fn is_confirmed(&self) -> bool {
matches!(self, Self::Confirmed(_))
matches!(self, Self::Confirmed { .. })
}
}

impl<A: Clone> ChainPosition<&A> {
/// Maps a [`ChainPosition<&A>`] into a [`ChainPosition<A>`] by cloning the contents.
pub fn cloned(self) -> ChainPosition<A> {
match self {
ChainPosition::Confirmed(a) => ChainPosition::Confirmed(a.clone()),
ChainPosition::Unconfirmed(last_seen) => ChainPosition::Unconfirmed(last_seen),
ChainPosition::Confirmed {
anchor,
transitively,
} => ChainPosition::Confirmed {
anchor: anchor.clone(),
transitively,
},
ChainPosition::Unconfirmed { last_seen } => ChainPosition::Unconfirmed { last_seen },
}
}
}
Expand All @@ -42,8 +62,10 @@ impl<A: Anchor> ChainPosition<A> {
/// Determines the upper bound of the confirmation height.
pub fn confirmation_height_upper_bound(&self) -> Option<u32> {
match self {
ChainPosition::Confirmed(a) => Some(a.confirmation_height_upper_bound()),
ChainPosition::Unconfirmed(_) => None,
ChainPosition::Confirmed { anchor, .. } => {
Some(anchor.confirmation_height_upper_bound())
}
ChainPosition::Unconfirmed { .. } => None,
}
}
}
Expand Down Expand Up @@ -73,14 +95,14 @@ impl<A: Anchor> FullTxOut<A> {
/// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
pub fn is_mature(&self, tip: u32) -> bool {
if self.is_on_coinbase {
let tx_height = match &self.chain_position {
ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
ChainPosition::Unconfirmed(_) => {
let conf_height = match self.chain_position.confirmation_height_upper_bound() {
Some(height) => height,
None => {
debug_assert!(false, "coinbase tx can never be unconfirmed");
return false;
}
};
let age = tip.saturating_sub(tx_height);
let age = tip.saturating_sub(conf_height);
if age + 1 < COINBASE_MATURITY {
return false;
}
Expand All @@ -103,17 +125,21 @@ impl<A: Anchor> FullTxOut<A> {
return false;
}

let confirmation_height = match &self.chain_position {
ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
ChainPosition::Unconfirmed(_) => return false,
let conf_height = match self.chain_position.confirmation_height_upper_bound() {
Some(height) => height,
None => return false,
};
if confirmation_height > tip {
if conf_height > tip {
return false;
}

// if the spending tx is confirmed within tip height, the txout is no longer spendable
if let Some((ChainPosition::Confirmed(spending_anchor), _)) = &self.spent_by {
if spending_anchor.anchor_block().height <= tip {
if let Some(spend_height) = self
.spent_by
.as_ref()
.and_then(|(pos, _)| pos.confirmation_height_upper_bound())
{
if spend_height <= tip {
return false;
}
}
Expand All @@ -132,22 +158,32 @@ mod test {

#[test]
fn chain_position_ord() {
let unconf1 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed(10);
let unconf2 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed(20);
let conf1 = ChainPosition::Confirmed(ConfirmationBlockTime {
confirmation_time: 20,
block_id: BlockId {
height: 9,
..Default::default()
let unconf1 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
last_seen: Some(10),
};
let unconf2 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
last_seen: Some(20),
};
let conf1 = ChainPosition::Confirmed {
anchor: ConfirmationBlockTime {
confirmation_time: 20,
block_id: BlockId {
height: 9,
..Default::default()
},
},
});
let conf2 = ChainPosition::Confirmed(ConfirmationBlockTime {
confirmation_time: 15,
block_id: BlockId {
height: 12,
..Default::default()
transitively: None,
};
let conf2 = ChainPosition::Confirmed {
anchor: ConfirmationBlockTime {
confirmation_time: 15,
block_id: BlockId {
height: 12,
..Default::default()
},
},
});
transitively: None,
};

assert!(unconf2 > unconf1, "higher last_seen means higher ord");
assert!(unconf1 > conf1, "unconfirmed is higher ord than confirmed");
Expand Down
15 changes: 11 additions & 4 deletions crates/chain/src/tx_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -770,7 +770,12 @@ impl<A: Anchor> TxGraph<A> {

for anchor in anchors {
match chain.is_block_in_chain(anchor.anchor_block(), chain_tip)? {
Some(true) => return Ok(Some(ChainPosition::Confirmed(anchor))),
Some(true) => {
return Ok(Some(ChainPosition::Confirmed {
anchor,
transitively: None,
}))
}
_ => continue,
}
}
Expand Down Expand Up @@ -877,7 +882,9 @@ impl<A: Anchor> TxGraph<A> {
}
}

Ok(Some(ChainPosition::Unconfirmed(last_seen)))
Ok(Some(ChainPosition::Unconfirmed {
last_seen: Some(last_seen),
}))
}

/// Get the position of the transaction in `chain` with tip `chain_tip`.
Expand Down Expand Up @@ -1146,14 +1153,14 @@ impl<A: Anchor> TxGraph<A> {
let (spk_i, txout) = res?;

match &txout.chain_position {
ChainPosition::Confirmed(_) => {
ChainPosition::Confirmed { .. } => {
if txout.is_confirmed_and_spendable(chain_tip.height) {
confirmed += txout.txout.value;
} else if !txout.is_mature(chain_tip.height) {
immature += txout.txout.value;
}
}
ChainPosition::Unconfirmed(_) => {
ChainPosition::Unconfirmed { .. } => {
if trust_predicate(&spk_i, txout.txout.script_pubkey) {
trusted_pending += txout.txout.value;
} else {
Expand Down
17 changes: 10 additions & 7 deletions crates/chain/tests/test_indexed_tx_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ fn test_list_owned_txouts() {
let confirmed_txouts_txid = txouts
.iter()
.filter_map(|(_, full_txout)| {
if matches!(full_txout.chain_position, ChainPosition::Confirmed(_)) {
if matches!(full_txout.chain_position, ChainPosition::Confirmed { .. }) {
Some(full_txout.outpoint.txid)
} else {
None
Expand All @@ -304,7 +304,7 @@ fn test_list_owned_txouts() {
let unconfirmed_txouts_txid = txouts
.iter()
.filter_map(|(_, full_txout)| {
if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) {
if matches!(full_txout.chain_position, ChainPosition::Unconfirmed { .. }) {
Some(full_txout.outpoint.txid)
} else {
None
Expand All @@ -315,7 +315,7 @@ fn test_list_owned_txouts() {
let confirmed_utxos_txid = utxos
.iter()
.filter_map(|(_, full_txout)| {
if matches!(full_txout.chain_position, ChainPosition::Confirmed(_)) {
if matches!(full_txout.chain_position, ChainPosition::Confirmed { .. }) {
Some(full_txout.outpoint.txid)
} else {
None
Expand All @@ -326,7 +326,7 @@ fn test_list_owned_txouts() {
let unconfirmed_utxos_txid = utxos
.iter()
.filter_map(|(_, full_txout)| {
if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) {
if matches!(full_txout.chain_position, ChainPosition::Unconfirmed { .. }) {
Some(full_txout.outpoint.txid)
} else {
None
Expand Down Expand Up @@ -618,7 +618,7 @@ fn test_get_chain_position() {
},
anchor: None,
last_seen: Some(2),
exp_pos: Some(ChainPosition::Unconfirmed(2)),
exp_pos: Some(ChainPosition::Unconfirmed { last_seen: Some(2) }),
},
TestCase {
name: "tx anchor in best chain - confirmed",
Expand All @@ -631,7 +631,10 @@ fn test_get_chain_position() {
},
anchor: Some(blocks[1]),
last_seen: None,
exp_pos: Some(ChainPosition::Confirmed(blocks[1])),
exp_pos: Some(ChainPosition::Confirmed {
anchor: blocks[1],
transitively: None,
}),
},
TestCase {
name: "tx unknown anchor with last_seen - unconfirmed",
Expand All @@ -644,7 +647,7 @@ fn test_get_chain_position() {
},
anchor: Some(block_id!(2, "B'")),
last_seen: Some(2),
exp_pos: Some(ChainPosition::Unconfirmed(2)),
exp_pos: Some(ChainPosition::Unconfirmed { last_seen: Some(2) }),
},
TestCase {
name: "tx unknown anchor - no chain pos",
Expand Down
45 changes: 30 additions & 15 deletions crates/chain/tests/test_tx_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -885,13 +885,16 @@ fn test_chain_spends() {
OutPoint::new(tx_0.compute_txid(), 0)
),
Some((
ChainPosition::Confirmed(&ConfirmationBlockTime {
block_id: BlockId {
hash: tip.get(98).unwrap().hash(),
height: 98,
ChainPosition::Confirmed {
anchor: &ConfirmationBlockTime {
block_id: BlockId {
hash: tip.get(98).unwrap().hash(),
height: 98,
},
confirmation_time: 100
},
confirmation_time: 100
}),
transitively: None
},
tx_1.compute_txid(),
)),
);
Expand All @@ -900,13 +903,16 @@ fn test_chain_spends() {
assert_eq!(
graph.get_chain_position(&local_chain, tip.block_id(), tx_0.compute_txid()),
// Some(ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected"))),
Some(ChainPosition::Confirmed(&ConfirmationBlockTime {
block_id: BlockId {
hash: tip.get(95).unwrap().hash(),
height: 95,
Some(ChainPosition::Confirmed {
anchor: &ConfirmationBlockTime {
block_id: BlockId {
hash: tip.get(95).unwrap().hash(),
height: 95,
},
confirmation_time: 100
},
confirmation_time: 100
}))
transitively: None
})
);

// Mark the unconfirmed as seen and check correct ObservedAs status is returned.
Expand All @@ -921,7 +927,12 @@ fn test_chain_spends() {
OutPoint::new(tx_0.compute_txid(), 1)
)
.unwrap(),
(ChainPosition::Unconfirmed(1234567), tx_2.compute_txid())
(
ChainPosition::Unconfirmed {
last_seen: Some(1234567)
},
tx_2.compute_txid()
)
);

// A conflicting transaction that conflicts with tx_1.
Expand Down Expand Up @@ -957,7 +968,9 @@ fn test_chain_spends() {
graph
.get_chain_position(&local_chain, tip.block_id(), tx_2_conflict.compute_txid())
.expect("position expected"),
ChainPosition::Unconfirmed(1234568)
ChainPosition::Unconfirmed {
last_seen: Some(1234568)
}
);

// Chain_spend now catches the new transaction as the spend.
Expand All @@ -970,7 +983,9 @@ fn test_chain_spends() {
)
.expect("expect observation"),
(
ChainPosition::Unconfirmed(1234568),
ChainPosition::Unconfirmed {
last_seen: Some(1234568)
},
tx_2_conflict.compute_txid()
)
);
Expand Down
21 changes: 13 additions & 8 deletions crates/wallet/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,12 +229,15 @@ pub fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoi
let latest_cp = wallet.latest_checkpoint();
let height = latest_cp.height();
let anchor = if height == 0 {
ChainPosition::Unconfirmed(0)
ChainPosition::Unconfirmed { last_seen: Some(0) }
} else {
ChainPosition::Confirmed(ConfirmationBlockTime {
block_id: latest_cp.block_id(),
confirmation_time: 0,
})
ChainPosition::Confirmed {
anchor: ConfirmationBlockTime {
block_id: latest_cp.block_id(),
confirmation_time: 0,
},
transitively: None,
}
};
receive_output(wallet, value, anchor)
}
Expand Down Expand Up @@ -270,11 +273,13 @@ pub fn receive_output_to_address(
insert_tx(wallet, tx);

match pos {
ChainPosition::Confirmed(anchor) => {
ChainPosition::Confirmed { anchor, .. } => {
insert_anchor(wallet, txid, anchor);
}
ChainPosition::Unconfirmed(last_seen) => {
insert_seen_at(wallet, txid, last_seen);
ChainPosition::Unconfirmed { last_seen } => {
if let Some(last_seen) = last_seen {
insert_seen_at(wallet, txid, last_seen);
}
}
}

Expand Down
Loading
Loading