From 6488fa96fa45d9f39f00815e75ab8e037fd6da29 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Tue, 7 Nov 2023 18:50:23 +0100 Subject: [PATCH] solana-ibc: add Ed25519 support (#82) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement Ed25519 verification using Solana’s native program. For now the code is unused but will be needed for checking validator’s signatures. No signing code is implemented since that’s never done on-chain. --- Cargo.toml | 2 + .../solana-ibc/programs/solana-ibc/Cargo.toml | 5 +- .../programs/solana-ibc/src/ed25519.rs | 265 ++++++++++++++++++ .../solana-ibc/programs/solana-ibc/src/lib.rs | 1 + 4 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 solana/solana-ibc/programs/solana-ibc/src/ed25519.rs diff --git a/Cargo.toml b/Cargo.toml index 5a08c245..0f485c8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ anchor-lang = {version = "0.28.0", features = ["init-if-needed"]} ascii = "1.1.0" base64 = { version = "0.21", default-features = false, features = ["alloc"] } borsh = { version = "0.10.3", default-features = false } +bytemuck = { version = "1.14", default-features = false } derive_more = "0.99.17" hex-literal = "0.4.1" ibc = { version = "0.47.0", default-features = false, features = ["serde", "borsh"] } @@ -41,6 +42,7 @@ solana-program = "1.16.14" solana-sdk = "1.16.14" strum = { version = "0.25.0", default-features = false, features = ["derive"] } +blockchain = { path = "common/blockchain" } lib = { path = "common/lib" } memory = { path = "common/memory" } sealable-trie = { path = "common/sealable-trie" } diff --git a/solana/solana-ibc/programs/solana-ibc/Cargo.toml b/solana/solana-ibc/programs/solana-ibc/Cargo.toml index c81238b7..5508b968 100644 --- a/solana/solana-ibc/programs/solana-ibc/Cargo.toml +++ b/solana/solana-ibc/programs/solana-ibc/Cargo.toml @@ -17,12 +17,15 @@ mocks = ["ibc/mocks", "ibc/std"] [dependencies] anchor-lang.workspace = true -ibc.workspace = true +base64.workspace = true +bytemuck.workspace = true ibc-proto.workspace = true +ibc.workspace = true serde.workspace = true serde_json.workspace = true strum.workspace = true +blockchain.workspace = true lib.workspace = true memory.workspace = true solana-trie.workspace = true diff --git a/solana/solana-ibc/programs/solana-ibc/src/ed25519.rs b/solana/solana-ibc/programs/solana-ibc/src/ed25519.rs new file mode 100644 index 00000000..efdc4902 --- /dev/null +++ b/solana/solana-ibc/programs/solana-ibc/src/ed25519.rs @@ -0,0 +1,265 @@ +use anchor_lang::prelude::{borsh, err}; +use anchor_lang::solana_program::account_info::AccountInfo; +use anchor_lang::solana_program::{ed25519_program, sysvar}; + +/// An Ed25519 public key used by guest validators to sign guest blocks. +#[derive( + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + borsh::BorshSerialize, + borsh::BorshDeserialize, +)] +pub struct PubKey([u8; Self::LENGTH]); + +impl PubKey { + pub const LENGTH: usize = 32; +} + +impl blockchain::PubKey for PubKey { + type Signature = Signature; +} + +/// A Ed25519 signature of a guest block. +#[derive( + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + borsh::BorshSerialize, + borsh::BorshDeserialize, +)] +pub struct Signature([u8; Self::LENGTH]); + +impl Signature { + pub const LENGTH: usize = 64; +} + +/// Implementation for validating Ed25519 signatures. +/// +/// Due to Solana’s weirdness this needs to be a stateful object holding account +/// information of the [Instruction sysvar]. The assumption is that instruction +/// just before the currently executed one is a call to the [Ed25519 native +/// program] which verified signatures. +/// +/// [Instruction sysvar]: https://docs.solana.com/developing/runtime-facilities/sysvars#instructions +/// [Ed25519 native program]: https://docs.solana.com/developing/runtime-facilities/programs#ed25519-program +pub struct Verifier(Vec); + +impl Verifier { + /// Constructs the versifier from the Instruction sysvar `AccountInfo`. + /// + /// Fetches instruction the one before the current one and verifies if it’s + /// a call to Ed25519 native program. If it is, stores that instruction’s + /// data to later use for signature verification. + /// + /// Returns error if `ix_sysver` is not `AccountInfo` for the Instruction + /// sysvar, there was no instruction prior to the current on or the previous + /// instruction was not a call to the Ed25519 native program. + #[allow(dead_code)] + pub fn new(ix_sysvar: &AccountInfo<'_>) -> anchor_lang::Result { + let ix = sysvar::instructions::get_instruction_relative(-1, ix_sysvar)?; + if ed25519_program::check_id(&ix.program_id) { + Ok(Self(ix.data)) + } else { + err!(anchor_lang::error::ErrorCode::InstructionMissing) + } + } +} + +impl blockchain::Verifier for Verifier { + #[inline] + fn verify( + &self, + message: &[u8], + pubkey: &PubKey, + signature: &Signature, + ) -> bool { + verify_impl(self.0.as_slice(), message, &pubkey.0, &signature.0) + } +} + +/// Parses the `data` as instruction data for the Ed25519 native program and +/// checks whether the call included verification of the given signature. +/// +/// `data` must be aligned to two bytes. This is in practice guaranteed if data +/// comes from a vector or in general points at a beginning of an allocation. +fn verify_impl( + data: &[u8], + message: &[u8], + pubkey: &[u8; PubKey::LENGTH], + signature: &[u8; Signature::LENGTH], +) -> bool { + let check = |offsets: &SignatureOffsets| { + offsets.signature_instruction_index == u16::MAX && + offsets.public_key_instruction_index == u16::MAX && + offsets.message_instruction_index == u16::MAX && + offsets.signature(data) == Some(&signature[..]) && + offsets.public_key(data) == Some(&pubkey[..]) && + offsets.message(data) == Some(message) + }; + + // The instruction data is: + // count: u8 + // unused: u8 + // offsets: [SignatureOffsets; count] + // rest: [u8] + let count = data.first().copied().unwrap_or_default(); + data.get(2..) + .unwrap_or(&[]) + .chunks_exact(core::mem::size_of::()) + .take(usize::from(count)) + .any(|chunk| { + // Solana SDK uses bytemuck::try_from_bytes to cast instruction data + // directly into the offsets structure. In practice vectors data is + // aligned to two bytes so this always works. However, MIRI doesn’t + // like that. I’m not ready to give up on bytemuck::from_bytes just + // yet so we’re using conditional compilation to make MIRI happy and + // avoid unaligned reads in production. + if cfg!(miri) { + check(&bytemuck::pod_read_unaligned(chunk)) + } else { + check(bytemuck::from_bytes(chunk)) + } + }) +} + +/// SignatureOffsets used by the Ed25519 native program in its instruction data. +/// +/// This is copied from Solana SDK. See +/// https://github.com/solana-labs/solana/blob/master/sdk/src/ed25519_instruction.rs +#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +struct SignatureOffsets { + /// Offset to ed25519 signature of 64 bytes. + signature_offset: u16, + /// Instruction index to find signature. We support `u16::MAX` only. + signature_instruction_index: u16, + /// Offset to public key of 32 bytes. + public_key_offset: u16, + /// Instruction index to find public key. We support `u16::MAX` only. + public_key_instruction_index: u16, + /// Offset to start of message data + message_data_offset: u16, + /// Size of message data. + message_data_size: u16, + /// Index of instruction data to get message data. We support `u16::MAX` + /// only. + message_instruction_index: u16, +} + +impl SignatureOffsets { + fn signature<'a>(&self, data: &'a [u8]) -> Option<&'a [u8]> { + Self::get(data, self.signature_offset, Signature::LENGTH) + } + + fn public_key<'a>(&self, data: &'a [u8]) -> Option<&'a [u8]> { + Self::get(data, self.public_key_offset, PubKey::LENGTH) + } + + fn message<'a>(&self, data: &'a [u8]) -> Option<&'a [u8]> { + Self::get( + data, + self.message_data_offset, + usize::from(self.message_data_size), + ) + } + + fn get(data: &[u8], start: u16, length: usize) -> Option<&[u8]> { + let start = usize::from(start); + data.get(start..(start + length)) + } +} + +impl core::fmt::Display for PubKey { + #[inline] + fn fmt(&self, fmtr: &mut core::fmt::Formatter) -> core::fmt::Result { + <&lib::hash::CryptoHash>::from(&self.0).fmt(fmtr) + } +} + +impl core::fmt::Debug for PubKey { + #[inline] + fn fmt(&self, fmtr: &mut core::fmt::Formatter) -> core::fmt::Result { + core::fmt::Display::fmt(self, fmtr) + } +} + +impl core::fmt::Display for Signature { + fn fmt(&self, fmtr: &mut core::fmt::Formatter) -> core::fmt::Result { + use base64::engine::general_purpose::STANDARD as BASE64_ENGINE; + use base64::Engine; + + const ENCODED_LENGTH: usize = (Signature::LENGTH + 2) / 3 * 4; + let mut buf = [0u8; ENCODED_LENGTH]; + let len = BASE64_ENGINE + .encode_slice(self.0.as_slice(), &mut buf[..]) + .unwrap(); + // SAFETY: base64 fills the buffer with ASCII characters only. + fmtr.write_str(unsafe { core::str::from_utf8_unchecked(&buf[..len]) }) + } +} + +impl core::fmt::Debug for Signature { + #[inline] + fn fmt(&self, fmtr: &mut core::fmt::Formatter) -> core::fmt::Result { + core::fmt::Display::fmt(self, fmtr) + } +} + +#[test] +fn test_verify() { + use blockchain::Verifier; + + // Construct signatures. + let pk = PubKey([128; 32]); + let msg1 = &b"hello, world"[..]; + let sig1 = Signature([1; 64]); + let msg2 = &b"Hello, world!"[..]; + let sig2 = Signature([2; 64]); + + // Constructs the Ed25519 program instruction data. + let mut data = vec![0; 2 + core::mem::size_of::() * 2]; + + let push = |data: &mut Vec, slice: &[u8]| { + let offset = u16::try_from(data.len()).unwrap(); + let len = u16::try_from(slice.len()).unwrap(); + data.extend_from_slice(slice); + (offset, len) + }; + + let (public_key_offset, _) = push(&mut data, &pk.0); + + for (sig, msg) in [(&sig1, msg1), (&sig2, msg2)] { + let (signature_offset, _) = push(&mut data, &sig.0); + let (message_data_offset, message_data_size) = push(&mut data, msg); + + let header = SignatureOffsets { + signature_offset, + signature_instruction_index: u16::MAX, + public_key_offset, + public_key_instruction_index: u16::MAX, + message_data_offset, + message_data_size, + message_instruction_index: u16::MAX, + }; + let header = bytemuck::bytes_of(&header); + let start = 2 + usize::from(data[0]) * header.len(); + data[start..start + header.len()].copy_from_slice(header); + data[0] += 1; + } + + // Test verification + let verifier = Verifier(data); + assert!(verifier.verify(msg1, &pk, &sig1)); + assert!(verifier.verify(msg2, &pk, &sig2)); + // Wrong signature + assert!(!verifier.verify(msg1, &pk, &sig2)); + // Wrong public key + assert!(!verifier.verify(msg1, &PubKey([129; 32]), &sig1)); +} diff --git a/solana/solana-ibc/programs/solana-ibc/src/lib.rs b/solana/solana-ibc/programs/solana-ibc/src/lib.rs index b59c092f..c73d4364 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/lib.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/lib.rs @@ -20,6 +20,7 @@ declare_id!("EnfDJsAK7BGgetnmKzBx86CsgC5kfSPcsktFCQ4YLC81"); mod client_state; mod consensus_state; +mod ed25519; mod execution_context; mod storage; #[cfg(test)]