From 8b9e472a6cc31decf0d064901c7b13c37741355f Mon Sep 17 00:00:00 2001 From: Sean Young Date: Fri, 3 Sep 2021 22:35:38 +0100 Subject: [PATCH] feat: add ed25519 signature verify program Solang requires a method for verify ed25519 signatures. Add a new builtin program at address Ed25519SigVerify111111111111111111111111111 which takes any number of ed25519 signature, public key, and message. If any of the signatures fails to verify, an error is returned. The changes for the web3.js package will go into another commit, since the tests test against a released solana node. Adding web3.js ed25519 testing will break CI. --- Cargo.lock | 33 ++ Cargo.toml | 1 + .../developing/runtime-facilities/programs.md | 42 +++ programs/bpf/Cargo.lock | 29 ++ programs/ed25519/Cargo.toml | 26 ++ programs/ed25519/src/lib.rs | 55 +++ runtime/Cargo.toml | 1 + runtime/src/builtins.rs | 27 +- sdk/Cargo.toml | 1 + sdk/program/src/ed25519_program.rs | 1 + sdk/program/src/fee_calculator.rs | 11 +- sdk/program/src/lib.rs | 1 + sdk/src/ed25519_instruction.rs | 328 ++++++++++++++++++ sdk/src/feature_set.rs | 7 +- sdk/src/lib.rs | 1 + sdk/src/transaction/mod.rs | 13 + sdk/src/transaction/sanitized.rs | 13 + 17 files changed, 577 insertions(+), 13 deletions(-) create mode 100644 programs/ed25519/Cargo.toml create mode 100644 programs/ed25519/src/lib.rs create mode 100644 sdk/program/src/ed25519_program.rs create mode 100644 sdk/src/ed25519_instruction.rs 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(())