Skip to content

Add integer operations for locktime Height types#2181

Closed
tcharding wants to merge 1 commit intorust-bitcoin:masterfrom
tcharding:11-08-height-ops
Closed

Add integer operations for locktime Height types#2181
tcharding wants to merge 1 commit intorust-bitcoin:masterfrom
tcharding:11-08-height-ops

Conversation

@tcharding
Copy link
Copy Markdown
Member

@tcharding tcharding commented Nov 7, 2023

To improve ergonomics it would be good to support a subset of integer operations that make sense for the two locktime heights.

  • Add checked_* and saturating_* functions for add, sub, div, mul
  • Add functions to increment (add 1) and decrement (sub 1) for each Height type. Call through to the respective saturating_ function.
  • Implement ops::* traits, Add, Mul, Sub, Div for two heights as well as height and an integer (on both sides of the equation). Also the assign traits.

For operations from ops we have some additional considerations.

relative::Height

We can implement Add<u16> and since the max value of relative::Height and u16 is the same we get the same unsurprising behavior in relation to overflow and panic.

absolute::Height

For the absolute type absolute::Height::MAX is less that the inner ints max value.

As suggested here: #1639 (comment) implement ops traits by unconditionally panicing if overflow occurs.

Done as part of #1639 and also to support the calculate next target stuff.

@tcharding tcharding changed the title WIP: Add ops to absolute::Height Add integer operations for locktime Height types Nov 8, 2023
@tcharding tcharding force-pushed the 11-08-height-ops branch 2 times, most recently from 30c6022 to e5bbfd6 Compare November 8, 2023 02:57
@tcharding tcharding marked this pull request as ready for review November 8, 2023 05:22
@yancyribbens
Copy link
Copy Markdown
Contributor

I added a comment to #1639 that I think relates to this PR.

@apoelstra
Copy link
Copy Markdown
Member

apoelstra commented Nov 10, 2023

I think it makes sense to add two heights. In fact I think it makes more sense than it does to add a height and an integer.

@tcharding
Copy link
Copy Markdown
Member Author

Consider this train of thought.:

  1. What is the block height of the next block - its inc(block_height)
  2. What is inc(block_height) - its block_height + 1
  3. What is 1, its an integer not a height, what about 6 blocks in the future, again seems like 6 is an integer

At one stage I implemented both ops::Add<Height> and ops::Add<u32> but I remove the default RHS (Height + Height) one. Do you think we should have both? Or do you still think we should not have Height + int at all still?

@apoelstra
Copy link
Copy Markdown
Member

I disagree with point 3.

For convenience's sake I think we should have both, but conceptually blockheight + 1 is adding one block to a count of blocks. It's not adding some sort of platonic integer.

@tcharding
Copy link
Copy Markdown
Member Author

No sweat, I'll add both in.

@Kixunil
Copy link
Copy Markdown
Collaborator

Kixunil commented Nov 14, 2023

I think this is very similar to Instant vs Duration in std. As such I don't think it makes sense to add two absolute lock times but it does make sense to add relative locktime to absolute or add two relative lock times together.

@tcharding
Copy link
Copy Markdown
Member Author

Good points, I'll re-spin. Thanks

@tcharding
Copy link
Copy Markdown
Member Author

tcharding commented Nov 14, 2023

Won't get green CI until rebased on top of #2188 because checked_sub is only available in Rust 1.52

@apoelstra
Copy link
Copy Markdown
Member

There's no need to actually use the std saturating_div if it's identical to /. So you can get MSRV to pass by just using /.

To improve ergonomics it would be good to support a subset of integer
operations that make sense for the two locktime heights.

- Add `checked_*` and `saturating_*` functions for add, sub, div, mul
- Add functions to increment (add 1) and decrement (sub 1) for each
 `Height` type. Call through to the respective `saturating_` function.
- Implement `ops::*` traits, `Add`, `Mul`, `Sub`, `Div` (as well as
  `AddAssign` and friends).

It makes sense to add a number of blocks onto both absolute and relative
heights so we implement, for example:

- `ops::Add<u32> for Height`
- `ops::Add<Height> for u32`
- `ops::AddAssign<u32> for Height`

It also makes sense to add a `relative::Height` to an `absolute::Height`
but not two absolute heights together. so we implement:

- relative height:
    - `ops::Add for Height`
    - `ops::AddAssign for Height`

- absolute height:
   - `ops::Add<relative::Height> for Height`
   - `ops::AddAssign<relative::Height> for Height`

Same for other operations.

We can implement `Add<u16>` and since the max value of
`relative::Height` and `u16` is the same we get the same unsurprising
behavior in relation to overflow and panic.

For the `absolute` type `absolute::Height::MAX` is less that the inner
ints max value.

As suggested here:

 rust-bitcoin#1639 (comment)

implement `ops` traits by unconditionally panicing if overflow occurs.

Done as part of rust-bitcoin#1639 and also to support the calculate next target stuff.
@tcharding
Copy link
Copy Markdown
Member Author

hah, good point. Used / in absolute and relative, checked_sub and saturating_sub - along with a comment.

Copy link
Copy Markdown
Collaborator

@Kixunil Kixunil left a comment

Choose a reason for hiding this comment

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

NACK, this has several problems.


/// Decrements height by 1, saturating at `Height::MIN`.
#[inline]
pub const fn decrement(self) -> Height { self.saturating_sub(1) }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

These seem arbitrary. There's a reason Rust doesn't have the ++ operator. I suggest having a method called offset that just accepts an int.

///
/// Computes self + rhs, returning `None` if result would overflow `Height::MAX`.
#[inline]
pub const fn checked_add(self, rhs: u32) -> Option<Self> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is not the same type which is unusual and possibly a foot gun but I understand something like that could be handy. I would call it offset to distinguish it from the usual operations.

///
/// Computes self - rhs, returning `None` if result would overflow `Height::MIN`.
#[inline]
pub const fn checked_sub(self, rhs: u32) -> Option<Height> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

neg_offset or other name?

}
}

impl ops::Add<u32> for Height {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This absolutely goes against the idea of having strong newtypes. I really, really, really don't like this.

fn sub(self, rhs: Height) -> Self::Output { rhs - self }
}

impl ops::Mul<relative::Height> for Height {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is definitely wrong. What does multiplying an instant with duration even mean? Or blocks squared? Even if we wanted it it'd need HeightSquared output type.

}

impl ops::Mul for Height {
type Output = Height;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This shouldn't exists because there's no HeaightSquared (Or BlockchainArea?)

}

impl ops::Div for Height {
type Output = Height;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This could actually exist returning a unitless ratio - the tye should then be u16 though, not Height.

type Output = Height;
#[inline]
fn div(self, rhs: u16) -> Self::Output { Height(self.0 / rhs) }
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

OK.

}

impl ops::Div<Height> for u16 {
type Output = Height;
Copy link
Copy Markdown
Collaborator

@Kixunil Kixunil Nov 17, 2023

Choose a reason for hiding this comment

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

Unitless.

Edit: just noticed this should be blocks^-1, not unitless. I don't think inverse block count is used anywhere.

impl ops::DivAssign<u16> for Height {
#[inline]
fn div_assign(&mut self, rhs: u16) { self.0.div_assign(rhs) }
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

*Assigns need to be according to the above.

@tcharding
Copy link
Copy Markdown
Member Author

hmm, thanks for the review man. These ops PRs are thorny. I'll come back to this.

@tcharding tcharding marked this pull request as draft November 17, 2023 19:26
@tcharding tcharding closed this May 22, 2024
@tcharding tcharding deleted the 11-08-height-ops branch May 22, 2024 23:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants