From a57997f086fbd01751ef1227787ab702be23a197 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Sun, 1 Sep 2024 20:49:08 +0200 Subject: [PATCH] solana-trie: add support for witness account MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Witness of the trie is an account which stores the trie’s hash. Using Solana’s account delta hash this allows us to construct a proof of the root. --- common/cf-solana/src/header.rs | 11 +- .../programs/solana-ibc/src/storage.rs | 2 +- solana/trie/src/account.rs | 1 + solana/trie/src/lib.rs | 80 ++++++++--- solana/trie/src/witness.rs | 135 ++++++++++++++++++ solana/witnessed-trie/src/api.rs | 65 +-------- solana/witnessed-trie/src/contract.rs | 20 +-- 7 files changed, 215 insertions(+), 99 deletions(-) create mode 100644 solana/trie/src/witness.rs diff --git a/common/cf-solana/src/header.rs b/common/cf-solana/src/header.rs index d705aa7f..01c1a41d 100644 --- a/common/cf-solana/src/header.rs +++ b/common/cf-solana/src/header.rs @@ -34,12 +34,11 @@ impl Header { /// trie and Solana block timestamp in seconds. /// /// Returns None if the witness account data has unexpected format - /// (e.g. it’s not 40-byte long). See `WitnessedData` in - /// `solana-witnessed-trie`. - // TODO(mina86): Ideally we would use wittrie::api::WitnessedData here but - // wittrie depends on Solana and we don’t want to introduce required Solana - // dependencies here. Moving WitnessData to a crate in common/ is an option - // but for the time being we’re duplicating the logic here. + /// (e.g. it’s not 40-byte long). See `witness::Data` in solana-trie. + // TODO(mina86): Ideally we would use solana_trie::witness::Data here but + // solana_trie depends on Solana and we don’t want to introduce required + // Solana dependencies here. Moving witness::Data to a crate in common/ is + // an option but for the time being we’re duplicating the logic here. pub fn decode_witness(&self) -> Option<(&CryptoHash, NonZeroU64)> { let data = self.witness_proof.account_hash_data.data().try_into().ok()?; diff --git a/solana/solana-ibc/programs/solana-ibc/src/storage.rs b/solana/solana-ibc/programs/solana-ibc/src/storage.rs index a57fac5c..178f810f 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/storage.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/storage.rs @@ -391,7 +391,7 @@ impl PrivateStorage { /// Provable storage, i.e. the trie, held in an account. pub type TrieAccount<'a, 'b> = - solana_trie::TrieAccount>; + solana_trie::TrieAccount<'a, solana_trie::ResizableAccount<'a, 'b>>; /// Checks contents of given unchecked account and returns a trie if it’s valid. /// diff --git a/solana/trie/src/account.rs b/solana/trie/src/account.rs index 2dc06106..5053ab99 100644 --- a/solana/trie/src/account.rs +++ b/solana/trie/src/account.rs @@ -7,6 +7,7 @@ use solana_program::rent::Rent; use solana_program::system_instruction::MAX_PERMITTED_DATA_LENGTH; use solana_program::sysvar::Sysvar; +/// An account backing a Trie which can be resized. #[derive(Debug)] pub struct ResizableAccount<'a, 'info> { account: &'a AccountInfo<'info>, diff --git a/solana/trie/src/lib.rs b/solana/trie/src/lib.rs index 9e6b137e..8ece8244 100644 --- a/solana/trie/src/lib.rs +++ b/solana/trie/src/lib.rs @@ -1,3 +1,4 @@ +use core::cell::RefMut; use core::mem::ManuallyDrop; #[cfg(test)] @@ -5,11 +6,13 @@ use pretty_assertions::assert_eq; use solana_program::account_info::AccountInfo; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; +use solana_program::sysvar::Sysvar; mod account; mod alloc; mod data_ref; mod header; +pub mod witness; pub use account::ResizableAccount; pub use data_ref::DataRef; @@ -17,12 +20,14 @@ pub use sealable_trie::Trie; /// Trie stored in a Solana account. -#[derive(Debug)] -pub struct TrieAccount( - ManuallyDrop>>, -); +pub struct TrieAccount<'a, D: DataRef + Sized>(ManuallyDrop>); -impl TrieAccount { +struct Inner<'a, D: DataRef + Sized> { + trie: sealable_trie::Trie>, + witness: Option>, +} + +impl<'a, D: DataRef + Sized> TrieAccount<'a, D> { /// Creates a new TrieAccount from data in an account. /// /// If the data in the account isn’t initialised (i.e. has zero @@ -30,11 +35,31 @@ impl TrieAccount { pub fn new(data: D) -> Option { let (alloc, root) = alloc::Allocator::new(data)?; let trie = sealable_trie::Trie::from_parts(alloc, root.0, root.1); - Some(Self(ManuallyDrop::new(trie))) + Some(Self(ManuallyDrop::new(Inner { trie, witness: None }))) + } + + /// Returns witness data if any. + pub fn witness(&self) -> Option<&RefMut<'a, witness::Data>> { + self.0.witness.as_ref() + } + + /// Sets the witness account. + /// + /// `witness` must be initialised, owned by `owner` and exactly 40 bytes + /// (see [`witness::Data::SIZE`]). Witness is updated automatically once + /// this object is dropped. + pub fn with_witness_account<'info>( + mut self, + witness: &'a AccountInfo<'info>, + owner: &Pubkey, + ) -> Result { + check_account(witness, owner)?; + self.0.witness = Some(witness::Data::from_account_info(witness)?); + Ok(self) } } -impl<'a, 'b> TrieAccount> { +impl<'a, 'info> TrieAccount<'a, RefMut<'a, &'info mut [u8]>> { /// Creates a new TrieAccount from data in an account specified by given /// info. /// @@ -43,7 +68,7 @@ impl<'a, 'b> TrieAccount> { /// Created TrieAccount holds exclusive reference on the account’s data thus /// no other code can access it while this object is alive. pub fn from_account_info( - account: &'a AccountInfo<'b>, + account: &'a AccountInfo<'info>, owner: &Pubkey, ) -> Result { check_account(account, owner)?; @@ -52,7 +77,7 @@ impl<'a, 'b> TrieAccount> { } } -impl<'a, 'b> TrieAccount> { +impl<'a, 'info> TrieAccount<'a, ResizableAccount<'a, 'info>> { /// Creates a new TrieAccount from data in an account specified by given /// info. /// @@ -64,9 +89,9 @@ impl<'a, 'b> TrieAccount> { /// If the account needs to increase in size, `payer`’s account is used to /// transfer lamports necessary to keep the account rent-exempt. pub fn from_account_with_payer( - account: &'a AccountInfo<'b>, + account: &'a AccountInfo<'info>, owner: &Pubkey, - payer: &'a AccountInfo<'b>, + payer: &'a AccountInfo<'info>, ) -> Result { check_account(account, owner)?; let data = ResizableAccount::new(account, payer)?; @@ -90,13 +115,22 @@ fn check_account( } } -impl core::ops::Drop for TrieAccount { +impl<'a, 'info, D: DataRef + Sized> core::ops::Drop for TrieAccount<'a, D> { /// Updates the header in the Solana account. fn drop(&mut self) { // SAFETY: Once we’re done with self.0 we are dropped and no one else is // going to have access to self.0. - let trie = unsafe { ManuallyDrop::take(&mut self.0) }; + let Inner { trie, witness } = + unsafe { ManuallyDrop::take(&mut self.0) }; let (mut alloc, root_ptr, root_hash) = trie.into_parts(); + + // Update witness + if let Some(mut witness) = witness { + let clock = solana_program::clock::Clock::get().unwrap(); + *witness = witness::Data::new(root_hash, &clock); + } + + // Update header let hdr = header::Header { root_ptr, root_hash, @@ -108,16 +142,28 @@ impl core::ops::Drop for TrieAccount { } } -impl core::ops::Deref for TrieAccount { +impl<'a, 'info, D: DataRef> core::ops::Deref for TrieAccount<'a, D> { type Target = sealable_trie::Trie>; - fn deref(&self) -> &Self::Target { &self.0 } + fn deref(&self) -> &Self::Target { &self.0.trie } } -impl core::ops::DerefMut for TrieAccount { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } +impl<'a, 'info, D: DataRef> core::ops::DerefMut for TrieAccount<'a, D> { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0.trie } } +impl core::fmt::Debug for TrieAccount<'_, D> { + fn fmt(&self, fmtr: &mut core::fmt::Formatter) -> core::fmt::Result { + let mut fmtr = fmtr.debug_struct("TrieAccount"); + fmtr.field("trie", &self.0.trie); + if let Some(witness) = self.0.witness.as_ref() { + let root: &witness::Data = &*witness; + fmtr.field("witness", root); + } + fmtr.finish() + } +} + #[test] fn test_trie_sanity() { const ONE: lib::hash::CryptoHash = lib::hash::CryptoHash([1; 32]); diff --git a/solana/trie/src/witness.rs b/solana/trie/src/witness.rs new file mode 100644 index 00000000..edbcb685 --- /dev/null +++ b/solana/trie/src/witness.rs @@ -0,0 +1,135 @@ +use core::cell::RefMut; + +use lib::hash::CryptoHash; +use solana_program::account_info::AccountInfo; +use solana_program::program_error::ProgramError; + +/// Encoding of the data in witness account. +#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +pub struct Data { + /// The root of the sealable trie. + pub trie_root: CryptoHash, + + /// Rest of the witness account encoding Solana block timestamp. + /// + /// The timestamp is encoded using only six bytes. The seventh byte is + /// a single byte of a slot number and the last byte is always zero. + /// + /// Single byte of slot is included so that data of the account changes for + /// every slot even if two slots are created at the same second. + /// + /// The last byte is zero for potential future use. + rest: [u8; 8], +} + +impl Data { + /// Size of the witness account data. + pub const SIZE: usize = core::mem::size_of::(); + + /// Formats new witness account data with timestamp and slot number taken + /// from Solana clock. + pub fn new( + trie_root: CryptoHash, + clock: &solana_program::clock::Clock, + ) -> Self { + let mut rest = clock.unix_timestamp.to_le_bytes(); + rest[6] = clock.slot as u8; + rest[7] = 0; + Self { trie_root, rest } + } + + /// Returns root of the saleable trie and Solana block timestamp in seconds. + /// + /// Returns `Err` if the account data is malformed. The error holds + /// reference to the full data of the account. This happens if the last + /// byte of the data is non-zero. + pub fn decode(&self) -> Result<(&CryptoHash, u64), &[u8; Data::SIZE]> { + if self.rest[7] != 0 { + return Err(bytemuck::cast_ref(self)); + } + let timestamp = u64::from_le_bytes(self.rest) & 0xffff_ffff_ffff; + Ok((&self.trie_root, timestamp)) + } + + /// Creates a new borrowed reference to the data held in given account. + /// + /// Checks that the account is mutable and exactly [`Data::SIZE`] bytes. If + /// so, updates the timestamp and slot of the account and returns reference + /// to the trie’s root held inside of the account. + pub(crate) fn from_account_info<'a, 'info>( + witness: &'a AccountInfo<'info>, + ) -> Result, ProgramError> { + RefMut::filter_map(witness.try_borrow_mut_data()?, |data| { + let data: &mut [u8] = data.as_mut(); + <&mut Data>::try_from(data).ok() + }) + .map_err(|_| ProgramError::InvalidAccountData) + } +} + + + +impl From<[u8; Data::SIZE]> for Data { + fn from(bytes: [u8; Data::SIZE]) -> Self { bytemuck::cast(bytes) } +} + +impl<'a> From<&'a [u8; Data::SIZE]> for &'a Data { + fn from(bytes: &'a [u8; Data::SIZE]) -> Self { bytemuck::cast_ref(bytes) } +} + +impl<'a> From<&'a [u8; Data::SIZE]> for Data { + fn from(bytes: &'a [u8; Data::SIZE]) -> Self { *bytemuck::cast_ref(bytes) } +} + +impl<'a> TryFrom<&'a [u8]> for &'a Data { + type Error = core::array::TryFromSliceError; + + fn try_from(bytes: &'a [u8]) -> Result { + <&[u8; Data::SIZE]>::try_from(bytes).map(Self::from) + } +} + +impl<'a> TryFrom<&'a [u8]> for Data { + type Error = core::array::TryFromSliceError; + + fn try_from(bytes: &'a [u8]) -> Result { + <&[u8; Data::SIZE]>::try_from(bytes).map(Data::from) + } +} + +impl<'a> From<&'a mut [u8; Data::SIZE]> for &'a mut Data { + fn from(bytes: &'a mut [u8; Data::SIZE]) -> Self { + bytemuck::cast_mut(bytes) + } +} + +impl<'a> TryFrom<&'a mut [u8]> for &'a mut Data { + type Error = core::array::TryFromSliceError; + + fn try_from(bytes: &'a mut [u8]) -> Result { + <&mut [u8; Data::SIZE]>::try_from(bytes).map(Self::from) + } +} + + +impl From for [u8; Data::SIZE] { + fn from(data: Data) -> Self { bytemuck::cast(data) } +} + +impl<'a> From<&'a Data> for &'a [u8; Data::SIZE] { + fn from(bytes: &'a Data) -> Self { bytes.as_ref() } +} + +impl<'a> From<&'a mut Data> for &'a mut [u8; Data::SIZE] { + fn from(bytes: &'a mut Data) -> Self { bytes.as_mut() } +} + + +impl AsRef<[u8; Data::SIZE]> for Data { + fn as_ref(&self) -> &[u8; Data::SIZE] { bytemuck::cast_ref(self) } +} + +impl AsMut<[u8; Data::SIZE]> for Data { + fn as_mut(&mut self) -> &mut [u8; Data::SIZE] { bytemuck::cast_mut(self) } +} diff --git a/solana/witnessed-trie/src/api.rs b/solana/witnessed-trie/src/api.rs index 04a19648..408492e5 100644 --- a/solana/witnessed-trie/src/api.rs +++ b/solana/witnessed-trie/src/api.rs @@ -2,6 +2,7 @@ use lib::hash::CryptoHash; use solana_program::pubkey::{Pubkey, MAX_SEED_LEN}; #[cfg(all(not(feature = "api"), feature = "api2"))] use solana_program_2 as solana_program; +pub use solana_trie::witness::Data as WitnessData; use crate::utils; @@ -220,66 +221,6 @@ impl From for ParseError { } -/// Encoding of the data in witness account. -#[derive(Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)] -#[repr(C)] -pub struct WitnessedData { - /// The root of the sealable trie. - trie_root: [u8; 32], - - /// Rest of the witness account encoding Solana block timestamp. - /// - /// The timestamp is encoded using only six bytes. The seventh byte is - /// a single byte of a slot number and the last byte is always zero. - /// - /// Single byte of slot is included so that data of the account changes for - /// every slot even if two slots are created at the same second. - /// - /// The last byte is zero for potential future use. - rest: [u8; 8], -} - -impl WitnessedData { - /// Formats new witness account data with timestamp and slot number taken - /// from Solana clock. - pub fn new( - trie_root: &CryptoHash, - clock: &solana_program::clock::Clock, - ) -> Self { - let mut rest = clock.unix_timestamp.to_le_bytes(); - rest[6] = clock.slot as u8; - rest[7] = 0; - Self { trie_root: trie_root.into(), rest } - } - - /// Returns root of the saleable trie and Solana block timestamp in seconds. - /// - /// Returns `Err` if the account data is malformed. The error holds - /// reference to the full data of the account. This happens if the last - /// byte of the data is non-zero. - pub fn decode(&self) -> Result<(&CryptoHash, u64), &[u8; 40]> { - if self.rest[7] != 0 { - return Err(bytemuck::cast_ref(self)); - } - let timestamp = u64::from_le_bytes(self.rest) & 0xffff_ffff_ffff; - Ok(((&self.trie_root).into(), timestamp)) - } -} - -impl From<[u8; 40]> for WitnessedData { - fn from(bytes: [u8; 40]) -> Self { bytemuck::cast(bytes) } -} - -impl From for [u8; 40] { - fn from(data: WitnessedData) -> Self { bytemuck::cast(data) } -} - -impl AsRef<[u8; 40]> for WitnessedData { - fn as_ref(&self) -> &[u8; 40] { bytemuck::cast_ref(self) } -} - - - /// Value returned from the contract in return data. /// /// It holds information about the witness account needed to compute its hash @@ -300,7 +241,7 @@ pub struct ReturnData { pub lamports: [u8; 8], pub rent_epoch: [u8; 8], #[deref] - pub data: WitnessedData, + pub data: WitnessData, } impl ReturnData { @@ -346,7 +287,7 @@ fn test_hash_account() { solana_program::pubkey!("ENEWG4MWwJQUfJxDgqarJQ1bf2P4fADsCYsPCjvLRaa2"); const OWNER: Pubkey = solana_program::pubkey!("4FjVmuvPYnE1fqBtvjGh5JF7QDwUmyBZ5wv1uygHvTey"); - const DATA: [u8; 40] = [ + const DATA: [u8; WitnessData::SIZE] = [ 0xa9, 0x1e, 0x26, 0xed, 0x91, 0x28, 0xdd, 0x6f, 0xed, 0xa2, 0xe8, 0x6a, 0xf7, 0x9b, 0xe2, 0xe1, 0x77, 0x89, 0xaf, 0x08, 0x72, 0x08, 0x69, 0x22, 0x13, 0xd3, 0x95, 0x5e, 0x07, 0x4c, 0xee, 0x9c, 1, 2, 3, 4, 5, 6, 7, 8, diff --git a/solana/witnessed-trie/src/contract.rs b/solana/witnessed-trie/src/contract.rs index 42616062..06d22229 100644 --- a/solana/witnessed-trie/src/contract.rs +++ b/solana/witnessed-trie/src/contract.rs @@ -1,7 +1,6 @@ use solana_program::account_info::AccountInfo; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; -use solana_program::sysvar::Sysvar; use crate::{accounts, api, utils}; @@ -40,6 +39,7 @@ pub(crate) fn process_instruction( ) -> Result { let data = api::Data::from_slice(instruction)?; + // Get the accounts (trie root and witness) let (mut trie, witness) = { let accounts = &mut accounts.iter(); let payer = accounts::get_payer(accounts)?; @@ -52,7 +52,8 @@ pub(crate) fn process_instruction( )?; let witness = accounts::get_witness(payer, accounts, program_id, root)?; let trie = solana_trie::TrieAccount::new(root.try_borrow_mut_data()?) - .ok_or(ProgramError::InvalidAccountData)?; + .ok_or(ProgramError::InvalidAccountData)? + .with_witness_account(witness, program_id)?; (trie, witness) }; @@ -70,22 +71,15 @@ pub(crate) fn process_instruction( })?; } - // Update witness - let clock = solana_program::clock::Clock::get()?; - let data = api::WitnessedData::new(trie.hash(), &clock); - - { - let mut dst = witness.try_borrow_mut_data()?; - let dst: &mut [u8] = &mut dst; - let dst: &mut [u8; 40] = dst.try_into().unwrap(); - *dst = data.into(); - } + // Drop the trie so that witness is updated. + core::mem::drop(trie); // Return enough information so that witness account can be hashed. let ret = api::ReturnData { lamports: witness.lamports().to_le_bytes(), rent_epoch: witness.rent_epoch.to_le_bytes(), - data, + data: api::WitnessData::try_from(&**witness.try_borrow_data()?) + .unwrap(), }; solana_program::program::set_return_data(bytemuck::bytes_of(&ret));