Skip to content

Commit 3407a3f

Browse files
authored
refactor with pinocchio framework (#2)
* refactor * refactoring * clean up * CI * Update Makefile * Update spellcheck.dic * refactor instruction data * clean up * address comments
1 parent 151827b commit 3407a3f

21 files changed

Lines changed: 527 additions & 1231 deletions

.config/spellcheck.dic

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
16
1+
20
22
CPI
33
Ed25519
44
Ed25519SignatureOffsets
@@ -8,10 +8,14 @@ SDK
88
SVM
99
Solana
1010
cofactored
11+
cryptographic
12+
deserializes
1113
ed25519
14+
entrypoint
1215
malleability
1316
precompile
1417
precompiles
1518
runtime
1619
syscall
1720
syscalls
21+
verifier

.github/workflows/main.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ on:
77
branches: [main]
88

99
env:
10-
SBPF_PROGRAM_PACKAGES: "['program']"
11-
RUST_PACKAGES: "['program']"
10+
SBPF_PROGRAM_PACKAGES: "['program', 'ed25519-verify']"
11+
RUST_PACKAGES: "['program', 'ed25519-verify']"
1212

1313
jobs:
1414
set_env:
@@ -41,3 +41,4 @@ jobs:
4141
rustfmt-toolchain: ${{ needs.set_env.outputs.RUST_TOOLCHAIN_NIGHTLY }}
4242
clippy-toolchain: ${{ needs.set_env.outputs.RUST_TOOLCHAIN_NIGHTLY }}
4343
solana-cli-version: ${{ needs.set_env.outputs.SOLANA_CLI_VERSION }}
44+
generate-clients: false

Cargo.lock

Lines changed: 50 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace]
2-
members = ["program"]
2+
members = ["ed25519-verify", "program"]
33
resolver = "2"
44

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

1212
[workspace.dependencies]
13-
bincode = "1.3.3"
1413
ed25519-dalek = "2.1.1"
1514
mollusk-svm = "0.13.1"
15+
pinocchio = "0.11.2"
1616
serde = { version = "1.0.228", default-features = false }
1717
serde_derive = "1.0.228"
18-
solana-account-info = "3.1.1"
18+
solana-account = "3.4.0"
1919
solana-curve25519 = "4.0.1"
20+
solana-ed25519-verify = { path = "ed25519-verify", version = "4.0.0", default-features = false }
2021
solana-instruction = "3.3.0"
21-
solana-program-entrypoint = "3.1.1"
2222
solana-program-error = "3.0.1"
2323
solana-program-runtime = { version = "4.0.0", features = ["agave-unstable-api"] }
2424
solana-pubkey = "4.1.0"
25-
solana-sdk-ids = "3.1.0"
2625
solana-sha512-hasher = { version = "1.0.1", features = ["sha2"] }

Makefile

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
RUST_TOOLCHAIN_NIGHTLY = nightly-2026-01-22
22
SOLANA_CLI_VERSION = v3.1.10
33
SBF_ARCH = v2
4-
PROGRAM_SO = solana_ed25519_program.so
54

65
nightly = +${RUST_TOOLCHAIN_NIGHTLY}
76

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

4948
test-%:
50-
@test -f target/deploy/$(PROGRAM_SO) || \
51-
(echo "SBF artifact not found: run make build-sbf-$* first" >&2; exit 1)
5249
SBF_OUT_DIR=$(PWD)/target/deploy cargo test \
5350
--locked \
5451
--manifest-path $(call make-path,$*)/Cargo.toml \

ed25519-verify/Cargo.toml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[package]
2+
name = "solana-ed25519-verify"
3+
version = "4.0.0"
4+
edition = "2021"
5+
6+
[lib]
7+
crate-type = ["rlib"]
8+
9+
[features]
10+
default = []
11+
# Enables the client-side constructor for invoking the standalone program.
12+
instruction = ["dep:solana-instruction", "dep:solana-pubkey"]
13+
14+
[dependencies]
15+
solana-curve25519 = { workspace = true }
16+
solana-instruction = { workspace = true, optional = true }
17+
solana-program-error = { workspace = true }
18+
solana-pubkey = { workspace = true, optional = true }
19+
solana-sha512-hasher = { workspace = true }
20+
21+
[dev-dependencies]
22+
ed25519-dalek = { workspace = true }
23+
solana-ed25519-verify = { path = ".", features = ["instruction"] }
24+
solana-pubkey = { workspace = true }
25+
26+
[lints]
27+
workspace = true

ed25519-verify/src/lib.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#![no_std]
2+
3+
//! Stateless Ed25519 verification utilities for Solana programs.
4+
//!
5+
//! This crate contains the reusable verifier used by
6+
//! `solana-ed25519-program`. Programs can also depend on it directly to verify
7+
//! Ed25519 signatures without invoking the standalone verifier program.
8+
//!
9+
//! The verifier performs ZIP-215 verification with canonical `S`.
10+
11+
#[cfg(feature = "instruction")]
12+
extern crate alloc;
13+
14+
#[cfg(feature = "instruction")]
15+
pub mod program;
16+
mod scalar;
17+
mod verifier;
18+
19+
#[cfg(feature = "instruction")]
20+
pub use program::ed25519_verify_instruction;
21+
pub use verifier::Ed25519Verifier;
22+
23+
pub const PUBKEY_SERIALIZED_SIZE: usize = 32;
24+
pub const SIGNATURE_SERIALIZED_SIZE: usize = 64;

