From 111e1032f24ed519000978e38cd7f6920d60a224 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Fri, 17 Apr 2026 18:36:51 +0400 Subject: [PATCH] feat: add customized solana-account crate --- .gitattributes | 3 + .gitignore | 1 + Cargo.toml | 38 ++ solana/account/Cargo.toml | 38 ++ solana/account/README.md | 36 ++ solana/account/src/account.rs | 255 +++++++++ solana/account/src/codec.rs | 23 + solana/account/src/cow/borrowed.rs | 308 +++++++++++ solana/account/src/cow/mod.rs | 663 +++++++++++++++++++++++ solana/account/src/cow/owned.rs | 139 +++++ solana/account/src/cow/tests.rs | 83 +++ solana/account/src/lib.rs | 33 ++ solana/account/src/patch.rs | 86 +++ solana/account/src/state_traits.rs | 90 +++ solana/account/src/sysvar.rs | 70 +++ solana/account/src/test_utils.rs | 58 ++ solana/account/src/tests/account.rs | 440 +++++++++++++++ solana/account/src/tests/mod.rs | 4 + solana/account/src/tests/state_traits.rs | 19 + solana/account/src/tests/sysvar.rs | 15 + solana/account/src/traits.rs | 233 ++++++++ src/lib.rs | 1 - 22 files changed, 2635 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 solana/account/Cargo.toml create mode 100644 solana/account/README.md create mode 100644 solana/account/src/account.rs create mode 100644 solana/account/src/codec.rs create mode 100644 solana/account/src/cow/borrowed.rs create mode 100644 solana/account/src/cow/mod.rs create mode 100644 solana/account/src/cow/owned.rs create mode 100644 solana/account/src/cow/tests.rs create mode 100644 solana/account/src/lib.rs create mode 100644 solana/account/src/patch.rs create mode 100644 solana/account/src/state_traits.rs create mode 100644 solana/account/src/sysvar.rs create mode 100644 solana/account/src/test_utils.rs create mode 100644 solana/account/src/tests/account.rs create mode 100644 solana/account/src/tests/mod.rs create mode 100644 solana/account/src/tests/state_traits.rs create mode 100644 solana/account/src/tests/sysvar.rs create mode 100644 solana/account/src/traits.rs delete mode 100644 src/lib.rs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ec98ed --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.rs linguist-language=Rust +Cargo.toml linguist-language=Rust +Cargo.lock linguist-language=Rust diff --git a/.gitignore b/.gitignore index 1798e8b..b5ced77 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target Cargo.lock AGENTS.md +CLAUDE.md diff --git a/Cargo.toml b/Cargo.toml index 85ad1f1..e722048 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +members = ["solana/account"] resolver = "3" [workspace.package] @@ -10,6 +11,43 @@ repository = "https://github.com/magicblock-labs/engine" rust-version = "1.94.1" version = "0.1.0" +[workspace.dependencies] +magic-root-interface = { path = "programs/magic-root-interface" } +magic-root-program = { path = "programs/magic-root-program" } +solana-account = { path = "solana/account" } + +ahash = "0.8.12" +arc-swap = "1.9.1" +bincode = "1.3.3" +bitflags = "2.11.1" +blake3 = "1.8.5" +qualifier_attr = "0.2.2" +rand = "0.8.5" +rustix = { version = "1.1.4" } +serde = "1.0.228" +serde_bytes = "0.11.19" +tar = "0.4.45" +tempfile = "3" +thiserror = "2.0.17" +tracing-subscriber = { version = "0.3.23", features = ["env-filter", "fmt"] } +wincode = "0.5.1" +zstd = { version = "0.13.3", default-features = false } + +agave-feature-set = { version = "3.1.14", features = ["agave-unstable-api"] } +agave-transaction-view = { version = "3.1.13", features = ["agave-unstable-api"] } +solana-account-info = "3.1.1" +solana-clock = "3.1.0" +solana-compute-budget-instruction = "4.1.1" +solana-cpi = "3.1.0" +solana-hash = "4.3.0" +solana-instruction-error = "2.3.0" +solana-program-error = "3.0.1" +solana-pubkey = "4.2.0" +solana-sdk-ids = "3.1.0" +solana-sysvar = "4.0.0" +solana-transaction-error = "3.2.0" + + [workspace.lints.rust] missing_docs = "deny" rust_2018_idioms = { level = "warn", priority = -1 } diff --git a/solana/account/Cargo.toml b/solana/account/Cargo.toml new file mode 100644 index 0000000..0545767 --- /dev/null +++ b/solana/account/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "solana-account" + +authors = { workspace = true } +description = "Solana Account type" +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +readme = "README.md" +repository = { workspace = true } +version = "4.3.1" + +[features] +bincode = ["dep:bincode", "dep:solana-sysvar", "serde"] +dev-context-only-utils = ["bincode", "dep:qualifier_attr"] +serde = ["dep:serde", "dep:serde_bytes", "serde/rc", "solana-pubkey/serde"] +wincode = ["bincode", "dep:wincode", "solana-pubkey/wincode"] + +[dependencies] +bincode = { workspace = true, optional = true } +bitflags = { workspace = true, features = ["serde"] } +qualifier_attr = { workspace = true, optional = true } +serde = { workspace = true, optional = true } +serde_bytes = { workspace = true, optional = true } +solana-account-info = { workspace = true } +solana-clock = { workspace = true } +solana-instruction-error = { workspace = true } +solana-pubkey = { workspace = true } +solana-sdk-ids = { workspace = true } +solana-sysvar = { workspace = true, features = ["bincode"], optional = true } +wincode = { workspace = true, features = ["alloc"], optional = true } + +[dev-dependencies] +solana-account = { path = ".", features = ["dev-context-only-utils"] } +solana-pubkey = { workspace = true, features = ["std"] } + +[lints] +workspace = true diff --git a/solana/account/README.md b/solana/account/README.md new file mode 100644 index 0000000..f274e5e --- /dev/null +++ b/solana/account/README.md @@ -0,0 +1,36 @@ +# `solana-account` + +A fork of Solana's account code, reshaped around the engine's storage model. It +owns how an account is represented in memory and how it's serialized; *where* an +account lives — persisted or volatile — is decided one layer up, in the storage +crates. + +The reason for the fork is copy-on-write. `Account` is the simple, fully-owned +form (`Vec`). `AccountSharedData` is the one the engine actually runs on: it +can point straight at an aligned external buffer and only promotes to owned heap +storage when a write no longer fits the borrowed allocation. That lets the engine +read and even mutate accounts directly out of the mmap'd persisted store without +copying them first. + +Borrowed storage is **layout-bound, not source-bound**: it doesn't care whether +the bytes come from an mmap, a file arena, or a test buffer, as long as the buffer +is 8-byte aligned, matches the `cow::borrowed` layout, and stays alive with unique +mutable access for the entire borrow. Violate any of those and the borrow is +unsound, so they're hard requirements, not guidelines. + +## Borrowed layout + +| Part | Offset | Contents | +| ------- | ------------- | ------------------------- | +| header | start | sequence and image size | +| pubkey | after header | account pubkey | +| image A | after pubkey | core state and data bytes | +| image B | after image A | core state and data bytes | + +There are two images because writes are double-buffered. The header's sequence +counter selects which image is *active*; the other is the shadow copy that +rewrites land in before being published. The pubkey sits once, shared between +them. + +Higher layers route on `AccountSharedData::mutable()` to decide persisted vs. +volatile. diff --git a/solana/account/src/account.rs b/solana/account/src/account.rs new file mode 100644 index 0000000..b5333b5 --- /dev/null +++ b/solana/account/src/account.rs @@ -0,0 +1,255 @@ +use { + crate::{AccountSharedData, ReadableAccount, traits::debug_fmt}, + solana_account_info::AccountInfo, + solana_clock::Epoch, + solana_pubkey::Pubkey, + solana_sdk_ids::{ + bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, loader_v4, native_loader, + }, + std::{cell::RefCell, fmt, rc::Rc}, +}; + +/// An on-chain account with owned data and an explicit rent epoch. +#[repr(C)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "wincode", derive(wincode::SchemaRead, wincode::SchemaWrite))] +#[derive(PartialEq, Eq, Clone, Default)] +pub struct Account { + /// Lamports in the account. + pub lamports: u64, + /// Data held in the account. + #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))] + pub data: Vec, + /// The program that owns this account. + pub owner: Pubkey, + /// Whether the account contains executable program data. + pub executable: bool, + /// The epoch at which this account next owes rent. + pub rent_epoch: Epoch, +} + +#[cfg(feature = "serde")] +mod account_serialize { + use { + crate::ReadableAccount, + serde::{Serialize, ser::Serializer}, + solana_clock::Epoch, + solana_pubkey::Pubkey, + }; + + #[repr(C)] + #[derive(serde::Serialize)] + #[serde(rename_all = "camelCase")] + /// Serialization shape shared by `Account` and `AccountSharedData`. + struct Account<'a> { + lamports: u64, + #[serde(with = "serde_bytes")] + data: &'a [u8], + owner: &'a Pubkey, + executable: bool, + rent_epoch: Epoch, + } + + /// Serializes any readable account using the canonical `Account` layout. + pub(crate) fn serialize_account( + account: &impl ReadableAccount, + serializer: S, + ) -> Result + where + S: Serializer, + { + let account = Account { + lamports: account.lamports(), + data: account.data(), + owner: account.owner(), + executable: account.executable(), + rent_epoch: account.rent_epoch(), + }; + account.serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl serde::ser::Serialize for Account { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + account_serialize::serialize_account(self, serializer) + } +} + +#[cfg(feature = "serde")] +impl serde::ser::Serialize for AccountSharedData { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + account_serialize::serialize_account(self, serializer) + } +} + +impl From for Account { + fn from(other: AccountSharedData) -> Self { + Self { + lamports: other.lamports(), + data: other.data().to_vec(), + owner: *other.owner(), + executable: other.executable(), + rent_epoch: other.rent_epoch(), + } + } +} + +impl fmt::Debug for Account { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + debug_fmt(self, f) + } +} + +impl fmt::Debug for AccountSharedData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + debug_fmt(self, f) + } +} + +impl Account { + /// Builds an account from its exact field set. + /// + /// Used by the constructors to keep the owned layout in one place. + fn from_parts( + lamports: u64, + data: Vec, + owner: Pubkey, + executable: bool, + rent_epoch: Epoch, + ) -> Self { + Self { + lamports, + data, + owner, + executable, + rent_epoch, + } + } + + /// Creates a new account with zero-filled data. + pub fn new(lamports: u64, space: usize, owner: &Pubkey) -> Self { + Self::new_rent_epoch(lamports, space, owner, Epoch::default()) + } + + /// Creates a new account wrapped in a `RefCell`. + pub fn new_ref(lamports: u64, space: usize, owner: &Pubkey) -> Rc> { + Rc::new(RefCell::new(Self::new(lamports, space, owner))) + } + + /// Creates a new account whose data is the serialized state. + #[cfg(feature = "bincode")] + pub fn new_data( + lamports: u64, + state: &T, + owner: &Pubkey, + ) -> Result { + let data = bincode::serialize(state)?; + Ok(Self::from_parts( + lamports, + data, + *owner, + false, + Epoch::default(), + )) + } + + /// Creates a new serialized account wrapped in a `RefCell`. + #[cfg(feature = "bincode")] + pub fn new_ref_data( + lamports: u64, + state: &T, + owner: &Pubkey, + ) -> Result, bincode::Error> { + Self::new_data(lamports, state, owner).map(RefCell::new) + } + + /// Creates a new account with fixed space and serialized state. + #[cfg(feature = "bincode")] + pub fn new_data_with_space( + lamports: u64, + state: &T, + space: usize, + owner: &Pubkey, + ) -> Result { + let mut account = Self::new(lamports, space, owner); + crate::codec::serialize_data(&mut account, state)?; + Ok(account) + } + + /// Creates a new fixed-size serialized account wrapped in a `RefCell`. + #[cfg(feature = "bincode")] + pub fn new_ref_data_with_space( + lamports: u64, + state: &T, + space: usize, + owner: &Pubkey, + ) -> Result, bincode::Error> { + Self::new_data_with_space(lamports, state, space, owner).map(RefCell::new) + } + + /// Creates a new account with an explicit rent epoch. + pub fn new_rent_epoch(lamports: u64, space: usize, owner: &Pubkey, rent_epoch: Epoch) -> Self { + Self::from_parts(lamports, vec![0; space], *owner, false, rent_epoch) + } + + /// Deserializes the account data as `T`. + #[cfg(feature = "bincode")] + pub fn deserialize_data(&self) -> Result { + crate::codec::deserialize_data(self) + } + + /// Serializes `state` into the existing account data buffer. + #[cfg(feature = "bincode")] + pub fn serialize_data(&mut self, state: &T) -> Result<(), bincode::Error> { + crate::codec::serialize_data(self, state) + } +} + +impl solana_account_info::Account for Account { + fn get(&mut self) -> (&mut u64, &mut [u8], &Pubkey, bool) { + ( + &mut self.lamports, + &mut self.data, + &self.owner, + self.executable, + ) + } +} + +/// Builds `AccountInfo` values for accounts and signer bits. +/// +/// The returned infos borrow the provided accounts directly. +pub fn create_is_signer_account_infos<'a>( + accounts: &'a mut [(&'a Pubkey, bool, &'a mut Account)], +) -> Vec> { + accounts + .iter_mut() + .map(|(key, is_signer, account)| { + AccountInfo::new( + key, + *is_signer, + false, + &mut account.lamports, + &mut account.data, + &account.owner, + account.executable, + ) + }) + .collect() +} + +/// Owners that imply the account contains a loaded program. +pub const PROGRAM_OWNERS: &[Pubkey] = &[ + native_loader::id(), + bpf_loader_upgradeable::id(), + bpf_loader::id(), + bpf_loader_deprecated::id(), + loader_v4::id(), +]; diff --git a/solana/account/src/codec.rs b/solana/account/src/codec.rs new file mode 100644 index 0000000..c8aa14c --- /dev/null +++ b/solana/account/src/codec.rs @@ -0,0 +1,23 @@ +//! Shared bincode helpers for account data. + +use serde::{Serialize, de::DeserializeOwned}; + +use crate::{ReadableAccount, WritableAccount}; + +/// Deserializes typed state from an account data slice. +pub(crate) fn deserialize_data( + account: &U, +) -> Result { + bincode::deserialize(account.data()) +} + +/// Serializes typed state into an existing account data buffer. +pub(crate) fn serialize_data( + account: &mut U, + state: &T, +) -> Result<(), bincode::Error> { + if bincode::serialized_size(state)? > account.data().len() as u64 { + return Err(Box::new(bincode::ErrorKind::SizeLimit)); + } + bincode::serialize_into(account.data_as_mut_slice(), state) +} diff --git a/solana/account/src/cow/borrowed.rs b/solana/account/src/cow/borrowed.rs new file mode 100644 index 0000000..5c698a0 --- /dev/null +++ b/solana/account/src/cow/borrowed.rs @@ -0,0 +1,308 @@ +//! Raw layout used by the borrowed zero-copy account view. +//! +//! The buffer is 8-byte aligned and contains a header followed by two images. +//! `AccountHeader::sequence` selects the active image; `translate` copies it to the shadow +//! image, `rollback` restores the old view, and `commit` publishes the shadow image. + +#![allow(unsafe_op_in_unsafe_fn)] + +use std::{ + ops::{Deref, DerefMut}, + ptr::NonNull, + slice, + sync::atomic::{AtomicU32, Ordering::*}, +}; + +use solana_pubkey::Pubkey; + +use super::owned::OwnedAccount; +use super::{ALIGNMENT, AccountCore, STORAGE_UNIT, StorageUnit}; + +/// Fixed bytes in one image after the shared pubkey prefix: core and data header. +pub(super) const STATIC_SIZE: usize = size_of::() + size_of::(); +/// Storage-unit offset from the header to the first image payload, including the pubkey prefix. +pub(super) const IMAGE_OFFSET: usize = + (size_of::() + size_of::()) / STORAGE_UNIT; + +/// Header that prefixes a double-allocation borrowed account buffer. +#[repr(C, align(8))] +pub(crate) struct AccountHeader { + /// Sequence counter; parity selects the active image. + pub(crate) sequence: AtomicU32, + /// Image size measured in `AccountHeader` units. + pub(crate) space: u32, +} + +impl AccountHeader { + /// Creates a header for one image size in storage units. + pub(crate) fn new(space: u32) -> Self { + // `space` stays in storage units so the active + // image can be indexed with one multiply. + Self { sequence: 0.into(), space } + } +} + +/// Pointer arithmetic relies on these size and alignment invariants. +const _: () = assert!(size_of::() == ALIGNMENT); +const _: () = assert!(size_of::() == STORAGE_UNIT); +const _: () = assert!((size_of::() + STORAGE_UNIT) / ALIGNMENT == IMAGE_OFFSET); + +/// Borrowed zero-copy account view into an aligned external buffer. +#[derive(Clone, Eq, PartialEq)] +pub struct BorrowedAccount { + /// Header pointer for the borrowed buffer. + pub(crate) header: NonNull, + /// Pointer to the active image's account core. + pub(crate) core: NonNull, + /// Borrowed data bytes for the active image. + pub(crate) data: DataSlice, +} + +/// Returns the byte offset for the active or shadow image. +#[inline] +fn offset(header: &AccountHeader, active: bool) -> usize { + // Even sequence => image A is active, odd sequence => image B is active. + let even = header.sequence.load(Acquire).is_multiple_of(2); + // Flip to the shadow image when `active` does not match the current parity. + let step = (active ^ even) as u32; + (step * header.space) as usize + IMAGE_OFFSET +} + +impl BorrowedAccount { + /// Returns the sequence value that selects the active image. + pub(crate) fn sequence(&self) -> u32 { + // SAFETY: borrowed account headers live for the account view. + unsafe { self.header.as_ref() }.sequence.load(Acquire) + } + /// Returns the total borrowed span in `StorageUnit`s. + /// + /// # Safety + /// + /// `ptr` must point to a valid borrowed buffer created by + /// [`OwnedAccount::serialize`]. + pub unsafe fn span(ptr: NonNull) -> u32 { + let space = ptr.cast::().as_ref().space; + space * 2 + IMAGE_OFFSET as u32 + } + + /// Reads the account's pubkey stored in the image prefix. + /// + /// # Safety + /// + /// `ptr` must point to a valid borrowed buffer created by + /// [`OwnedAccount::serialize`]. + pub unsafe fn pubkey(ptr: NonNull) -> Pubkey { + *ptr.add(1).cast().as_ref() + } + + /// Builds a borrowed account view from an aligned account buffer. + /// + /// # Safety + /// + /// `buffer` must be 8-byte aligned and point to a valid borrowed account + /// buffer whose first bytes are the account header, followed by two + /// image-sized payloads. The active image is selected from the header + /// sequence parity. + pub unsafe fn init(buffer: NonNull) -> Self { + let header = buffer.cast(); + let offset = offset(header.as_ref(), true); + + let core = header.add(offset).cast(); + let data = DataSlice::init(core.add(1).cast()); + + Self { header, core, data } + } + + /// Copies the active image into the shadow image and switches to it. + /// + /// # Safety + /// + /// The borrowed image must still be the one selected by `init`. + pub unsafe fn translate(&mut self) { + let offset = offset(self.header.as_ref(), false); + + // Copy bytes in bulk from active image to the shadow + let dst = self.header.add(offset).cast(); + let src = self.core.cast::(); + let count = self.header.as_ref().space as usize; + dst.copy_from_nonoverlapping(src, count); + // Switch the pointers to the shadow view + self.core = dst.cast(); + self.data = DataSlice::init(self.core.add(1).cast()); + } + + /// Commits the shadow image by advancing the sequence counter. + pub fn commit(&self) { + // SAFETY: the header is part of the borrowed buffer for the lifetime of `self`. + unsafe { self.header.as_ref().sequence.fetch_add(1, Release) }; + } + + /// Repoints this view to the currently active image without copying data. + /// + /// This is used after a sequence change or abandoned shadow write. + pub unsafe fn reset(&mut self) { + let offset = offset(self.header.as_ref(), true); + self.core = self.header.add(offset).cast(); + self.data = DataSlice::init(self.core.add(1).cast()); + } + + /// Undoes the latest commit, by adjusting the sequence counter + /// + /// # Safety + /// + /// Call this only after `commit` to avoid data corruption; + pub unsafe fn rollback(&self) { + // SAFETY: the header is part of the borrowed buffer for the lifetime of `self`. + unsafe { self.header.as_ref().sequence.fetch_sub(1, Release) }; + } + + /// Returns the owner pubkey from the active image. + pub fn owner(&self) -> Pubkey { + // SAFETY: `core` points at a live `AccountCore` inside the borrowed buffer. + unsafe { self.core.as_ref() }.owner + } + + /// Returns the serialized active image bytes that define account state. + /// + /// The slice starts at `AccountCore`, includes the `DataHeader`, and stops + /// after initialized data. It excludes the shared header, pubkey prefix, + /// inactive shadow image, and spare data capacity. + pub fn storage(&self) -> &[u8] { + let len = STATIC_SIZE + self.data.len(); + // SAFETY: `core` points at the active image and `len` only covers its + // initialized state bytes: core, data header, and initialized data. + unsafe { slice::from_raw_parts(self.core.as_ptr().cast(), len) } + } +} + +impl From<&BorrowedAccount> for OwnedAccount { + fn from(value: &BorrowedAccount) -> Self { + Self { + // SAFETY: `BorrowedAccount` guarantees `core` points at a live account + // header inside the borrowed buffer for the lifetime of the borrow. + core: *unsafe { value.core.as_ref() }, + data: value.data.deref().to_vec().into(), + } + } +} + +/// Mutable byte slice backed by a borrowed account buffer. +#[derive(Clone, Eq, PartialEq)] +pub(crate) struct DataSlice { + /// Header carrying length and capacity. + header: NonNull, + /// Pointer to the first data byte. + ptr: NonNull, +} + +/// Data header stored immediately before the raw byte slice. +#[repr(C)] +pub(crate) struct DataHeader { + /// Initialized data length. + len: u32, + /// Total writable capacity. + cap: u32, +} + +impl DataHeader { + /// Creates a data header for one image. + pub(crate) fn new(len: u32, allocation: u32) -> Self { + // `cap` is the writable tail after `AccountCore` and `DataHeader`. + let cap = (allocation as usize * STORAGE_UNIT - STATIC_SIZE) as u32; + debug_assert!(len <= cap); + Self { len, cap } + } +} + +impl DataSlice { + /// Builds a borrowed slice from a data header. + /// + /// # Safety + /// + /// `header` must point at a valid `DataHeader` followed by initialized data. + unsafe fn init(header: NonNull) -> Self { + let ptr = header.add(1).cast(); + Self { header, ptr } + } + + /// Returns the initialized byte length. + pub(crate) fn len(&self) -> usize { + // SAFETY: `header` points at the live data header for this borrowed slice. + let header = unsafe { self.header.as_ref() }; + header.len as usize + } + + /// Returns the total writable capacity. + pub(crate) fn capacity(&self) -> usize { + // SAFETY: `header` points at the live data header for this borrowed slice. + let header = unsafe { self.header.as_ref() }; + header.cap as usize + } + + /// Returns the remaining writable capacity. + pub(crate) fn spare(&self) -> usize { + self.capacity() - self.len() + } + + /// Resizes the initialized range in place. + /// + /// # Safety + /// + /// `len` must not exceed the borrowed capacity. + pub(crate) unsafe fn resize(&mut self, len: usize, val: u8) { + let prev = self.len(); + debug_assert!(prev <= self.capacity()); + debug_assert!(len <= self.capacity()); + let delta = len.saturating_sub(prev); + if delta > 0 { + self.ptr.as_ptr().add(prev).write_bytes(val, delta); + } + self.header.as_mut().len = len as u32; + } + + /// Appends bytes in place. + /// + /// # Safety + /// + /// `data` must fit in the remaining borrowed capacity and not overlap. + pub(crate) unsafe fn extend(&mut self, data: &[u8]) { + let len = self.len(); + debug_assert!(data.len() <= self.capacity().saturating_sub(len)); + let dst = self.ptr.as_ptr().add(len); + dst.copy_from_nonoverlapping(data.as_ptr(), data.len()); + self.header.as_mut().len += data.len() as u32; + } + + /// Replaces the initialized bytes in place. + /// + /// # Safety + /// + /// `data` must fit in the borrowed capacity and not overlap. + pub(crate) unsafe fn set(&mut self, data: &[u8]) { + debug_assert!(data.len() <= self.capacity()); + self.ptr.as_ptr().copy_from_nonoverlapping(data.as_ptr(), data.len()); + self.header.as_mut().len = data.len() as u32; + } +} + +impl Deref for DataSlice { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + // SAFETY: `len` bytes from `ptr` are initialized account data owned by + // the borrowed buffer described by this `DataSlice`. + unsafe { slice::from_raw_parts(self.ptr.as_ptr(), self.len()) } + } +} + +impl DerefMut for DataSlice { + fn deref_mut(&mut self) -> &mut Self::Target { + // SAFETY: the borrowed buffer grants unique mutable access through this borrow. + unsafe { slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len()) } + } +} + +// SAFETY: `BorrowedAccount` points into external storage and only exposes +// shared reads unless the caller holds `&mut self`; moving the view to another +// thread does not weaken the buffer lifetime and aliasing requirements. +unsafe impl Send for BorrowedAccount {} diff --git a/solana/account/src/cow/mod.rs b/solana/account/src/cow/mod.rs new file mode 100644 index 0000000..023e176 --- /dev/null +++ b/solana/account/src/cow/mod.rs @@ -0,0 +1,663 @@ +//! Copy-on-write account data with zero-copy access to aligned external storage. +//! +//! `borrowed` defines the raw buffer layout and `owned` holds the heap-backed form. +#![allow(unsafe_op_in_unsafe_fn)] + +mod borrowed; +mod owned; + +pub use borrowed::BorrowedAccount; +pub use owned::{AccountBuilder, OwnedAccount}; + +use crate::{Account, ReadableAccount, WritableAccount}; +use solana_clock::{Epoch, Slot}; +use solana_pubkey::Pubkey; +use std::{ + cell::RefCell, + ops::{Deref, DerefMut}, + rc::Rc, + sync::Arc, +}; + +#[cfg(feature = "dev-context-only-utils")] +use qualifier_attr::qualifiers; + +use CoWAccount::*; + +/// Borrowed buffers must be aligned to this many bytes. +pub const ALIGNMENT: usize = 8; +/// Bytes in one storage unit. +pub const STORAGE_UNIT: usize = size_of::(); +/// Minimum addressable storage unit for borrowed account images. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct StorageUnit(pub u64); + +/// Shared account data that borrows directly from an aligned external buffer +/// until a write requires promotion to owned heap storage. +/// +/// Higher layers use `mutable()` to decide whether the account is routed to +/// the persisted or volatile store. +#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(from = "Account"))] +#[derive(PartialEq, Eq, Clone, Default)] +pub struct AccountSharedData { + /// Backing storage, borrowed until promotion or direct construction. + pub(crate) cow: CoWAccount, + /// Fields changed through the writable APIs. + pub(crate) dirty: DirtyMarkers, +} + +/// Core account state shared by the borrowed and owned representations. +#[repr(C)] +#[derive(Clone, Copy, Default, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct AccountCore { + /// Lamport balance. + pub(crate) lamports: u64, + /// Account owner. + pub(crate) owner: Pubkey, + /// On-chain slot, at which the account was cloned. + pub(crate) slot: Slot, + /// Mutually exclusive mode of existence for the account. + pub(crate) mode: AccountMode, + /// Independent state modifier flags such as executable or compressed. + pub(crate) flags: StateFlags, + /// Reserved bytes that make the serialized representation deterministic. + _padding: [u8; 6], +} + +impl Deref for AccountSharedData { + type Target = AccountCore; + + fn deref(&self) -> &Self::Target { + match &self.cow { + Borrowed(account) => { + // SAFETY: `BorrowedAccount` owns the invariant that `core` points at + // a live `AccountCore` inside the borrowed buffer. + unsafe { account.core.as_ref() } + } + Owned(account) => &account.core, + } + } +} + +impl DerefMut for AccountSharedData { + fn deref_mut(&mut self) -> &mut Self::Target { + match &mut self.cow { + Borrowed(account) => { + // SAFETY: `&mut self` guarantees unique access to the borrowed image. + unsafe { account.core.as_mut() } + } + Owned(account) => &mut account.core, + } + } +} + +impl AccountSharedData { + /// Returns a reference to the inner copy-on-write representation. + pub fn cow(&self) -> &CoWAccount { + &self.cow + } + + /// Returns mutable access to the inner copy-on-write representation. + pub fn cow_mut(&mut self) -> &mut CoWAccount { + &mut self.cow + } + + /// Returns the account's on-chain slot. + pub fn slot(&self) -> Slot { + self.slot + } + + /// Copies a clean borrowed image into the shadow buffer before mutation. + pub fn translate(&mut self) { + if self.dirty() { + return; + } + if let Borrowed(ref mut acc) = self.cow { + // SAFETY: this runs before the first dirty marker, so the borrowed + // view still points at the active image selected by `init`. + unsafe { acc.translate() }; + } + } + + /// Returns an owned copy of the current account state. + pub fn owned(&self) -> OwnedAccount { + match self.cow() { + Borrowed(a) => a.into(), + Owned(a) => a.clone(), + } + } + + /// Returns `true` for engine-exclusive modes that may be mutated + pub fn mutable(&self) -> bool { + use AccountMode::*; + matches!(self.mode, Delegated | Ephemeral | Transient) + } + + /// Returns `true` when the account is in `mode`. + pub fn is(&self, mode: AccountMode) -> bool { + self.mode == mode + } + + /// Returns the account modifier flags. + pub fn flags(&self) -> &StateFlags { + &self.flags + } + + /// Returns the dirty-field markers. + pub fn markers(&self) -> &DirtyMarkers { + &self.dirty + } + + /// Marks the data buffer as modified. + pub(crate) fn mark_data_dirty(&mut self) { + self.dirty.insert(DirtyMarkers::DATA); + } + + /// Returns `true` when the owned buffer has more than one strong reference. + pub fn is_shared(&self) -> bool { + self.cow.is_shared() + } + + /// Returns `true` if any field has been modified. + pub fn dirty(&self) -> bool { + self.dirty.intersects(DirtyMarkers::all()) + } + + /// Returns the current data capacity. + pub fn capacity(&self) -> usize { + self.cow.capacity() + } + + /// Returns a shared owned copy of the current data bytes. + pub fn data_clone(&self) -> Arc> { + self.cow.data_clone() + } + + /// Resizes the account data. + pub fn resize(&mut self, len: usize, val: u8) { + self.translate(); + self.mark_data_dirty(); + self.cow.resize(len, val); + } + + /// Appends bytes to the account data. + pub fn extend_from_slice(&mut self, data: &[u8]) { + self.translate(); + self.mark_data_dirty(); + self.cow.extend_from_slice(data); + } + + /// Replaces the account data with the provided bytes. + pub fn set_data_from_slice(&mut self, data: &[u8]) { + self.translate(); + self.mark_data_dirty(); + self.cow.set_data_from_slice(data); + } + + /// Sets the account mode and marks the mode dirty. + pub fn set_mode(&mut self, mode: AccountMode) { + self.translate(); + self.dirty.set(DirtyMarkers::MODE, true); + self.mode = mode; + } + + /// Writes bytes at `offset`, extending and zero-filling as needed. + pub(crate) fn set_data_at(&mut self, offset: usize, data: &[u8]) { + self.translate(); + self.mark_data_dirty(); + let len = self.data().len(); + if offset > len { + // Grow to `offset`, zero-filling the gap; the write below then + // appends `data` past it via `extend_from_slice`. + self.resize(offset, 0); + } + + // Write the overlap in place, then append any remaining tail. This + // keeps borrowed buffers on the fast path when the write fits. + let n = self.data().len().saturating_sub(offset).min(data.len()); + self.data_as_mut_slice()[offset..offset + n].copy_from_slice(&data[..n]); + self.extend_from_slice(&data[n..]); + } + + /// Replaces the account data with `data`. + #[allow(unused)] + #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] + pub(crate) fn set_data(&mut self, data: Vec) { + self.translate(); + self.mark_data_dirty(); + self.cow.set_data(data); + } + + /// Sets the account's on-chain slot. + pub(crate) fn set_slot(&mut self, slot: Slot) { + self.translate(); + self.dirty.set(DirtyMarkers::SLOT, true); + self.slot = slot; + } + + /// Sets or clears a state flag and marks the flags dirty. + pub(crate) fn set_flag(&mut self, flag: StateFlags, val: bool) { + self.translate(); + self.dirty.set(DirtyMarkers::FLAGS, true); + self.flags.set(flag, val); + } + + /// Creates a new owned shared-data account with zero-filled data. + pub fn new(lamports: u64, space: usize, owner: &Pubkey) -> Self { + AccountBuilder::default() + .lamports(lamports) + .data(vec![0; space]) + .owner(*owner) + .build() + } + /// Creates a new shared-data account wrapped in a `RefCell`. + pub fn new_ref(lamports: u64, space: usize, owner: &Pubkey) -> Rc> { + Rc::new(RefCell::new(Self::new(lamports, space, owner))) + } + + /// Creates a new account with serialized data. + #[cfg(feature = "bincode")] + pub fn new_data( + lamports: u64, + state: &T, + owner: &Pubkey, + ) -> Result { + let data = bincode::serialize(state)?; + Ok(Self::create_from_existing_shared_data( + lamports, + Arc::new(data), + *owner, + false, + Epoch::default(), + )) + } + + /// Creates a new serialized account wrapped in a `RefCell`. + #[cfg(feature = "bincode")] + pub fn new_ref_data( + lamports: u64, + state: &T, + owner: &Pubkey, + ) -> Result, bincode::Error> { + Self::new_data(lamports, state, owner).map(RefCell::new) + } + + /// Creates a new fixed-size account with serialized data. + #[cfg(feature = "bincode")] + pub fn new_data_with_space( + lamports: u64, + state: &T, + space: usize, + owner: &Pubkey, + ) -> Result { + let mut account = Self::new(lamports, space, owner); + crate::codec::serialize_data(&mut account, state)?; + Ok(account) + } + + /// Creates a new fixed-size serialized account wrapped in a `RefCell`. + #[cfg(feature = "bincode")] + pub fn new_ref_data_with_space( + lamports: u64, + state: &T, + space: usize, + owner: &Pubkey, + ) -> Result, bincode::Error> { + Self::new_data_with_space(lamports, state, space, owner).map(RefCell::new) + } + + /// Creates a new shared-data account. + /// + /// `rent_epoch` is ignored because this type does not store it. + pub fn new_rent_epoch(lamports: u64, space: usize, owner: &Pubkey, _: Epoch) -> Self { + Self::new(lamports, space, owner) + } + + /// Deserializes the account data as `T`. + #[cfg(feature = "bincode")] + pub fn deserialize_data(&self) -> Result { + crate::codec::deserialize_data(self) + } + + /// Serializes `state` into the existing account data buffer. + #[cfg(feature = "bincode")] + pub fn serialize_data(&mut self, state: &T) -> Result<(), bincode::Error> { + crate::codec::serialize_data(self, state) + } + + /// Creates an owned shared-data account from existing shared bytes. + /// + /// `rent_epoch` is ignored because this type does not store it. + pub fn create_from_existing_shared_data( + lamports: u64, + data: Arc>, + owner: Pubkey, + executable: bool, + _: Epoch, + ) -> Self { + AccountBuilder::default() + .lamports(lamports) + .data(data) + .owner(owner) + .executable(executable) + .build() + } +} + +bitflags::bitflags! { + /// Account state modifier flags. + #[derive(Clone, Copy, Default, PartialEq, Eq, Debug)] + #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] + pub struct StateFlags: u8 { + /// Executable account data. + const EXECUTABLE = 1 << 0; + /// Compressed account using the Light protocol representation. + const COMPRESSED = 1 << 1; + } + + /// Bits that record which fields changed through `AccountSharedData`. + #[derive(Clone, Copy, Default, PartialEq, Eq)] + pub struct DirtyMarkers: u8 { + /// Data bytes changed. + const DATA = 1 << 0; + /// Owner changed. + const OWNER = 1 << 1; + /// Lamports changed. + const LAMPORTS = 1 << 2; + /// Slot changed. + const SLOT = 1 << 3; + /// Mode changed. + const MODE = 1 << 4; + /// State flags changed. + const FLAGS = 1 << 5; + } +} + +/// `wincode` codec for `StateFlags`, which is a `bitflags!` newtype and so +/// cannot use the derives. Routed through `bincode`/`serde`, which encodes the +/// single bits byte identically to a plain `u8`. +#[cfg(feature = "wincode")] +const _: () = { + use core::mem::MaybeUninit; + use wincode::{ + ReadError, ReadResult, SchemaRead, SchemaWrite, TypeMeta, WriteError, WriteResult, + config::ConfigCore, + io::{Reader, Writer}, + }; + + // SAFETY: encodes exactly one byte; matches `TYPE_META` / `size_of`. + unsafe impl SchemaWrite for StateFlags { + type Src = StateFlags; + const TYPE_META: TypeMeta = TypeMeta::Static { size: 1, zero_copy: false }; + + fn size_of(_: &Self::Src) -> WriteResult { + Ok(1) + } + + fn write(mut writer: impl Writer, src: &Self::Src) -> WriteResult<()> { + let bytes = bincode::serialize(src).map_err(|_| WriteError::Custom("StateFlags"))?; + writer.write(&bytes)?; + Ok(()) + } + } + + // SAFETY: consumes exactly one byte; matches `TYPE_META`. + unsafe impl<'de, C: ConfigCore> SchemaRead<'de, C> for StateFlags { + type Dst = StateFlags; + const TYPE_META: TypeMeta = TypeMeta::Static { size: 1, zero_copy: false }; + + fn read(mut reader: impl Reader<'de>, dst: &mut MaybeUninit) -> ReadResult<()> { + let bytes = reader.take_array::<1>()?; + dst.write(bincode::deserialize(&bytes).map_err(|_| ReadError::Custom("StateFlags"))?); + Ok(()) + } + } +}; + +/// Backing storage for `AccountSharedData`. +#[derive(PartialEq, Eq, Clone)] +pub enum CoWAccount { + /// Borrowed image, a view into static backing buffer. + Borrowed(BorrowedAccount), + /// Heap-owned image. + Owned(OwnedAccount), +} + +impl CoWAccount { + /// Promotes borrowed storage to the owned form. + pub(crate) fn promote(&mut self) { + let Self::Borrowed(account) = self else { + return; + }; + *self = Self::Owned(account.deref().into()); + } + + /// Returns the current data slice. + pub(crate) fn data(&self) -> &[u8] { + match self { + Self::Borrowed(account) => &account.data, + Self::Owned(account) => &account.data, + } + } + + /// Returns `true` when the heap buffer has multiple owners. + pub(crate) fn is_shared(&self) -> bool { + match self { + Self::Borrowed(_) => false, + Self::Owned(account) => Arc::strong_count(&account.data) > 1, + } + } + + /// Returns the current data capacity. + pub(crate) fn capacity(&self) -> usize { + match self { + Self::Borrowed(account) => account.data.capacity(), + Self::Owned(account) => account.data.capacity(), + } + } + + /// Returns a shared owned copy of the current data bytes. + pub(crate) fn data_clone(&self) -> Arc> { + match self { + Self::Borrowed(account) => Arc::new(account.data.to_vec()), + Self::Owned(account) => Arc::clone(&account.data), + } + } + + /// Returns mutable data, promoting borrowed storage only when needed. + pub(crate) fn data_mut(&mut self) -> &mut [u8] { + match self { + Self::Borrowed(account) => &mut account.data, + Self::Owned(account) => Arc::>::make_mut(&mut account.data).as_mut_slice(), + } + } + + /// Reserves additional space for the account data. + pub fn reserve(&mut self, additional: usize) { + if let Self::Borrowed(a) = self + && a.data.spare() >= additional + { + return; + } + self.promote(); + if let Self::Owned(account) = self { + Arc::make_mut(&mut account.data).reserve(additional); + } + } + + /// Resizes the account data. + pub(crate) fn resize(&mut self, len: usize, val: u8) { + if let Self::Borrowed(a) = self + && len <= a.data.capacity() + { + // SAFETY: this stays in the borrowed image only while the resized + // range fits within the borrowed capacity. + unsafe { a.data.resize(len, val) }; + return; + } + + self.promote(); + if let Self::Owned(account) = self { + Arc::make_mut(&mut account.data).resize(len, val); + } + } + + /// Appends bytes to the account data. + pub(crate) fn extend_from_slice(&mut self, data: &[u8]) { + self.reserve(data.len()); + + match self { + Self::Borrowed(account) => { + // SAFETY: `reserve` keeps the borrowed image only when the appended + // bytes fit in the remaining borrowed capacity. + unsafe { account.data.extend(data) }; + } + Self::Owned(account) => Arc::make_mut(&mut account.data).extend_from_slice(data), + } + } + + /// Replaces the account data with the provided bytes. + pub(crate) fn set_data_from_slice(&mut self, data: &[u8]) { + let additional = data.len().saturating_sub(self.data().len()); + self.reserve(additional); + + match self { + Self::Borrowed(account) => { + // SAFETY: `reserve` keeps the borrowed image only when the + // replacement bytes fit in the borrowed capacity. + unsafe { account.data.set(data) }; + } + Self::Owned(account) => { + let data_buf = Arc::make_mut(&mut account.data); + data_buf.clear(); + data_buf.extend_from_slice(data); + } + } + } + + /// Replaces the account data with the provided owned bytes. + pub(crate) fn set_data(&mut self, data: Vec) { + if matches!(self, Self::Borrowed(_)) { + self.set_data_from_slice(&data); + return; + } + + if let Self::Owned(account) = self { + account.data = data.into(); + } + } +} + +/// Mutually exclusive modes an account can occupy in the ephemeral rollup (ER). +#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)] +#[repr(u8)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "wincode", derive(wincode::SchemaRead, wincode::SchemaWrite))] +pub enum AccountMode { + /// Default account state. Not writable by users (exists on chain, but not delegated) + #[default] + ReadOnly, + /// Empty account used to avoid frequent chain syncs. + Placeholder, + /// Internal account used for sysvars, features, and precompiles. + System, + /// Account delegated to the current ER node instance. + Delegated, + /// Account that exists only inside the ER. + Ephemeral, + /// Temporary state during mode transitions (e.g. delegated -> readonly). + Transient, + /// Closed account that should be removed from storage. + Closed, +} + +/// Read wrapper that retries borrowed account reads when a concurrent publish +/// changes the backing image. +pub struct AccountSeqLock { + account: AccountSharedData, + sequence: Option, +} + +impl AccountSeqLock { + /// Creates a read lock with the sequence that matches the current account view. + pub fn new(account: AccountSharedData) -> Self { + let mut sequence = None; + if let Borrowed(ref acc) = account.cow { + sequence.replace(acc.sequence()); + } + Self { account, sequence } + } + + /// Runs `reader` against a stable account image. + /// + /// For borrowed accounts, the sequence is checked after the read. If a + /// writer published a new image meanwhile, the account view is reset to that + /// active image and the read is retried. + pub fn read(&mut self, reader: F) -> R + where + F: Fn(&AccountSharedData) -> R, + { + loop { + // sequence is always present for borrowed accounts + let pre = self.sequence.unwrap_or_default(); + let result = reader(&self.account); + match self.account.cow_mut() { + Borrowed(acc) => { + let post = acc.sequence(); + if pre == post { + return result; + } + // SAFETY: a changed sequence means the active image may have + // moved, so the borrowed view must be repointed before retrying. + unsafe { acc.reset() }; + self.sequence = Some(post); + } + Owned(_) => return result, + } + } + } +} + +impl Default for CoWAccount { + fn default() -> Self { + Self::Owned(OwnedAccount::default()) + } +} + +/// Wraps an owned account in `AccountSharedData`. +impl From for AccountSharedData { + fn from(value: OwnedAccount) -> Self { + Self { + cow: Owned(value), + dirty: DirtyMarkers::default(), + } + } +} + +/// Wraps a borrowed account in `AccountSharedData`. +impl From for AccountSharedData { + fn from(value: BorrowedAccount) -> Self { + Self { + cow: Borrowed(value), + dirty: DirtyMarkers::default(), + } + } +} + +/// Converts a plain `Account` into shared data. +impl From for AccountSharedData { + fn from(value: Account) -> Self { + AccountBuilder::default() + .lamports(value.lamports) + .data(value.data) + .owner(value.owner) + .executable(value.executable) + .build() + } +} + +/// We only access AccountSharedData via transaction lock in the +/// execution layer or with a SeqLock semantics outside of execution +unsafe impl Sync for AccountSharedData {} diff --git a/solana/account/src/cow/owned.rs b/solana/account/src/cow/owned.rs new file mode 100644 index 0000000..b52ff24 --- /dev/null +++ b/solana/account/src/cow/owned.rs @@ -0,0 +1,139 @@ +use super::borrowed::{AccountHeader, DataHeader, STATIC_SIZE}; +use super::{ALIGNMENT, StateFlags}; +use crate::cow::AccountCore; +use crate::cow::borrowed::IMAGE_OFFSET; +use crate::{Account, AccountMode, AccountSharedData, StorageUnit}; +use solana_clock::Slot; +use solana_pubkey::Pubkey; +use std::{ptr::NonNull, sync::Arc}; + +/// Heap-backed account, used after promotion from borrowed or direct construction. +#[derive(Clone, Default, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct OwnedAccount { + /// Core account fields. + pub(crate) core: AccountCore, + /// Heap-owned data buffer. + pub(crate) data: Arc>, +} + +impl OwnedAccount { + /// Returns the exact storage units needed to serialize this account. + pub fn units(&self) -> u32 { + self.allocation() * 2 + IMAGE_OFFSET as u32 + } + + /// Returns the storage units needed for one image, rounded up to alignment. + fn allocation(&self) -> u32 { + (STATIC_SIZE + self.data.len()).div_ceil(ALIGNMENT) as u32 + } + + /// Writes the account into a buffer sized by `units`. + /// + /// # Safety + /// + /// `buf` must be exactly `units()` storage units long. + /// `pubkey` is written into the image prefix so borrowed iteration can + /// recover the full account key without consulting the index. + pub unsafe fn serialize(&self, buf: &mut [StorageUnit], pubkey: &Pubkey) { + let ptr = NonNull::new_unchecked(buf.as_mut_ptr()); + debug_assert_eq!(self.units() as usize, buf.len()); + + fn write(ptr: NonNull, v: T) -> NonNull { + // SAFETY: `serialize` requires a buffer sized for the full layout. + unsafe { + ptr.cast().write(v); + ptr.cast().add(1) + } + } + + let allocation = self.allocation(); + let ptr = write(ptr, AccountHeader::new(allocation)); + // The image prefix stores the account pubkey for later iteration. + let ptr = write(ptr, *pubkey); + let ptr = write(ptr, self.core); + let len = self.data.len(); + let ptr = write(ptr, DataHeader::new(len as u32, allocation)).cast(); + self.data.as_ptr().copy_to_nonoverlapping(ptr.as_ptr(), len); + } + + /// Returns the owner pubkey. + pub fn owner(&self) -> Pubkey { + self.core.owner + } +} + +/// Builder for an owned account representation. +/// +/// Use this when the account does not start from a borrowed external buffer. +#[derive(Default)] +pub struct AccountBuilder { + /// Account under construction. + acc: OwnedAccount, +} + +impl AccountBuilder { + /// Sets the lamport balance. + pub fn lamports(mut self, lamports: u64) -> Self { + self.acc.core.lamports = lamports; + self + } + + /// Sets the data buffer. + pub fn data(mut self, data: impl Into>>) -> Self { + self.acc.data = data.into(); + self + } + + /// Sets the owner. + pub fn owner(mut self, owner: Pubkey) -> Self { + self.acc.core.owner = owner; + self + } + + /// Sets the account persistence mode of the account + pub fn mode(mut self, mode: AccountMode) -> Self { + self.acc.core.mode = mode; + self + } + + /// Sets the executable flag. + pub fn executable(mut self, executable: bool) -> Self { + self.acc.core.flags.set(StateFlags::EXECUTABLE, executable); + self + } + + /// Sets the compressed flag. + pub fn compressed(mut self, compressed: bool) -> Self { + self.acc.core.flags.set(StateFlags::COMPRESSED, compressed); + self + } + + /// Sets the on chain slot. + pub fn slot(mut self, slot: Slot) -> Self { + self.acc.core.slot = slot; + self + } + + /// Finishes building the owned account. + pub fn build>(self) -> A { + self.acc.into() + } +} + +impl From for OwnedAccount { + fn from(value: Account) -> Self { + AccountBuilder::default() + .lamports(value.lamports) + .data(value.data) + .owner(value.owner) + .executable(value.executable) + .build() + } +} + +impl From for AccountBuilder { + fn from(value: AccountSharedData) -> Self { + Self { acc: value.owned() } + } +} diff --git a/solana/account/src/cow/tests.rs b/solana/account/src/cow/tests.rs new file mode 100644 index 0000000..89ad2b6 --- /dev/null +++ b/solana/account/src/cow/tests.rs @@ -0,0 +1,83 @@ +use super::borrowed::BorrowedAccount; +use super::{StorageUnit, init, serialize_buf}; +use crate::AccountBuilder; +use solana_pubkey::Pubkey; +use std::sync::atomic::Ordering::Acquire; + +const BORROWED_LAMPORTS: u64 = 5; +const ACTIVE_DATA: &[u8] = &[1, 2, 3]; +const COMMIT_DATA: &[u8] = &[4, 5, 6]; +const COMMITTED_DATA: &[u8] = &[9, 2, 3]; +const ACTIVE_WRITE: u8 = 9; +const ROLLBACK_WRITE: u8 = 8; +const INITIAL_SEQUENCE: u32 = 0; +const COMMITTED_SEQUENCE: u32 = 1; + +// Serializes an owned account into a borrowed buffer image. +fn make_buf(data: &[u8]) -> Vec { + let owner = Pubkey::new_unique(); + let owned = AccountBuilder::default() + .lamports(BORROWED_LAMPORTS) + .data(data.to_vec()) + .owner(owner) + .build(); + serialize_buf(&owned) +} + +// Reads the active sequence counter. +fn seq(acc: &BorrowedAccount) -> u32 { + // SAFETY: test helpers only call this on a live borrowed buffer. + unsafe { acc.header.as_ref().sequence.load(Acquire) } +} + +// Returns the active image bytes for direct assertions. +fn data(acc: &BorrowedAccount) -> &[u8] { + &acc.data +} + +#[test] +// `init` should read the active image without changing the sequence. +fn test_init_reads_active_image() { + let mut buf = make_buf(ACTIVE_DATA); + let borrowed = init(&mut buf); + + assert_eq!(data(&borrowed), ACTIVE_DATA); + assert_eq!(seq(&borrowed), INITIAL_SEQUENCE); +} + +#[test] +// `translate` should copy the active image into the shadow view, and `commit` should publish it. +fn test_translate_commit_publishes_shadow_image() { + let mut buf = make_buf(ACTIVE_DATA); + let mut borrowed = init(&mut buf); + + // SAFETY: `borrowed` still points at the live borrowed image selected by `init`. + unsafe { borrowed.translate() }; + assert_eq!(seq(&borrowed), INITIAL_SEQUENCE); + + borrowed.data[0] = ACTIVE_WRITE; + borrowed.commit(); + assert_eq!(seq(&borrowed), COMMITTED_SEQUENCE); + + let borrowed = init(&mut buf); + assert_eq!(data(&borrowed), COMMITTED_DATA); +} + +#[test] +// `rollback` should discard shadow writes and restore the active view. +fn test_translate_rollback_discards_shadow_writes() { + let mut buf = make_buf(COMMIT_DATA); + let mut borrowed = init(&mut buf); + + // SAFETY: `borrowed` still points at the live borrowed image selected by `init`. + unsafe { borrowed.translate() }; + borrowed.data[0] = ROLLBACK_WRITE; + + // SAFETY: `reset` is paired with the preceding `translate`. + unsafe { borrowed.reset() }; + assert_eq!(seq(&borrowed), INITIAL_SEQUENCE); + assert_eq!(data(&borrowed), COMMIT_DATA); + + let borrowed = init(&mut buf); + assert_eq!(data(&borrowed), COMMIT_DATA); +} diff --git a/solana/account/src/lib.rs b/solana/account/src/lib.rs new file mode 100644 index 0000000..07e5904 --- /dev/null +++ b/solana/account/src/lib.rs @@ -0,0 +1,33 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc = include_str!("../README.md")] + +mod account; +#[cfg(feature = "bincode")] +mod codec; +mod cow; +mod patch; +#[cfg(feature = "bincode")] +pub mod state_traits; +#[cfg(feature = "bincode")] +mod sysvar; +/// Test-only helpers for borrowed account buffers. +#[cfg(feature = "dev-context-only-utils")] +pub mod test_utils; +mod traits; + +pub use account::{Account, PROGRAM_OWNERS, create_is_signer_account_infos}; +pub use cow::{ + ALIGNMENT, AccountBuilder, AccountMode, AccountSeqLock, AccountSharedData, BorrowedAccount, + CoWAccount, DirtyMarkers, OwnedAccount, STORAGE_UNIT, StateFlags, StorageUnit, +}; +pub use patch::AccountFieldPatch; +#[cfg(feature = "bincode")] +pub use sysvar::{ + DUMMY_INHERITABLE_ACCOUNT_FIELDS, InheritableAccountFields, create_account_for_test, + create_account_shared_data_for_test, create_account_shared_data_with_fields, + create_account_with_fields, from_account, to_account, +}; +pub use traits::{ReadableAccount, WritableAccount, accounts_equal}; + +#[cfg(test)] +mod tests; diff --git a/solana/account/src/patch.rs b/solana/account/src/patch.rs new file mode 100644 index 0000000..fc52743 --- /dev/null +++ b/solana/account/src/patch.rs @@ -0,0 +1,86 @@ +use core::fmt; + +use solana_clock::Slot; +use solana_pubkey::Pubkey; + +use crate::{AccountMode, AccountSharedData, OwnedAccount, WritableAccount, cow::StateFlags}; + +const MAX_DATA_CHUNK_SIZE: usize = u16::MAX as usize - 256; + +/// A single-field account patch. +#[cfg_attr(feature = "wincode", derive(wincode::SchemaRead, wincode::SchemaWrite))] +pub enum AccountFieldPatch { + /// Replaces the lamport balance. + Lamports(u64), + /// Replaces the owner. + Owner(Pubkey), + /// Writes bytes starting at `offset`, extending the account data if needed. + DataAt { + /// Byte offset into the current data buffer. + offset: usize, + /// Bytes to write. + data: Vec, + }, + /// Replaces the slot. + Slot(Slot), + /// Replaces the account mode. + Mode(AccountMode), + /// Sets or clears a state flag. + Flag { + /// Flag to update. + flag: StateFlags, + /// Whether to set the flag. + val: bool, + }, +} + +impl AccountFieldPatch { + /// Applies this patch to `account`. + /// + /// The account methods mark dirtiness and preserve the writable invariants. + pub fn apply(self, account: &mut AccountSharedData) { + match self { + Self::Lamports(v) => account.set_lamports(v), + Self::Slot(v) => account.set_slot(v), + Self::Owner(v) => account.set_owner(v), + Self::Flag { flag, val } => account.set_flag(flag, val), + Self::Mode(v) => account.set_mode(v), + Self::DataAt { offset, data } => account.set_data_at(offset, &data), + } + } + + /// Decomposes an owned account into the ordered sequence of patches that + /// reconstruct it: lamports, slot, owner, each set flag, mode, then the data + /// in `MAX_DATA_CHUNK_SIZE`-sized chunks. + pub fn sequence(account: OwnedAccount) -> Vec { + let mut sequence = Vec::with_capacity(6); + sequence.push(Self::Lamports(account.core.lamports)); + sequence.push(Self::Slot(account.core.slot)); + sequence.push(Self::Owner(account.core.owner)); + for flag in account.core.flags.iter() { + sequence.push(Self::Flag { flag, val: true }); + } + sequence.push(Self::Mode(account.core.mode)); + let mut offset = 0; + for data in account.data.chunks(MAX_DATA_CHUNK_SIZE) { + sequence.push(Self::DataAt { offset, data: data.into() }); + offset += data.len(); + } + sequence + } +} + +/// Concise, log-friendly rendering: scalar fields show their value, `DataAt` +/// shows only `offset+len` (never the raw bytes). +impl fmt::Debug for AccountFieldPatch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Lamports(v) => write!(f, "lamports={v}"), + Self::Owner(v) => write!(f, "owner={v}"), + Self::Slot(v) => write!(f, "slot={v}"), + Self::Mode(v) => write!(f, "mode={v:?}"), + Self::Flag { flag, val } => write!(f, "flag={flag:?}={val}"), + Self::DataAt { offset, data } => write!(f, "data@{offset}+{}", data.len()), + } + } +} diff --git a/solana/account/src/state_traits.rs b/solana/account/src/state_traits.rs new file mode 100644 index 0000000..4c1d3a4 --- /dev/null +++ b/solana/account/src/state_traits.rs @@ -0,0 +1,90 @@ +//! Typed bincode access to account data. + +use { + crate::{Account, AccountSharedData}, + bincode::ErrorKind, + solana_instruction_error::InstructionError, + std::cell::Ref, +}; + +/// Reads and writes typed account state through a mutable account handle. +pub trait StateMut { + /// Deserializes the account data as `T`. + fn state(&self) -> Result; + + /// Serializes `state` into the existing account data buffer. + fn set_state(&mut self, state: &T) -> Result<(), InstructionError>; +} + +/// Reads and writes typed account state through a shared handle. +/// +/// Writing through `Ref<'_, AccountSharedData>` is rejected. +pub trait State { + /// Deserializes the account data as `T`. + fn state(&self) -> Result; + + /// Serializes `state` into the existing account data buffer. + fn set_state(&self, state: &T) -> Result<(), InstructionError>; +} + +/// Deserializes typed state from account data. +/// +/// Invalid bytes map to `InstructionError::InvalidAccountData`. +fn state(account: &impl crate::ReadableAccount) -> Result +where + T: serde::de::DeserializeOwned, +{ + crate::codec::deserialize_data(account).map_err(|_| InstructionError::InvalidAccountData) +} + +/// Serializes typed state into an existing account buffer. +/// +/// Oversized payloads map to `AccountDataTooSmall`; all other failures map to `GenericError`. +fn set_state( + account: &mut impl crate::WritableAccount, + state: &T, +) -> Result<(), InstructionError> +where + T: serde::Serialize, +{ + crate::codec::serialize_data(account, state).map_err(|err| match *err { + ErrorKind::SizeLimit => InstructionError::AccountDataTooSmall, + _ => InstructionError::GenericError, + }) +} + +impl StateMut for Account +where + T: serde::Serialize + serde::de::DeserializeOwned, +{ + fn state(&self) -> Result { + state(self) + } + fn set_state(&mut self, state: &T) -> Result<(), InstructionError> { + set_state(self, state) + } +} + +impl StateMut for AccountSharedData +where + T: serde::Serialize + serde::de::DeserializeOwned, +{ + fn state(&self) -> Result { + state(self) + } + fn set_state(&mut self, state: &T) -> Result<(), InstructionError> { + set_state(self, state) + } +} + +impl StateMut for Ref<'_, AccountSharedData> +where + T: serde::Serialize + serde::de::DeserializeOwned, +{ + fn state(&self) -> Result { + state(&**self) + } + fn set_state(&mut self, _state: &T) -> Result<(), InstructionError> { + Err(InstructionError::ReadonlyDataModified) + } +} diff --git a/solana/account/src/sysvar.rs b/solana/account/src/sysvar.rs new file mode 100644 index 0000000..ad108e3 --- /dev/null +++ b/solana/account/src/sysvar.rs @@ -0,0 +1,70 @@ +use { + crate::{Account, AccountSharedData, ReadableAccount, WritableAccount}, + solana_clock::{Epoch, INITIAL_RENT_EPOCH}, + solana_sysvar::SysvarSerialize, +}; + +/// Fields copied into test sysvar accounts. +pub type InheritableAccountFields = (u64, Epoch); + +/// Default lamports and rent epoch used by test sysvar account helpers. +pub const DUMMY_INHERITABLE_ACCOUNT_FIELDS: InheritableAccountFields = (1, INITIAL_RENT_EPOCH); + +/// Serializes a sysvar into account data and pads it to the declared size. +/// +/// Serialization failure falls back to zeroed bytes sized to `S::size_of()`. +/// That keeps the helper infallible while preserving the advertised layout. +fn account_data(sysvar: &S) -> Vec { + let mut data = bincode::serialize(sysvar).unwrap_or_default(); + data.resize(data.len().max(S::size_of()), 0); + data +} + +/// Creates an [`Account`] that contains a serialized sysvar value. +pub fn create_account_with_fields( + sysvar: &S, + (lamports, rent_epoch): InheritableAccountFields, +) -> Account { + Account { + lamports, + data: account_data(sysvar), + owner: solana_sdk_ids::sysvar::id(), + executable: false, + rent_epoch, + } +} + +/// Creates a test sysvar [`Account`]. +pub fn create_account_for_test(sysvar: &S) -> Account { + create_account_with_fields(sysvar, DUMMY_INHERITABLE_ACCOUNT_FIELDS) +} + +/// Creates an [`AccountSharedData`] that contains a serialized sysvar value. +pub fn create_account_shared_data_with_fields( + sysvar: &S, + fields: InheritableAccountFields, +) -> AccountSharedData { + AccountSharedData::from(create_account_with_fields(sysvar, fields)) +} + +/// Creates a test sysvar [`AccountSharedData`]. +pub fn create_account_shared_data_for_test(sysvar: &S) -> AccountSharedData { + create_account_shared_data_with_fields(sysvar, DUMMY_INHERITABLE_ACCOUNT_FIELDS) +} + +/// Deserializes a sysvar value from account data. +/// +/// Returns `None` on decode failure. +pub fn from_account(account: &T) -> Option { + bincode::deserialize(account.data()).ok() +} + +/// Serializes a sysvar value into account data. +/// +/// Returns `None` on encode failure. +pub fn to_account( + sysvar: &S, + account: &mut T, +) -> Option<()> { + bincode::serialize_into(account.data_as_mut_slice(), sysvar).ok() +} diff --git a/solana/account/src/test_utils.rs b/solana/account/src/test_utils.rs new file mode 100644 index 0000000..d97de86 --- /dev/null +++ b/solana/account/src/test_utils.rs @@ -0,0 +1,58 @@ +use { + crate::{ + AccountBuilder, AccountSharedData, BorrowedAccount, OwnedAccount, ReadableAccount, + StorageUnit, + }, + solana_pubkey::Pubkey, + std::{ + alloc::{self, Layout}, + ptr::NonNull, + }, +}; + +/// Builds a borrowed account image backed by serialized owned state. +pub fn borrowed_account_buffer(data: Vec, owner: Pubkey) -> Vec { + let pubkey = Pubkey::new_unique(); + let owned = AccountBuilder::default() + .lamports(1) + .data(data) + .owner(owner) + .build::(); + serialize_account_buffer(&owned, &pubkey) +} + +/// Serializes an owned account into the borrowed layout used by tests. +pub fn serialize_account_buffer(owned: &OwnedAccount, pubkey: &Pubkey) -> Vec { + let mut buf = aligned_account_buffer(owned.units() as usize); + // SAFETY: `buf` is sized from `units` and allocated with the alignment + // required by the borrowed account layout. + unsafe { owned.serialize(&mut buf, pubkey) }; + buf +} + +fn aligned_account_buffer(units: usize) -> Vec { + let layout = Layout::array::(units).expect("account buffer layout"); + // SAFETY: `layout` has non-zero size and is the exact layout later used by + // `Vec` for deallocation. + let ptr = unsafe { alloc::alloc_zeroed(layout) }.cast(); + // SAFETY: `ptr` was allocated for `units` initialized `StorageUnit`s with + // the same layout `Vec` will use to deallocate it. + unsafe { Vec::from_raw_parts(ptr, units, units) } +} + +/// Builds a borrowed account view over a test buffer. +pub fn init_borrowed_account(buf: &mut [StorageUnit]) -> BorrowedAccount { + let ptr = NonNull::from(&mut buf[..]).cast(); + // SAFETY: test buffers are created with the borrowed account layout. + unsafe { BorrowedAccount::init(ptr) } +} + +/// Wraps a borrowed account test buffer in `AccountSharedData`. +pub fn borrowed_shared_data(buf: &mut [StorageUnit]) -> AccountSharedData { + AccountSharedData::from(init_borrowed_account(buf)) +} + +/// Reinitializes a borrowed test buffer and returns its active data image. +pub fn active_borrowed_data(buf: &mut [StorageUnit]) -> Vec { + borrowed_shared_data(buf).data().to_vec() +} diff --git a/solana/account/src/tests/account.rs b/solana/account/src/tests/account.rs new file mode 100644 index 0000000..8e9e8dc --- /dev/null +++ b/solana/account/src/tests/account.rs @@ -0,0 +1,440 @@ +use crate::{ + Account, AccountBuilder, AccountFieldPatch, AccountSeqLock, AccountSharedData, CoWAccount, + OwnedAccount, ReadableAccount, StorageUnit, WritableAccount, accounts_equal, + test_utils::{init_borrowed_account, serialize_account_buffer}, +}; +#[cfg(feature = "bincode")] +use bincode::ErrorKind; +use solana_clock::Epoch; +use solana_instruction_error::LamportsError; +use solana_pubkey::Pubkey; +use std::cell::{Cell, RefCell}; + +// Builds matching owned and shared accounts for baseline assertions. +fn make_two_accounts() -> (Pubkey, Account, AccountSharedData) { + let key = Pubkey::new_unique(); + let mut account = Account::new(1, 2, &key); + account.executable = true; + account.rent_epoch = Epoch::MAX; + + let mut shared = AccountSharedData::new(1, 2, &key); + shared.set_executable(true); + shared.set_rent_epoch(4); + + assert!(accounts_equal(&account, &shared)); + (key, account, shared) +} + +// Builds a borrowed account image backed by serialized owned state. +fn make_borrowed(data: Vec) -> (Vec, AccountSharedData) { + let pubkey = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let owned = AccountBuilder::default().lamports(5).data(data).owner(owner).build(); + let mut buf = serialize_account_buffer(&owned, &pubkey); + let borrowed = init_borrowed_account(&mut buf); + (buf, AccountSharedData::from(borrowed)) +} + +fn assert_add_err(mut account: T) { + assert!(matches!( + account.checked_add_lamports(u64::MAX), + Err(LamportsError::ArithmeticOverflow) + )); +} + +fn assert_sub_err(mut account: T) { + assert!(matches!( + account.checked_sub_lamports(u64::MAX), + Err(LamportsError::ArithmeticUnderflow) + )); +} + +fn assert_saturating_add( + mut account: T, + start: u64, + add: u64, + expected: u64, +) { + account.set_lamports(start); + account.saturating_add_lamports(add); + assert_eq!(account.lamports(), expected); +} + +fn assert_saturating_sub( + mut account: T, + start: u64, + sub: u64, + expected: u64, +) { + account.set_lamports(start); + account.saturating_sub_lamports(sub); + assert_eq!(account.lamports(), expected); +} + +#[test] +// Owner bytes should copy into both account representations identically. +fn test_account_data_copy_as_slice() { + let key2 = Pubkey::new_unique(); + let (_, mut account1, mut account2) = make_two_accounts(); + account1.copy_into_owner_from_slice(key2.as_ref()); + account2.copy_into_owner_from_slice(key2.as_ref()); + assert!(accounts_equal(&account1, &account2)); + assert_eq!(account1.owner(), &key2); +} + +#[test] +// set_data_from_slice should overwrite, grow, shrink, and preserve contents. +fn test_account_set_data_from_slice() { + let (_, _, mut account) = make_two_accounts(); + assert_eq!(account.data(), &[0, 0]); + account.set_data_from_slice(&[1, 2]); + assert_eq!(account.data(), &[1, 2]); + account.set_data_from_slice(&[1, 2, 3]); + assert_eq!(account.data(), &[1, 2, 3]); + account.set_data_from_slice(&[4, 5, 6]); + assert_eq!(account.data(), &[4, 5, 6]); + account.set_data_from_slice(&[4, 5, 6, 0]); + assert_eq!(account.data(), &[4, 5, 6, 0]); + account.set_data_from_slice(&[]); + assert_eq!(account.data(), &[]); + account.set_data_from_slice(&[44]); + assert_eq!(account.data(), &[44]); + account.set_data_from_slice(&[44]); + assert_eq!(account.data(), &[44]); +} + +#[test] +// set_data should replace the buffer and handle shrinking to empty. +fn test_account_data_set_data() { + let (_, _, mut account) = make_two_accounts(); + assert_eq!(account.data(), &[0, 0]); + account.set_data(vec![1, 2]); + assert_eq!(account.data(), &[1, 2]); + account.set_data(Vec::new()); + assert_eq!(account.data(), &[]); +} + +#[test] +// Data patches should write in place and extend when needed. +fn test_account_field_patch_data_at() { + let owner = Pubkey::new_unique(); + let mut account = AccountSharedData::new(1, 2, &owner); + account.set_data_from_slice(&[1, 2, 3, 4]); + + AccountFieldPatch::DataAt { + offset: 1, + data: vec![9, 8, 7, 6], + } + .apply(&mut account); + assert_eq!(account.data(), &[1, 9, 8, 7, 6]); + + AccountFieldPatch::DataAt { offset: 6, data: vec![5, 4] }.apply(&mut account); + assert_eq!(account.data(), &[1, 9, 8, 7, 6, 0, 5, 4]); +} + +#[test] +// Deserialization should fail on a non-bincode payload. +fn test_account_deserialize() { + let (_, account1, _) = make_two_accounts(); + assert!(account1.deserialize_data::().is_err()); +} + +#[test] +// Serialization should reject values larger than the data buffer. +fn test_account_serialize() { + let (_, mut account1, _) = make_two_accounts(); + let err = account1.serialize_data(&"hello world").unwrap_err(); + assert!(matches!(*err, ErrorKind::SizeLimit)); +} + +#[test] +// Shared accounts should fail deserialization on the same invalid payload. +fn test_account_cow_deserialize() { + let (_, _, account2) = make_two_accounts(); + assert!(account2.deserialize_data::().is_err()); +} + +#[test] +// Shared accounts should reject oversized serialization too. +fn test_account_cow_serialize() { + let (_, _, mut account2) = make_two_accounts(); + let err = account2.serialize_data(&"hello world").unwrap_err(); + assert!(matches!(*err, ErrorKind::SizeLimit)); +} + +#[test] +// Account and AccountSharedData should expose the same visible state. +fn test_account_cow() { + let (key, account1, account2) = make_two_accounts(); + assert!(accounts_equal(&account1, &account2)); + + assert_eq!(account1.lamports, 1); + assert_eq!(account1.lamports(), 1); + assert_eq!(account1.data.len(), 2); + assert_eq!(account1.data().len(), 2); + assert_eq!(account1.owner, key); + assert_eq!(account1.owner(), &key); + assert!(account1.executable); + assert!(account1.executable()); + assert_eq!(account1.rent_epoch, Epoch::MAX); + assert_eq!(account1.rent_epoch(), Epoch::MAX); + + assert_eq!(account2.lamports(), 1); + assert_eq!(account2.data().len(), 2); + assert_eq!(account2.owner(), &key); + assert!(account2.executable()); + assert_eq!(account2.rent_epoch(), Epoch::MAX); +} + +#[test] +// Checked lamport mutation should keep both account forms in sync. +fn test_account_add_sub_lamports() { + let (_, mut account1, mut account2) = make_two_accounts(); + assert!(accounts_equal(&account1, &account2)); + assert!(matches!(account1.checked_add_lamports(1), Ok(()))); + assert!(matches!(account2.checked_add_lamports(1), Ok(()))); + assert!(accounts_equal(&account1, &account2)); + assert_eq!(account1.lamports(), 2); + assert!(matches!(account1.checked_sub_lamports(2), Ok(()))); + assert!(matches!(account2.checked_sub_lamports(2), Ok(()))); + assert!(accounts_equal(&account1, &account2)); + assert_eq!(account1.lamports(), 0); +} + +#[test] +// Checked lamport arithmetic should report overflow and underflow. +fn test_account_checked_lamport_errors() { + let (_, account1, account2) = make_two_accounts(); + + assert_add_err(account1.clone()); + assert_sub_err(account1); + assert_add_err(account2.clone()); + assert_sub_err(account2); +} + +#[test] +// Saturating lamport arithmetic should clamp on both account forms. +fn test_account_saturating_lamports() { + let (_, account1, account2) = make_two_accounts(); + + assert_saturating_add(account1.clone(), u64::MAX - 22, 44, u64::MAX); + assert_saturating_add(account2.clone(), u64::MAX - 22, 44, u64::MAX); + assert_saturating_sub(account1, 33, 66, 0); + assert_saturating_sub(account2, 33, 66, 0); +} + +#[test] +// Shrinking data should replace the contents and allow regrowth. +fn test_account_cow_set_data_from_slice_shrinks() { + let owner = Pubkey::new_unique(); + let mut shared = AccountSharedData::new(1, 4, &owner); + + shared.set_data_from_slice(&[1, 2, 3, 4]); + assert_eq!(shared.data(), &[1, 2, 3, 4]); + + shared.set_data_from_slice(&[]); + assert_eq!(shared.data(), &[]); + + shared.set_data_from_slice(&[9]); + assert_eq!(shared.data(), &[9]); +} + +#[test] +// Cloning should share storage until a write forces promotion. +fn test_account_cow_is_copy_on_write() { + let owner = Pubkey::new_unique(); + let mut shared = AccountSharedData::new(1, 2, &owner); + shared.set_data_from_slice(&[1, 2]); + + let cloned = shared.clone(); + assert!(shared.is_shared()); + assert!(cloned.is_shared()); + + shared.extend_from_slice(&[3]); + assert_eq!(shared.data(), &[1, 2, 3]); + assert_eq!(cloned.data(), &[1, 2]); +} + +#[test] +// Borrowed serialization should round-trip back to shared state. +fn test_account_cow_borrowed_round_trip() { + let pubkey = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let owned = AccountBuilder::default() + .lamports(5) + .data(vec![7, 8]) + .owner(owner) + .executable(true) + .build::(); + let expected: AccountSharedData = owned.clone().into(); + let mut buf = serialize_account_buffer(&owned, &pubkey); + let borrowed = init_borrowed_account(&mut buf); + let shared = AccountSharedData::from(borrowed); + + assert!(accounts_equal(&shared, &expected)); +} + +#[test] +// Writing past borrowed capacity should promote to owned storage. +fn test_account_cow_borrowed_extend_promotes() { + let (_buf, mut shared) = make_borrowed(vec![7, 8]); + + shared.extend_from_slice(&[9]); + + assert_eq!(shared.data(), &[7, 8, 9]); +} + +#[test] +// Exact-capacity borrowed writes should keep the existing bytes intact. +fn test_account_cow_borrowed_exact_capacity_writes() { + let (_buf, mut shared) = make_borrowed(vec![1, 2]); + let snap = shared.data_clone(); + let cap = shared.capacity(); + + assert!(cap > shared.data().len()); + + shared.resize(cap, 0x55); + assert_eq!(shared.data().len(), cap); + assert_eq!(&shared.data()[..2], &[1, 2]); + assert!(shared.data()[2..].iter().all(|&b| b == 0x55)); + + let repl = vec![0x9a; cap]; + shared.set_data_from_slice(&repl); + assert_eq!(shared.data(), repl.as_slice()); + assert_eq!(snap.as_ref(), &[1, 2]); +} + +#[test] +// Borrowed resize must write the shadow image before commit publishes it. +fn test_account_cow_borrowed_resize_survives_commit() { + let (mut buf, mut shared) = make_borrowed(vec![1, 2]); + let cap = shared.capacity(); + + shared.resize(cap, 0x55); + let CoWAccount::Borrowed(borrowed) = shared.cow() else { + panic!("resize within borrowed capacity should not promote"); + }; + borrowed.commit(); + drop(shared); + + let borrowed = init_borrowed_account(&mut buf); + assert_eq!(borrowed.data.len(), cap); + assert_eq!(&borrowed.data[..2], &[1, 2]); + assert!(borrowed.data[2..].iter().all(|&b| b == 0x55)); +} + +#[test] +// Overflowing borrowed writes should preserve existing bytes through promotion. +fn test_account_cow_borrowed_overflow_promotes_without_corruption() { + let (_buf, mut shared) = make_borrowed(vec![3, 4]); + let snap = shared.data_clone(); + let cap = shared.capacity(); + let extra = vec![0xab; cap - shared.data().len() + 1]; + let mut exp = vec![3, 4]; + exp.extend_from_slice(&extra); + + shared.extend_from_slice(&extra); + + assert_eq!(shared.data(), exp.as_slice()); + assert_eq!(snap.as_ref(), &[3, 4]); +} + +#[test] +fn test_cow_set_data_at_borrowed_promotes_once() { + // In-place overlap: `offset < len` writes entirely through `data_as_mut_slice`. + let (_buf, mut shared) = make_borrowed(vec![1, 2, 3, 4]); + shared.set_data_at(1, &[9, 9]); + assert_eq!(shared.data(), &[1, 9, 9, 4]); + + // In-place head write that then grows past `len` via `extend_from_slice`. + let (_buf, mut shared) = make_borrowed(vec![1, 2, 3]); + shared.set_data_at(2, &[7, 8, 9]); + assert_eq!(shared.data(), &[1, 2, 7, 8, 9]); +} + +#[test] +// `init` should read the active image without changing the sequence. +fn test_cow_init_reads_active_image() { + let pubkey = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let owned = AccountBuilder::default().lamports(5).data(vec![1, 2, 3]).owner(owner).build(); + let mut buf = serialize_account_buffer(&owned, &pubkey); + let borrowed = init_borrowed_account(&mut buf); + + assert_eq!(&*borrowed.data, &[1, 2, 3]); + assert_eq!(borrowed.sequence(), 0); +} + +#[test] +// `translate` should copy the active image into the shadow view, and `commit` should publish it. +fn test_cow_translate_commit_publishes_shadow_image() { + let pubkey = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let owned = AccountBuilder::default().lamports(5).data(vec![1, 2, 3]).owner(owner).build(); + let mut buf = serialize_account_buffer(&owned, &pubkey); + let mut borrowed = init_borrowed_account(&mut buf); + + // SAFETY: `borrowed` still points at the live borrowed image selected by `init`. + unsafe { borrowed.translate() }; + assert_eq!(borrowed.sequence(), 0); + + borrowed.data[0] = 9; + borrowed.commit(); + assert_eq!(borrowed.sequence(), 1); + + let borrowed = init_borrowed_account(&mut buf); + assert_eq!(&*borrowed.data, &[9, 2, 3]); +} + +#[test] +// Reset should discard shadow writes and re-read the active image. +fn test_cow_translate_rollback_discards_shadow_writes() { + let pubkey = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let owned = AccountBuilder::default().lamports(5).data(vec![4, 5, 6]).owner(owner).build(); + let mut buf = serialize_account_buffer(&owned, &pubkey); + let mut borrowed = init_borrowed_account(&mut buf); + + // SAFETY: `borrowed` still points at the live borrowed image selected by `init`. + unsafe { borrowed.translate() }; + borrowed.data[0] = 8; + + // SAFETY: `reset` only repoints this view back to the active image. + unsafe { borrowed.reset() }; + assert_eq!(borrowed.sequence(), 0); + assert_eq!(&*borrowed.data, &[4, 5, 6]); + + let borrowed = init_borrowed_account(&mut buf); + assert_eq!(&*borrowed.data, &[4, 5, 6]); +} + +#[test] +// AccountSeqLock should retry against the newly published borrowed image. +fn test_account_seq_lock_read_retries_after_borrowed_publish() { + let pubkey = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let owned = AccountBuilder::default().lamports(5).data(vec![1, 2, 3]).owner(owner).build(); + let mut buf = serialize_account_buffer(&owned, &pubkey); + let borrowed = init_borrowed_account(&mut buf); + let writer = RefCell::new(init_borrowed_account(&mut buf)); + let mut lock = AccountSeqLock::new(AccountSharedData::from(borrowed)); + let calls = Cell::new(0); + + let data = lock.read(|account| { + let call = calls.get(); + calls.set(call + 1); + + if call == 0 { + let mut writer = writer.borrow_mut(); + // SAFETY: the writer still points at the image selected by `init`. + unsafe { writer.translate() }; + writer.data[0] = 9; + writer.commit(); + } + + account.data().to_vec() + }); + + assert_eq!(calls.get(), 2); + assert_eq!(data, vec![9, 2, 3]); +} diff --git a/solana/account/src/tests/mod.rs b/solana/account/src/tests/mod.rs new file mode 100644 index 0000000..5a5acba --- /dev/null +++ b/solana/account/src/tests/mod.rs @@ -0,0 +1,4 @@ +mod account; +mod state_traits; +#[cfg(feature = "bincode")] +mod sysvar; diff --git a/solana/account/src/tests/state_traits.rs b/solana/account/src/tests/state_traits.rs new file mode 100644 index 0000000..96b6f58 --- /dev/null +++ b/solana/account/src/tests/state_traits.rs @@ -0,0 +1,19 @@ +use { + crate::{AccountSharedData, state_traits::StateMut}, + solana_instruction_error::InstructionError, + solana_pubkey::Pubkey, + std::mem::size_of, +}; + +#[test] +fn test_account_state() { + let state = 42; + assert!(AccountSharedData::default().set_state(&state).is_err()); + let res = AccountSharedData::default().state() as Result; + assert!(res.is_err()); + + let mut account = AccountSharedData::new(0, size_of::(), &Pubkey::default()); + + assert!(account.set_state(&state).is_ok()); + assert_eq!(account.state(), Ok(state)); +} diff --git a/solana/account/src/tests/sysvar.rs b/solana/account/src/tests/sysvar.rs new file mode 100644 index 0000000..c5b539c --- /dev/null +++ b/solana/account/src/tests/sysvar.rs @@ -0,0 +1,15 @@ +use { + crate::{create_account_with_fields, from_account}, + solana_clock::{Clock, Epoch}, +}; + +#[test] +fn test_create_account_with_fields_round_trips_sysvar() { + let clock = Clock { epoch: 7, ..Clock::default() }; + + let account = create_account_with_fields(&clock, (3, Epoch::MAX)); + + assert_eq!(account.lamports, 3); + assert_eq!(account.rent_epoch, Epoch::MAX); + assert_eq!(from_account::(&account), Some(clock)); +} diff --git a/solana/account/src/traits.rs b/solana/account/src/traits.rs new file mode 100644 index 0000000..ea93e6d --- /dev/null +++ b/solana/account/src/traits.rs @@ -0,0 +1,233 @@ +use { + crate::{ + Account, AccountSharedData, + cow::{DirtyMarkers, StateFlags}, + }, + solana_account_info::debug_account_data::debug_account_data, + solana_clock::Epoch, + solana_instruction_error::LamportsError, + solana_pubkey::Pubkey, + std::{fmt, ops::Deref}, +}; + +/// Read-only access to account state. +pub trait ReadableAccount: Sized { + /// Returns the lamport balance. + fn lamports(&self) -> u64; + + /// Returns the account data. + fn data(&self) -> &[u8]; + + /// Returns the account owner. + fn owner(&self) -> &Pubkey; + + /// Returns whether the account is executable. + fn executable(&self) -> bool; + + /// Returns the rent epoch view for this account. + fn rent_epoch(&self) -> Epoch; +} + +/// Writable access to account state. +pub trait WritableAccount: ReadableAccount { + /// Replaces the lamport balance. + fn set_lamports(&mut self, lamports: u64); + + /// Adds lamports or returns an overflow error. + fn checked_add_lamports(&mut self, lamports: u64) -> Result<(), LamportsError> { + self.set_lamports( + self.lamports().checked_add(lamports).ok_or(LamportsError::ArithmeticOverflow)?, + ); + Ok(()) + } + + /// Subtracts lamports or returns an underflow error. + fn checked_sub_lamports(&mut self, lamports: u64) -> Result<(), LamportsError> { + self.set_lamports( + self.lamports() + .checked_sub(lamports) + .ok_or(LamportsError::ArithmeticUnderflow)?, + ); + Ok(()) + } + + /// Adds lamports and saturates on overflow. + fn saturating_add_lamports(&mut self, lamports: u64) { + self.set_lamports(self.lamports().saturating_add(lamports)) + } + + /// Subtracts lamports and saturates on underflow. + fn saturating_sub_lamports(&mut self, lamports: u64) { + self.set_lamports(self.lamports().saturating_sub(lamports)) + } + + /// Returns mutable access to the account data. + fn data_as_mut_slice(&mut self) -> &mut [u8]; + + /// Replaces the owner. + fn set_owner(&mut self, owner: Pubkey); + + /// Copies 32 raw bytes into the owner pubkey. + fn copy_into_owner_from_slice(&mut self, source: &[u8]); + + /// Sets the executable flag. + fn set_executable(&mut self, executable: bool); + + /// Sets the rent epoch view if the implementation stores one. + /// + /// Implementations that do not store rent epoch may ignore this. + fn set_rent_epoch(&mut self, epoch: Epoch); +} + +/// Returns `true` when the readable account fields match. +/// +/// This ignores storage form and any non-readable metadata. +pub fn accounts_equal(me: &T, other: &U) -> bool { + me.lamports() == other.lamports() + && me.executable() == other.executable() + && me.rent_epoch() == other.rent_epoch() + && me.owner() == other.owner() + && me.data() == other.data() +} + +/// Formats readable accounts with the same debug shape as `Account`. +pub(crate) fn debug_fmt(item: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut f = f.debug_struct("Account"); + + f.field("lamports", &item.lamports()) + .field("data.len", &item.data().len()) + .field("owner", &item.owner()) + .field("executable", &item.executable()) + .field("rent_epoch", &item.rent_epoch()); + debug_account_data(item.data(), &mut f); + + f.finish() +} + +impl ReadableAccount for T +where + T: Deref, + T::Target: ReadableAccount, +{ + fn lamports(&self) -> u64 { + self.deref().lamports() + } + + fn data(&self) -> &[u8] { + self.deref().data() + } + + fn owner(&self) -> &Pubkey { + self.deref().owner() + } + + fn executable(&self) -> bool { + self.deref().executable() + } + + fn rent_epoch(&self) -> Epoch { + self.deref().rent_epoch() + } +} + +impl ReadableAccount for Account { + fn lamports(&self) -> u64 { + self.lamports + } + + fn data(&self) -> &[u8] { + &self.data + } + + fn owner(&self) -> &Pubkey { + &self.owner + } + + fn executable(&self) -> bool { + self.executable + } + + fn rent_epoch(&self) -> Epoch { + self.rent_epoch + } +} + +impl WritableAccount for Account { + fn set_lamports(&mut self, lamports: u64) { + self.lamports = lamports; + } + + fn data_as_mut_slice(&mut self) -> &mut [u8] { + &mut self.data + } + + fn set_owner(&mut self, owner: Pubkey) { + self.owner = owner; + } + + fn copy_into_owner_from_slice(&mut self, source: &[u8]) { + self.owner.as_mut().copy_from_slice(source); + } + + fn set_executable(&mut self, executable: bool) { + self.executable = executable; + } + + fn set_rent_epoch(&mut self, epoch: Epoch) { + self.rent_epoch = epoch; + } +} + +impl ReadableAccount for AccountSharedData { + fn lamports(&self) -> u64 { + self.lamports + } + + fn data(&self) -> &[u8] { + self.cow.data() + } + + fn owner(&self) -> &Pubkey { + &self.owner + } + + fn executable(&self) -> bool { + self.flags.contains(StateFlags::EXECUTABLE) + } + + fn rent_epoch(&self) -> Epoch { + Epoch::MAX + } +} + +impl WritableAccount for AccountSharedData { + fn set_lamports(&mut self, lamports: u64) { + self.translate(); + self.dirty.insert(DirtyMarkers::LAMPORTS); + self.lamports = lamports; + } + + fn data_as_mut_slice(&mut self) -> &mut [u8] { + self.translate(); + self.mark_data_dirty(); + self.cow.data_mut() + } + + fn set_owner(&mut self, owner: Pubkey) { + self.translate(); + self.dirty.insert(DirtyMarkers::OWNER); + self.owner = owner; + } + + fn copy_into_owner_from_slice(&mut self, source: &[u8]) { + self.translate(); + self.dirty.insert(DirtyMarkers::OWNER); + self.owner.as_mut().copy_from_slice(source); + } + + fn set_executable(&mut self, executable: bool) { + self.set_flag(StateFlags::EXECUTABLE, executable); + } + + fn set_rent_epoch(&mut self, _: Epoch) {} +} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -