diff --git a/programs/mpl-core/src/error.rs b/programs/mpl-core/src/error.rs index f9464fc7..6b8ef529 100644 --- a/programs/mpl-core/src/error.rs +++ b/programs/mpl-core/src/error.rs @@ -96,6 +96,14 @@ pub enum MplCoreError { /// 20 - Missing new owner #[error("Missing new owner")] MissingNewOwner, + + /// 21 - Missing system program + #[error("Missing system program")] + MissingSystemProgram, + + /// 22 - Not implemented + #[error("Not implemented")] + NotImplemented, } impl PrintProgramError for MplCoreError { diff --git a/programs/mpl-core/src/instruction.rs b/programs/mpl-core/src/instruction.rs index 5f09a9f3..676f039b 100644 --- a/programs/mpl-core/src/instruction.rs +++ b/programs/mpl-core/src/instruction.rs @@ -142,7 +142,8 @@ pub(crate) enum MplAssetInstruction { #[account(2, signer, name="authority", desc = "The owner or delegate of the asset")] #[account(3, optional, writable, signer, name="payer", desc = "The account paying for the storage fees")] #[account(4, name="new_owner", desc = "The new owner to which to transfer the asset")] - #[account(5, optional, name="log_wrapper", desc = "The SPL Noop Program")] + #[account(5, optional, name="system_program", desc = "The system program")] + #[account(6, optional, name="log_wrapper", desc = "The SPL Noop Program")] Transfer(TransferArgs), /// Update an mpl-core. diff --git a/programs/mpl-core/src/processor/burn.rs b/programs/mpl-core/src/processor/burn.rs index fec4ee0d..944af82c 100644 --- a/programs/mpl-core/src/processor/burn.rs +++ b/programs/mpl-core/src/processor/burn.rs @@ -6,7 +6,7 @@ use crate::{ error::MplCoreError, instruction::accounts::{BurnAccounts, BurnCollectionAccounts}, plugins::{Plugin, PluginType}, - state::{Asset, Collection, Compressible, CompressionProof, Key}, + state::{Asset, Collection, CompressionProof, Key}, utils::{ close_program_account, load_key, validate_asset_permissions, validate_collection_permissions, verify_proof, @@ -41,7 +41,7 @@ pub(crate) fn burn<'a>(accounts: &'a [AccountInfo<'a>], args: BurnArgs) -> Progr // TODO: Check delegates in compressed case. - asset.wrap()?; + //asset.wrap()?; } Key::Asset => { let _ = validate_asset_permissions( diff --git a/programs/mpl-core/src/processor/compress.rs b/programs/mpl-core/src/processor/compress.rs index e17165b4..b1e326aa 100644 --- a/programs/mpl-core/src/processor/compress.rs +++ b/programs/mpl-core/src/processor/compress.rs @@ -1,17 +1,13 @@ use borsh::{BorshDeserialize, BorshSerialize}; use mpl_utils::assert_signer; -use solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, program_memory::sol_memcpy, -}; +use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult}; use crate::{ error::MplCoreError, instruction::accounts::CompressAccounts, - plugins::{Plugin, PluginType, RegistryRecord}, - state::{ - Asset, Collection, Compressible, HashablePluginSchema, HashedAsset, HashedAssetSchema, Key, - }, - utils::{fetch_core_data, load_key, resize_or_reallocate_account, validate_asset_permissions}, + plugins::{Plugin, PluginType}, + state::{Asset, Collection, Key, Wrappable}, + utils::{compress_into_account_space, fetch_core_data, load_key, validate_asset_permissions}, }; #[repr(C)] @@ -48,50 +44,15 @@ pub(crate) fn compress<'a>(accounts: &'a [AccountInfo<'a>], _args: CompressArgs) Plugin::validate_compress, )?; - let mut plugin_hashes = vec![]; - if let Some(plugin_registry) = plugin_registry { - let mut registry_records = plugin_registry.registry; - - // It should already be sorted but we just want to make sure. - registry_records.sort_by(RegistryRecord::compare_offsets); - - for (i, record) in registry_records.into_iter().enumerate() { - let plugin = Plugin::deserialize( - &mut &(*ctx.accounts.asset.data).borrow()[record.offset..], - )?; - - let hashable_plugin_schema = HashablePluginSchema { - index: i, - authority: record.authority, - plugin, - }; - - let plugin_hash = hashable_plugin_schema.hash()?; - plugin_hashes.push(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( + let compression_proof = compress_into_account_space( + asset, + plugin_registry, ctx.accounts.asset, payer, ctx.accounts.system_program, - serialized_data.len(), )?; - sol_memcpy( - &mut ctx.accounts.asset.try_borrow_mut_data()?, - &serialized_data, - serialized_data.len(), - ); + compression_proof.wrap()?; } Key::HashedAsset => return Err(MplCoreError::AlreadyCompressed.into()), _ => return Err(MplCoreError::IncorrectAccount.into()), diff --git a/programs/mpl-core/src/processor/create.rs b/programs/mpl-core/src/processor/create.rs index bfb51685..74ac9b4f 100644 --- a/programs/mpl-core/src/processor/create.rs +++ b/programs/mpl-core/src/processor/create.rs @@ -1,7 +1,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use mpl_utils::assert_signer; use solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, program::invoke, + account_info::AccountInfo, entrypoint::ProgramResult, msg, program::invoke, program_memory::sol_memcpy, rent::Rent, system_instruction, system_program, sysvar::Sysvar, }; @@ -12,7 +12,7 @@ use crate::{ create_meta_idempotent, initialize_plugin, CheckResult, Plugin, PluginType, ValidationResult, }, - state::{Asset, Compressible, DataState, HashedAsset, Key, UpdateAuthority, COLLECT_AMOUNT}, + state::{Asset, DataState, Key, UpdateAuthority, COLLECT_AMOUNT}, utils::fetch_core_data, }; @@ -69,9 +69,8 @@ pub(crate) fn create<'a>(accounts: &'a [AccountInfo<'a>], args: CreateArgs) -> P let serialized_data = match args.data_state { DataState::AccountState => serialized_data, DataState::LedgerState => { - new_asset.wrap()?; - let hashed_asset = HashedAsset::new(new_asset.hash()?); - hashed_asset.try_to_vec()? + msg!("Error: Minting compressed is not implemented"); + return Err(MplCoreError::NotImplemented.into()); } }; diff --git a/programs/mpl-core/src/processor/decompress.rs b/programs/mpl-core/src/processor/decompress.rs index f27fc3ef..a28e4398 100644 --- a/programs/mpl-core/src/processor/decompress.rs +++ b/programs/mpl-core/src/processor/decompress.rs @@ -1,16 +1,15 @@ use borsh::{BorshDeserialize, BorshSerialize}; use mpl_utils::assert_signer; -use solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, program_memory::sol_memcpy, - system_program, -}; +use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, system_program}; use crate::{ error::MplCoreError, instruction::accounts::DecompressAccounts, - plugins::{create_meta_idempotent, initialize_plugin, Plugin, PluginType}, + plugins::{Plugin, PluginType}, state::{Asset, Collection, CompressionProof, Key}, - utils::{load_key, resize_or_reallocate_account, validate_asset_permissions, verify_proof}, + utils::{ + load_key, rebuild_account_state_from_proof_data, validate_asset_permissions, verify_proof, + }, }; #[repr(C)] @@ -41,41 +40,19 @@ pub(crate) fn decompress<'a>( match load_key(ctx.accounts.asset, 0)? { Key::HashedAsset => { - // Verify the proof and rebuild Asset struct in account. + // Verify the proof and rebuild Asset struct in account space. let (asset, plugins) = verify_proof(ctx.accounts.asset, &args.compression_proof)?; - let serialized_data = asset.try_to_vec()?; - resize_or_reallocate_account( + // Use the data from the compression proof to rebuild the account. + rebuild_account_state_from_proof_data( + asset, + plugins, ctx.accounts.asset, payer, ctx.accounts.system_program, - serialized_data.len(), )?; - sol_memcpy( - &mut ctx.accounts.asset.try_borrow_mut_data()?, - &serialized_data, - serialized_data.len(), - ); - - // Add the plugins. - if !plugins.is_empty() { - create_meta_idempotent(ctx.accounts.asset, payer, ctx.accounts.system_program)?; - - for plugin in plugins { - initialize_plugin::( - &plugin.plugin, - &plugin.authority, - ctx.accounts.asset, - payer, - ctx.accounts.system_program, - )?; - } - } - - // Validate permissions. - //let (asset, _, plugin_registry) = fetch_core_data::(ctx.accounts.asset)?; - + // Validate asset permissions. let _ = validate_asset_permissions( ctx.accounts.authority, ctx.accounts.asset, diff --git a/programs/mpl-core/src/processor/transfer.rs b/programs/mpl-core/src/processor/transfer.rs index 594de83e..8912433b 100644 --- a/programs/mpl-core/src/processor/transfer.rs +++ b/programs/mpl-core/src/processor/transfer.rs @@ -6,8 +6,11 @@ use crate::{ error::MplCoreError, instruction::accounts::TransferAccounts, plugins::{Plugin, PluginType}, - state::{Asset, Collection, Compressible, CompressionProof, HashedAsset, Key, SolanaAccount}, - utils::{load_key, validate_asset_permissions, verify_proof}, + state::{Asset, Collection, CompressionProof, Key, SolanaAccount, Wrappable}, + utils::{ + compress_into_account_space, load_key, rebuild_account_state_from_proof_data, + validate_asset_permissions, verify_proof, + }, }; #[repr(C)] @@ -22,29 +25,61 @@ pub(crate) fn transfer<'a>(accounts: &'a [AccountInfo<'a>], args: TransferArgs) // Guards. assert_signer(ctx.accounts.authority)?; - if let Some(payer) = ctx.accounts.payer { + let payer = if let Some(payer) = ctx.accounts.payer { assert_signer(payer)?; - } + payer + } else { + ctx.accounts.authority + }; match load_key(ctx.accounts.asset, 0)? { Key::HashedAsset => { let compression_proof = args .compression_proof .ok_or(MplCoreError::MissingCompressionProof)?; - let (mut asset, _) = verify_proof(ctx.accounts.asset, &compression_proof)?; - if ctx.accounts.authority.key != &asset.owner { - return Err(MplCoreError::InvalidAuthority.into()); - } + let system_program = ctx + .accounts + .system_program + .ok_or(MplCoreError::MissingSystemProgram)?; - // TODO: Check delegates in compressed case. + // Verify the proof and rebuild Asset struct in account space. + let (mut asset, plugins) = verify_proof(ctx.accounts.asset, &compression_proof)?; + // Set the new owner. asset.owner = *ctx.accounts.new_owner.key; - asset.wrap()?; + // Use the data from the compression proof to rebuild the account. + rebuild_account_state_from_proof_data( + asset, + plugins, + ctx.accounts.asset, + payer, + system_program, + )?; + + let (asset, _, plugin_registry) = validate_asset_permissions( + ctx.accounts.authority, + ctx.accounts.asset, + ctx.accounts.collection, + Some(ctx.accounts.new_owner), + Asset::check_transfer, + Collection::check_transfer, + PluginType::check_transfer, + Asset::validate_transfer, + Collection::validate_transfer, + Plugin::validate_transfer, + )?; + + let compression_proof = compress_into_account_space( + asset, + plugin_registry, + ctx.accounts.asset, + payer, + system_program, + )?; - // Make a new hashed asset with updated owner and save to account. - HashedAsset::new(asset.hash()?).save(ctx.accounts.asset, 0) + compression_proof.wrap() } Key::Asset => { let (mut asset, _, _) = validate_asset_permissions( @@ -60,6 +95,7 @@ pub(crate) fn transfer<'a>(accounts: &'a [AccountInfo<'a>], args: TransferArgs) Plugin::validate_transfer, )?; + // Set the new owner. asset.owner = *ctx.accounts.new_owner.key; asset.save(ctx.accounts.asset, 0) } diff --git a/programs/mpl-core/src/state/compression_proof.rs b/programs/mpl-core/src/state/compression_proof.rs new file mode 100644 index 00000000..a46de91c --- /dev/null +++ b/programs/mpl-core/src/state/compression_proof.rs @@ -0,0 +1,35 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::pubkey::Pubkey; + +use crate::state::{Asset, HashablePluginSchema, UpdateAuthority, Wrappable}; + +/// A simple struct to store the compression proof of an asset. +#[repr(C)] +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] +pub struct CompressionProof { + /// The owner of the asset. + pub owner: Pubkey, //32 + /// The update authority of the asset. + pub update_authority: UpdateAuthority, //33 + /// The name of the asset. + pub name: String, //4 + /// The URI of the asset that points to the off-chain data. + pub uri: String, //4 + /// The plugins for the asset. + pub plugins: Vec, //4 +} + +impl CompressionProof { + /// Create a new `CompressionProof`. + pub fn new(asset: Asset, plugins: Vec) -> Self { + Self { + owner: asset.owner, + update_authority: asset.update_authority, + name: asset.name, + uri: asset.uri, + plugins, + } + } +} + +impl Wrappable for CompressionProof {} diff --git a/programs/mpl-core/src/state/hashable_plugin_schema.rs b/programs/mpl-core/src/state/hashable_plugin_schema.rs new file mode 100644 index 00000000..5b397a34 --- /dev/null +++ b/programs/mpl-core/src/state/hashable_plugin_schema.rs @@ -0,0 +1,30 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use std::cmp::Ordering; + +use crate::{ + plugins::Plugin, + state::{Authority, Compressible}, +}; + +/// A type that stores a plugin's authorities and deserialized data into a +/// schema that will be later hashed into a hashed asset. Also used in +/// `CompressionProof`. +#[derive(Clone, BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq)] +pub struct HashablePluginSchema { + /// This is the order the plugins are stored in the account, allowing us + /// to keep track of their order in the hashing. + pub index: usize, + /// The authorities who have permission to utilize a plugin. + pub authority: Authority, + /// The deserialized plugin. + pub plugin: Plugin, +} + +impl HashablePluginSchema { + /// Associated function for sorting `RegistryRecords` by offset. + pub fn compare_indeces(a: &HashablePluginSchema, b: &HashablePluginSchema) -> Ordering { + a.index.cmp(&b.index) + } +} + +impl Compressible for HashablePluginSchema {} diff --git a/programs/mpl-core/src/state/hashed_asset_schema.rs b/programs/mpl-core/src/state/hashed_asset_schema.rs index ff80728a..8864e4c1 100644 --- a/programs/mpl-core/src/state/hashed_asset_schema.rs +++ b/programs/mpl-core/src/state/hashed_asset_schema.rs @@ -1,32 +1,6 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use std::cmp::Ordering; -use crate::{ - plugins::Plugin, - state::{Authority, Compressible}, -}; - -/// A type that stores a plugin's authorities and deserialized data into a -/// schema that will be later hashed into a hashed asset. -#[derive(Clone, BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq)] -pub struct HashablePluginSchema { - /// This is the order the plugins are stored in the account, allowing us - /// to keep track of their order in the hashing. - pub index: usize, - /// The authorities who have permission to utilize a plugin. - pub authority: Authority, - /// The deserialized plugin. - pub plugin: Plugin, -} - -impl HashablePluginSchema { - /// Associated function for sorting `RegistryRecords` by offset. - pub fn compare_indeces(a: &HashablePluginSchema, b: &HashablePluginSchema) -> Ordering { - a.index.cmp(&b.index) - } -} - -impl Compressible for HashablePluginSchema {} +use crate::state::Compressible; /// The hashed asset schema is a schema that contains a hash of the asset and a vec of plugin hashes. #[derive(Clone, BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq)] diff --git a/programs/mpl-core/src/state/mod.rs b/programs/mpl-core/src/state/mod.rs index 0955c4ad..2bb56ce4 100644 --- a/programs/mpl-core/src/state/mod.rs +++ b/programs/mpl-core/src/state/mod.rs @@ -1,21 +1,27 @@ mod asset; pub use asset::*; -mod hashed_asset; -pub use hashed_asset::*; +mod collect; +pub(crate) use collect::*; + +mod collection; +pub use collection::*; + +mod compression_proof; +pub use compression_proof::*; + +mod hashable_plugin_schema; +pub use hashable_plugin_schema::*; mod hashed_asset_schema; pub use hashed_asset_schema::*; +mod hashed_asset; +pub use hashed_asset::*; + mod traits; pub use traits::*; -mod collection; -pub use collection::*; - -mod collect; -pub(crate) use collect::*; - mod update_authority; pub use update_authority::*; @@ -103,19 +109,3 @@ impl Key { 1 } } - -/// A simple struct to store the compression proof of an asset. -#[repr(C)] -#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] -pub struct CompressionProof { - /// The owner of the asset. - pub owner: Pubkey, //32 - /// The update authority of the asset. - pub update_authority: UpdateAuthority, //33 - /// The name of the asset. - pub name: String, //4 - /// The URI of the asset that points to the off-chain data. - pub uri: String, //4 - /// The plugins for the asset. - pub plugins: Vec, //4 -} diff --git a/programs/mpl-core/src/state/traits.rs b/programs/mpl-core/src/state/traits.rs index 76fe8b4e..008b6c45 100644 --- a/programs/mpl-core/src/state/traits.rs +++ b/programs/mpl-core/src/state/traits.rs @@ -44,14 +44,17 @@ pub trait SolanaAccount: BorshSerialize + BorshDeserialize { } } -/// A trait for assets that can be compressed and hashed. +/// A trait for data that can be compressed. pub trait Compressible: BorshSerialize + BorshDeserialize { /// Get the hash of the compressed data. fn hash(&self) -> Result<[u8; 32], ProgramError> { let serialized_data = self.try_to_vec()?; Ok(keccak::hash(serialized_data.as_slice()).to_bytes()) } +} +/// A trait for data that can be wrapped by the spl-noop program. +pub trait Wrappable: BorshSerialize + BorshDeserialize { /// Write the data to ledger state by wrapping it in a noop instruction. fn wrap(&self) -> ProgramResult { let serialized_data = self.try_to_vec()?; diff --git a/programs/mpl-core/src/utils.rs b/programs/mpl-core/src/utils.rs index 10d85645..597c8fc3 100644 --- a/programs/mpl-core/src/utils.rs +++ b/programs/mpl-core/src/utils.rs @@ -1,15 +1,17 @@ +use borsh::{BorshDeserialize, BorshSerialize}; use num_traits::{FromPrimitive, ToPrimitive}; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, program::invoke, - program_error::ProgramError, rent::Rent, system_instruction, sysvar::Sysvar, + program_error::ProgramError, program_memory::sol_memcpy, rent::Rent, system_instruction, + sysvar::Sysvar, }; use std::collections::BTreeMap; use crate::{ error::MplCoreError, plugins::{ - validate_plugin_checks, CheckResult, Plugin, PluginHeader, PluginRegistry, PluginType, - RegistryRecord, ValidationResult, + create_meta_idempotent, initialize_plugin, validate_plugin_checks, CheckResult, Plugin, + PluginHeader, PluginRegistry, PluginType, RegistryRecord, ValidationResult, }, state::{ Asset, Authority, Collection, Compressible, CompressionProof, CoreAsset, DataBlob, @@ -202,7 +204,7 @@ pub(crate) fn resize_or_reallocate_account<'a>( } #[allow(clippy::too_many_arguments)] -/// Validate asset permissions using asset, collection, and plugin lifecycle validations. +/// Validate asset permissions using lifecycle validations for asset, collection, and plugins. pub fn validate_asset_permissions<'a>( authority: &AccountInfo<'a>, asset: &AccountInfo<'a>, @@ -268,10 +270,6 @@ pub fn validate_asset_permissions<'a>( }; solana_program::msg!("approved: {:#?}", approved); - // let custom_args = |plugin: &Plugin, authority_info: &AccountInfo<'a>, authority: &Authority| { - // Plugin::validate_transfer(plugin, authority_info, new_owner, authority) - // }; - approved = validate_plugin_checks( Key::Collection, &checks, @@ -299,7 +297,7 @@ pub fn validate_asset_permissions<'a>( Ok((deserialized_asset, plugin_header, plugin_registry)) } -/// Validate collection permissions using collection and plugin lifecycle validations. +/// Validate collection permissions using lifecycle validations for collection and plugins. pub fn validate_collection_permissions<'a>( authority: &AccountInfo<'a>, collection: &AccountInfo<'a>, @@ -359,3 +357,90 @@ pub fn validate_collection_permissions<'a>( Ok((deserialized_collection, plugin_header, plugin_registry)) } + +/// Take an `Asset` and Vec of `HashablePluginSchema` and rebuild the asset in account space. +pub fn rebuild_account_state_from_proof_data<'a>( + asset: Asset, + plugins: Vec, + asset_info: &AccountInfo<'a>, + payer: &AccountInfo<'a>, + system_program: &AccountInfo<'a>, +) -> ProgramResult { + let serialized_data = asset.try_to_vec()?; + resize_or_reallocate_account(asset_info, payer, system_program, serialized_data.len())?; + + sol_memcpy( + &mut asset_info.try_borrow_mut_data()?, + &serialized_data, + serialized_data.len(), + ); + + // Add the plugins. + if !plugins.is_empty() { + create_meta_idempotent(asset_info, payer, system_program)?; + + for plugin in plugins { + initialize_plugin::( + &plugin.plugin, + &plugin.authority, + asset_info, + payer, + system_program, + )?; + } + } + + Ok(()) +} + +/// Take `Asset` and `PluginRegistry` for a decompressed asset, and compress into account space. +pub fn compress_into_account_space<'a>( + asset: Asset, + plugin_registry: Option, + asset_info: &AccountInfo<'a>, + payer: &AccountInfo<'a>, + system_program: &AccountInfo<'a>, +) -> Result { + let asset_hash = asset.hash()?; + let mut compression_proof = CompressionProof::new(asset, vec![]); + let mut plugin_hashes = vec![]; + if let Some(plugin_registry) = plugin_registry { + let mut registry_records = plugin_registry.registry; + + // It should already be sorted but we just want to make sure. + registry_records.sort_by(RegistryRecord::compare_offsets); + + for (i, record) in registry_records.into_iter().enumerate() { + let plugin = Plugin::deserialize(&mut &(*asset_info.data).borrow()[record.offset..])?; + + let hashable_plugin_schema = HashablePluginSchema { + index: i, + authority: record.authority, + plugin, + }; + + let plugin_hash = hashable_plugin_schema.hash()?; + plugin_hashes.push(plugin_hash); + + compression_proof.plugins.push(hashable_plugin_schema); + } + } + + 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(asset_info, payer, system_program, serialized_data.len())?; + + sol_memcpy( + &mut asset_info.try_borrow_mut_data()?, + &serialized_data, + serialized_data.len(), + ); + + Ok(compression_proof) +}