Skip to content

Bump ed25519-dalek to 2.1.1, ed25519-dalek-bip32 to 0.3.0 #26

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

kevinheavey
Copy link
Contributor

Ported from anza-xyz/agave#3088

Summary of Changes

  • Bump the two crate versions as in title and fix what breaks
  • This involves one breaking change: Keypair::generate now relies on traits from a newer version of the rand crate - it no longer works with rand 0.7.

@seanyoung
Copy link
Contributor

How does this affect performance (the bench should tell us that, I think).

@kevinheavey
Copy link
Contributor Author

Looks like the new version is faster. Found this in the CI bench output https://github.com/anza-xyz/solana-sdk/actions/runs/13304474459/job/37152326064?pr=26:

test bench_ed25519_len_032 ... bench:      48,795.52 ns/iter (+/- 238.09)
test bench_ed25519_len_128 ... bench:      49,085.67 ns/iter (+/- 368.34)
test bench_ed25519_len_32k ... bench:     110,201.12 ns/iter (+/- 1,078.12)
test bench_ed25519_len_max ... bench:     171,307.76 ns/iter (+/- 1,212.43)

Meanwhile in another unrelated PR: https://github.com/anza-xyz/solana-sdk/actions/runs/13344518453/job/37273344074?pr=40

test bench_ed25519_len_032 ... bench:      62,532.14 ns/iter (+/- 1,077.31)
test bench_ed25519_len_128 ... bench:      62,774.78 ns/iter (+/- 597.11)
test bench_ed25519_len_32k ... bench:     123,763.83 ns/iter (+/- 1,565.07)
test bench_ed25519_len_max ... bench:     184,640.96 ns/iter (+/- 1,562.99)

