From 744559bb4e6dc06064bc2c4031631aa942c94449 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Wed, 21 Feb 2024 22:17:45 -0800 Subject: [PATCH] Initial compress implementation --- programs/mpl-asset/src/error.rs | 8 ++ programs/mpl-asset/src/plugins/mod.rs | 4 +- programs/mpl-asset/src/plugins/utils.rs | 18 ++-- programs/mpl-asset/src/processor/compress.rs | 83 +++++++++++++++++-- programs/mpl-asset/src/state/asset.rs | 17 +--- .../src/state/hashed_asset_schema.rs | 18 ++++ programs/mpl-asset/src/state/mod.rs | 6 ++ programs/mpl-asset/src/state/traits.rs | 21 +++-- 8 files changed, 142 insertions(+), 33 deletions(-) create mode 100644 programs/mpl-asset/src/state/hashed_asset_schema.rs diff --git a/programs/mpl-asset/src/error.rs b/programs/mpl-asset/src/error.rs index c615ee68..c09e7ca5 100644 --- a/programs/mpl-asset/src/error.rs +++ b/programs/mpl-asset/src/error.rs @@ -75,6 +75,14 @@ pub enum MplAssetError { /// 16 - Numerical overflow #[error("Numerical overflow")] NumericalOverflowError, + + /// 17 - Already compressed account + #[error("Already compressed account")] + AlreadyCompressed, + + /// 18 - Already decompressed + #[error("Already decompressed account")] + AlreadyDecompressed, } impl PrintProgramError for MplAssetError { diff --git a/programs/mpl-asset/src/plugins/mod.rs b/programs/mpl-asset/src/plugins/mod.rs index 415a541b..f9e77c97 100644 --- a/programs/mpl-asset/src/plugins/mod.rs +++ b/programs/mpl-asset/src/plugins/mod.rs @@ -26,7 +26,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use crate::{ error::MplAssetError, - state::{Authority, DataBlob}, + state::{Authority, Compressible, DataBlob}, }; #[repr(u16)] @@ -53,6 +53,8 @@ impl Plugin { } } +impl Compressible for Plugin {} + #[repr(u16)] #[derive(Clone, Copy, Debug, BorshSerialize, BorshDeserialize, Eq, PartialEq)] pub enum PluginType { diff --git a/programs/mpl-asset/src/plugins/utils.rs b/programs/mpl-asset/src/plugins/utils.rs index 6b438383..6c94b0f8 100644 --- a/programs/mpl-asset/src/plugins/utils.rs +++ b/programs/mpl-asset/src/plugins/utils.rs @@ -67,8 +67,7 @@ pub fn fetch_plugin( account: &AccountInfo, plugin_type: PluginType, ) -> Result<(Vec, Plugin, usize), ProgramError> { - let mut bytes: &[u8] = &(*account.data).borrow(); - let asset = Asset::deserialize(&mut bytes)?; + let asset = Asset::load(account, 0)?; let header = PluginHeader::load(account, asset.get_size())?; let PluginRegistry { registry, .. } = @@ -102,10 +101,19 @@ pub fn fetch_plugin( Ok((plugin_data.authorities.clone(), plugin, plugin_data.offset)) } +pub fn fetch_plugins(account: &AccountInfo) -> Result, ProgramError> { + let asset = Asset::load(account, 0)?; + + let header = PluginHeader::load(account, asset.get_size())?; + let PluginRegistry { registry, .. } = + PluginRegistry::load(account, header.plugin_registry_offset)?; + + Ok(registry) +} + /// Create plugin header and registry if it doesn't exist pub fn list_plugins(account: &AccountInfo) -> Result, ProgramError> { - let mut bytes: &[u8] = &(*account.data).borrow(); - let asset = Asset::deserialize(&mut bytes)?; + let asset = Asset::load(account, 0)?; let header = PluginHeader::load(account, asset.get_size())?; let PluginRegistry { registry, .. } = @@ -283,7 +291,7 @@ pub fn delete_plugin<'a>( data: _, }| type_iter == plugin_type, ) { - let registry_record = plugin_registry.registry.swap_remove(index); + let registry_record = plugin_registry.registry.remove(index); let serialized_registry_record = registry_record.try_to_vec()?; // Only allow the default authority to delete the plugin. diff --git a/programs/mpl-asset/src/processor/compress.rs b/programs/mpl-asset/src/processor/compress.rs index e1a05abc..3e1fc598 100644 --- a/programs/mpl-asset/src/processor/compress.rs +++ b/programs/mpl-asset/src/processor/compress.rs @@ -1,20 +1,93 @@ use borsh::{BorshDeserialize, BorshSerialize}; use mpl_utils::assert_signer; +use mpl_utils::resize_or_reallocate_account_raw; use solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, program::invoke, - program_memory::sol_memcpy, rent::Rent, system_instruction, system_program, sysvar::Sysvar, + account_info::AccountInfo, entrypoint::ProgramResult, program_memory::sol_memcpy, }; use crate::{ error::MplAssetError, - instruction::accounts::CreateAccounts, - state::{Asset, Compressible, DataState, HashedAsset, Key}, + instruction::accounts::CompressAccounts, + plugins::{fetch_plugins, Plugin}, + state::{ + Asset, AuthorityVec, Compressible, DataBlob, HashedAsset, HashedAssetSchema, Key, + PluginHash, SolanaAccount, + }, + utils::load_key, }; #[repr(C)] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] pub struct CompressArgs {} -pub(crate) fn compress<'a>(accounts: &'a [AccountInfo<'a>], args: CompressArgs) -> ProgramResult { +pub(crate) fn compress<'a>(accounts: &'a [AccountInfo<'a>], _args: CompressArgs) -> ProgramResult { + // Accounts. + let ctx = CompressAccounts::context(accounts)?; + + // Guards. + assert_signer(ctx.accounts.owner)?; + let payer = if let Some(payer) = ctx.accounts.payer { + assert_signer(payer)?; + payer + } else { + ctx.accounts.owner + }; + + match load_key(ctx.accounts.asset_address, 0)? { + Key::Asset => { + let asset = Asset::load(ctx.accounts.asset_address, 0)?; + + if ctx.accounts.owner.key != &asset.owner { + return Err(MplAssetError::InvalidAuthority.into()); + } + + // TODO: Delegated compress/decompress authority. + + if asset.get_size() != ctx.accounts.asset_address.data_len() { + let registry_records = fetch_plugins(ctx.accounts.asset_address)?; + + let mut plugin_hashes = Vec::with_capacity(registry_records.len()); + for record in registry_records { + let authorities: AuthorityVec = record.data.authorities; + let plugin_authorities_hash = authorities.hash()?; + + let plugin = Plugin::deserialize( + &mut &(*ctx.accounts.asset_address.data).borrow()[record.data.offset..], + )?; + let plugin_hash = plugin.hash()?; + + plugin_hashes.push(PluginHash { + plugin_authorities_hash, + plugin_hash, + }); + } + + let asset_hash = asset.hash()?; + let hashed_asset_schema = HashedAssetSchema { + asset_hash, + plugin_hashes, + }; + + let hashed_asset = HashedAsset::new(hashed_asset_schema.hash()?); + let serialized_data = hashed_asset.try_to_vec()?; + + resize_or_reallocate_account_raw( + ctx.accounts.asset_address, + payer, + ctx.accounts.system_program, + serialized_data.len(), + )?; + + sol_memcpy( + &mut ctx.accounts.asset_address.try_borrow_mut_data()?, + &serialized_data, + serialized_data.len(), + ); + } + } + Key::HashedAsset => return Err(MplAssetError::AlreadyCompressed.into()), + _ => return Err(MplAssetError::IncorrectAccount.into()), + } + Ok(()) } diff --git a/programs/mpl-asset/src/state/asset.rs b/programs/mpl-asset/src/state/asset.rs index 9628d009..912abd8f 100644 --- a/programs/mpl-asset/src/state/asset.rs +++ b/programs/mpl-asset/src/state/asset.rs @@ -1,9 +1,6 @@ use borsh::{BorshDeserialize, BorshSerialize}; use shank::ShankAccount; -use solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, keccak, program::invoke, - program_error::ProgramError, pubkey::Pubkey, -}; +use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; use crate::{ error::MplAssetError, @@ -99,17 +96,7 @@ impl Asset { } } -impl Compressible for Asset { - fn hash(&self) -> Result<[u8; 32], ProgramError> { - let serialized_data = self.try_to_vec()?; - Ok(keccak::hash(serialized_data.as_slice()).to_bytes()) - } - - fn wrap(&self) -> ProgramResult { - let serialized_data = self.try_to_vec()?; - invoke(&spl_noop::instruction(serialized_data), &[]) - } -} +impl Compressible for Asset {} impl DataBlob for Asset { fn get_initial_size() -> usize { diff --git a/programs/mpl-asset/src/state/hashed_asset_schema.rs b/programs/mpl-asset/src/state/hashed_asset_schema.rs new file mode 100644 index 00000000..3347fb10 --- /dev/null +++ b/programs/mpl-asset/src/state/hashed_asset_schema.rs @@ -0,0 +1,18 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use shank::ShankAccount; + +use crate::state::Compressible; + +#[derive(Clone, BorshSerialize, BorshDeserialize, Debug, ShankAccount, PartialEq, Eq)] +pub struct PluginHash { + pub plugin_authorities_hash: [u8; 32], + pub plugin_hash: [u8; 32], +} + +#[derive(Clone, BorshSerialize, BorshDeserialize, Debug, ShankAccount, PartialEq, Eq)] +pub struct HashedAssetSchema { + pub asset_hash: [u8; 32], + pub plugin_hashes: Vec, +} + +impl Compressible for HashedAssetSchema {} diff --git a/programs/mpl-asset/src/state/mod.rs b/programs/mpl-asset/src/state/mod.rs index e4de61a8..8879ccd6 100644 --- a/programs/mpl-asset/src/state/mod.rs +++ b/programs/mpl-asset/src/state/mod.rs @@ -4,6 +4,9 @@ pub use asset::*; mod hashed_asset; pub use hashed_asset::*; +mod hashed_asset_schema; +pub use hashed_asset_schema::*; + mod traits; pub use traits::*; @@ -29,6 +32,9 @@ pub enum Authority { Permanent { address: Pubkey }, } +pub type AuthorityVec = Vec; +impl Compressible for AuthorityVec {} + #[repr(C)] #[derive(Clone, BorshSerialize, BorshDeserialize, Debug, Eq, PartialEq)] pub enum ExtraAccounts { diff --git a/programs/mpl-asset/src/state/traits.rs b/programs/mpl-asset/src/state/traits.rs index 7a284e60..b7a0ff80 100644 --- a/programs/mpl-asset/src/state/traits.rs +++ b/programs/mpl-asset/src/state/traits.rs @@ -1,9 +1,9 @@ use crate::{error::MplAssetError, state::Key, utils::load_key}; use borsh::{BorshDeserialize, BorshSerialize}; -use solana_program::account_info::AccountInfo; -use solana_program::entrypoint::ProgramResult; -use solana_program::msg; -use solana_program::program_error::ProgramError; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, keccak, msg, program::invoke, + program_error::ProgramError, +}; pub trait DataBlob: BorshSerialize + BorshDeserialize { fn get_initial_size() -> usize; @@ -35,7 +35,14 @@ pub trait SolanaAccount: BorshSerialize + BorshDeserialize { } } -pub trait Compressible { - fn hash(&self) -> Result<[u8; 32], ProgramError>; - fn wrap(&self) -> ProgramResult; +pub trait Compressible: BorshSerialize + BorshDeserialize { + fn hash(&self) -> Result<[u8; 32], ProgramError> { + let serialized_data = self.try_to_vec()?; + Ok(keccak::hash(serialized_data.as_slice()).to_bytes()) + } + + fn wrap(&self) -> ProgramResult { + let serialized_data = self.try_to_vec()?; + invoke(&spl_noop::instruction(serialized_data), &[]) + } }