Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(chain,wallet)!: Transitive ChainPosition #1733

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
98 changes: 67 additions & 31 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>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ I think calling the field either "child" or "descendant" would be best.

},
/// 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,9 +95,9 @@ 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 tx_height = match self.chain_position.confirmation_height_upper_bound() {
Some(conf_height_upper_bound) => conf_height_upper_bound,
None => {
debug_assert!(false, "coinbase tx can never be unconfirmed");
return false;
}
Expand Down Expand Up @@ -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 confirmation_height = match self.chain_position.confirmation_height_upper_bound() {
Some(h) => h,
None => return false,
};
if confirmation_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