@@ -76,12 +71,12 @@ impl Keypair {

/// Returns this `Keypair` as a base58-encoded string
pub fn to_base58_string(&self) -> String {
bs58::encode(&self.0.to_bytes()).into_string()
bs58::encode(&self.to_bytes()).into_string()
}

/// Gets this `Keypair`'s SecretKey
pub fn secret(&self) -> &ed25519_dalek::SecretKey {

Choose a reason for hiding this comment

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

I think this should just return [u8; 32] directly, skip the type alias of ed25519-dalek.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This function has been deprecated and will be removed anyway

@@ -271,11 +262,9 @@ pub fn keypair_from_seed(seed: &[u8]) -> Result<Keypair, Box<dyn error::Error>>
if seed.len() < ed25519_dalek::SECRET_KEY_LENGTH {
return Err("Seed is too short".into());
}
let secret = ed25519_dalek::SecretKey::from_bytes(&seed[..ed25519_dalek::SECRET_KEY_LENGTH])
let secret_key = ed25519_dalek::SecretKey::try_from(&seed[..ed25519_dalek::SECRET_KEY_LENGTH])

Choose a reason for hiding this comment

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

this function never fail, should call unwrap here

@joncinque
Copy link
Collaborator

We're ready to integrate this now! Do you mind rebasing your branch? Otherwise, I can take care of it. Just let me know

@kevinheavey kevinheavey force-pushed the ed25519-dalek-2.1.1 branch from c15a222 to aabc669 Compare July 11, 2025 09:46
Copy link
Collaborator

@joncinque joncinque left a comment

Choose a reason for hiding this comment

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

Looks great to me! Just some small things

@samkim-crypto can you also take a quick look and make sure it's all ok?

@@ -78,19 +75,19 @@ impl Keypair {
/// Returns this `Keypair` as a base58-encoded string
pub fn to_base58_string(&self) -> String {
let mut out = [0u8; five8::BASE58_ENCODED_64_MAX_LEN];
let len = five8::encode_64(&self.0.to_bytes(), &mut out);
let len = five8::encode_64(&self.to_bytes(), &mut out);
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: to avoid the copy, this should be able to use as_bytes()

Suggested change
let len = five8::encode_64(&self.to_bytes(), &mut out);
let len = five8::encode_64(self.as_bytes(), &mut out);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

self.0.as_bytes() would give just the secret 32 bytes, not the full 64 bytes

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah you're right, sorry, I was looking at methods on SigningKey

@@ -76,12 +71,12 @@ impl Keypair {

/// Returns this `Keypair` as a base58-encoded string
pub fn to_base58_string(&self) -> String {
bs58::encode(&self.0.to_bytes()).into_string()
bs58::encode(&self.to_bytes()).into_string()
}

/// Gets this `Keypair`'s SecretKey
pub fn secret(&self) -> &ed25519_dalek::SecretKey {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This function has been deprecated and will be removed anyway

@@ -158,8 +158,8 @@ curve25519-dalek = { version = "4.1.3", features = ["digest", "rand_core"] }
dashmap = { version = "5.5.3", features = ["serde"] }
derivation-path = { version = "0.2.0", default-features = false }
digest = "0.10.7"
ed25519-dalek = "=1.0.1"
ed25519-dalek-bip32 = "0.2.0"
ed25519-dalek = "=2.1.1"
Copy link
Collaborator

Choose a reason for hiding this comment

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

note to self for later: 2.2.0 just got released, so we can bump to that after this lands

.map_err(SignatureError::from_source)?;
let expected_public = ed25519_dalek::PublicKey::from(&secret);
};
// compiler elides the second len check so try_into().unwrap() is as fast as unsafe code
Copy link
Collaborator

Choose a reason for hiding this comment

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

cool! Just for my own knowledge, how did you learn / measure that?

Copy link
Contributor Author

@kevinheavey kevinheavey Jul 12, 2025

Choose a reason for hiding this comment

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

compared safe and unsafe versions with godbolt, and saw they were the same: https://godbolt.org/z/qf4cnaWax

Comment on lines 115 to 129
let keypair_bytes: [u8; ed25519_dalek::KEYPAIR_LENGTH] = keypair_slice.try_into().unwrap();
let secret: ed25519_dalek::SecretKey = keypair_bytes[..ed25519_dalek::SECRET_KEY_LENGTH]
.try_into()
.unwrap();
let signing_key = ed25519_dalek::SigningKey::from_bytes(&secret);
let public = ed25519_dalek::VerifyingKey::try_from(
&keypair_bytes[ed25519_dalek::SECRET_KEY_LENGTH..],
)
.map_err(SignatureError::from_source)?;
let expected_public = signing_key.verifying_key();
(public == expected_public)
.then_some(Self(ed25519_dalek::Keypair { secret, public }))
.then_some(Self(signing_key))
.ok_or(SignatureError::from_source(String::from(
"keypair bytes do not specify same pubkey as derived from their secret key",
)))
Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks like it's doing pretty much the same exact thing as from_keypair_bytes

How about just calling that instead?

Copy link
Collaborator

@joncinque joncinque left a comment

Choose a reason for hiding this comment

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

Looks good to me!

@@ -78,19 +75,19 @@ impl Keypair {
/// Returns this `Keypair` as a base58-encoded string
pub fn to_base58_string(&self) -> String {
let mut out = [0u8; five8::BASE58_ENCODED_64_MAX_LEN];
let len = five8::encode_64(&self.0.to_bytes(), &mut out);
let len = five8::encode_64(&self.to_bytes(), &mut out);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah you're right, sorry, I was looking at methods on SigningKey

@samkim-crypto samkim-crypto self-requested a review July 13, 2025 00:37
Copy link
Contributor

@samkim-crypto samkim-crypto left a comment

Choose a reason for hiding this comment

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

Some minor comments below, but looks good to me!

)))
};
// compiler elides the second len check so try_into().unwrap() is as fast as unsafe code
let keypair_bytes: &[u8; ed25519_dalek::KEYPAIR_LENGTH] = keypair_slice.try_into().unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about merging the length check with this line? It seems like the code will be slightly more concise and we can remove the unwrap as well.

let keypair_bytes: &[u8; ed25519_dalek::KEYPAIR_LENGTH] = bytes
    .try_into()
    .map_err(|_| SignatureError::from_source(String::from(
        "candidate keypair byte array is the wrong length",
    )))?;

Ok(Keypair(dalek_keypair))
// this won't fail as we've already checked the length
let secret_key =
ed25519_dalek::SecretKey::try_from(&seed[..ed25519_dalek::SECRET_KEY_LENGTH]).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: an unwrap should be safe here, but why not just use ??

Suggested change
ed25519_dalek::SecretKey::try_from(&seed[..ed25519_dalek::SECRET_KEY_LENGTH]).unwrap();
ed25519_dalek::SecretKey::try_from(&seed[..ed25519_dalek::SECRET_KEY_LENGTH])?;

I can't imagine this changing, but this can future-proof any api changes that introduce a new way of erroring.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking PR contains breaking changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants