Skip to content

Conversation

@evanlinjin
Copy link
Member

@evanlinjin evanlinjin commented Sep 18, 2025

Description

This PR adds the ability to calculate median-time-past (MTP) for CheckPoint structures, implementing the functionality described in #2036.

Notes to the reviewers

CheckPoint::median_time_past calculates the MTP value by looking at the previous 11 blocks (including the current block).

Changelog notice

Added

  • Introduced ToBlockTime trait for types that can return a block time.
  • Added median_time_past() method to CheckPoint for calculating MTP according to BIP113

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature

Fixes #2036

@evanlinjin evanlinjin self-assigned this Sep 18, 2025
@evanlinjin evanlinjin added the new feature New feature or request label Sep 18, 2025
@evanlinjin evanlinjin moved this to Needs Review in BDK Chain Sep 18, 2025
@evanlinjin evanlinjin added this to the Wallet 3.0.0 milestone Sep 18, 2025
Copy link
Contributor

@oleonardolima oleonardolima left a comment

Choose a reason for hiding this comment

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

cACK 0f75c22

I'll just give it a try with some other test scenarios before final ACK.

@evanlinjin evanlinjin force-pushed the feature/mtp branch 2 times, most recently from f97553f to 46e751f Compare September 19, 2025 01:00
Comment on lines 236 to 240
pub fn median_time_past(&self) -> Option<u32> {
let current_height = self.height();
let earliest_height = current_height.saturating_sub(Self::MTP_BLOCK_COUNT);
self._median_time_past(earliest_height..current_height)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm curious if these new APIs could output wrong/invalid values if the blocks inserted have a wrong/invalid time field e.g malicious block source.

Copy link
Member Author

Choose a reason for hiding this comment

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

Definitely. We are trusting the chain source here (and everywhere in BDK). Eventually, we should think about how to verify all consensus rules in BDK: difficulty, PoW, MTP, etc.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is it not inclusive of the current height?

Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: I want to avoid the temptation to resort to using these underscore methods.

Copy link
Member Author

Choose a reason for hiding this comment

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

Why is it not inclusive of the current height?

This method is asking "what's the MTP that was used to validate block 100".

However, if the caller is asking "what MTP should I use to validate a transaction going into the next block", next_median_time_past() can be used.

Copy link
Member Author

Choose a reason for hiding this comment

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

nit: I want to avoid the temptation to resort to using these underscore methods.

I renamed the private method to middle_timestamp_of_range.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is it not inclusive of the current height?

This method is asking "what's the MTP that was used to validate block 100".

However, if the caller is asking "what MTP should I use to validate a transaction going into the next block", next_median_time_past() can be used.

I imagine there would just be one function called median_time_past with a range that includes the current tip. If I wanted to know the last MTP, I would just call this function on the previous checkpoint, right?

Copy link
Member Author

@evanlinjin evanlinjin Jan 21, 2026

Choose a reason for hiding this comment

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

I was wrong about the definition of MTP. MTP of block h includes the time of block h. However, when validating whether a tx can be included into a block at height x, consensus uses MTP(x-1).

I imagine there would just be one function called median_time_past with a range that includes the current tip.

Yes. This makes sense as MTP(spending_block - 1) is essentially MTP(tip).

If I wanted to know the last MTP, I would just call this function on the previous checkpoint, right?

This is true. However, a prev-MTP helper will be useful for relative-timelocked txs as we may need to get the confirmation block hash as well as MTP(conf_block - 1).

Edit: I agree with you. Less is more.

Copy link
Contributor

@oleonardolima oleonardolima left a comment

Choose a reason for hiding this comment

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

cACK 74d30d8

I didn't really find other test vectors that we could use.

Another thing that should probably be handled in a future PR is updating the calculation of Balance for the immature category, we probably should update it to consider transactions that are not yet mature considering the Locktime and MTP for it.

Comment on lines 77 to 79
fn block_time(&self) -> Option<u32> {
None
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hm but then the trait would need to be called ToBlockHashAndHeaderTime? Maybe just have a new impl where D is convertible to a Header (granted, the time is only a subset of the information contained in a header).

impl<D> CheckPoint<D>
where
    D: Into<Header>,
{
    /// Calculates the median time past (MTP) of this `CheckPoint`.
    pub fn median_time_past(&self) -> Option<u32> { ... }
}

Copy link
Member Author

Choose a reason for hiding this comment

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

What do you think about a separate ToBlockTime trait?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes or maybe stay with 1 trait but redefine ToBlockHash to be a more general Header-like interface.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think 2 traits is nicer otherwise the to_blocktime method needs to return an Option.

Comment on lines 236 to 240
pub fn median_time_past(&self) -> Option<u32> {
let current_height = self.height();
let earliest_height = current_height.saturating_sub(Self::MTP_BLOCK_COUNT);
self._median_time_past(earliest_height..current_height)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: I want to avoid the temptation to resort to using these underscore methods.

Copy link
Member

@luisschwab luisschwab left a comment

Choose a reason for hiding this comment

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

ACK 00b0ca8

})
.collect::<Option<Vec<u32>>>()?;
timestamps.sort_unstable();
Some(timestamps[timestamps.len().checked_sub(1)? / 2])
Copy link
Member

@luisschwab luisschwab Jan 20, 2026

Choose a reason for hiding this comment

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

super nit: BIP-0113 specifies len / 2. In the case of 11 blocks, both len / 2 and (len - 1)/2 yield 5, which is fine, but for even lengths, we get two elements. The median of an even number of elements is the average of the middle elements. Here we must choose one of the values, so perhaps remove the .checked_sub(1) to keep on par with the BIP?

Copy link
Member Author

Choose a reason for hiding this comment

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

Arggggg

Add MTP calculation methods for CheckPoint following Bitcoin's BIP113:
- `median_time_past()`: calculates MTP for current block using 11 blocks
  starting from the current block
- Returns None when required blocks are missing

Includes comprehensive tests for all edge cases.
@rustaceanrob
Copy link
Contributor

FYI in units = 1.0.0 there will be a median time past struct

@evanlinjin
Copy link
Member Author

FYI in units = 1.0.0 there will be a median time past struct

Thanks! Unfortunately, I don't think we can use that here 😢

@evanlinjin
Copy link
Member Author

CI is failing no_std builds. #2099 will fix this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new feature New feature or request

Projects

Status: Needs Review

Development

Successfully merging this pull request may close these issues.

Add median-time-past (MTP) calculation to CheckPoint

5 participants