From 2413f8dc519603deb7995298a222a420f89cd47f Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Tue, 7 Nov 2023 14:20:07 +0100 Subject: [PATCH 1/3] solana-ibc: add Ed25519 support 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 | 249 ++++++++++++++++++ .../solana-ibc/programs/solana-ibc/src/lib.rs | 1 + 4 files changed, 256 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..7c73d6e2 --- /dev/null +++ b/solana/solana-ibc/programs/solana-ibc/src/ed25519.rs @@ -0,0 +1,249 @@ +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. +fn verify_impl( + data: &[u8], + message: &[u8], + pubkey: &[u8; PubKey::LENGTH], + signature: &[u8; Signature::LENGTH], +) -> bool { + // 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)) + .map(bytemuck::from_bytes::) + .any(|offsets| { + 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) + }) +} + +/// 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)] From 1a4de4335f2de3ec5ce7e34aed4a0a62af920976 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Tue, 7 Nov 2023 18:30:35 +0100 Subject: [PATCH 2/3] fix miri --- .../programs/solana-ibc/src/ed25519.rs | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/solana/solana-ibc/programs/solana-ibc/src/ed25519.rs b/solana/solana-ibc/programs/solana-ibc/src/ed25519.rs index 7c73d6e2..c32e5551 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/ed25519.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/ed25519.rs @@ -92,6 +92,15 @@ fn verify_impl( 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 @@ -102,14 +111,18 @@ fn verify_impl( .unwrap_or(&[]) .chunks_exact(core::mem::size_of::()) .take(usize::from(count)) - .map(bytemuck::from_bytes::) - .any(|offsets| { - 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) + .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)) + } }) } From a4f9b3766d057051f62744b5cf533e5507716f0c Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Tue, 7 Nov 2023 18:33:24 +0100 Subject: [PATCH 3/3] docs --- solana/solana-ibc/programs/solana-ibc/src/ed25519.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/solana/solana-ibc/programs/solana-ibc/src/ed25519.rs b/solana/solana-ibc/programs/solana-ibc/src/ed25519.rs index c32e5551..efdc4902 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/ed25519.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/ed25519.rs @@ -86,6 +86,9 @@ impl blockchain::Verifier for Verifier { /// 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],