Skip to content

Commit

Permalink
solana-trie: add support for witness account
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mina86 committed Sep 1, 2024
1 parent 0ed4c09 commit a57997f
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 99 deletions.
11 changes: 5 additions & 6 deletions common/cf-solana/src/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()?;
Expand Down
2 changes: 1 addition & 1 deletion solana/solana-ibc/programs/solana-ibc/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::ResizableAccount<'a, 'b>>;
solana_trie::TrieAccount<'a, solana_trie::ResizableAccount<'a, 'b>>;

/// Checks contents of given unchecked account and returns a trie if it’s valid.
///
Expand Down
1 change: 1 addition & 0 deletions solana/trie/src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down
80 changes: 63 additions & 17 deletions solana/trie/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,40 +1,65 @@
use core::cell::RefMut;
use core::mem::ManuallyDrop;

#[cfg(test)]
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;
pub use sealable_trie::Trie;


/// Trie stored in a Solana account.
#[derive(Debug)]
pub struct TrieAccount<D: DataRef + Sized>(
ManuallyDrop<sealable_trie::Trie<alloc::Allocator<D>>>,
);
pub struct TrieAccount<'a, D: DataRef + Sized>(ManuallyDrop<Inner<'a, D>>);

impl<D: DataRef + Sized> TrieAccount<D> {
struct Inner<'a, D: DataRef + Sized> {
trie: sealable_trie::Trie<alloc::Allocator<D>>,
witness: Option<RefMut<'a, witness::Data>>,
}

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
/// discriminant) initialises a new empty trie.
pub fn new(data: D) -> Option<Self> {
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<Self, ProgramError> {
check_account(witness, owner)?;
self.0.witness = Some(witness::Data::from_account_info(witness)?);
Ok(self)
}
}

impl<'a, 'b> TrieAccount<core::cell::RefMut<'a, &'b mut [u8]>> {
impl<'a, 'info> TrieAccount<'a, RefMut<'a, &'info mut [u8]>> {
/// Creates a new TrieAccount from data in an account specified by given
/// info.
///
Expand All @@ -43,7 +68,7 @@ impl<'a, 'b> TrieAccount<core::cell::RefMut<'a, &'b mut [u8]>> {
/// 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<Self, ProgramError> {
check_account(account, owner)?;
Expand All @@ -52,7 +77,7 @@ impl<'a, 'b> TrieAccount<core::cell::RefMut<'a, &'b mut [u8]>> {
}
}

impl<'a, 'b> TrieAccount<ResizableAccount<'a, 'b>> {
impl<'a, 'info> TrieAccount<'a, ResizableAccount<'a, 'info>> {
/// Creates a new TrieAccount from data in an account specified by given
/// info.
///
Expand All @@ -64,9 +89,9 @@ impl<'a, 'b> TrieAccount<ResizableAccount<'a, 'b>> {
/// 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<Self, ProgramError> {
check_account(account, owner)?;
let data = ResizableAccount::new(account, payer)?;
Expand All @@ -90,13 +115,22 @@ fn check_account(
}
}

impl<D: DataRef + Sized> core::ops::Drop for TrieAccount<D> {
impl<'a, 'info, D: DataRef + Sized> core::ops::Drop for TrieAccount<'a, D> {

Check failure on line 118 in solana/trie/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

this lifetime isn't used in the impl

error: this lifetime isn't used in the impl --> solana/trie/src/lib.rs:118:10 | 118 | impl<'a, 'info, D: DataRef + Sized> core::ops::Drop for TrieAccount<'a, D> { | ^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#extra_unused_lifetimes = note: `-D clippy::extra-unused-lifetimes` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(clippy::extra_unused_lifetimes)]`
/// 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,
Expand All @@ -108,16 +142,28 @@ impl<D: DataRef + Sized> core::ops::Drop for TrieAccount<D> {
}
}

impl<D: DataRef> core::ops::Deref for TrieAccount<D> {
impl<'a, 'info, D: DataRef> core::ops::Deref for TrieAccount<'a, D> {

Check failure on line 145 in solana/trie/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

this lifetime isn't used in the impl

error: this lifetime isn't used in the impl --> solana/trie/src/lib.rs:145:10 | 145 | impl<'a, 'info, D: DataRef> core::ops::Deref for TrieAccount<'a, D> { | ^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#extra_unused_lifetimes
type Target = sealable_trie::Trie<alloc::Allocator<D>>;
fn deref(&self) -> &Self::Target { &self.0 }
fn deref(&self) -> &Self::Target { &self.0.trie }
}

impl<D: DataRef> core::ops::DerefMut for TrieAccount<D> {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
impl<'a, 'info, D: DataRef> core::ops::DerefMut for TrieAccount<'a, D> {

Check failure on line 150 in solana/trie/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

this lifetime isn't used in the impl

error: this lifetime isn't used in the impl --> solana/trie/src/lib.rs:150:10 | 150 | impl<'a, 'info, D: DataRef> core::ops::DerefMut for TrieAccount<'a, D> { | ^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#extra_unused_lifetimes
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0.trie }
}


impl<D: DataRef + core::fmt::Debug> 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;

Check failure on line 160 in solana/trie/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

deref on an immutable reference

error: deref on an immutable reference --> solana/trie/src/lib.rs:160:40 | 160 | let root: &witness::Data = &*witness; | ^^^^^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#borrow_deref_ref = note: `-D clippy::borrow-deref-ref` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(clippy::borrow_deref_ref)]` help: if you would like to reborrow, try removing `&*` | 160 | let root: &witness::Data = witness; | ~~~~~~~ help: if you would like to deref, try using `&**` | 160 | 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]);
Expand Down
135 changes: 135 additions & 0 deletions solana/trie/src/witness.rs
Original file line number Diff line number Diff line change
@@ -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::<Data>();

/// 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>(

Check failure on line 60 in solana/trie/src/witness.rs

View workflow job for this annotation

GitHub Actions / clippy

the following explicit lifetimes could be elided: 'info

error: the following explicit lifetimes could be elided: 'info --> solana/trie/src/witness.rs:60:41 | 60 | pub(crate) fn from_account_info<'a, 'info>( | ^^^^^ 61 | witness: &'a AccountInfo<'info>, | ^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes = note: `-D clippy::needless-lifetimes` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(clippy::needless_lifetimes)]` help: elide the lifetimes | 60 ~ pub(crate) fn from_account_info<'a>( 61 ~ witness: &'a AccountInfo<'_>, |
witness: &'a AccountInfo<'info>,
) -> Result<RefMut<'a, Self>, ProgramError> {
RefMut::filter_map(witness.try_borrow_mut_data()?, |data| {
let data: &mut [u8] = data.as_mut();

Check failure on line 64 in solana/trie/src/witness.rs

View workflow job for this annotation

GitHub Actions / clippy

this call to `as_mut` does nothing

error: this call to `as_mut` does nothing --> solana/trie/src/witness.rs:64:35 | 64 | let data: &mut [u8] = data.as_mut(); | ^^^^^^^^^^^^^ help: try: `data` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#useless_asref = note: `-D clippy::useless-asref` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(clippy::useless_asref)]`
<&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<Self, Self::Error> {
<&[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<Self, Self::Error> {
<&[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<Self, Self::Error> {
<&mut [u8; Data::SIZE]>::try_from(bytes).map(Self::from)
}
}


impl From<Data> 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) }
}
Loading

0 comments on commit a57997f

Please sign in to comment.