-
-
Notifications
You must be signed in to change notification settings - Fork 8
Bitcoin keys initial implementation #1
base: dummy1
Are you sure you want to change the base?
Conversation
This reverts commit d8ab011.
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 this is very elegant work!
I left a lot of comments, many of them are questions or stylistic improvements. There are however some suggestions on a better API and also on implementing more key arithmetics. I haven't mentioned in the comments also that I propose to add core::ops::Neg
for all key types (doing modulo inversion for private keys and parity inversion for public`).
/// Distinguishes compressed keys from uncompressed ones (runtime). | ||
/// | ||
/// This is a more readable alternative to `bool`. | ||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] |
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.
Usual derives discussion :)
Why not Ord
and Hash
? Ord
allows putting types into BTreeSet
and as keys into BTreeMap
; so it is not that much about types having "ordering" property but rather about types which can be deterministically serialized as a collection members.
Also I think it is worth having manual Default
impl with defaulting to Compressed
.
alloc = [] | ||
|
||
[dependencies] | ||
secp256k1 = "0.23.0" |
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.
shouldn't we make it optional for apps which do not require key validity (like in Lightning where node keys are not always checked due to a performance considerations)? As we discussed in rust-bitcoin/rust-bitcoin#919
If you agree, probably it will be nice to have it as a follow-up PR, not to re-do this one
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 believe the idea was to make secp256k1-sys
optional? I'd absolutely make it optional here if that attempt fails.
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.
Not sure I understand. Secp256k1 provides no functionality on top of secp256k1-sys and basically an API wrapper. Sys crate would never be optional inside secp256k1...
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.
Hmm, I guess we got confused but TBH I really like the idea of trying it out here and if sys becomes optional we can move it.
} | ||
} | ||
|
||
impl<K: PublicKey> Legacy<K> { |
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.
add_scalar_exp
and mul_scalar
are missed
} | ||
|
||
impl<K: PrivateKey> Legacy<K> { | ||
/// Computes a public key from this private key |
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.
add_scalar
and mul_scalar
are missed
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 now I realized, do they make sense for pre-taproot keys? I think that's why I originally didn't add them.
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.
Yes, they are required in BIP32 derivations on extended priv keys + whenever P2C commitments are happening.
/// | ||
/// This is generally **not** presented to the user, just used to generate Bitcoin script. | ||
#[inline] | ||
pub fn serialize_public_key(&self) -> [u8; 33] { |
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 uncompressed serialization we have a type guarantees on the immutability of the serialized data. Here we do not.
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 didn't implement mut
stuff on SerializedPublicKey
because I find mutating it useless but at the same time preventing mutation (not really, just making it harder) doesn't seem to provide much value either. Do you have a particular reason to do one or the other beyond "consistency" or do you think conistency is a good enough reason for more code?
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 do not need mutability and I'd like to be consistent. Mutating public key data directly is a bad idea, so it would be nice to have type guarantees about immutability of the serialzied data.
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.
Hmm, that'll be a pretty significant boilerplate. I guess some part of it can be deduplicated using a macro but still...
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.
You can have just a single generic wrapper type for both cases, like pub struct SerializedKey<const LEN: usize>([u8; LEN])
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.
We can't use const generics in 1.41.1 and even if we could, as_slice()
would be quite different. Also I don't like the idea of exposing such thing to consumers.
} | ||
|
||
impl<K: PublicKey> Compressed<K> { | ||
/// Serializes the public key into bytes in compressed format. |
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.
add_scalar_exp
and mul_scalar
are missed
} | ||
|
||
impl<K: PrivateKey> Compressed<K> { | ||
/// Computes a public key from this private key |
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.
add_scalar
and mul_scalar
are missed
I also did several issues in this repo to summarize main todos which can be done as separate PRs |
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'm got a bit cold so not in a very productive state but was able to at least respond.
alloc = [] | ||
|
||
[dependencies] | ||
secp256k1 = "0.23.0" |
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 believe the idea was to make secp256k1-sys
optional? I'd absolutely make it optional here if that attempt fails.
@@ -0,0 +1,340 @@ | |||
//! Types handling ECDSA public keys. |
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.
My understanding is that on lowest level they don't but we can still have type-level markers for what the keys are intended for. The main idea is to avoid confusing situations like deriving a different address and then not seeing incoming coins even though strictly it's not a loss. I actually first hand witnessed numerous similar cases where people sent BTC to LTC address and the recovery was annoying.
Not sure if pre_tr
/tr
is better naming and not in good mental state to think deeply about it but will when I get better.
mod sealed { | ||
use secp256k1::Secp256k1; | ||
|
||
pub trait Key: Copy + Eq {} |
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.
TBH, I don't remember why exactly I added those two, I think for some convenience. This is an internal trait anyway, so shouldn't matter much.
/// Key pair that may be serialized as uncompressed, used in legacy addresses only. | ||
/// | ||
/// You probably want to use this alias instead of explicitly writing out the type. | ||
pub type LegacyKeyPair = ecdsa::Legacy<secp256k1::KeyPair>; |
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.
Maybe I've read the docs wrong but I think signing multiple messages with key pair is faster than signing them with just private key.
@@ -0,0 +1,69 @@ | |||
//! Keys intended to be used in Schnorr sinatures - in P2TR. |
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, yeah, bip340
sounds like a better name than schnorr
.
/// Private key intended for schnorr signatures. | ||
/// | ||
/// This type wraps [`secp256k1::SecretKey`] to prevent accidental use in ECDSA signatures. | ||
/// It is mostly used to sign P2TR spends or derive P2TR addresses. |
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 the API of ExtendedPrivKey
should eventually change to directly support known standards including P2TR.
/// | ||
/// This type wraps [`secp256k1::SecretKey`] to prevent accidental use in ECDSA signatures. | ||
/// It is mostly used to sign P2TR spends or derive P2TR addresses. | ||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] |
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.
secp256k1::PublicKey::from_secret_key(context, &self.key).into() | ||
} | ||
|
||
pub fn add_tweak(&self, tweak: &Scalar) -> Result<Self, secp256k1::Error> { |
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.
2 I would prefer to improve error upstream
4 Unfortunately it doesn't work that way :( We could #[must_use]
whole type but it could be annoying if people compute it for side effects.
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 wish you get well soon! Do not hurry up, take your time. I have responded to your comments.
Regarding implementing all arithm functions, I wrote this: #3
alloc = [] | ||
|
||
[dependencies] | ||
secp256k1 = "0.23.0" |
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.
Not sure I understand. Secp256k1 provides no functionality on top of secp256k1-sys and basically an API wrapper. Sys crate would never be optional inside secp256k1...
mod sealed { | ||
use secp256k1::Secp256k1; | ||
|
||
pub trait Key: Copy + Eq {} |
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.
The trait is internal, but Copy+Eq
requirement got externalized via outer pub trait Key: sealed::Key
.
fn private_key(&self) -> secp256k1::SecretKey; | ||
|
||
#[inline] | ||
fn compute_public_key<C: secp256k1::Signing>(&self, context: &Secp256k1<C>) -> secp256k1::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 lost a track on the status of static context in secp lib. However indeed it is the stuff that hugely pollutes all APIs
/// **Warning:** make sure to supply the correct key format. Incorrect format may lead to a | ||
/// different address making spending difficult or even impossible for non-technical people. | ||
#[inline] | ||
pub fn from_raw(key: K, format: KeyFormat) -> Self { |
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.
As I explained below, we should prevent constructing uncompressed keys as much as possible. So I rather will have a one-argument constructor for compressed key and some ugly-named function for uncompressed.
/// Private key intended for schnorr signatures. | ||
/// | ||
/// This type wraps [`secp256k1::SecretKey`] to prevent accidental use in ECDSA signatures. | ||
/// It is mostly used to sign P2TR spends or derive P2TR addresses. |
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.
Nope, I talked to Sipa on that and he confirmed multiple times that taproot must not affect BIP32-related APIs and they should remain working with full pub keys (not xonly)
/// | ||
/// This type wraps [`secp256k1::SecretKey`] to prevent accidental use in ECDSA signatures. | ||
/// It is mostly used to sign P2TR spends or derive P2TR addresses. | ||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] |
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.
not sure I understand this non-constant time story. But I clearly would like to have them in a BTreeSet
in deterministic order, for instance for serialization
secp256k1::PublicKey::from_secret_key(context, &self.key).into() | ||
} | ||
|
||
pub fn add_tweak(&self, tweak: &Scalar) -> Result<Self, secp256k1::Error> { |
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.
2 I would prefer to improve error upstream
will take ages and we just have a very specific error conditions here (upstream the Error must be split into multiple types IMO)
Unfortunately it doesn't work that way :(
I see :(
@@ -0,0 +1,340 @@ | |||
//! Types handling ECDSA public keys. |
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.
As we discussed below in https://github.com/BP-WG/bp/pull/1/files#r916009576, probably bip340
and legacy
would be better than schnorr
and ecdsa
.
alloc = [] | ||
|
||
[dependencies] | ||
secp256k1 = "0.23.0" |
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.
Hmm, I guess we got confused but TBH I really like the idea of trying it out here and if sys becomes optional we can move it.
/// **Warning:** make sure to supply the correct key format. Incorrect format may lead to a | ||
/// different address making spending difficult or even impossible for non-technical people. | ||
#[inline] | ||
pub fn from_raw(key: K, format: KeyFormat) -> Self { |
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.
Maybe from_raw(Key)
and from_raw_with_format(Key, Format)
?
/// | ||
/// This is generally **not** presented to the user, just used to generate Bitcoin script. | ||
#[inline] | ||
pub fn serialize_public_key(&self) -> [u8; 33] { |
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.
Hmm, that'll be a pretty significant boilerplate. I guess some part of it can be deduplicated using a macro but still...
/// Returns the serialized bytes as a slice. | ||
/// | ||
/// The length of the returned slice will be either 33 or 65, depending on the format of the | ||
/// kye this was created from. |
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.
Note to self: s/kye/key/
/// Private key intended for schnorr signatures. | ||
/// | ||
/// This type wraps [`secp256k1::SecretKey`] to prevent accidental use in ECDSA signatures. | ||
/// It is mostly used to sign P2TR spends or derive P2TR addresses. |
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.
There's still a separate purpose
value defined for P2TR (don't remember the value). I was thinking something like:
impl ExtendedPrivKey {
pub fn derive_raw(&self, path: Path) -> secp256k1::SecretKey;
// AFAIR uncompressed aren't supported by BIP32
// purpose must be omitted from path
pub fn derive_pre_segwit(&self, path: Path) -> bitcoin_keys:CompressedPrivateKey;
pub fn derive_p2tr(&self, path: Path) -> bitcoin_keys::Bip340PrivateKey;
}
/// | ||
/// This type wraps [`secp256k1::SecretKey`] to prevent accidental use in ECDSA signatures. | ||
/// It is mostly used to sign P2TR spends or derive P2TR addresses. | ||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] |
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.
If you compare keys you may leak them by timing information. The code should be changed to avoid that.
secp256k1::PublicKey::from_secret_key(context, &self.key).into() | ||
} | ||
|
||
pub fn add_tweak(&self, tweak: &Scalar) -> Result<Self, secp256k1::Error> { |
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 try to send a PR.
This makes whole code to be added in a "PR" to enable code review by @dr-orlovsky