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

Expose valid (min, max) difficulty transition thresholds #1820

Merged
merged 1 commit into from May 6, 2023

Conversation

wpaulino
Copy link
Contributor

Once U256 was made private, we lost the ability to check whether a valid difficulty transition was made in the chain, since Target doesn't expose any operations. We only choose to expose Shl<u32> and Shr<u32> such that we can compute the min and max target thresholds allowed for a difficulty transition.

This is something we realized was missing after bumping to rust-bitcoin v0.30.0 in rust-lightning, specifically for our lightning-block-sync crate. It may also be worth having a helper in rust-bitcoin that checks a header properly builds upon the previous, but that can be left for future work.

tcharding
tcharding previously approved these changes Apr 29, 2023
Copy link
Member

@tcharding tcharding left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution! Sorry for rugging you :)

My review is just for code correctness, I have no knowledge on whether or not Target should expose these, the use case sounds sane to me though.

ACK 669f248

@Kixunil
Copy link
Collaborator

Kixunil commented Apr 29, 2023

Isn't block::Header::validate_pow what you need?

If not, I would still prefer having a method to do the verification rather than ad-hoc shr/shl because presumably more people will want to do this kind of verification, not just LDK. Also this shouldn't block you because you can implement it manually:

// this is shl, shr is similar
let bytes = target.to_be_bytes();
let high = u128::from_be_bytes(bytes[..16].try_into().unwrap());
let low = u128::from_be_bytes(bytes[16..].try_into().unwrap());
let transferred_bits = low >> (128 - bits_to_shift);
let high = high << bits_to_shift | transferred_bits;
let low = low << bits_to_shift;
let mut shifted_bytes = [0u8; 64];
shifted_bytes[..16].copy_from_slice(&high.to_be_bytes());
shifted_bytes[16..].copy_from_slice(&high.to_le_bytes());
Target::from_le_bytes(shifted_bytes)

I know it's not nice but at least it's not impossible and we can clean it up soon.

@apoelstra
Copy link
Member

@Kixunil I think the shift operators make sense here, since they shift "how many bits of difficulty" the target is.

I'm leaning toward ACKing this because the operators do make conceptual sense, and they don't make sense for Difficulty which is the only type I think people might get confused with, so they're not likely to cause accidents.

@apoelstra
Copy link
Member

apoelstra commented Apr 29, 2023

But yes, the specific "determine difficulty rails after an adjustment" algorithm that @wpaulino is describing is something we should have first-class support for!

Note that the difficulty adjustment is bounded by "divide by 4" below and "multiply by 4" above. So this is a nearly trivial operation even if your only operators are shifts. It would be a shame to turn 2 lines into 20.

Edit It's actually a little more subtle, I remembered, since there's a hard upper bound at difficulty 1. We probably should provide such a method (and maybe one for Difficulty too).

apoelstra
apoelstra previously approved these changes Apr 29, 2023
Copy link
Member

@apoelstra apoelstra left a comment

Choose a reason for hiding this comment

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

ACK 669f248

@Kixunil
Copy link
Collaborator

Kixunil commented Apr 30, 2023

Isn't it a bit awkward to have only shift exposed and not other operations? Also can't find it now but IIRC division was also needed to adjust difficulty.

One thing that bothers me about this is that lowering target by an amount that can not be expressed as shift is still valid operation and does increase difficulty but we don't allow it.

@apoelstra
Copy link
Member

@Kixunil for sure, computing an actual difficulty adjustment will require more than shifts. But computing the maximum and minimum adjustment for a given period (which apparently is what @wpaulino is trying to do) does not.

@wpaulino
Copy link
Contributor Author

wpaulino commented May 1, 2023

Also this shouldn't block you because you can implement it manually:

Fair enough. I figured since it was something already supported by the API previously, and it's required for a valid use-case, it would make sense to expose it again.

Isn't it a bit awkward to have only shift exposed and not other operations?

Yeah, I thought so too. I guess it depends on what plans you all have for rust-bitcoin's API. IMO, it should either expose the operations to do anything you may need to yourself, add new helper methods on Target/Difficulty covering the most common use cases, or implement both.

@TheBlueMatt
Copy link
Member

I'd also strongly prefer if rust-bitcoin had a method for this, but the context-less validate_pow isn't what we need - we want to verify that a difficulty adjustment is sane. This is a really critical operation to prevent all kinds of DoS attacks on clients which download headers, and while it would be great if we could call out to rust-bitcoin and have it tell us if a difficulty transition given a pile of headers is correct, just checking the 4x bounds at least addresses the DoS issues for the most part.

@apoelstra
Copy link
Member

I think we should expose both functions (compute min/max bounds, and compute exact adjustment). And we shouldn't expose the shifts.

@wpaulino could you adjust this PR to expose a min/max function instead? (You could also implement a full difficulty calculation if you're feeling generous, but that'd be 10x the work and it sounds like you'd be satisfied with just the min/max for now.)

@wpaulino wpaulino dismissed stale reviews from apoelstra and tcharding via 3357db8 May 1, 2023 21:56
@wpaulino wpaulino changed the title Expose Shl<u32> and Shr<u32> impl for Target Expose valid (min, max) difficulty transition thresholds May 1, 2023
@wpaulino
Copy link
Contributor Author

wpaulino commented May 1, 2023

@apoelstra only exposed the thresholds for now, but I may come back around to implement the exact adjustment computation in the future.

@tcharding
Copy link
Member

Needs cargo +nightly fmt running please mate.

tcharding
tcharding previously approved these changes May 2, 2023
Copy link
Member

@tcharding tcharding left a comment

Choose a reason for hiding this comment

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

ACK 6187983

@tcharding
Copy link
Member

Thanks man!

@apoelstra
Copy link
Member

@wpaulino the max is not correct, since the target is bounded above by the minimum difficulty being 1.

@wpaulino
Copy link
Contributor Author

wpaulino commented May 2, 2023

Whoops, thanks. Also bounded the min result, even though it shouldn't happen in practice, since you can currently get a Target above Target::MAX with Target::from_be_bytes.

Comment on lines 245 to 249
if min > Self::MAX {
Self::MAX
} else {
min
}
Copy link
Member

Choose a reason for hiding this comment

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

I'm not a fan of this, it reads funnily. If we are not upholding the invariant that Self is less than or equal to Self::MAX then we should fix that and not band aid over it here.

Copy link
Member

Choose a reason for hiding this comment

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

I raised an issue: #1828

Copy link
Collaborator

Choose a reason for hiding this comment

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

Agree.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this something we want to block this PR on, or can it come after this lands?

I also realized that MAX here is network dependent. The current value is valid for mainnet and testnet, but regtest and signet have higher values, invalidating the current MAX constant. This makes me think that until we have network dependent max values, we shouldn't do any bounds checking, unless we only plan to support mainnet for such operations.

Copy link
Member

Choose a reason for hiding this comment

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

o.O you can have difficulty < 1 for regtest and signet? What is the corresponding target value?

Copy link
Member

Choose a reason for hiding this comment

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

Or do you mean that the conversion between difficulty and target is different on these networks....which would require we modify our conversion functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The conversion is the same, but the highest target/PoW limit varies by network. IIUC, the difficulty is just the ratio between the highest target and the current target, so a difficulty of 1 just represents the highest target for that given network. With that said, it seems Target::difficulty currently only works for mainnet and testnet values, since they both share the same max target.

Copy link
Member

Choose a reason for hiding this comment

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

:( so the conversion is different for different networks.

That is really obscure and does not seem necessary. But ok, we'll find a way to deal with it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How would you like to proceed then? Since we're not doing any bounds checking at all, and it depends on the active network, we could defer that to future work and not hold back the changes proposed here?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Agree let's not hold this up.

Copy link
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.

I think the overflow should be fixed unless I'm missing something.

bitcoin/src/pow.rs Outdated Show resolved Hide resolved
Comment on lines 245 to 249
if min > Self::MAX {
Self::MAX
} else {
min
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Agree.

bitcoin/src/pow.rs Outdated Show resolved Hide resolved
bitcoin/src/pow.rs Outdated Show resolved Hide resolved
Once `U256` was made private, we lost the ability to check whether a
valid difficulty transition was made in the chain, since `Target`
no longer exposes any arithmetic operations.
Copy link
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.

ACK 8e6f953

///
/// The difficulty can only decrease or increase by a factor of 4 max on each difficulty
/// adjustment period.
pub fn max_difficulty_transition_threshold(&self) -> Self { Self(self.0 << 2) }
Copy link
Collaborator

Choose a reason for hiding this comment

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

This will debug-panic on large values, is this OK/correct?

Copy link
Member

Choose a reason for hiding this comment

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

What makes you say that @Kixunil ? We use wrapping_shl to implement << so I thought this couldn't panic, what am I missing?

@wpaulino why did the check against MAX get removed here? In the previous iteration I only meant to comment that I didn't like the check in the min_difficulty_transition_threshold method, in the max one I thought it was correct.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

why did the check against MAX get removed here? In the previous iteration I only meant to comment that I didn't like the check in the min_difficulty_transition_threshold method, in the max one I thought it was correct.

Because MAX currently is only applicable to mainnet and testnet. I guess that's fine since this is also the case with Target::difficulty, but I figured since we want this to be network agnostic at some point, it doesn't make sense to bound by MAX until we have the network specific max constants.

Copy link
Member

Choose a reason for hiding this comment

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

Oh yes, sorry, my bad for not remembering all the context when reviewing. I did read that before.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, sorry I got confused, it'd only panic if the argument was too large, 2 is not too large.

Copy link
Member

Choose a reason for hiding this comment

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

I constantly have this same confusion about left-shift. It's a little weird, this notion of "overflow" is different from that for the arithmetic opcodes.

sanket1729
sanket1729 previously approved these changes May 4, 2023
Copy link
Member

@sanket1729 sanket1729 left a comment

Choose a reason for hiding this comment

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

ACK 8e6f953.

@sanket1729 sanket1729 dismissed their stale review May 4, 2023 20:01

Retracting my ACK. I might need some more time to check this completely. bitcoin core does these operations of in64_t. and then converts to uint256_t. Need to make sure if the way we are doing it is correct

@wpaulino
Copy link
Contributor Author

wpaulino commented May 4, 2023

Retracting my ACK. I might need some more time to check this completely. bitcoin core does these operations of in64_t. and then converts to uint256_t. Need to make sure if the way we are doing it is correct

@sanket1729 could you point to what you were looking at? There should only be a conversion from the compact representation (Header::bits) as a u32 to a u256.

Copy link
Member

@sanket1729 sanket1729 left a comment

Choose a reason for hiding this comment

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

ACK 8e6f953 . Sorry, was confused by some details.

@sanket1729
Copy link
Member

sanket1729 commented May 6, 2023

Implementors of difficulty transition algorithms should take care to only operate on CompactTarget types as there are many conversions from u32 to uint256_t and vice-versa.

Some of these conversions are lossy, and could lead to subtle bugs.

@apoelstra
Copy link
Member

Implementors of difficulty transition algorithms should take care to only operate on CompactTarget types

cc #1839 #1828 which discuss this discrepancy.

@apoelstra
Copy link
Member

apoelstra commented May 6, 2023

I'm happy to merge this as-is. We don't expose enough functionality yet for somebody to compute actual difficulty targets so I'm not too worried about somebody using the incorrect-on-extreme-values logic to cause consensus faults (also, even if you did, the first 10000 or so Bitcoin blocks hit the min-difficulty clamp so this would be immediately visible). And as written, this function serves well as a simple anti-DoS sanity check on a provided chain.

Interesting that Target and Difficulty are network-specific values ... I guess philosophically so are Block, Header, Transaction, Txid, etc. I wonder if there's some general way we could handle this that would be reasonably ergonomic and also fix our difficulties with Address..

Copy link
Member

@apoelstra apoelstra left a comment

Choose a reason for hiding this comment

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

ACK 8e6f953

@apoelstra apoelstra merged commit 4abbdc2 into rust-bitcoin:master May 6, 2023
10 checks passed
@wpaulino wpaulino deleted the impl-shift-target branch May 6, 2023 19:49
@tcharding
Copy link
Member

Have I gone mad, where are these changes gone? They are not on master anymore.

git show 8e6f953aa72eabbdad53937765e33c52c1a63c8b shows the patch but we've lost them somehow?

cc @apoelstra

@tcharding
Copy link
Member

ugh, I am a total wombat, I had an old tag checked out from yesterday. Face palm.

@tcharding tcharding mentioned this pull request Nov 16, 2023
tcharding added a commit to tcharding/rust-bitcoin that referenced this pull request Nov 16, 2023
We just backported PR rust-bitcoin#1820, add a changelog entry and bump the crate
version.
apoelstra added a commit that referenced this pull request Nov 16, 2023
ba99aa4 Bump version to 0.30.2 (Tobin C. Harding)
3c9bbca Expose valid (min, max) difficulty transition thresholds (Wilmer Paulino)

Pull request description:

  Patch one backports #1820, I believe this is all that is needed for LDK to be able to upgrade to `rust-bitcoin v0.30`.

  Context:

  - #1820
  - lightningdevkit/rust-lightning#2124

  Patch 2 adds a changelog entry and bumps the minor version.

ACKs for top commit:
  Kixunil:
    ACK ba99aa4

Tree-SHA512: 93adb52bbc8e61ece63e5653dea7cf6d2d25028f25cb8bbda14606d9ebf74becaee0a0c4f4f3d7fde56c0e2b8a57f8c9be263e04833161a1da0baaf8fcca8764
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.

None yet

6 participants