diff --git a/Cargo.lock b/Cargo.lock index 445b601115..0db3b0264d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,6 +507,26 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e" +[[package]] +name = "bytemuck" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e215f8c2f9f79cb53c8335e687ffd07d5bfcb6fe5fc80723762d0be46e7cc54" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.9", + "syn 1.0.67", +] + [[package]] name = "byteorder" version = "1.4.3" @@ -4606,6 +4626,17 @@ dependencies = [ "tar", ] +[[package]] +name = "solana-ed25519-program" +version = "1.8.0" +dependencies = [ + "bytemuck", + "ed25519-dalek", + "rand 0.7.3", + "solana-logger 1.8.0", + "solana-sdk", +] + [[package]] name = "solana-entry" version = "1.8.0" @@ -5455,6 +5486,7 @@ dependencies = [ "serde_derive", "solana-compute-budget-program", "solana-config-program", + "solana-ed25519-program", "solana-frozen-abi 1.8.0", "solana-frozen-abi-macro 1.8.0", "solana-logger 1.8.0", @@ -5492,6 +5524,7 @@ dependencies = [ "borsh-derive", "bs58 0.4.0", "bv", + "bytemuck", "byteorder", "chrono", "curve25519-dalek 3.2.0", diff --git a/Cargo.toml b/Cargo.toml index b6df6f2ccb..212ed3c9cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ members = [ "programs/bpf_loader", "programs/compute-budget", "programs/config", + "programs/ed25519", "programs/failure", "programs/noop", "programs/ownable", diff --git a/docs/src/developing/runtime-facilities/programs.md b/docs/src/developing/runtime-facilities/programs.md index c66afe9841..5dd1722001 100644 --- a/docs/src/developing/runtime-facilities/programs.md +++ b/docs/src/developing/runtime-facilities/programs.md @@ -65,6 +65,48 @@ to the BPF Upgradeable Loader to process the instruction. [More information about deployment](cli/deploy-a-program.md) +## Ed25519 Program + +Verify ed25519 signature program. This program takes an ed25519 signature, public key, and message. +Multiple signatures can be verified. If any of the signatures fail to verify, an error is returned. + +- Program id: `Ed25519SigVerify111111111111111111111111111` +- Instructions: [new_ed25519_instruction](https://github.com/solana-labs/solana/blob/master/sdk/src/ed25519_instruction.rs#L31) + +The ed25519 program processes an instruction. The first `u8` is a count of the number of +signatures to check, which is followed by a single byte padding. After that, the +following struct is serialized, one for each signature to check. + +``` +struct Ed25519SignatureOffsets { + signature_offset: u16, // offset to ed25519 signature of 64 bytes + signature_instruction_index: u16, // instruction index to find signature + public_key_offset: u16, // offset to public key of 32 bytes + public_key_instruction_index: u16, // instruction index to find public key + message_data_offset: u16, // offset to start of message data + message_data_size: u16, // size of message data + message_instruction_index: u16, // index of instruction data to get message data +} +``` + +Pseudo code of the operation: + +``` +process_instruction() { + for i in 0..count { + // i'th index values referenced: + instructions = &transaction.message().instructions + signature = instructions[ed25519_signature_instruction_index].data[ed25519_signature_offset..ed25519_signature_offset + 64] + pubkey = instructions[ed25519_pubkey_instruction_index].data[ed25519_pubkey_offset..ed25519_pubkey_offset + 32] + message = instructions[ed25519_message_instruction_index].data[ed25519_message_data_offset..ed25519_message_data_offset + ed25519_message_data_size] + if pubkey.verify(signature, message) != Success { + return Error + } + } + return Success +} +``` + ## Secp256k1 Program Verify secp256k1 public key recovery operations (ecrecover). diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index 7125ba8154..ae60b23f52 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -287,6 +287,26 @@ dependencies = [ "serde", ] +[[package]] +name = "bytemuck" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e215f8c2f9f79cb53c8335e687ffd07d5bfcb6fe5fc80723762d0be46e7cc54" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.6", + "syn 1.0.67", +] + [[package]] name = "byteorder" version = "0.5.3" @@ -2898,6 +2918,13 @@ dependencies = [ "winapi", ] +[[package]] +name = "solana-ed25519-program" +version = "1.8.0" +dependencies = [ + "solana-sdk", +] + [[package]] name = "solana-faucet" version = "1.8.0" @@ -3209,6 +3236,7 @@ dependencies = [ "serde_derive", "solana-compute-budget-program", "solana-config-program", + "solana-ed25519-program", "solana-frozen-abi 1.8.0", "solana-frozen-abi-macro 1.8.0", "solana-logger 1.8.0", @@ -3237,6 +3265,7 @@ dependencies = [ "borsh-derive", "bs58 0.4.0", "bv", + "bytemuck", "byteorder 1.4.3", "chrono", "derivation-path", diff --git a/programs/ed25519/Cargo.toml b/programs/ed25519/Cargo.toml new file mode 100644 index 0000000000..cce6269bdd --- /dev/null +++ b/programs/ed25519/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "solana-ed25519-program" +description = "Solana Ed25519 program" +version = "1.8.0" +homepage = "https://solana.com/" +documentation = "https://docs.rs/solana-ed25519-program" +repository = "https://github.com/solana-labs/solana" +authors = ["Solana Maintainers "] +license = "Apache-2.0" +edition = "2018" + +[dependencies] +solana-sdk = { path = "../../sdk", version = "=1.8.0" } + +[dev-dependencies] +bytemuck = { version = "1.7.2", features = ["derive"] } +ed25519-dalek = "=1.0.1" +rand = "0.7.0" +solana-logger = { path = "../../logger", version = "=1.8.0" } + +[lib] +crate-type = ["lib"] +name = "solana_ed25519_program" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/programs/ed25519/src/lib.rs b/programs/ed25519/src/lib.rs new file mode 100644 index 0000000000..da579fb1d1 --- /dev/null +++ b/programs/ed25519/src/lib.rs @@ -0,0 +1,55 @@ +use solana_sdk::{ + instruction::InstructionError, process_instruction::InvokeContext, pubkey::Pubkey, +}; + +pub fn process_instruction( + _program_id: &Pubkey, + _data: &[u8], + _invoke_context: &mut dyn InvokeContext, +) -> Result<(), InstructionError> { + // Should be already checked by now. + Ok(()) +} + +#[cfg(test)] +pub mod test { + use rand::{thread_rng, Rng}; + use solana_sdk::{ + ed25519_instruction::new_ed25519_instruction, + feature_set::FeatureSet, + hash::Hash, + signature::{Keypair, Signer}, + transaction::Transaction, + }; + use std::sync::Arc; + + #[test] + fn test_ed25519() { + solana_logger::setup(); + + let privkey = ed25519_dalek::Keypair::generate(&mut thread_rng()); + let message_arr = b"hello"; + let mut instruction = new_ed25519_instruction(&privkey, message_arr); + let mint_keypair = Keypair::new(); + let feature_set = Arc::new(FeatureSet::all_enabled()); + + let tx = Transaction::new_signed_with_payer( + &[instruction.clone()], + Some(&mint_keypair.pubkey()), + &[&mint_keypair], + Hash::default(), + ); + + assert!(tx.verify_precompiles(&feature_set).is_ok()); + + let index = thread_rng().gen_range(0, instruction.data.len()); + instruction.data[index] = instruction.data[index].wrapping_add(12); + let tx = Transaction::new_signed_with_payer( + &[instruction], + Some(&mint_keypair.pubkey()), + &[&mint_keypair], + Hash::default(), + ); + assert!(tx.verify_precompiles(&feature_set).is_err()); + } +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index efb309b055..4884270f07 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -36,6 +36,7 @@ serde = { version = "1.0.130", features = ["rc"] } serde_derive = "1.0.103" solana-config-program = { path = "../programs/config", version = "=1.8.0" } solana-compute-budget-program = { path = "../programs/compute-budget", version = "=1.8.0" } +solana-ed25519-program = { path = "../programs/ed25519", version = "=1.8.0" } solana-frozen-abi = { path = "../frozen-abi", version = "=1.8.0" } solana-frozen-abi-macro = { path = "../frozen-abi/macro", version = "=1.8.0" } solana-logger = { path = "../logger", version = "=1.8.0" } diff --git a/runtime/src/builtins.rs b/runtime/src/builtins.rs index b7c7aaf24c..e0e59a2a69 100644 --- a/runtime/src/builtins.rs +++ b/runtime/src/builtins.rs @@ -135,15 +135,26 @@ fn genesis_builtins() -> Vec { /// normal child Bank creation. /// https://github.com/solana-labs/solana/blob/84b139cc94b5be7c9e0c18c2ad91743231b85a0d/runtime/src/bank.rs#L1723 fn feature_builtins() -> Vec<(Builtin, Pubkey, ActivationType)> { - vec![( - Builtin::new( - "compute_budget_program", - solana_sdk::compute_budget::id(), - solana_compute_budget_program::process_instruction, + vec![ + ( + Builtin::new( + "compute_budget_program", + solana_sdk::compute_budget::id(), + solana_compute_budget_program::process_instruction, + ), + feature_set::tx_wide_compute_cap::id(), + ActivationType::NewProgram, ), - feature_set::tx_wide_compute_cap::id(), - ActivationType::NewProgram, - )] + ( + Builtin::new( + "ed25519_program", + solana_sdk::ed25519_program::id(), + solana_ed25519_program::process_instruction, + ), + feature_set::ed25519_program_enabled::id(), + ActivationType::NewProgram, + ), + ] } pub(crate) fn get() -> Builtins { diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 2ebb2c9bc6..21a56867d9 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -40,6 +40,7 @@ full = [ [dependencies] assert_matches = { version = "1.5.0", optional = true } bincode = "1.3.3" +bytemuck = { version = "1.7.2", features = ["derive"] } borsh = "0.9.0" borsh-derive = "0.9.0" bs58 = "0.4.0" diff --git a/sdk/program/src/ed25519_program.rs b/sdk/program/src/ed25519_program.rs new file mode 100644 index 0000000000..6301e3ab6c --- /dev/null +++ b/sdk/program/src/ed25519_program.rs @@ -0,0 +1 @@ +crate::declare_id!("Ed25519SigVerify111111111111111111111111111"); diff --git a/sdk/program/src/fee_calculator.rs b/sdk/program/src/fee_calculator.rs index af448e9ef0..f7855b105f 100644 --- a/sdk/program/src/fee_calculator.rs +++ b/sdk/program/src/fee_calculator.rs @@ -1,5 +1,6 @@ #![allow(clippy::integer_arithmetic)] use crate::clock::DEFAULT_MS_PER_SLOT; +use crate::ed25519_program; use crate::message::Message; use crate::secp256k1_program; use log::*; @@ -32,20 +33,22 @@ impl FeeCalculator { note = "Please do not use, will no longer be available in the future" )] pub fn calculate_fee(&self, message: &Message) -> u64 { - let mut num_secp256k1_signatures: u64 = 0; + let mut num_signatures: u64 = 0; for instruction in &message.instructions { let program_index = instruction.program_id_index as usize; // Message may not be sanitized here if program_index < message.account_keys.len() { let id = message.account_keys[program_index]; - if secp256k1_program::check_id(&id) && !instruction.data.is_empty() { - num_secp256k1_signatures += instruction.data[0] as u64; + if (secp256k1_program::check_id(&id) || ed25519_program::check_id(&id)) + && !instruction.data.is_empty() + { + num_signatures += instruction.data[0] as u64; } } } self.lamports_per_signature - * (u64::from(message.header.num_required_signatures) + num_secp256k1_signatures) + * (u64::from(message.header.num_required_signatures) + num_signatures) } } diff --git a/sdk/program/src/lib.rs b/sdk/program/src/lib.rs index c8894aff95..6e8a5c85ba 100644 --- a/sdk/program/src/lib.rs +++ b/sdk/program/src/lib.rs @@ -13,6 +13,7 @@ pub mod bpf_loader_deprecated; pub mod bpf_loader_upgradeable; pub mod clock; pub mod decode_error; +pub mod ed25519_program; pub mod entrypoint; pub mod entrypoint_deprecated; pub mod epoch_schedule; diff --git a/sdk/src/ed25519_instruction.rs b/sdk/src/ed25519_instruction.rs new file mode 100644 index 0000000000..b8e814e3e4 --- /dev/null +++ b/sdk/src/ed25519_instruction.rs @@ -0,0 +1,328 @@ +#![cfg(feature = "full")] + +use crate::{decode_error::DecodeError, instruction::Instruction}; +use bytemuck::{bytes_of, Pod, Zeroable}; +use ed25519_dalek::{ed25519::signature::Signature, Signer, Verifier}; +use thiserror::Error; + +#[derive(Error, Debug, Clone, PartialEq)] +pub enum Ed25519Error { + #[error("ed25519 public key is not valid")] + InvalidPublicKey, + #[error("ed25519 signature is not valid")] + InvalidSignature, + #[error("offset not valid")] + InvalidDataOffsets, + #[error("instruction is incorrect size")] + InvalidInstructionDataSize, +} + +impl DecodeError for Ed25519Error { + fn type_of() -> &'static str { + "Ed25519Error" + } +} + +pub const PUBKEY_SERIALIZED_SIZE: usize = 32; +pub const SIGNATURE_SERIALIZED_SIZE: usize = 64; +pub const SIGNATURE_OFFSETS_SERIALIZED_SIZE: usize = 14; +// bytemuck requires structures to be aligned +pub const SIGNATURE_OFFSETS_START: usize = 2; +pub const DATA_START: usize = SIGNATURE_OFFSETS_SERIALIZED_SIZE + SIGNATURE_OFFSETS_START; + +#[derive(Default, Debug, Copy, Clone, Zeroable, Pod)] +#[repr(C)] +pub struct Ed25519SignatureOffsets { + signature_offset: u16, // offset to ed25519 signature of 64 bytes + signature_instruction_index: u16, // instruction index to find signature + public_key_offset: u16, // offset to public key of 32 bytes + public_key_instruction_index: u16, // instruction index to find public key + message_data_offset: u16, // offset to start of message data + message_data_size: u16, // size of message data + message_instruction_index: u16, // index of instruction data to get message data +} + +pub fn new_ed25519_instruction(keypair: &ed25519_dalek::Keypair, message: &[u8]) -> Instruction { + let signature = keypair.sign(message).to_bytes(); + let pubkey = keypair.public.to_bytes(); + + assert_eq!(pubkey.len(), PUBKEY_SERIALIZED_SIZE); + assert_eq!(signature.len(), SIGNATURE_SERIALIZED_SIZE); + + let mut instruction_data = Vec::with_capacity( + DATA_START + .saturating_add(SIGNATURE_SERIALIZED_SIZE) + .saturating_add(PUBKEY_SERIALIZED_SIZE) + .saturating_add(message.len()), + ); + + let num_signatures: u8 = 1; + let public_key_offset = DATA_START; + let signature_offset = public_key_offset.saturating_add(PUBKEY_SERIALIZED_SIZE); + let message_data_offset = signature_offset.saturating_add(SIGNATURE_SERIALIZED_SIZE); + + // add padding byte so that offset structure is aligned + instruction_data.extend_from_slice(bytes_of(&[num_signatures, 0])); + + let offsets = Ed25519SignatureOffsets { + signature_offset: signature_offset as u16, + signature_instruction_index: 0, + public_key_offset: public_key_offset as u16, + public_key_instruction_index: 0, + message_data_offset: message_data_offset as u16, + message_data_size: message.len() as u16, + message_instruction_index: 0, + }; + + instruction_data.extend_from_slice(bytes_of(&offsets)); + + debug_assert_eq!(instruction_data.len(), public_key_offset); + + instruction_data.extend_from_slice(&pubkey); + + debug_assert_eq!(instruction_data.len(), signature_offset); + + instruction_data.extend_from_slice(&signature); + + debug_assert_eq!(instruction_data.len(), message_data_offset); + + instruction_data.extend_from_slice(message); + + Instruction { + program_id: solana_sdk::ed25519_program::id(), + accounts: vec![], + data: instruction_data, + } +} + +pub fn verify_signatures(data: &[u8], instruction_datas: &[&[u8]]) -> Result<(), Ed25519Error> { + if data.len() < SIGNATURE_OFFSETS_START { + return Err(Ed25519Error::InvalidInstructionDataSize); + } + let num_signatures = data[0] as usize; + if num_signatures == 0 && data.len() > SIGNATURE_OFFSETS_START { + return Err(Ed25519Error::InvalidInstructionDataSize); + } + let expected_data_size = num_signatures + .saturating_mul(SIGNATURE_OFFSETS_SERIALIZED_SIZE) + .saturating_add(SIGNATURE_OFFSETS_START); + if data.len() < expected_data_size { + return Err(Ed25519Error::InvalidInstructionDataSize); + } + for i in 0..num_signatures { + let start = i + .saturating_mul(SIGNATURE_OFFSETS_SERIALIZED_SIZE) + .saturating_add(SIGNATURE_OFFSETS_START); + let end = start.saturating_add(SIGNATURE_OFFSETS_SERIALIZED_SIZE); + + // bytemuck wants structures aligned + let offsets: &Ed25519SignatureOffsets = bytemuck::try_from_bytes(&data[start..end]) + .map_err(|_| Ed25519Error::InvalidDataOffsets)?; + + // Parse out signature + let signature_index = offsets.signature_instruction_index as usize; + if signature_index >= instruction_datas.len() { + return Err(Ed25519Error::InvalidDataOffsets); + } + let signature_instruction = instruction_datas[signature_index]; + let sig_start = offsets.signature_offset as usize; + let sig_end = sig_start.saturating_add(SIGNATURE_SERIALIZED_SIZE); + if sig_end >= signature_instruction.len() { + return Err(Ed25519Error::InvalidDataOffsets); + } + + let signature = + ed25519_dalek::Signature::from_bytes(&signature_instruction[sig_start..sig_end]) + .map_err(|_| Ed25519Error::InvalidSignature)?; + + // Parse out pubkey + let pubkey = get_data_slice( + instruction_datas, + offsets.public_key_instruction_index, + offsets.public_key_offset, + PUBKEY_SERIALIZED_SIZE, + )?; + + let publickey = ed25519_dalek::PublicKey::from_bytes(pubkey) + .map_err(|_| Ed25519Error::InvalidPublicKey)?; + + // Parse out message + let message = get_data_slice( + instruction_datas, + offsets.message_instruction_index, + offsets.message_data_offset, + offsets.message_data_size as usize, + )?; + + publickey + .verify(message, &signature) + .map_err(|_| Ed25519Error::InvalidSignature)?; + } + Ok(()) +} + +fn get_data_slice<'a>( + instruction_datas: &'a [&[u8]], + instruction_index: u16, + offset_start: u16, + size: usize, +) -> Result<&'a [u8], Ed25519Error> { + let signature_index = instruction_index as usize; + if signature_index >= instruction_datas.len() { + return Err(Ed25519Error::InvalidDataOffsets); + } + let signature_instruction = &instruction_datas[signature_index]; + let start = offset_start as usize; + let end = start.saturating_add(size); + if end > signature_instruction.len() { + return Err(Ed25519Error::InvalidDataOffsets); + } + + Ok(&instruction_datas[signature_index][start..end]) +} + +#[cfg(test)] +pub mod test { + use super::*; + + fn test_case( + num_signatures: u16, + offsets: &Ed25519SignatureOffsets, + ) -> Result<(), Ed25519Error> { + assert_eq!( + bytemuck::bytes_of(offsets).len(), + SIGNATURE_OFFSETS_SERIALIZED_SIZE + ); + + let mut instruction_data = vec![0u8; DATA_START]; + instruction_data[0..SIGNATURE_OFFSETS_START].copy_from_slice(bytes_of(&num_signatures)); + instruction_data[SIGNATURE_OFFSETS_START..DATA_START].copy_from_slice(bytes_of(offsets)); + + verify_signatures(&instruction_data, &[&[0u8; 100]]) + } + + #[test] + fn test_invalid_offsets() { + solana_logger::setup(); + + let mut instruction_data = vec![0u8; DATA_START]; + let offsets = Ed25519SignatureOffsets::default(); + instruction_data[0..SIGNATURE_OFFSETS_START].copy_from_slice(bytes_of(&1u16)); + instruction_data[SIGNATURE_OFFSETS_START..DATA_START].copy_from_slice(bytes_of(&offsets)); + instruction_data.truncate(instruction_data.len() - 1); + + assert_eq!( + verify_signatures(&instruction_data, &[&[0u8; 100]]), + Err(Ed25519Error::InvalidInstructionDataSize) + ); + + let offsets = Ed25519SignatureOffsets { + signature_instruction_index: 1, + ..Ed25519SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(Ed25519Error::InvalidDataOffsets) + ); + + let offsets = Ed25519SignatureOffsets { + message_instruction_index: 1, + ..Ed25519SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(Ed25519Error::InvalidDataOffsets) + ); + + let offsets = Ed25519SignatureOffsets { + public_key_instruction_index: 1, + ..Ed25519SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(Ed25519Error::InvalidDataOffsets) + ); + } + + #[test] + fn test_message_data_offsets() { + let offsets = Ed25519SignatureOffsets { + message_data_offset: 99, + message_data_size: 1, + ..Ed25519SignatureOffsets::default() + }; + assert_eq!(test_case(1, &offsets), Err(Ed25519Error::InvalidSignature)); + + let offsets = Ed25519SignatureOffsets { + message_data_offset: 100, + message_data_size: 1, + ..Ed25519SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(Ed25519Error::InvalidDataOffsets) + ); + + let offsets = Ed25519SignatureOffsets { + message_data_offset: 100, + message_data_size: 1000, + ..Ed25519SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(Ed25519Error::InvalidDataOffsets) + ); + + let offsets = Ed25519SignatureOffsets { + message_data_offset: std::u16::MAX, + message_data_size: std::u16::MAX, + ..Ed25519SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(Ed25519Error::InvalidDataOffsets) + ); + } + + #[test] + fn test_pubkey_offset() { + let offsets = Ed25519SignatureOffsets { + public_key_offset: std::u16::MAX, + ..Ed25519SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(Ed25519Error::InvalidDataOffsets) + ); + + let offsets = Ed25519SignatureOffsets { + public_key_offset: 100 - PUBKEY_SERIALIZED_SIZE as u16 + 1, + ..Ed25519SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(Ed25519Error::InvalidDataOffsets) + ); + } + + #[test] + fn test_signature_offset() { + let offsets = Ed25519SignatureOffsets { + signature_offset: std::u16::MAX, + ..Ed25519SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(Ed25519Error::InvalidDataOffsets) + ); + + let offsets = Ed25519SignatureOffsets { + signature_offset: 100 - SIGNATURE_SERIALIZED_SIZE as u16 + 1, + ..Ed25519SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(Ed25519Error::InvalidDataOffsets) + ); + } +} diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index fd3313e66f..f2b1748460 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -195,6 +195,10 @@ pub mod demote_program_write_locks { solana_sdk::declare_id!("3E3jV7v9VcdJL8iYZUMax9DiDno8j7EWUVbhm9RtShj2"); } +pub mod ed25519_program_enabled { + solana_sdk::declare_id!("E1TvTNipX8TKNHrhRC8SMuAwQmGY58TZ4drdztP3Gxwc"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -234,11 +238,12 @@ lazy_static! { (gate_large_block::id(), "validator checks block cost against max limit in realtime, reject if exceeds."), (mem_overlap_fix::id(), "memory overlap fix"), (versioned_tx_message_enabled::id(), "enable versioned transaction message processing"), - (libsecp256k1_fail_on_bad_count::id(), "Fail libsec256k1_verify if count appears wrong"), + (libsecp256k1_fail_on_bad_count::id(), "fail libsec256k1_verify if count appears wrong"), (instructions_sysvar_owned_by_sysvar::id(), "fix owner for instructions sysvar"), (close_upgradeable_program_accounts::id(), "enable closing upgradeable program accounts"), (stake_program_advance_activating_credits_observed::id(), "Enable advancing credits observed for activation epoch #19309"), (demote_program_write_locks::id(), "demote program write locks to readonly #19593"), + (ed25519_program_enabled::id(), "enable builtin ed25519 signature verify program"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 15492a534a..cb2558587a 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -17,6 +17,7 @@ pub mod commitment_config; pub mod compute_budget; pub mod derivation_path; pub mod deserialize_utils; +pub mod ed25519_instruction; pub mod entrypoint; pub mod entrypoint_deprecated; pub mod entrypoint_native; diff --git a/sdk/src/transaction/mod.rs b/sdk/src/transaction/mod.rs index d24d64753e..14e4217f68 100644 --- a/sdk/src/transaction/mod.rs +++ b/sdk/src/transaction/mod.rs @@ -4,6 +4,7 @@ use { crate::{ + ed25519_instruction::verify_signatures, hash::Hash, instruction::{CompiledInstruction, Instruction, InstructionError}, message::{Message, SanitizeMessageError}, @@ -450,6 +451,18 @@ impl Transaction { feature_set.is_active(&feature_set::libsecp256k1_fail_on_bad_count::id()), ); e.map_err(|_| TransactionError::InvalidAccountIndex)?; + } else if crate::ed25519_program::check_id(program_id) + && feature_set.is_active(&feature_set::ed25519_program_enabled::id()) + { + let instruction_datas: Vec<_> = self + .message() + .instructions + .iter() + .map(|instruction| instruction.data.as_ref()) + .collect(); + let data = &instruction.data; + let e = verify_signatures(data, &instruction_datas); + e.map_err(|_| TransactionError::InvalidAccountIndex)?; } } Ok(()) diff --git a/sdk/src/transaction/sanitized.rs b/sdk/src/transaction/sanitized.rs index e7e38c8519..e638a46240 100644 --- a/sdk/src/transaction/sanitized.rs +++ b/sdk/src/transaction/sanitized.rs @@ -2,6 +2,7 @@ use { crate::{ + ed25519_instruction::verify_signatures, hash::Hash, message::{v0, MappedAddresses, MappedMessage, SanitizedMessage, VersionedMessage}, nonce::NONCED_TX_MARKER_IX_INDEX, @@ -222,6 +223,18 @@ impl SanitizedTransaction { feature_set.is_active(&feature_set::libsecp256k1_fail_on_bad_count::id()), ); e.map_err(|_| TransactionError::InvalidAccountIndex)?; + } else if crate::ed25519_program::check_id(program_id) + && feature_set.is_active(&feature_set::ed25519_program_enabled::id()) + { + let instruction_datas: Vec<_> = self + .message() + .instructions() + .iter() + .map(|instruction| instruction.data.as_ref()) + .collect(); + let data = &instruction.data; + let e = verify_signatures(data, &instruction_datas); + e.map_err(|_| TransactionError::InvalidAccountIndex)?; } } Ok(())