Skip to content
This repository has been archived by the owner on Sep 5, 2024. It is now read-only.

Bitcoin keys initial implementation #1

Open
wants to merge 1 commit into
base: dummy1
Choose a base branch
from
Open

Bitcoin keys initial implementation #1

wants to merge 1 commit into from

Conversation

Kixunil
Copy link
Collaborator

@Kixunil Kixunil commented Jul 4, 2022

This makes whole code to be added in a "PR" to enable code review by @dr-orlovsky

Copy link
Member

@dr-orlovsky dr-orlovsky 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 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)]
Copy link
Member

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.

bitcoin_keys/src/ecdsa/mod.rs Show resolved Hide resolved
bitcoin_keys/src/ecdsa/mod.rs Show resolved Hide resolved
alloc = []

[dependencies]
secp256k1 = "0.23.0"
Copy link
Member

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

Copy link
Collaborator Author

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.

Copy link
Member

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

Copy link
Collaborator Author

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.

bitcoin_keys/src/ecdsa/mod.rs Show resolved Hide resolved
}
}

impl<K: PublicKey> Legacy<K> {
Copy link
Member

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

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

Copy link
Collaborator Author

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.

Copy link
Member

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] {
Copy link
Member

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.

Copy link
Collaborator Author

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?

Copy link
Member

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.

Copy link
Collaborator Author

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

Copy link
Member

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])

Copy link
Collaborator Author

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

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

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

@dr-orlovsky dr-orlovsky changed the title PR to enable whole-crate code review Bitcoin keys initial implementation Jul 6, 2022
@dr-orlovsky
Copy link
Member

I also did several issues in this repo to summarize main todos which can be done as separate PRs

Copy link
Collaborator Author

@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'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"
Copy link
Collaborator Author

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.

bitcoin_keys/README.md Show resolved Hide resolved
@@ -0,0 +1,340 @@
//! Types handling ECDSA public keys.
Copy link
Collaborator Author

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.

bitcoin_keys/src/ecdsa/mod.rs Show resolved Hide resolved
mod sealed {
use secp256k1::Secp256k1;

pub trait Key: Copy + Eq {}
Copy link
Collaborator Author

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>;
Copy link
Collaborator Author

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

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

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)]
Copy link
Collaborator Author

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> {
Copy link
Collaborator Author

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.

Copy link
Member

@dr-orlovsky dr-orlovsky left a 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"
Copy link
Member

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

bitcoin_keys/README.md Show resolved Hide resolved
mod sealed {
use secp256k1::Secp256k1;

pub trait Key: Copy + Eq {}
Copy link
Member

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 {
Copy link
Member

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 {
Copy link
Member

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

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)]
Copy link
Member

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> {
Copy link
Member

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

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.

bitcoin_keys/src/ecdsa/mod.rs Show resolved Hide resolved
alloc = []

[dependencies]
secp256k1 = "0.23.0"
Copy link
Collaborator Author

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.

bitcoin_keys/src/ecdsa/mod.rs Show resolved Hide resolved
/// **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 {
Copy link
Collaborator Author

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] {
Copy link
Collaborator Author

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

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

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)]
Copy link
Collaborator Author

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> {
Copy link
Collaborator Author

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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants