diff --git a/Cargo.lock b/Cargo.lock index 65d341bf..bfa1a78e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5706,6 +5706,7 @@ dependencies = [ "base64 0.21.7", "bytemuck", "cf-guest", + "cf-solana", "derive_more", "guestchain", "hex-literal", diff --git a/common/cf-solana/Cargo.toml b/common/cf-solana/Cargo.toml index 169b06f0..cc265624 100644 --- a/common/cf-solana/Cargo.toml +++ b/common/cf-solana/Cargo.toml @@ -22,7 +22,7 @@ serde = { workspace = true, optional = true } solana-program = { workspace = true, optional = true } cf-guest.workspace = true -lib = { workspace = true, features = ["bs58", "serde"] } +lib = { workspace = true, features = ["bs58"] } proto-utils = { workspace = true, features = ["ibc"] } stdx.workspace = true trie-ids.workspace = true @@ -43,7 +43,12 @@ solana-program-2 = { package = "solana-program", git = "https://github.com/Compo lib = { workspace = true, features = ["test_utils"] } [features] +no-blake3-syscall = [] rayon = ["lib/rayon"] +serde = [ + "dep:serde", + "lib/serde", +] solana-program = [ "dep:solana-program", "lib/solana-program", diff --git a/common/cf-solana/src/blake3.rs b/common/cf-solana/src/blake3.rs index fc4013ab..977ca569 100644 --- a/common/cf-solana/src/blake3.rs +++ b/common/cf-solana/src/blake3.rs @@ -2,17 +2,19 @@ pub use ::blake3::Hasher; use crate::types::Hash; +const CONSIDER_SOL: bool = + !cfg!(feature = "no-blake3-syscall") && cfg!(target_os = "solana-program"); +const HAS_SOL: bool = + cfg!(feature = "solana-program") || cfg!(feature = "solana-program-2"); +const USE_SOL: bool = CONSIDER_SOL && HAS_SOL; + /// Calculates Blake3 hash of given byte slice. /// /// When `solana-program` or `solana-program-2` feature is enabled and /// building a solana program, this is using Solana’s `sol_blake3` syscall. /// Otherwise, the calculation is done by `blake3` crate. -#[allow(dead_code)] pub fn hash(bytes: &[u8]) -> Hash { - if cfg!(target_os = "solana-program") && - (cfg!(feature = "solana-program") || - cfg!(feature = "solana-program-2")) - { + if USE_SOL { hashv(&[bytes]) } else { Hash(::blake3::hash(bytes).into()) @@ -24,23 +26,18 @@ pub fn hash(bytes: &[u8]) -> Hash { /// When `solana` or `solana2` feature is enabled and building a Solana /// program, this is using Solana’s `sol_blake3` syscall. Otherwise, the /// calculation is done by `blake3` crate. -#[allow(dead_code)] +#[allow(unreachable_code)] pub fn hashv(slices: &[&[u8]]) -> Hash { - #[cfg(all(target_os = "solana-program", feature = "solana-program-2"))] - return Hash(solana_program_2::blake3::hashv(slices).0); - #[cfg(all( - target_os = "solana-program", - feature = "solana-program", - not(feature = "solana-program-2") - ))] - return Hash(solana_program::blake3::hashv(slices).0); + if USE_SOL { + #[cfg(feature = "solana-program-2")] + return Hash(solana_program_2::blake3::hashv(slices).0); + #[cfg(feature = "solana-program")] + return Hash(solana_program::blake3::hashv(slices).0); + } - #[allow(dead_code)] - { - let mut hasher = Hasher::default(); - for bytes in slices { - hasher.update(bytes); - } - hasher.finalize().into() + let mut hasher = Hasher::default(); + for bytes in slices { + hasher.update(bytes); } + hasher.finalize().into() } diff --git a/common/guestchain/src/manager.rs b/common/guestchain/src/manager.rs index 282b78a8..eb0a7412 100644 --- a/common/guestchain/src/manager.rs +++ b/common/guestchain/src/manager.rs @@ -198,13 +198,13 @@ impl ChainManager { host_height: crate::HostHeight, host_timestamp: NonZeroU64, state_root: CryptoHash, - ) -> Result { + ) -> Result<(), GenerateError> { let next_epoch = self.validate_generate_next( host_height, host_timestamp, &state_root, )?; - let epoch_ends = self.header.next_epoch_commitment.is_some(); + let has_next_epoch = next_epoch.is_some(); let next_block = self.header.generate_next( host_height, host_timestamp, @@ -227,11 +227,21 @@ impl ChainManager { signers: Set::new(), signing_stake: 0, }); - self.candidates.clear_changed_flag(); - Ok(epoch_ends) + if has_next_epoch { + self.candidates.clear_changed_flag(); + } + + Ok(()) } + /// Verifies whether new block can be generated. + /// + /// Like [`generate_next`] returns an error if the new block cannot be + /// generated. If it can, returns an `Ok` value. + /// + /// If the new block should contain a next epoch commitment, returns `Some` + /// new epoch. Otherwise returns `None`. pub fn validate_generate_next( &self, host_height: crate::HostHeight, diff --git a/common/lib/src/par.rs b/common/lib/src/par.rs index e13a77bb..2ebe309d 100644 --- a/common/lib/src/par.rs +++ b/common/lib/src/par.rs @@ -24,6 +24,7 @@ pub type Chunks<'a, T> = core::slice::Chunks<'a, T>; /// # Example /// /// ``` +/// #[allow(unused_imports)] /// use lib::par::prelude::*; /// /// let chunks = lib::par::chunks(&[0, 1, 2, 3, 4], 3) diff --git a/solana/solana-ibc/programs/solana-ibc/Cargo.toml b/solana/solana-ibc/programs/solana-ibc/Cargo.toml index 51210ad8..d9c228c6 100644 --- a/solana/solana-ibc/programs/solana-ibc/Cargo.toml +++ b/solana/solana-ibc/programs/solana-ibc/Cargo.toml @@ -43,6 +43,7 @@ uint.workspace = true guestchain.workspace = true cf-guest.workspace = true +cf-solana = { workspace = true, features = ["solana-program", "no-blake3-syscall"] } lib = { workspace = true, features = ["solana-program"] } memory.workspace = true solana-allocator = { workspace = true, optional = true } diff --git a/solana/solana-ibc/programs/solana-ibc/src/client_state.rs b/solana/solana-ibc/programs/solana-ibc/src/client_state.rs index 23572ca5..9b10c32a 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/client_state.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/client_state.rs @@ -14,6 +14,7 @@ type Result = core::result::Result; pub enum AnyClientState { Tendermint(ibc::tm::ClientState), Wasm(ibc::wasm::ClientState), + Rollup(cf_solana::ClientState), #[cfg(any(test, feature = "mocks"))] Mock(ibc::mock::MockClientState), } @@ -26,6 +27,7 @@ impl ibc::Protobuf for AnyClientState {} enum AnyClientStateTag { Tendermint = 0, Wasm = 1, + Rollup = 2, #[cfg(any(test, feature = "mocks"))] Mock = 255, } @@ -37,6 +39,7 @@ impl AnyClientStateTag { match url { AnyClientState::TENDERMINT_TYPE => Some(Self::Tendermint), AnyClientState::WASM_TYPE => Some(Self::Wasm), + AnyClientState::ROLLUP_TYPE => Some(Self::Rollup), #[cfg(any(test, feature = "mocks"))] AnyClientState::MOCK_TYPE => Some(Self::Mock), _ => None, @@ -50,6 +53,9 @@ impl AnyClientState { ibc::tm::TENDERMINT_CLIENT_STATE_TYPE_URL; /// Protobuf type URL for WASM client state used in Any message. const WASM_TYPE: &'static str = ibc::wasm::WASM_CLIENT_STATE_TYPE_URL; + /// Protobuf type URL for Rollup client state used in Any message. + const ROLLUP_TYPE: &'static str = + cf_solana::proto::ClientState::IBC_TYPE_URL; #[cfg(any(test, feature = "mocks"))] /// Protobuf type URL for Mock client state used in Any message. const MOCK_TYPE: &'static str = ibc::mock::MOCK_CLIENT_STATE_TYPE_URL; @@ -78,6 +84,11 @@ impl AnyClientState { Self::WASM_TYPE, Protobuf::::encode_vec(state), ), + Self::Rollup(state) => ( + AnyClientStateTag::Rollup, + Self::ROLLUP_TYPE, + Protobuf::::encode_vec(state), + ), #[cfg(any(test, feature = "mocks"))] Self::Mock(state) => ( AnyClientStateTag::Mock, @@ -103,6 +114,11 @@ impl AnyClientState { .map_err(|err| err.to_string()) .map(Self::Wasm) } + AnyClientStateTag::Rollup => { + Protobuf::::decode_vec(&value) + .map_err(|err| err.to_string()) + .map(Self::Rollup) + } #[cfg(any(test, feature = "mocks"))] AnyClientStateTag::Mock => { Protobuf::::decode_vec(&value) @@ -323,6 +339,104 @@ impl guestchain::Verifier for IbcStorage<'_, '_> { } } +impl cf_solana::CommonContext for IbcStorage<'_, '_> { + type ConversionError = &'static str; + type AnyClientState = AnyClientState; + type AnyConsensusState = AnyConsensusState; + + fn host_metadata(&self) -> Result<(ibc::Timestamp, ibc::Height)> { + let timestamp = self.borrow().chain.head()?.timestamp_ns.get(); + let timestamp = + ibc::Timestamp::from_nanoseconds(timestamp).map_err(|err| { + ibc::ClientError::Other { description: err.to_string() } + })?; + + let height = u64::from(self.borrow().chain.head()?.block_height); + let height = ibc::Height::new(1, height)?; + + Ok((timestamp, height)) + } + + fn set_client_state( + &mut self, + client_id: &ibc::ClientId, + state: Self::AnyClientState, + ) -> Result<()> { + self.store_client_state_impl(client_id, state) + } + + fn consensus_state( + &self, + client_id: &ibc::ClientId, + height: ibc::Height, + ) -> Result { + self.consensus_state_impl(client_id, height) + } + + fn consensus_state_neighbourhood( + &self, + client_id: &ibc::ClientId, + height: ibc::Height, + ) -> Result> { + use core::cmp::Ordering; + + let height = (height.revision_number(), height.revision_height()); + let mut prev = ((0, 0), None); + let mut next = ((u64::MAX, u64::MAX), None); + + let storage = self.borrow(); + let states = &storage.private.client(client_id)?.consensus_states; + for (key, value) in states.iter() { + let key = (key.revision_number(), key.revision_height()); + match key.cmp(&height) { + Ordering::Less if key >= prev.0 => prev = (key, Some(value)), + Ordering::Greater if key <= next.0 => next = (key, Some(value)), + Ordering::Equal => { + return value.state().map(cf_guest::Neighbourhood::This) + } + _ => (), + } + } + + let prev = prev.1.map(|state| state.state()).transpose()?; + let next = next.1.map(|state| state.state()).transpose()?; + Ok(cf_guest::Neighbourhood::Neighbours(prev, next)) + } + + fn store_consensus_state_and_metadata( + &mut self, + client_id: &ibc::ClientId, + height: ibc::Height, + consensus: Self::AnyConsensusState, + _host_timestamp: ibc::Timestamp, + _host_height: ibc::Height, + ) -> Result { + self.store_consensus_state_impl(client_id, height, consensus) + } + + fn delete_consensus_state_and_metadata( + &mut self, + client_id: &ibc::ClientId, + height: ibc::Height, + ) -> Result { + self.delete_consensus_state_impl(client_id, height) + } + + /// Returns `None`. + /// + /// This method is used by the light client to prune old states. However, + /// we are limiting number of consensus states we’re keeping in + /// store_consensus_state_and_metadata method, which makes it unnecessary + /// for the light client to perform the pruning. Because of that, this + /// method returns `None`. + fn earliest_consensus_state( + &self, + _client_id: &ibc::ClientId, + ) -> Result> { + Ok(None) + } +} + #[cfg(any(test, feature = "mocks"))] impl ibc::mock::MockClientContext for IbcStorage<'_, '_> { type ConversionError = &'static str; diff --git a/solana/solana-ibc/programs/solana-ibc/src/client_state/impls.rs b/solana/solana-ibc/programs/solana-ibc/src/client_state/impls.rs index 3a9357f5..01a8bb30 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/client_state/impls.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/client_state/impls.rs @@ -18,6 +18,7 @@ macro_rules! delegate { match self { AnyClientState::Tendermint(cs) => cs.$name($($arg),*), AnyClientState::Wasm(_) => unimplemented!(), + AnyClientState::Rollup(cs) => cs.$name($($arg),*), #[cfg(any(test, feature = "mocks"))] AnyClientState::Mock(cs) => cs.$name($($arg),*), } @@ -53,6 +54,7 @@ impl ibc::ClientStateCommon for AnyClientState { ) } AnyClientState::Wasm(_) => unimplemented!(), + AnyClientState::Rollup(_) => unimplemented!(), #[cfg(any(test, feature = "mocks"))] AnyClientState::Mock(cs) => cs.verify_upgrade_client( upgraded_client_state, @@ -84,6 +86,9 @@ impl ibc::ClientStateCommon for AnyClientState { ) } AnyClientState::Wasm(_) => unimplemented!(), + AnyClientState::Rollup(cs) => { + cs.verify_membership(prefix, proof, root, path, value) + } #[cfg(any(test, feature = "mocks"))] AnyClientState::Mock(cs) => { cs.verify_membership(prefix, proof, root, path, value) @@ -107,6 +112,9 @@ impl ibc::ClientStateCommon for AnyClientState { ) } AnyClientState::Wasm(_) => unimplemented!(), + AnyClientState::Rollup(cs) => { + cs.verify_non_membership(prefix, proof, root, path) + } #[cfg(any(test, feature = "mocks"))] AnyClientState::Mock(cs) => { cs.verify_non_membership(prefix, proof, root, path) @@ -132,6 +140,9 @@ impl<'a, 'b> ibc::ClientStateValidation> for AnyClientState { ) } AnyClientState::Wasm(_) => unimplemented!(), + AnyClientState::Rollup(cs) => { + cs.verify_client_message(ctx, client_id, client_message) + } #[cfg(any(test, feature = "mocks"))] AnyClientState::Mock(cs) => { cs.verify_client_message(ctx, client_id, client_message) @@ -155,6 +166,7 @@ impl<'a, 'b> ibc::ClientStateValidation> for AnyClientState { ) } AnyClientState::Wasm(_) => unimplemented!(), + AnyClientState::Rollup(_) => unimplemented!(), #[cfg(any(test, feature = "mocks"))] AnyClientState::Mock(_) => unimplemented!(), } diff --git a/solana/solana-ibc/programs/solana-ibc/src/consensus_state.rs b/solana/solana-ibc/programs/solana-ibc/src/consensus_state.rs index 46f3d4b3..17a5bd55 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/consensus_state.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/consensus_state.rs @@ -15,6 +15,7 @@ use crate::ibc::{self, Protobuf}; pub enum AnyConsensusState { Tendermint(ibc::tm::ConsensusState), Wasm(ibc::wasm::ConsensusState), + Rollup(cf_solana::ConsensusState), #[cfg(any(test, feature = "mocks"))] Mock(ibc::mock::MockConsensusState), } @@ -25,6 +26,7 @@ pub enum AnyConsensusState { enum AnyConsensusStateTag { Tendermint = 0, Wasm = 1, + Rollup = 2, #[cfg(any(test, feature = "mocks"))] Mock = 255, } @@ -36,6 +38,7 @@ impl AnyConsensusStateTag { match url { AnyConsensusState::TENDERMINT_TYPE => Some(Self::Tendermint), AnyConsensusState::WASM_TYPE => Some(Self::Wasm), + AnyConsensusState::ROLLUP_TYPE => Some(Self::Rollup), #[cfg(any(test, feature = "mocks"))] AnyConsensusState::MOCK_TYPE => Some(Self::Mock), _ => None, @@ -44,13 +47,16 @@ impl AnyConsensusStateTag { } impl AnyConsensusState { - /// Protobuf type URL for Tendermint client state used in Any message. + /// Protobuf type URL for Tendermint consensus state used in Any message. const TENDERMINT_TYPE: &'static str = ibc::tm::TENDERMINT_CONSENSUS_STATE_TYPE_URL; - /// Protobuf type URL for WASM client state used in Any message. + /// Protobuf type URL for WASM consensus state used in Any message. const WASM_TYPE: &'static str = ibc::wasm::WASM_CONSENSUS_STATE_TYPE_URL; + /// Protobuf type URL for Rollup consensus state used in Any message. + const ROLLUP_TYPE: &'static str = + cf_solana::proto::ConsensusState::IBC_TYPE_URL; #[cfg(any(test, feature = "mocks"))] - /// Protobuf type URL for Mock client state used in Any message. + /// Protobuf type URL for Mock consensus state used in Any message. const MOCK_TYPE: &'static str = ibc::mock::MOCK_CONSENSUS_STATE_TYPE_URL; /// Encodes the payload and returns discriminants that allow decoding the @@ -58,7 +64,7 @@ impl AnyConsensusState { /// /// Returns a `(tag, type, value)` triple where `tag` is discriminant /// identifying variant of the enum, `type` is protobuf type URL - /// corresponding to the client state and `value` is the client state + /// corresponding to the consensus state and `value` is the consensus state /// encoded as protobuf. /// /// `(tag, value)` is used when borsh-encoding and `(type, value)` is used @@ -77,6 +83,11 @@ impl AnyConsensusState { Self::WASM_TYPE, Protobuf::::encode_vec(state), ), + AnyConsensusState::Rollup(state) => ( + AnyConsensusStateTag::Rollup, + Self::ROLLUP_TYPE, + Protobuf::::encode_vec(state), + ), #[cfg(any(test, feature = "mocks"))] AnyConsensusState::Mock(state) => ( AnyConsensusStateTag::Mock, @@ -102,6 +113,11 @@ impl AnyConsensusState { .map_err(|err| err.to_string()) .map(Self::Wasm) } + AnyConsensusStateTag::Rollup => { + Protobuf::::decode_vec(&value) + .map_err(|err| err.to_string()) + .map(Self::Rollup) + } #[cfg(any(test, feature = "mocks"))] AnyConsensusStateTag::Mock => { Protobuf::::decode_vec(&value) diff --git a/solana/solana-ibc/programs/solana-ibc/src/lib.rs b/solana/solana-ibc/programs/solana-ibc/src/lib.rs index aa58c9c9..843e02bf 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/lib.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/lib.rs @@ -956,7 +956,7 @@ fn check_staking_program(program_id: &Pubkey) -> Result<()> { "738b7c23e23543d25ac128b2ed4c676194c0bb20fad0154e1a5b1e639c9c4de0" )), Pubkey::new_from_array(hex_literal::hex!( - "a1d0177376e0e90b580181247c1a63b73e473b47bc5b06f70a6a4844e0b05015" + "79890dbcf24e48972b57e5094e5889be2742ed560c8e8d4842a6fea84b5e9c37" )), ]; match expected_program_ids.contains(program_id) { @@ -968,7 +968,7 @@ fn check_staking_program(program_id: &Pubkey) -> Result<()> { #[test] fn test_staking_program() { const GOOD_ONE: &str = "8n3FHwYxFgQCQc2FNFkwDUf9mcqupxXcCvgfHbApMLv3"; - const GOOD_TWO: &str = "BtegF7pQSriyP7gSkDpAkPDMvTS8wfajHJSmvcVoC7kg"; + const GOOD_TWO: &str = "9BRYTakYsrFkSNr5VPYWnM1bQV5yZnX5uM8Ny2q5Nixv"; const BAD: &str = "75pAU4CJcp8Z9eoXcL6pSU8sRK5vn3NEpgvV9VJtc5hy"; check_staking_program(&GOOD_ONE.parse().unwrap()).unwrap(); check_staking_program(&GOOD_TWO.parse().unwrap()).unwrap();