Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .config/spellcheck.dic
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
16
20
CPI
Ed25519
Ed25519SignatureOffsets
Expand All @@ -8,10 +8,14 @@ SDK
SVM
Solana
cofactored
cryptographic
deserializes
ed25519
entrypoint
malleability
precompile
precompiles
runtime
syscall
syscalls
verifier
5 changes: 3 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ on:
branches: [main]

env:
SBPF_PROGRAM_PACKAGES: "['program']"
RUST_PACKAGES: "['program']"
SBPF_PROGRAM_PACKAGES: "['program', 'ed25519-verify']"
RUST_PACKAGES: "['program', 'ed25519-verify']"

jobs:
set_env:
Expand Down Expand Up @@ -41,3 +41,4 @@ jobs:
rustfmt-toolchain: ${{ needs.set_env.outputs.RUST_TOOLCHAIN_NIGHTLY }}
clippy-toolchain: ${{ needs.set_env.outputs.RUST_TOOLCHAIN_NIGHTLY }}
solana-cli-version: ${{ needs.set_env.outputs.SOLANA_CLI_VERSION }}
generate-clients: false
57 changes: 50 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 4 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["program"]
members = ["ed25519-verify", "program"]
resolver = "2"

[workspace.lints.rust]
Expand All @@ -10,17 +10,16 @@ unexpected_cfgs = { level = "warn", check-cfg = [
] }

[workspace.dependencies]
bincode = "1.3.3"
ed25519-dalek = "2.1.1"
mollusk-svm = "0.13.1"
pinocchio = "0.11.2"
serde = { version = "1.0.228", default-features = false }
serde_derive = "1.0.228"
solana-account-info = "3.1.1"
solana-account = "3.4.0"
solana-curve25519 = "4.0.1"
solana-ed25519-verify = { path = "ed25519-verify", version = "4.0.0", default-features = false }
solana-instruction = "3.3.0"
solana-program-entrypoint = "3.1.1"
solana-program-error = "3.0.1"
solana-program-runtime = { version = "4.0.0", features = ["agave-unstable-api"] }
solana-pubkey = "4.1.0"
solana-sdk-ids = "3.1.0"
solana-sha512-hasher = { version = "1.0.1", features = ["sha2"] }
3 changes: 0 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
RUST_TOOLCHAIN_NIGHTLY = nightly-2026-01-22
SOLANA_CLI_VERSION = v3.1.10
SBF_ARCH = v2

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit for future work: v2 is getting removed, so we should use v3

PROGRAM_SO = solana_ed25519_program.so

nightly = +${RUST_TOOLCHAIN_NIGHTLY}

Expand Down Expand Up @@ -47,8 +46,6 @@ build-sbf-%:
cargo build-sbf --arch $(SBF_ARCH) --manifest-path $(call make-path,$*)/Cargo.toml -- --locked $(ARGS)

test-%:
@test -f target/deploy/$(PROGRAM_SO) || \
(echo "SBF artifact not found: run make build-sbf-$* first" >&2; exit 1)
SBF_OUT_DIR=$(PWD)/target/deploy cargo test \
--locked \
--manifest-path $(call make-path,$*)/Cargo.toml \
Expand Down
27 changes: 27 additions & 0 deletions ed25519-verify/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[package]
name = "solana-ed25519-verify"
version = "4.0.0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: we can start with v1 or v0.1

edition = "2021"

[lib]
crate-type = ["rlib"]

[features]
default = []
# Enables the client-side constructor for invoking the standalone program.
instruction = ["dep:solana-instruction", "dep:solana-pubkey"]

[dependencies]
solana-curve25519 = { workspace = true }
solana-instruction = { workspace = true, optional = true }
solana-program-error = { workspace = true }
solana-pubkey = { workspace = true, optional = true }
solana-sha512-hasher = { workspace = true }

[dev-dependencies]
ed25519-dalek = { workspace = true }
solana-ed25519-verify = { path = ".", features = ["instruction"] }
solana-pubkey = { workspace = true }

[lints]
workspace = true
24 changes: 24 additions & 0 deletions ed25519-verify/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#![no_std]

//! Stateless Ed25519 verification utilities for Solana programs.
//!
//! This crate contains the reusable verifier used by
//! `solana-ed25519-program`. Programs can also depend on it directly to verify
//! Ed25519 signatures without invoking the standalone verifier program.
//!
//! The verifier performs ZIP-215 verification with canonical `S`.

#[cfg(feature = "instruction")]
extern crate alloc;

#[cfg(feature = "instruction")]
pub mod program;
mod scalar;
mod verifier;

#[cfg(feature = "instruction")]
pub use program::ed25519_verify_instruction;
pub use verifier::Ed25519Verifier;

pub const PUBKEY_SERIALIZED_SIZE: usize = 32;
pub const SIGNATURE_SERIALIZED_SIZE: usize = 64;
24 changes: 24 additions & 0 deletions ed25519-verify/src/program.rs

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: can we call this file instruction.rs? That'll line up better with other clients

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
extern crate alloc;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: is this line needed since it's already enabled in lib.rs?


use {
crate::{PUBKEY_SERIALIZED_SIZE, SIGNATURE_SERIALIZED_SIZE},
alloc::{vec, vec::Vec},
solana_instruction::Instruction,
solana_pubkey::Pubkey,
};

/// Constructs an on-chain instruction to invoke `solana-ed25519-program`.
pub fn ed25519_verify_instruction(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: let's just call this "verify"

program_id: &Pubkey,
public_key: &[u8; PUBKEY_SERIALIZED_SIZE],
signature: &[u8; SIGNATURE_SERIALIZED_SIZE],
message: &[u8],
) -> Instruction {
let mut data =
Vec::with_capacity(PUBKEY_SERIALIZED_SIZE + SIGNATURE_SERIALIZED_SIZE + message.len());
data.extend_from_slice(public_key);
data.extend_from_slice(signature);
data.extend_from_slice(message);

Instruction::new_with_bytes(*program_id, &data, vec![])
}
File renamed without changes.
82 changes: 82 additions & 0 deletions ed25519-verify/src/verifier.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use {
crate::{scalar, PUBKEY_SERIALIZED_SIZE, SIGNATURE_SERIALIZED_SIZE},
solana_curve25519::{
edwards::{
multiply_edwards, multiscalar_multiply_edwards, subtract_edwards, PodEdwardsPoint,
},
scalar::PodScalar,
},
solana_program_error::ProgramError,
};

const ED25519_BASEPOINT_COMPRESSED: PodEdwardsPoint = PodEdwardsPoint([
0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
]);
/// Identity point of the Edwards curve, in compressed form.
pub(crate) const EDWARDS_IDENTITY_COMPRESSED_BYTES: [u8; PUBKEY_SERIALIZED_SIZE] = [
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
const EDWARDS_IDENTITY_COMPRESSED: PodEdwardsPoint =
PodEdwardsPoint(EDWARDS_IDENTITY_COMPRESSED_BYTES);
const EIGHT_SCALAR: PodScalar = PodScalar([
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]);

/// Stateless, zero-allocation Ed25519 verifier.
#[derive(Debug, Clone, Copy, Default)]
pub struct Ed25519Verifier;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In the future, we should add a bunch of configuration options. For example, I am thinking we should support different variations organized in https://hdevalence.ca/blog/2020-10-04-its-25519am/). But let's do this in follow-ups since this PR is already quite big.


impl Ed25519Verifier {
/// Initializes a new verifier.
pub const fn new() -> Self {
Self
}
Comment on lines +33 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: I'm always a fan of less code rather than more code, and this doesn't seem to give very much


/// Performs ZIP-215 Ed25519 verification for one signature.
///
/// Uses the cofactored equation `[8](S*B - H(R || A || M)*A) == [8]R`.
/// The combined multiply-add minus `R` is performed first, then multiplied
/// by 8 and compared with the identity, matching the ed25519-zebra batch
/// verification shape. Canonical `S` is still required.
pub fn verify_signature(
&self,
signature: &[u8; SIGNATURE_SERIALIZED_SIZE],
public_key: &[u8; PUBKEY_SERIALIZED_SIZE],
message: &[u8],
) -> Result<(), ProgramError> {
let (r_bytes, s_bytes) = signature.split_at(32);
let r_bytes: &[u8; 32] = r_bytes.try_into().unwrap();
let s_bytes: &[u8; 32] = s_bytes.try_into().unwrap();
if !scalar::is_canonical_scalar(s_bytes) {
return Err(ProgramError::InvalidArgument);
}

let r_point = PodEdwardsPoint(*r_bytes);
let public_key_point = PodEdwardsPoint(*public_key);

let challenge = compute_challenge(r_bytes, public_key, message);
let minus_challenge = scalar::negate(&challenge);
let lhs = multiscalar_multiply_edwards(
&[PodScalar(*s_bytes), PodScalar(minus_challenge)],
&[ED25519_BASEPOINT_COMPRESSED, public_key_point],
)
.ok_or(ProgramError::InvalidArgument)?;
let difference = subtract_edwards(&lhs, &r_point).ok_or(ProgramError::InvalidArgument)?;
let difference_cofactored =
multiply_edwards(&EIGHT_SCALAR, &difference).ok_or(ProgramError::InvalidArgument)?;

if difference_cofactored != EDWARDS_IDENTITY_COMPRESSED {
return Err(ProgramError::InvalidArgument);
}

Ok(())
}
}

fn compute_challenge(signature_r: &[u8; 32], public_key: &[u8; 32], message: &[u8]) -> [u8; 32] {
let digest = solana_sha512_hasher::hashv(&[signature_r, public_key, message]).to_bytes();
scalar::reduce_wide(&digest)
}
Loading
Loading