ed25519-verify/src/program.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
extern crate alloc;
2+
3+
use {
4+
crate::{PUBKEY_SERIALIZED_SIZE, SIGNATURE_SERIALIZED_SIZE},
5+
alloc::{vec, vec::Vec},
6+
solana_instruction::Instruction,
7+
solana_pubkey::Pubkey,
8+
};
9+
10+
/// Constructs an on-chain instruction to invoke `solana-ed25519-program`.
11+
pub fn ed25519_verify_instruction(
12+
program_id: &Pubkey,
13+
public_key: &[u8; PUBKEY_SERIALIZED_SIZE],
14+
signature: &[u8; SIGNATURE_SERIALIZED_SIZE],
15+
message: &[u8],
16+
) -> Instruction {
17+
let mut data =
18+
Vec::with_capacity(PUBKEY_SERIALIZED_SIZE + SIGNATURE_SERIALIZED_SIZE + message.len());
19+
data.extend_from_slice(public_key);
20+
data.extend_from_slice(signature);
21+
data.extend_from_slice(message);
22+
23+
Instruction::new_with_bytes(*program_id, &data, vec![])
24+
}

ed25519-verify/src/verifier.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
use {
2+
crate::{scalar, PUBKEY_SERIALIZED_SIZE, SIGNATURE_SERIALIZED_SIZE},
3+
solana_curve25519::{
4+
edwards::{
5+
multiply_edwards, multiscalar_multiply_edwards, subtract_edwards, PodEdwardsPoint,
6+
},
7+
scalar::PodScalar,
8+
},
9+
solana_program_error::ProgramError,
10+
};
11+
12+
const ED25519_BASEPOINT_COMPRESSED: PodEdwardsPoint = PodEdwardsPoint([
13+
0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
14+
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
15+
]);
16+
/// Identity point of the Edwards curve, in compressed form.
17+
pub(crate) const EDWARDS_IDENTITY_COMPRESSED_BYTES: [u8; PUBKEY_SERIALIZED_SIZE] = [
18+
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
19+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
20+
];
21+
const EDWARDS_IDENTITY_COMPRESSED: PodEdwardsPoint =
22+
PodEdwardsPoint(EDWARDS_IDENTITY_COMPRESSED_BYTES);
23+
const EIGHT_SCALAR: PodScalar = PodScalar([
24+
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
25+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
26+
]);
27+
28+
/// Stateless, zero-allocation Ed25519 verifier.
29+
#[derive(Debug, Clone, Copy, Default)]
30+
pub struct Ed25519Verifier;
31+
32+
impl Ed25519Verifier {
33+
/// Initializes a new verifier.
34+
pub const fn new() -> Self {
35+
Self
36+
}
37+
38+
/// Performs ZIP-215 Ed25519 verification for one signature.
39+
///
40+
/// Uses the cofactored equation `[8](S*B - H(R || A || M)*A) == [8]R`.
41+
/// The combined multiply-add minus `R` is performed first, then multiplied
42+
/// by 8 and compared with the identity, matching the ed25519-zebra batch
43+
/// verification shape. Canonical `S` is still required.
44+
pub fn verify_signature(
45+
&self,
46+
signature: &[u8; SIGNATURE_SERIALIZED_SIZE],
47+
public_key: &[u8; PUBKEY_SERIALIZED_SIZE],
48+
message: &[u8],
49+
) -> Result<(), ProgramError> {
50+
let (r_bytes, s_bytes) = signature.split_at(32);
51+
let r_bytes: &[u8; 32] = r_bytes.try_into().unwrap();
52+
let s_bytes: &[u8; 32] = s_bytes.try_into().unwrap();
53+
if !scalar::is_canonical_scalar(s_bytes) {
54+
return Err(ProgramError::InvalidArgument);
55+
}
56+
57+
let r_point = PodEdwardsPoint(*r_bytes);
58+
let public_key_point = PodEdwardsPoint(*public_key);
59+
60+
let challenge = compute_challenge(r_bytes, public_key, message);
61+
let minus_challenge = scalar::negate(&challenge);
62+
let lhs = multiscalar_multiply_edwards(
63+
&[PodScalar(*s_bytes), PodScalar(minus_challenge)],
64+
&[ED25519_BASEPOINT_COMPRESSED, public_key_point],
65+
)
66+
.ok_or(ProgramError::InvalidArgument)?;
67+
let difference = subtract_edwards(&lhs, &r_point).ok_or(ProgramError::InvalidArgument)?;
68+
let difference_cofactored =
69+
multiply_edwards(&EIGHT_SCALAR, &difference).ok_or(ProgramError::InvalidArgument)?;
70+
71+
if difference_cofactored != EDWARDS_IDENTITY_COMPRESSED {
72+
return Err(ProgramError::InvalidArgument);
73+
}
74+
75+
Ok(())
76+
}
77+
}
78+
79+
fn compute_challenge(signature_r: &[u8; 32], public_key: &[u8; 32], message: &[u8]) -> [u8; 32] {
80+
let digest = solana_sha512_hasher::hashv(&[signature_r, public_key, message]).to_bytes();
81+
scalar::reduce_wide(&digest)
82+
}

0 commit comments

Comments
 (0)