From 796bf68ff0e401162be54931352b1bc9e5f1b2fe Mon Sep 17 00:00:00 2001 From: OnlyF0uR <29165327+OnlyF0uR@users.noreply.github.com> Date: Mon, 11 Nov 2024 19:59:12 +0100 Subject: [PATCH] feat: early persistent storage implementation --- .gitignore | 3 +- Cargo.lock | 36 +- crates/cesium-crypto/src/id.rs | 5 + crates/cesium-nebula/Cargo.toml | 5 +- crates/cesium-nebula/src/accounts.rs | 497 ++++++++++++++++-- crates/cesium-nebula/src/lib.rs | 1 + crates/cesium-nebula/src/metadata/currency.rs | 36 ++ crates/cesium-nebula/src/metadata/mod.rs | 2 + crates/cesium-nebula/src/metadata/nft.rs | 75 +++ crates/cesium-storage/Cargo.toml | 7 +- crates/cesium-storage/src/account.rs | 109 ---- crates/cesium-storage/src/data.rs | 33 -- crates/cesium-storage/src/errors.rs | 16 + crates/cesium-storage/src/lib.rs | 112 +++- 14 files changed, 696 insertions(+), 241 deletions(-) create mode 100644 crates/cesium-nebula/src/metadata/currency.rs create mode 100644 crates/cesium-nebula/src/metadata/mod.rs create mode 100644 crates/cesium-nebula/src/metadata/nft.rs delete mode 100644 crates/cesium-storage/src/account.rs delete mode 100644 crates/cesium-storage/src/data.rs create mode 100644 crates/cesium-storage/src/errors.rs diff --git a/.gitignore b/.gitignore index d958c09..9e34c85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target .vscode/ -.idea/ \ No newline at end of file +.idea/ +**/.cesiumdb/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index af15527..9a7b4f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,15 +134,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - [[package]] name = "bindgen" version = "0.69.5" @@ -334,8 +325,9 @@ name = "cesium-nebula" version = "0.2.0" dependencies = [ "cesium-crypto", - "once_cell", + "cesium-storage", "selenide-runtime", + "tokio", ] [[package]] @@ -366,11 +358,10 @@ dependencies = [ name = "cesium-storage" version = "0.2.0" dependencies = [ - "bincode", "cesium-crypto", + "once_cell", "rocksdb", - "serde", - "tempfile", + "tokio", ] [[package]] @@ -861,12 +852,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" -[[package]] -name = "fastrand" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" - [[package]] name = "filetime" version = "0.2.25" @@ -2059,19 +2044,6 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" -[[package]] -name = "tempfile" -version = "3.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" -dependencies = [ - "cfg-if", - "fastrand", - "once_cell", - "rustix", - "windows-sys 0.59.0", -] - [[package]] name = "thiserror" version = "1.0.67" diff --git a/crates/cesium-crypto/src/id.rs b/crates/cesium-crypto/src/id.rs index 9b3383f..2d68025 100644 --- a/crates/cesium-crypto/src/id.rs +++ b/crates/cesium-crypto/src/id.rs @@ -22,6 +22,11 @@ pub fn generate_id() -> Vec { result } +pub fn generate_id_slice() -> [u8; PUB_BYTE_LEN] { + let id = generate_id(); + id.try_into().unwrap() +} + pub fn generate_id_from_data(data: &[u8]) -> String { let mut hasher = sha3::Sha3_384::new(); hasher.update(data); diff --git a/crates/cesium-nebula/Cargo.toml b/crates/cesium-nebula/Cargo.toml index 08b9e3f..b29f4c2 100644 --- a/crates/cesium-nebula/Cargo.toml +++ b/crates/cesium-nebula/Cargo.toml @@ -7,5 +7,8 @@ publish = false [dependencies] cesium-crypto.workspace = true +cesium-storage.workspace = true selenide-runtime.workspace = true -once_cell.workspace = true \ No newline at end of file + +[dev-dependencies] +tokio.workspace = true \ No newline at end of file diff --git a/crates/cesium-nebula/src/accounts.rs b/crates/cesium-nebula/src/accounts.rs index 7c836a9..6ce122c 100644 --- a/crates/cesium-nebula/src/accounts.rs +++ b/crates/cesium-nebula/src/accounts.rs @@ -1,53 +1,108 @@ use std::rc::Rc; -use cesium_crypto::{id::to_readable_id, keys::PublicKeyBytes}; +use cesium_crypto::{ + id::to_readable_id, + keys::{PublicKeyBytes, PUB_BYTE_LEN}, +}; +use cesium_storage::{errors::StorageError, RocksDBStore}; use selenide_runtime::errors::RuntimeError; -// TODO: Actually do include some useful creation and -// retrieval processes for these accounts +macro_rules! bounds_check { + ($bytes:expr, $pub_byte_len:expr) => { + if $bytes.len() < $pub_byte_len { + // TODO: Return an error instead of panicking + panic!("Out of bounds data account bytes"); + } + }; +} pub struct UserAccount { id: PublicKeyBytes, - currency_account_ids: Rc>, + data_account_count: u32, data_account_ids: Rc>, } impl UserAccount { #[must_use] - pub fn new( - id: PublicKeyBytes, - currency_account_ids: Rc>, - data_account_ids: Rc>, - ) -> UserAccount { + pub fn new(id: PublicKeyBytes, data_account_ids: Rc>) -> UserAccount { UserAccount { id, - currency_account_ids, + data_account_count: data_account_ids.len() as u32, data_account_ids, } } - pub fn from_id(_id: PublicKeyBytes) -> ContractAccount { - // Retrieve the data corresponding to the account id - todo!() + pub async fn from_id(id: PublicKeyBytes) -> Option { + let result = match RocksDBStore::instance().async_get(id.to_vec()).await { + Ok(result) => match result { + Some(bytes) => Some(UserAccount::from_bytes(&bytes)), + None => None, + }, + Err(e) => { + eprintln!("Error getting user account: {:?}", e); + return None; + } + }; + + result } pub fn address(&self) -> String { to_readable_id(&self.id) } - pub fn get_currency_account(&self, id: &PublicKeyBytes) -> Option<&PublicKeyBytes> { - self.currency_account_ids.iter().find(|&ca| ca == id) - } - pub fn get_data_account(&self, id: &PublicKeyBytes) -> Option<&PublicKeyBytes> { self.data_account_ids.iter().find(|&da| da == id) } + + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&self.id); + bytes.extend_from_slice(&self.data_account_count.to_le_bytes()); + for id in self.data_account_ids.iter() { + bytes.extend_from_slice(id); + } + bytes + } + + pub async fn write(&self) -> Result<(), StorageError> { + let bytes = self.to_bytes(); + RocksDBStore::instance() + .async_put(self.id.to_vec(), bytes) + .await + } + + pub fn from_bytes(bytes: &[u8]) -> UserAccount { + bounds_check!(bytes, PUB_BYTE_LEN); + let id: [u8; PUB_BYTE_LEN] = bytes[0..PUB_BYTE_LEN].try_into().unwrap(); + let mut offset = PUB_BYTE_LEN; + + bounds_check!(bytes, offset + 4); + let data_account_count = u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()); + offset = offset + 4; + + bounds_check!(bytes, offset + data_account_count as usize * PUB_BYTE_LEN); + let mut data_account_ids = Vec::new(); + for i in 0..data_account_count { + let start = offset + i as usize * PUB_BYTE_LEN; + let end = start + PUB_BYTE_LEN; + data_account_ids.push(bytes[start..end].try_into().unwrap()); + } + + UserAccount { + id, + data_account_count, + data_account_ids: Rc::new(data_account_ids), + } + } } pub struct ContractAccount { id: PublicKeyBytes, - program_binary: Rc>, + state_account_len: u32, state_account_id: Option, + program_binary_len: u32, + program_binary: Rc>, } impl ContractAccount { @@ -57,15 +112,30 @@ impl ContractAccount { program_binary: Rc>, state_account_id: Option, ) -> ContractAccount { + let state_account_len = state_account_id.is_some() as u32; + let program_binary_len = program_binary.len() as u32; ContractAccount { id, - program_binary, + state_account_len, state_account_id, + program_binary_len, + program_binary, } } - pub fn from_id(_id: PublicKeyBytes) -> ContractAccount { - todo!() + pub async fn from_id(id: PublicKeyBytes) -> Option { + let result = match RocksDBStore::instance().async_get(id.to_vec()).await { + Ok(result) => match result { + Some(bytes) => Some(ContractAccount::from_bytes(&bytes)), + None => None, + }, + Err(e) => { + eprintln!("Error getting contract account: {:?}", e); + return None; + } + }; + + result } pub fn address(&self) -> String { @@ -81,15 +151,173 @@ impl ContractAccount { println!("Binary: {:?}", self.program_binary); todo!() } + + pub async fn write(&self) -> Result<(), StorageError> { + let bytes = self.to_bytes(); + RocksDBStore::instance() + .async_put(self.id.to_vec(), bytes) + .await + } + + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&self.id); + bytes.extend_from_slice(&self.state_account_len.to_le_bytes()); + if let Some(id) = &self.state_account_id { + bytes.extend_from_slice(id); + } + bytes.extend_from_slice(&self.program_binary_len.to_le_bytes()); + bytes.extend_from_slice(&self.program_binary); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> ContractAccount { + bounds_check!(bytes, PUB_BYTE_LEN); + let id: [u8; PUB_BYTE_LEN] = bytes[0..PUB_BYTE_LEN].try_into().unwrap(); + let mut offset = PUB_BYTE_LEN; + + bounds_check!(bytes, offset + 4); + let state_account_len = u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()); + offset = offset + 4; + + bounds_check!(bytes, offset + PUB_BYTE_LEN); + let state_account_id = if state_account_len > 0 { + let id: [u8; PUB_BYTE_LEN] = bytes[offset..offset + PUB_BYTE_LEN].try_into().unwrap(); + offset = offset + PUB_BYTE_LEN; + Some(id) + } else { + None + }; + + bounds_check!(bytes, offset + 4); + let program_binary_len = u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()); + offset = offset + 4; + + let program_binary = Rc::new(bytes[offset..offset + program_binary_len as usize].to_vec()); + + ContractAccount { + id, + state_account_len, + state_account_id, + program_binary_len, + program_binary, + } + } +} + +pub struct DataAccount { + id: PublicKeyBytes, + owner: PublicKeyBytes, + updater: PublicKeyBytes, + data_len: u32, + data: Vec, +} + +impl DataAccount { + #[must_use] + pub fn new( + id: PublicKeyBytes, + owner: PublicKeyBytes, + updater: PublicKeyBytes, + data: Vec, + ) -> DataAccount { + DataAccount { + id, + owner, + updater, + data_len: data.len() as u32, + data, + } + } + + pub async fn from_id(id: PublicKeyBytes) -> Option { + let result = match RocksDBStore::instance().async_get(id.to_vec()).await { + Ok(result) => match result { + Some(bytes) => Some(DataAccount::from_bytes(&bytes)), + None => None, + }, + Err(e) => { + eprintln!("Error getting data account: {:?}", e); + return None; + } + }; + + result + } + + pub fn address(&self) -> String { + to_readable_id(&self.id) + } + + pub fn owner_address(&self) -> String { + to_readable_id(&self.owner) + } + + pub fn update_updater(&self) -> String { + to_readable_id(&self.updater) + } + + pub fn data(&self) -> &[u8] { + &self.data + } + + pub async fn write(&self) -> Result<(), StorageError> { + let bytes = self.to_bytes(); + RocksDBStore::instance() + .async_put(self.id.to_vec(), bytes) + .await + } + + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&self.id); + bytes.extend_from_slice(&self.owner); + bytes.extend_from_slice(&self.updater); + bytes.extend_from_slice(&self.data_len.to_le_bytes()); + bytes.extend_from_slice(&self.data); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> DataAccount { + bounds_check!(bytes, PUB_BYTE_LEN); + let id: [u8; PUB_BYTE_LEN] = bytes[0..PUB_BYTE_LEN].try_into().unwrap(); + let offset = PUB_BYTE_LEN; + + bounds_check!(bytes, offset + PUB_BYTE_LEN); + let owner: [u8; PUB_BYTE_LEN] = bytes[offset..offset + PUB_BYTE_LEN].try_into().unwrap(); + let offset = offset + PUB_BYTE_LEN; + + bounds_check!(bytes, offset + PUB_BYTE_LEN); + let updater: [u8; PUB_BYTE_LEN] = bytes[offset..offset + PUB_BYTE_LEN].try_into().unwrap(); + let offset = offset + PUB_BYTE_LEN; + + bounds_check!(bytes, offset + 4); + let data_len = u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()); + let offset = offset + 4; + + bounds_check!(bytes, offset + data_len as usize); + let data = bytes[offset..offset + data_len as usize].to_vec(); + + DataAccount { + id, + owner, + updater, + data_len, + data, + } + } } pub struct CurrencyAccount { id: PublicKeyBytes, owner: PublicKeyBytes, + decimals: u8, + minter_len: u32, + minter: Option, + short_name_len: u32, short_name: String, + long_name_len: u32, long_name: String, - decimals: u8, - mint_authority: Option, } impl CurrencyAccount { @@ -97,18 +325,21 @@ impl CurrencyAccount { pub fn new( id: PublicKeyBytes, owner: PublicKeyBytes, - mint_authority: Option, short_name: String, long_name: String, decimals: u8, + minter: Option, ) -> CurrencyAccount { CurrencyAccount { id, owner, + decimals, + minter_len: minter.is_some() as u32, + minter, + short_name_len: short_name.len() as u32, short_name, + long_name_len: long_name.len() as u32, long_name, - decimals, - mint_authority, } } @@ -124,10 +355,6 @@ impl CurrencyAccount { to_readable_id(&self.owner) } - pub fn mint_authority_address(&self) -> Option { - self.mint_authority.as_ref().map(|pk| to_readable_id(pk)) - } - pub fn short_name(&self) -> &str { &self.short_name } @@ -139,48 +366,202 @@ impl CurrencyAccount { pub fn decimals(&self) -> u8 { self.decimals } -} -pub struct DataAccount { - id: PublicKeyBytes, - owner: PublicKeyBytes, - update_authority: PublicKeyBytes, - data: Vec, -} + pub fn minter_address(&self) -> Option { + self.minter.as_ref().map(|minter| to_readable_id(minter)) + } -impl DataAccount { - #[must_use] - pub fn new( - id: PublicKeyBytes, - owner: PublicKeyBytes, - update_authority: PublicKeyBytes, - data: Vec, - ) -> DataAccount { - DataAccount { + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&self.id); + bytes.extend_from_slice(&self.owner); + bytes.extend_from_slice(&self.decimals.to_le_bytes()); + bytes.extend_from_slice(&self.minter_len.to_le_bytes()); + if let Some(minter) = &self.minter { + bytes.extend_from_slice(minter); + } + bytes.extend_from_slice(&self.short_name_len.to_le_bytes()); + bytes.extend_from_slice(self.short_name.as_bytes()); + bytes.extend_from_slice(&self.long_name_len.to_le_bytes()); + bytes.extend_from_slice(self.long_name.as_bytes()); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> CurrencyAccount { + bounds_check!(bytes, PUB_BYTE_LEN); + let id: [u8; PUB_BYTE_LEN] = bytes[0..PUB_BYTE_LEN].try_into().unwrap(); + let mut offset = PUB_BYTE_LEN; + + bounds_check!(bytes, offset + PUB_BYTE_LEN); + let owner: [u8; PUB_BYTE_LEN] = bytes[offset..offset + PUB_BYTE_LEN].try_into().unwrap(); + offset = offset + PUB_BYTE_LEN; + + bounds_check!(bytes, offset + 1); + let decimals = bytes[offset]; + offset = offset + 1; + + bounds_check!(bytes, offset + 4); + let minter_len = u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()); + offset = offset + 4; + + let minter = if minter_len > 0 { + bounds_check!(bytes, offset + PUB_BYTE_LEN); + let minter: [u8; PUB_BYTE_LEN] = + bytes[offset..offset + PUB_BYTE_LEN].try_into().unwrap(); + offset = offset + PUB_BYTE_LEN; + Some(minter) + } else { + None + }; + + bounds_check!(bytes, offset + 4); + let short_name_len = u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()); + offset = offset + 4; + + bounds_check!(bytes, offset + short_name_len as usize); + let short_name = + String::from_utf8(bytes[offset..offset + short_name_len as usize].to_vec()).unwrap(); + offset = offset + short_name_len as usize; + + bounds_check!(bytes, offset + 4); + let long_name_len = u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()); + offset = offset + 4; + + bounds_check!(bytes, offset + long_name_len as usize); + let long_name = + String::from_utf8(bytes[offset..offset + long_name_len as usize].to_vec()).unwrap(); + + CurrencyAccount { id, owner, - update_authority, - data, + decimals, + minter_len, + minter, + short_name_len, + short_name, + long_name_len, + long_name, } } +} - pub fn from_id(_id: PublicKeyBytes) -> ContractAccount { - todo!() +#[cfg(test)] +mod tests { + use cesium_crypto::{id::generate_id_slice, keys::Account}; + + use super::*; + + #[test] + fn test_user_account() { + let id: [u8; 48] = generate_id_slice(); + + let d_id1 = generate_id_slice(); + let d_id2 = generate_id_slice(); + let data_account_ids = Rc::new(vec![d_id1, d_id2]); + + let user_account = UserAccount::new(id, data_account_ids.clone()); + assert_eq!(user_account.address(), to_readable_id(&id)); + + let bytes = user_account.to_bytes(); + let user_account2 = UserAccount::from_bytes(&bytes); + assert_eq!(user_account2.address(), user_account.address()); + assert_eq!(user_account2.data_account_ids, data_account_ids); } - pub fn address(&self) -> String { - to_readable_id(&self.id) + #[test] + fn test_contract_account() { + let id: [u8; 48] = generate_id_slice(); + let state_account_id = Some(generate_id_slice()); + let program_binary = Rc::new(vec![1, 2, 3, 4]); + + let contract_account = ContractAccount::new(id, program_binary.clone(), state_account_id); + assert_eq!(contract_account.address(), to_readable_id(&id)); + + let bytes = contract_account.to_bytes(); + let contract_account2 = ContractAccount::from_bytes(&bytes); + assert_eq!(contract_account2.address(), contract_account.address()); + assert_eq!(contract_account2.program_binary, program_binary); + assert_eq!(contract_account2.state_account_id, state_account_id); } - pub fn owner_address(&self) -> String { - to_readable_id(&self.owner) + #[test] + fn test_data_account() { + let id = generate_id_slice(); + let owner = *Account::create().to_public_key_bytes(); + let updater = *Account::create().to_public_key_bytes(); + let data = vec![1, 2, 3, 4]; + let data_account = DataAccount::new(id, owner, updater, data.clone()); + assert_eq!(data_account.address(), to_readable_id(&id)); + assert_eq!(data_account.owner_address(), to_readable_id(&owner)); + assert_eq!(data_account.update_updater(), to_readable_id(&updater)); + + let bytes = data_account.to_bytes(); + let data_account2 = DataAccount::from_bytes(&bytes); + assert_eq!(data_account2.address(), data_account.address()); + assert_eq!(data_account2.owner_address(), data_account.owner_address()); + assert_eq!( + data_account2.update_updater(), + data_account.update_updater() + ); + assert_eq!(data_account2.data(), data.as_slice()); } - pub fn update_authority_address(&self) -> String { - to_readable_id(&self.update_authority) + #[test] + fn test_currency_account() { + let id = generate_id_slice(); + let owner = *Account::create().to_public_key_bytes(); + let short_name = "ABC".to_string(); + let long_name = "Alpha Beta Charlie".to_string(); + let decimals = 2; + let minter = Some(owner); + let currency_account = CurrencyAccount::new( + id, + owner, + short_name.clone(), + long_name.clone(), + decimals, + minter, + ); + assert_eq!(currency_account.address(), to_readable_id(&id)); + assert_eq!(currency_account.owner_address(), to_readable_id(&owner)); + assert_eq!(currency_account.short_name(), short_name); + assert_eq!(currency_account.long_name(), long_name); + assert_eq!(currency_account.decimals(), decimals); + assert_eq!( + currency_account.minter_address(), + Some(to_readable_id(&owner)) + ); + + let bytes = currency_account.to_bytes(); + let currency_account2 = CurrencyAccount::from_bytes(&bytes); + assert_eq!(currency_account2.address(), currency_account.address()); + assert_eq!( + currency_account2.owner_address(), + currency_account.owner_address() + ); + assert_eq!( + currency_account2.short_name(), + currency_account.short_name() + ); + assert_eq!(currency_account2.long_name(), currency_account.long_name()); + assert_eq!(currency_account2.decimals(), currency_account.decimals()); + assert_eq!( + currency_account2.minter_address(), + currency_account.minter_address() + ); } - pub fn data(&self) -> &[u8] { - &self.data + #[tokio::test] + async fn test_storage_user_account() { + let account = Account::create(); + + let id = account.to_public_key_bytes(); + let data_account_ids = Rc::new(vec![*Account::create().to_public_key_bytes()]); + let user_account = UserAccount::new(*id, data_account_ids.clone()); + + user_account.write().await.unwrap(); + + let user_account2 = UserAccount::from_id(*id).await.unwrap(); + assert_eq!(user_account2.address(), user_account.address()); } } diff --git a/crates/cesium-nebula/src/lib.rs b/crates/cesium-nebula/src/lib.rs index 9be80ba..38b6e76 100644 --- a/crates/cesium-nebula/src/lib.rs +++ b/crates/cesium-nebula/src/lib.rs @@ -1,3 +1,4 @@ pub mod accounts; pub mod instruction; +pub mod metadata; pub mod transaction; diff --git a/crates/cesium-nebula/src/metadata/currency.rs b/crates/cesium-nebula/src/metadata/currency.rs new file mode 100644 index 0000000..249f920 --- /dev/null +++ b/crates/cesium-nebula/src/metadata/currency.rs @@ -0,0 +1,36 @@ +use cesium_crypto::keys::{PublicKeyBytes, PUB_BYTE_LEN}; + +pub struct CurrencyAmountMetadata { + currency: PublicKeyBytes, + amount: u128, +} + +macro_rules! bounds_check { + ($bytes:expr, $pub_byte_len:expr) => { + if $bytes.len() < $pub_byte_len { + // TODO: Return an error instead of panicking + panic!("Out of bounds currency metadata bytes"); + } + }; +} + +impl CurrencyAmountMetadata { + pub fn try_from_bytes(bytes: &[u8]) -> Result> { + let mut offset = 0; + bounds_check!(bytes, offset + PUB_BYTE_LEN); + let currency = bytes[offset..offset + PUB_BYTE_LEN].try_into()?; + offset += PUB_BYTE_LEN; + + bounds_check!(bytes, offset + 16); + let amount = u128::from_le_bytes(bytes[offset..offset + 16].try_into()?); + + Ok(Self { currency, amount }) + } + + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&self.currency); + bytes.extend_from_slice(&self.amount.to_le_bytes()); + bytes + } +} diff --git a/crates/cesium-nebula/src/metadata/mod.rs b/crates/cesium-nebula/src/metadata/mod.rs new file mode 100644 index 0000000..c715eee --- /dev/null +++ b/crates/cesium-nebula/src/metadata/mod.rs @@ -0,0 +1,2 @@ +pub mod currency; +pub mod nft; diff --git a/crates/cesium-nebula/src/metadata/nft.rs b/crates/cesium-nebula/src/metadata/nft.rs new file mode 100644 index 0000000..c0617b7 --- /dev/null +++ b/crates/cesium-nebula/src/metadata/nft.rs @@ -0,0 +1,75 @@ +use cesium_crypto::keys::{PublicKeyBytes, PUB_BYTE_LEN}; + +pub struct NFTMetadata { + name_len: u32, + name: String, + url_len: u32, + uri: String, + creator_count: u32, + creators: Vec, +} + +macro_rules! bounds_check { + ($bytes:expr, $pub_byte_len:expr) => { + if $bytes.len() < $pub_byte_len { + // TODO: Return an error instead of panicking + panic!("Out of bounds NFT metadata bytes"); + } + }; +} + +impl NFTMetadata { + pub fn try_from_bytes(bytes: &[u8]) -> Result> { + // Each field is prefixed with a usize length + let mut offset = 0; + bounds_check!(bytes, offset + 4); + let name_len = u32::from_le_bytes(bytes[offset..offset + 4].try_into()?); + offset += 4; + + bounds_check!(bytes, offset + name_len as usize); + let name = String::from_utf8(bytes[offset..(offset + name_len as usize)].to_vec())?; + offset += name_len as usize; + + bounds_check!(bytes, offset + 4); + let url_len = u32::from_le_bytes(bytes[offset..offset + 4].try_into()?); + offset += 4; + + bounds_check!(bytes, offset + url_len as usize); + let uri = String::from_utf8(bytes[offset..(offset + url_len as usize)].to_vec())?; + offset += url_len as usize; + + bounds_check!(bytes, offset + 4); + let creator_count = u32::from_le_bytes(bytes[offset..offset + 4].try_into()?); + offset += 4; + + bounds_check!(bytes, offset + PUB_BYTE_LEN * creator_count as usize); + let mut creators = Vec::new(); + for _ in 0..creator_count { + let pk: [u8; PUB_BYTE_LEN] = bytes[offset..offset + PUB_BYTE_LEN].try_into().unwrap(); + offset = offset + PUB_BYTE_LEN; + creators.push(pk); + } + + Ok(Self { + name_len, + name, + url_len, + uri, + creator_count, + creators, + }) + } + + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&self.name_len.to_le_bytes()); + bytes.extend_from_slice(self.name.as_bytes()); + bytes.extend_from_slice(&self.url_len.to_le_bytes()); + bytes.extend_from_slice(self.uri.as_bytes()); + bytes.extend_from_slice(&self.creator_count.to_le_bytes()); + for creator in &self.creators { + bytes.extend_from_slice(creator); + } + bytes + } +} diff --git a/crates/cesium-storage/Cargo.toml b/crates/cesium-storage/Cargo.toml index 434cd2a..7914d58 100644 --- a/crates/cesium-storage/Cargo.toml +++ b/crates/cesium-storage/Cargo.toml @@ -5,11 +5,8 @@ version.workspace = true license = "GPL-3.0" publish = false - [dependencies] cesium-crypto.workspace = true rocksdb.workspace = true -# tokio.workspace = true -bincode.workspace = true -serde.workspace = true -tempfile.workspace = true \ No newline at end of file +tokio.workspace = true +once_cell.workspace = true \ No newline at end of file diff --git a/crates/cesium-storage/src/account.rs b/crates/cesium-storage/src/account.rs deleted file mode 100644 index 9b5e820..0000000 --- a/crates/cesium-storage/src/account.rs +++ /dev/null @@ -1,109 +0,0 @@ -use std::sync::Arc; - -use cesium_crypto::id::{generate_id, to_readable_id}; -use rocksdb::{Options, WriteBatch, DB}; -use serde::{Deserialize, Serialize}; - -use crate::data::DataObject; - -#[derive(Debug, Serialize, Deserialize)] -pub struct Account { - pub address: String, - pub program_binary: Option>, - pub data_account_ids: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct DataAccount { - pub id: String, - pub data: Vec, -} - -pub struct DataAccountManager { - db: Arc, -} - -impl DataAccountManager { - pub fn new(path: &str) -> Result> { - let mut options = Options::default(); - options.create_if_missing(true); - options.increase_parallelism(2); - options.set_max_background_jobs(2); - options.set_write_buffer_size(64 * 1024 * 1024); // 64MB - options.set_max_write_buffer_number(3); - - let db = Arc::new(DB::open(&options, path)?); - Ok(Self { db }) - } - - pub fn create_data_account( - &self, - user_address: &str, - obj: &[DataObject], - ) -> Result> { - let result = generate_id(); - let id = to_readable_id(&result); - - let account_key = format!("account:{}", user_address); - - // Use a write batch for atomic operations - let mut batch = WriteBatch::default(); - - // Get current account or create new one - let mut account = match self.db.get(account_key.as_bytes())? { - Some(data) => bincode::deserialize(&data)?, - None => Account { - address: user_address.to_string(), - program_binary: None, - data_account_ids: Vec::with_capacity(1), - }, - }; - - // Create new data account - let new_data_account = DataAccount { - id: id.clone(), - data: obj.to_vec(), - }; - - // Prepare batch operations - let data_account_key = format!("data_account:{}", id); - batch.put( - data_account_key.as_bytes(), - bincode::serialize(&new_data_account)?, - ); - - account.data_account_ids.push(id.clone()); - batch.put(account_key.as_bytes(), bincode::serialize(&account)?); - - // Atomic write - self.db.write(batch)?; - - Ok(id) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use cesium_crypto::keys::Account; - use tempfile::TempDir; - - #[test] - fn test_create_data_account() { - let temp_dir = TempDir::new().unwrap(); - let manager = DataAccountManager::new(temp_dir.path().to_str().unwrap()).unwrap(); - - let kp = Account::create(); - let address = kp.to_public_key_readable(); - - let data_object = DataObject { - type_id: *Account::create().to_public_key_bytes(), - data: vec![0x01, 0x02, 0x03], - }; - - let id = manager - .create_data_account(&address, &[data_object]) - .unwrap(); - assert!(!id.is_empty()); - } -} diff --git a/crates/cesium-storage/src/data.rs b/crates/cesium-storage/src/data.rs deleted file mode 100644 index 9003b90..0000000 --- a/crates/cesium-storage/src/data.rs +++ /dev/null @@ -1,33 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use cesium_crypto::serializer::Array48; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct DataObject { - #[serde(with = "Array48")] - pub type_id: [u8; 48], // most commonly a token id in byte form - pub data: Vec, // Use a slice for immutable data -} - -impl DataObject { - pub fn to_bytes(&self) -> Vec { - let mut bytes = Vec::with_capacity(48 + self.data.len()); - bytes.extend(&self.type_id); - bytes.extend(&self.data); - bytes - } - - pub fn from_bytes( - bytes: &[u8], - ) -> Result> { - if bytes.len() < 48 { - return Err("DataObject is empty".into()); - } - - let mut type_id = [0; 48]; - type_id.copy_from_slice(&bytes[0..48]); - - let data = bytes[48..].to_vec(); - Ok(DataObject { type_id, data }) - } -} diff --git a/crates/cesium-storage/src/errors.rs b/crates/cesium-storage/src/errors.rs new file mode 100644 index 0000000..6a87949 --- /dev/null +++ b/crates/cesium-storage/src/errors.rs @@ -0,0 +1,16 @@ +#[derive(Debug)] +pub enum StorageError { + RocksDBError(rocksdb::Error), + AsyncError(String), +} + +impl std::fmt::Display for StorageError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + StorageError::RocksDBError(err) => write!(f, "RocksDB error: {}", err), + StorageError::AsyncError(err) => write!(f, "Async error: {}", err), + } + } +} + +impl std::error::Error for StorageError {} diff --git a/crates/cesium-storage/src/lib.rs b/crates/cesium-storage/src/lib.rs index 05ef9e3..ce41ece 100644 --- a/crates/cesium-storage/src/lib.rs +++ b/crates/cesium-storage/src/lib.rs @@ -1,2 +1,110 @@ -pub mod account; -pub mod data; +use errors::StorageError; +use once_cell::sync::Lazy; +use rocksdb::{Options, DB}; +use std::sync::Arc; + +pub struct RocksDBStore { + db: Arc, // Arc for thread-safe shared access to DB +} + +const DB_PATH: &str = ".cesiumdb"; + +impl RocksDBStore { + pub fn instance() -> &'static RocksDBStore { + static INSTANCE: Lazy = Lazy::new(|| RocksDBStore::new(DB_PATH).unwrap()); + &INSTANCE + } + + /// Creates a new instance of `RocksDBStore` with optimized settings for performance. + /// Takes the path to the database as input. + fn new(db_path: &str) -> Result { + let mut options = Options::default(); + + // General options + options.create_if_missing(true); + + // Performance optimizations + options.set_max_background_jobs(4); // Compaction threads + options.set_write_buffer_size(64 * 1024 * 1024); // Write buffer size (64MB) + options.set_max_write_buffer_number(3); // Number of write buffers + options.set_target_file_size_base(64 * 1024 * 1024); // Target file size for SST files + options.set_level_compaction_dynamic_level_bytes(true); + options.increase_parallelism(2); + + // resolve path based on cwd + let db_path = std::env::current_dir().unwrap().join(db_path); + let db_path = db_path.to_str().unwrap(); + + // Initialize RocksDB + let db = DB::open(&options, db_path).map_err(|e| StorageError::RocksDBError(e))?; + println!("Opened RocksDB at path: {:?}", db.path()); + + Ok(RocksDBStore { db: Arc::new(db) }) + } + + /// Stores a key-value pair in the database. + pub fn put(&self, key: &[u8], value: &[u8]) -> Result<(), StorageError> { + self.db + .put(key, value) + .map_err(|e| StorageError::RocksDBError(e)) + } + + /// Retrieves a value for the given key from the database. + pub fn get(&self, key: &[u8]) -> Result>, StorageError> { + self.db.get(key).map_err(|e| StorageError::RocksDBError(e)) + } + + /// Asynchronously stores a key-value pair in the database. + pub async fn async_put(&self, key: Vec, value: Vec) -> Result<(), StorageError> { + let db = Arc::clone(&self.db); + tokio::task::spawn_blocking(move || db.put(key, value)) + .await + .map_err(|e| StorageError::AsyncError(e.to_string()))? + .map_err(|e| StorageError::RocksDBError(e)) + } + + /// Asynchronously retrieves a value for the given key from the database. + pub async fn async_get(&self, key: Vec) -> Result>, StorageError> { + let db = Arc::clone(&self.db); + tokio::task::spawn_blocking(move || db.get(key)) + .await + .map_err(|e| StorageError::AsyncError(e.to_string()))? + .map_err(|e| StorageError::RocksDBError(e)) + } +} + +pub mod errors; + +#[cfg(test)] +mod tests { + use super::*; + use cesium_crypto::keys::Account; + + #[test] + fn test_storage_put() { + let store = RocksDBStore::instance(); + let account = Account::create(); + + let key = account.to_public_key_bytes(); + let value = "hello world".as_bytes(); + + store.put(key, value).unwrap(); + + let result = store.get(key).unwrap(); + assert_eq!(result.unwrap(), value); + } + + #[tokio::test] + async fn test_storage_put_async() { + let store = RocksDBStore::instance(); + let account = Account::create(); + + let key = account.to_public_key_bytes(); + let value = "hello world".as_bytes(); + + store.async_put(key.to_vec(), value.to_vec()).await.unwrap(); + + let result = store.async_get(key.to_vec()).await.unwrap(); + assert_eq!(result.unwrap(), value); + } +}