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
impl PartialOrd/Ord for PublicKey #524
Conversation
src/util/key.rs
Outdated
pub struct PublicKey { | ||
/// Whether this public key should be serialized as compressed | ||
pub compressed: bool, | ||
/// The actual ECDSA key | ||
pub key: secp256k1::PublicKey, | ||
} | ||
|
||
impl Ord for PublicKey{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cargo fmt
👍
src/util/key.rs
Outdated
match (self.compressed, other.compressed){ | ||
(true, false) => cmp::Ordering::Less, | ||
(false, true) => cmp::Ordering::Greater, | ||
(true, true) => self.key.serialize().cmp(&other.key.serialize()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
serialize
is expensive. If someone tries to run sorting on a vector of PublicKey
s that might be a problem, as these will be run over and over.
That might be a fundamental problem and why cmp
should not be directly implemented on PublicKey
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Maybe, what we need a method for sorting instead of cmp
on PublicKey
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can also add that as an efficient method for sorting. I am not sure whether we should impl Ord
on PublicKey
or not. But I do think we should remove the default derive impl because that is incorrect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like https://docs.rs/secp256k1/0.19.0/secp256k1/key/struct.PublicKey.html has Ord
and under the hood it's all just a pub struct PublicKey([c_uchar; 64]);
so you should be able to use raw ordering of the bytes, instead of trying to serialize anything first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But, bip67(sortedmulti) requires us to convert that 33-byte representation for lexical comparison which requires seralize. My guess is that 64 bytes in ffi::PublicKey
have the full x and y co-ordinate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dpc, this is not calling serialize on bitcoin::PublicKey
, it is calling it on underlying secp key.
@apoelstra, the serialize function does do the conversion from 64-byte secp key to 33 byte one. I think that involves minimal crypto operation of checking the sign of y-coordinate. In theory, we do that log(n)
extra times whenever we call sort operation using the default PartialOrd
.
On the other hand, having a custom sort function should allow do the seralize only once(instead of log(n)
) in sorting. I do get that there is some performance benefit to this, but don't have a strong intuition to quantify it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does two field normalizations, which are 15nsec each on my system, and then checks the least significant bit of y.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, OK. No allocation is better, thought it is still not ideal. Judgment call, but seems reasonable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just make sure there potentially won't be any trouble if PublicKey
has to change internally or anything like that. The fact that this order is a bit arbitrary and doesn't feel "obvious and natural" still worries me a little.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's unfortunately "obvious and natural" in Bitcoin Core, and it is Core's ordering that we are trying to match.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
utACK
Hmm, just to give my 2 cents. When there is not obviously just one way to order objects (like numbers etc have some natural order to them), I think It doesn't matter if this ordering is consistent with any external ordering and I also don't like to make the ordering part of the type's definition if it's not a natural order. That's why I think deriving If there are some BIPs or externally specified orderings, I think it's better to specify those external to the type itself. Most sorting- and order-related functions provide ways to define custom orderings. So creating methods that easily plug into those APIs is IMO a better way to add support for special external orderings. So if this PR's main intention is to implement an order defined in BIP67, I'd suggest having a module use std::cmp;
pub struct SortKey( ... );
impl cmp::Ord for SortKey { ... }
pub fn sort_key(pk: &PublicKey) -> SortKey { .. }
pub fn cmp(pk1: &PublicKey, pk2: &PublicKey) -> cmp::Ordering {
cmp::Ord::cmp(sort_key(pk1), sort_key(pk2))
}
#[cfg(test)]
mod test {
#[test]
fn test_bip67_order() {
let mut slice: [PublicKey; 3] = ["xx".parse().unwrap(), "yy".parse().unwrap(), "zz".parse().unwrap()];
slice[..].sort_by_key(super::sort_key);
assert_eq!(slice, [ ... ]);
}
} Though in BIP67's case, I might not be objected too much against using Ord itself. Thought in that case, I'd definitely want to have that documented on the |
@stevenroose Perfectly explained. In this case it's not too bad, but still doesn't sit well with me. |
To be clear, BIP67 forbids uncompressed keys, and pretty-much any sane ordering would do the right thing with compressed keys. The unnaturalness comes from Bitcoin Core doing unnatural things with uncompressed keys and then exposing this in the |
I will add the methods as @stevenroose suggested in
But I think we should not derive |
FWIW |
sort_by_cached_key is not available in Rust 1.29 |
Something feels off in assuming bitcoin-core as externally specified ordering when IMO rust-bitcoin should try to match bitcoin core. Overall, I don't feel so strongly about this so I have implemented changes as requested by @stevenroose. I have also added a warning to warn users they might not get the same results as in bitcoin core when using Ord/PartialOrd from rust-bitcoin. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. I realize the API is not the most ideal. (Made a suggestion, but still.) Any specific reason to not call the module bip67
? Because it is more broad and supports uncompressed keys?
/// instead of naturally derived order which compares underlying | ||
/// 64 byte secp key. | ||
#[derive(Debug, Eq, PartialEq, Clone)] | ||
pub struct SortKey<'pk>(&'pk PublicKey); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In fact to make this a bit more ergonomic, this could be a Cow<PublicKey>
so that this can also be used in owned form (like to build a map structure without having to keep the keys separately).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think sorting according to BIP-67 and Bitcoin Core rules should be clearly separated; with the first one being PartialOrd
only. See my suggestions in comments
/// bytes when comparing the two PublicKeys in compressed form | ||
/// instead of naturally derived order which compares underlying | ||
/// 64 byte secp key. | ||
#[derive(Debug, Eq, PartialEq, Clone)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also should be Copy
and Hash
} | ||
|
||
#[cfg(test)] | ||
mod test { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will be really great to have all test vectors from https://github.com/bitcoin/bips/blob/master/bip-0067.mediawiki#test-vectors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will add along with bip67Key type
} | ||
} | ||
|
||
impl<'pk> PartialOrd for SortKey<'pk> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW, according to BIP-67, "Uncompressed keys are incompatible with this specificiation. A compatible implementation should not automatically compress keys. Receiving an uncompressed key from a multisig participant should be interpreted as a sign that the user has an incompatible implementation."
So my proposal here is to make PartialOrd
returning None
if any of the keys is uncompressed (and leave Ord
as is)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or, alternatively, two types should be used: Bip67Key
, which will be PartialOrd
only; and SortedKey
for Bitcoin Core-style sorting with the current code unchainged. Top-namespace cmp
function should be split into two, bip67_partial_cmp -> Option<cmp::Ordering>
and cmp
, left as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the second idea with two types. In fact, my primary motivation was to have only the SortKey
type to emulate core behavior with no focus on bip67
all. This is because in sortedmulti()
descriptor, bitcoin core does allow uncompressed keys and sorts them with this logic.
Then, it also makes sense to add testcases from bip67.
I will amend the PR with two separate types.
Using |
I also assume that we probably should use bitcoin-core/secp256k1#850 |
Any updates? Needs a rebase, but apart from that looks good with the newtype for ordering. One question: should it maybe support both owned and borrowed keys? Maybe via |
I will work on this today. |
Perhaps wait for #635 so that |
This PR could be revived if you have time @sanket1729. |
Thanks for the ping, @tcharding. I will work this one this week |
This PR is currently marked as part of the 0.29 milestone, are you still hoping to get this in before the 0.29 release? |
@tcharding, sorry for the delay. I am caught with a bunch of things on my plate right now. I might come back to attend this sometime later. This is open for grabs meanwhile. We can remove the 0.29 milestone from this. |
No worries, cheers man. |
I can pick this up. Let me know if this TODO list is accurate:
|
Looking at this now, I think with // Not sure if another type is very useful since the representation will not change anyway
// Maybe we could just return `[u8; 33]` or `type Bip67SortKey = [u8; 33];`
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Bip67SortKey([u8; 33]);
pub struct CompressedKey(ffi::PublicKey);
impl CompressedKey {
pub fn bip67_sort_key(&self) -> Bip67SortKey {
Bip67SortKey(self.serialize())
}
}
// in consumer code:
keys.sort_by_cached_key(CompressedKey::bip67_sort_key); I find the usage elegant. :) Note that this incorporates #826 per BIP67:
I strongly believe that having a newtype for compressed keys is better than silent conversion or wrong order. Of course there may still be the need for use in use core::borrow::Borrow;
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Bip67SortedPublicKey<K = CompressedPublicKey>(pub K) where K: Borrow<CompressedPublicKey>;
// manually impl Ord, Partial ord Note that while it may be tempting to Edit: added a default for |
What about:
Or
|
Adding a new module just for a function looks too much to me. Adding a global module for various sorting utilities is even worse because it goes against the idea of splitting up the crate. |
Yeah, I guess so... Perhaps this could just be an associated function that takes &self on PublicKey and returns [u8] for now. Then if/when CompressedKey lands it can also have a similar associated function. I am personally against naming the API itself after BIP67... since the bip is not widely known by number (in the sense that someone thinking "I want to sort these keys" won't think "hmm, I wonder if there's a BIP67 fn or mod somewhere..." even though they might search for bip39 when thinking about mnemonics. In the docs for the PublicKey fn we can say that it is BIP67 compliant under the condition that all PublicKeys in the slice are compressed. Then in the docs for CompressedKey we can say it's always BIP67 compliant. Like OP said, Bitcoin Core sorts any mixture of un/compressed pubkeys, so this is what I think would be best. If there are no objections I will work on it this weekend. |
If we provide it for uncompressed keys then it must not be named BIP67. :) What's the correct sort order of uncompressed keys? |
Bitcoin Core uses lexicographic order of serialized keys. This is the closest thing to "correct" that we have. I agree that we shouldn't call this BIP67 if it supports uncompressed keys. Also I don't really like BIP67 :). But we could comment that, if you want a BIP67 sort, you can filter out uncompressed/hybrid keys and then use this logic. |
Ok. So pretty much the implementation is straight forward, basically just like to_bytes but without all the Vec allocation. Perhaps Then we can mention sort_by_cached_key in the docs as a concrete example, and mention BIP67 (for searchability) and how to use |
See #1084 |
@junderw it can not be I suggest to copy the code from there and change |
Please check out my PR and give any feedback. I return a [u8; 65] for explicit use with the *_by_key sort methods on slice. Zero allocations. |
…y key 24f0441 Add PublicKey::to_sort_key method for use with sorting by key (junderw) Pull request description: Replaces #524 See previous PR for reasoning. This solution is a little more straightforward. The name and documentation should be enough to prevent misuse. We can also impl a to_sort_key for any CompressedKey added later. (or just impl Ord in a BIP67 compliant way) TODO: - [x] Add more sorting test vectors. Ideas of edge cases to test are welcome. ACKs for top commit: apoelstra: ACK 24f0441 tcharding: ACK 24f0441 Kixunil: ACK 24f0441 Tree-SHA512: 92d68cccaf32e224dd7328edeb3481dd7dcefb2f9090b7381e135e897c21f79ade922815cc766c5adb7ba751b71b51a773d103af6ba13fc081e1f5bfb846b201
I think this can be closed now. |
No description provided.