From dc14c21857be9e60105b3370edf53395c170652c Mon Sep 17 00:00:00 2001 From: Vadim <31490938+n00m4d@users.noreply.github.com> Date: Mon, 19 Aug 2024 15:28:05 +0200 Subject: [PATCH] feat: use bubblegum batch mint sdk for batch mint verification (#205) --- blockbuster/blockbuster/Cargo.toml | 1 + .../blockbuster/src/programs/bubblegum/mod.rs | 23 +- .../integration_tests/batch_mint_tests.rs | 19 +- .../src/batch_minting/batch_mint_persister.rs | 308 +++-------------- .../src/batch_minting/merkle_tree_wrapper.rs | 299 ----------------- program_transformers/src/batch_minting/mod.rs | 2 - .../src/batch_minting/tests.rs | 311 ------------------ 7 files changed, 76 insertions(+), 887 deletions(-) delete mode 100644 program_transformers/src/batch_minting/merkle_tree_wrapper.rs delete mode 100644 program_transformers/src/batch_minting/tests.rs diff --git a/blockbuster/blockbuster/Cargo.toml b/blockbuster/blockbuster/Cargo.toml index 9919a3e4..c385ed6a 100644 --- a/blockbuster/blockbuster/Cargo.toml +++ b/blockbuster/blockbuster/Cargo.toml @@ -31,6 +31,7 @@ spl-pod = { version = "0.1.0", features = ["serde-traits"] } serde = "1.0.140" solana-zk-token-sdk = "~1.18.11" anchor-lang = { version = "0.29.0" } +bubblegum-batch-sdk = { workspace = true } [dev-dependencies] flatbuffers = "23.1.21" diff --git a/blockbuster/blockbuster/src/programs/bubblegum/mod.rs b/blockbuster/blockbuster/src/programs/bubblegum/mod.rs index d074bc0e..582becdb 100644 --- a/blockbuster/blockbuster/src/programs/bubblegum/mod.rs +++ b/blockbuster/blockbuster/src/programs/bubblegum/mod.rs @@ -5,6 +5,7 @@ use crate::{ programs::ProgramParseResult, }; use borsh::{BorshDeserialize, BorshSerialize}; +use bubblegum_batch_sdk::model::BatchMintInstruction; use log::warn; use mpl_bubblegum::{ get_instruction_type, @@ -13,7 +14,7 @@ use mpl_bubblegum::{ UnverifyCreatorInstructionArgs, UpdateMetadataInstructionArgs, VerifyCreatorInstructionArgs, }, - types::{BubblegumEventType, MetadataArgs, UpdateArgs}, + types::{BubblegumEventType, MetadataArgs, UpdateArgs, Version}, }; pub use mpl_bubblegum::{ types::{LeafSchema, UseMethod}, @@ -97,6 +98,26 @@ pub struct BubblegumInstruction { pub payload: Option, } +impl From<&BatchMintInstruction> for BubblegumInstruction { + fn from(value: &BatchMintInstruction) -> Self { + let hash = value.leaf_update.hash(); + Self { + instruction: InstructionName::MintV1, + tree_update: Some((&value.tree_update).into()), + leaf_update: Some(LeafSchemaEvent::new( + Version::V1, + value.leaf_update.clone(), + hash, + )), + payload: Some(Payload::MintV1 { + args: value.mint_args.clone(), + authority: value.authority, + tree_id: value.tree_update.id, + }), + } + } +} + impl BubblegumInstruction { pub fn new(ix: InstructionName) -> Self { BubblegumInstruction { diff --git a/integration_tests/tests/integration_tests/batch_mint_tests.rs b/integration_tests/tests/integration_tests/batch_mint_tests.rs index 58594957..2a4853b2 100644 --- a/integration_tests/tests/integration_tests/batch_mint_tests.rs +++ b/integration_tests/tests/integration_tests/batch_mint_tests.rs @@ -1,6 +1,8 @@ use crate::common::TestSetup; use borsh::BorshSerialize; use bubblegum_batch_sdk::batch_mint_client::BatchMintClient; +use bubblegum_batch_sdk::batch_mint_validations::generate_batch_mint; +use bubblegum_batch_sdk::model::BatchMint; use bubblegum_batch_sdk::model::CollectionConfig; use cadence::{NopMetricSink, StatsdClient}; use cadence_macros::set_global_default; @@ -19,9 +21,8 @@ use nft_ingester::plerkle::PlerkleTransactionInfo; use plerkle_serialization::root_as_transaction_info; use plerkle_serialization::serializer::serialize_transaction; use program_transformers::batch_minting::batch_mint_persister::{ - BatchMint, BatchMintPersister, MockBatchMintDownloader, + BatchMintPersister, MockBatchMintDownloader, }; -use program_transformers::batch_minting::tests::generate_batch_mint; use program_transformers::error::BatchMintValidationError; use sea_orm::sea_query::OnConflict; use sea_orm::{ColumnTrait, ConnectionTrait, DbBackend, IntoActiveModel, QueryTrait, Set}; @@ -193,7 +194,7 @@ async fn batch_mint_persister_test() { set_global_default(client); let setup = TestSetup::new("batch_mint_persister_test".to_string()).await; - let test_batch_mint = generate_batch_mint(10, false); + let test_batch_mint = generate_batch_mint(10); let tmp_dir = tempfile::TempDir::new().unwrap(); let tmp_file = File::create(tmp_dir.path().join("batch-mint-10.json")).unwrap(); @@ -321,7 +322,7 @@ async fn batch_mint_persister_download_fail_test() { set_global_default(client); let setup = TestSetup::new("batch_mint_persister_download_fail_test".to_string()).await; - let test_batch_mint = generate_batch_mint(10, false); + let test_batch_mint = generate_batch_mint(10); let tmp_dir = tempfile::TempDir::new().unwrap(); let tmp_file = File::create(tmp_dir.path().join("batch-mint-10.json")).unwrap(); serde_json::to_writer(tmp_file, &test_batch_mint).unwrap(); @@ -552,7 +553,15 @@ async fn batch_mint_with_unverified_creators_test() { // generate batch mint with creators verified value set to true // but signatures will not be attached // batch should not be saved - let test_batch_mint = generate_batch_mint(10, true); + let mut test_batch_mint = generate_batch_mint(10); + + // set creators verified to true for this test case + for b_mint in test_batch_mint.batch_mints.iter_mut() { + for creator in b_mint.mint_args.creators.iter_mut() { + creator.verified = true; + } + } + let tmp_dir = tempfile::TempDir::new().unwrap(); let tmp_file = File::create(tmp_dir.path().join("batch-mint-10.json")).unwrap(); diff --git a/program_transformers/src/batch_minting/batch_mint_persister.rs b/program_transformers/src/batch_minting/batch_mint_persister.rs index 33b73810..4668f8d6 100644 --- a/program_transformers/src/batch_minting/batch_mint_persister.rs +++ b/program_transformers/src/batch_minting/batch_mint_persister.rs @@ -1,151 +1,29 @@ -use anchor_lang::AnchorSerialize; use async_channel::Receiver; -use bubblegum_batch_sdk::batch_mint_builder::{verify_signature, MetadataArgsHash}; -use solana_sdk::signature::Signature; -use std::collections::HashMap; use std::sync::Arc; -use crate::batch_minting::merkle_tree_wrapper; use crate::bubblegum; use crate::error::{BatchMintValidationError, ProgramTransformerError, ProgramTransformerResult}; use async_trait::async_trait; use blockbuster::instruction::InstructionBundle; -use blockbuster::programs::bubblegum::{BubblegumInstruction, Payload}; +use bubblegum_batch_sdk::{batch_mint_validations::validate_batch_mint, model::BatchMint}; use cadence_macros::{statsd_count, statsd_histogram}; use digital_asset_types::dao::sea_orm_active_enums::{ BatchMintFailStatus, BatchMintPersistingState, }; use digital_asset_types::dao::{batch_mint, batch_mint_to_verify}; use mockall::automock; -use mpl_bubblegum::types::{LeafSchema, MetadataArgs, Version}; -use mpl_bubblegum::utils::get_asset_id; -use mpl_bubblegum::{InstructionName, LeafSchemaEvent}; use sea_orm::sea_query::{LockType, OnConflict}; use sea_orm::ActiveValue::Set; use sea_orm::{ ActiveModelTrait, ColumnTrait, Condition, ConnectionTrait, DbBackend, EntityTrait, IntoActiveModel, QueryFilter, QueryOrder, QuerySelect, QueryTrait, TransactionTrait, }; -use serde::{Deserialize, Serialize}; -use serde_json::value::RawValue; -use serde_with::DisplayFromStr; -use solana_sdk::keccak; -use solana_sdk::keccak::Hash; use solana_sdk::pubkey::Pubkey; use tokio::time::Instant; use tracing::{error, info}; pub const MAX_BATCH_MINT_DOWNLOAD_ATTEMPTS: u8 = 5; -#[derive(Serialize, Deserialize, Clone)] -pub struct BatchMint { - #[serde(with = "serde_with::As::")] - pub tree_id: Pubkey, - pub batch_mints: Vec, - pub raw_metadata_map: HashMap>, // map by uri - pub max_depth: u32, - pub max_buffer_size: u32, - - // derived data - pub merkle_root: [u8; 32], - pub last_leaf_hash: [u8; 32], -} - -#[derive(Serialize, Deserialize, Clone)] -pub struct BatchedMintInstruction { - pub tree_update: ChangeLogEventV1, - pub leaf_update: LeafSchema, - pub mint_args: MetadataArgs, - #[serde(with = "serde_with::As::")] - pub authority: Pubkey, - #[serde(with = "serde_with::As::>>")] - pub creator_signature: Option>, // signatures of the asset with the creator pubkey to ensure verified creator -} - -#[derive(Default, Clone)] -pub struct BatchMintInstruction { - pub max_depth: u32, - pub max_buffer_size: u32, - pub num_minted: u64, - pub root: [u8; 32], - pub leaf: [u8; 32], - pub index: u32, - pub metadata_url: String, - pub file_checksum: String, -} - -#[derive(Serialize, Deserialize, Clone)] -pub struct ChangeLogEventV1 { - #[serde(with = "serde_with::As::")] - pub id: Pubkey, - pub path: Vec, - pub seq: u64, - pub index: u32, -} - -#[derive(Serialize, Deserialize, PartialEq, Copy, Clone)] -pub struct PathNode { - pub node: [u8; 32], - pub index: u32, -} - -impl From<&PathNode> for spl_account_compression::state::PathNode { - fn from(value: &PathNode) -> Self { - Self { - node: value.node, - index: value.index, - } - } -} -impl From for PathNode { - fn from(value: spl_account_compression::state::PathNode) -> Self { - Self { - node: value.node, - index: value.index, - } - } -} -impl From<&ChangeLogEventV1> for blockbuster::programs::bubblegum::ChangeLogEventV1 { - fn from(value: &ChangeLogEventV1) -> Self { - Self { - id: value.id, - path: value.path.iter().map(Into::into).collect::>(), - seq: value.seq, - index: value.index, - } - } -} -impl From for ChangeLogEventV1 { - fn from(value: blockbuster::programs::bubblegum::ChangeLogEventV1) -> Self { - Self { - id: value.id, - path: value.path.into_iter().map(Into::into).collect::>(), - seq: value.seq, - index: value.index, - } - } -} - -impl From<&BatchedMintInstruction> for BubblegumInstruction { - fn from(value: &BatchedMintInstruction) -> Self { - let hash = value.leaf_update.hash(); - Self { - instruction: InstructionName::MintV1, - tree_update: Some((&value.tree_update).into()), - leaf_update: Some(LeafSchemaEvent::new( - Version::V1, - value.leaf_update.clone(), - hash, - )), - payload: Some(Payload::MintV1 { - args: value.mint_args.clone(), - authority: value.authority, - tree_id: value.tree_update.id, - }), - } - } -} - #[automock] #[async_trait] pub trait BatchMintDownloader { @@ -375,8 +253,12 @@ impl BatchMintPer Ok(r) => { let query = batch_mint::Entity::insert(batch_mint::ActiveModel { file_hash: Set(batch_mint_to_verify.file_hash.clone()), - batch_mint_binary_bincode: Set(bincode::serialize(batch_mint) - .map_err(|e| ProgramTransformerError::SerializatonError(e.to_string()))?), + batch_mint_binary_bincode: Set( + bincode::serialize(&r) + .map_err(|e| { + ProgramTransformerError::SerializatonError(e.to_string()) + })?, + ), }) .on_conflict( OnConflict::columns([batch_mint::Column::FileHash]) @@ -465,23 +347,27 @@ impl BatchMintPer batch_mint_to_verify: &mut batch_mint_to_verify::Model, batch_mint: &BatchMint, ) { + let collection_raw_key: Option<[u8; 32]> = + if let Some(key) = batch_mint_to_verify.collection.clone() { + match key.try_into() { + Ok(key_as_array) => Some(key_as_array), + Err(e) => { + error!("Could not convert collection key received from DB: {:?}", e); + + self.mark_persisting_failed(batch_mint_to_verify).await; + return; + } + } + } else { + None + }; + if let Err(e) = - validate_batch_mint(batch_mint, batch_mint_to_verify.collection.clone()).await + validate_batch_mint(batch_mint, collection_raw_key.map(|key| Pubkey::from(key))).await { error!("Error while validating batch mint: {}", e.to_string()); - statsd_count!("batch_mint.validating_fail", 1); - batch_mint_to_verify.batch_mint_persisting_state = - BatchMintPersistingState::FailedToPersist; - if let Err(err) = self - .save_batch_mint_as_failed( - BatchMintFailStatus::BatchMintVerifyFailed, - batch_mint_to_verify, - ) - .await - { - error!("Save batch mint as failed: {}", err); - }; + self.mark_persisting_failed(batch_mint_to_verify).await; return; } statsd_count!("batch_mint.validating_success", 1); @@ -489,6 +375,21 @@ impl BatchMintPer BatchMintPersistingState::SuccessfullyValidate; } + async fn mark_persisting_failed(&self, batch_mint_to_verify: &mut batch_mint_to_verify::Model) { + statsd_count!("batch_mint.validating_fail", 1); + batch_mint_to_verify.batch_mint_persisting_state = + BatchMintPersistingState::FailedToPersist; + if let Err(err) = self + .save_batch_mint_as_failed( + BatchMintFailStatus::BatchMintVerifyFailed, + batch_mint_to_verify, + ) + .await + { + error!("Save batch mint as failed: {}", err); + } + } + async fn store_batch_mint_update( &self, batch_mint_to_verify: &mut batch_mint_to_verify::Model, @@ -555,137 +456,6 @@ impl BatchMintPer } } -pub async fn validate_batch_mint( - batch_mint: &BatchMint, - collection_mint: Option>, -) -> Result<(), BatchMintValidationError> { - let mut leaf_hashes = Vec::new(); - for asset in batch_mint.batch_mints.iter() { - verify_creators_signatures( - &batch_mint.tree_id, - asset, - asset.creator_signature.clone().unwrap_or_default(), - )?; - - if let Some(ref collection) = asset.mint_args.collection { - match &collection_mint { - None => { - if collection.verified { - return Err(BatchMintValidationError::WrongCollectionVerified( - collection.key.to_string(), - )); - } - } - Some(collection_mint) => { - if collection.verified && collection_mint != collection.key.to_bytes().as_ref() - { - return Err(BatchMintValidationError::VerifiedCollectionMismatch( - bs58::encode(collection_mint.clone()).into_string(), - collection.key.to_string(), - )); - } - } - } - } - - let leaf_hash = match get_leaf_hash(asset, &batch_mint.tree_id) { - Ok(leaf_hash) => leaf_hash, - Err(e) => { - return Err(e); - } - }; - leaf_hashes.push(leaf_hash); - } - - merkle_tree_wrapper::validate_change_logs( - batch_mint.max_depth, - batch_mint.max_buffer_size, - &leaf_hashes, - batch_mint, - ) -} - -// TODO: move this func to SDK once this crate will import data types from SDK -fn verify_creators_signatures( - tree_key: &Pubkey, - rolled_mint: &BatchedMintInstruction, - creator_signatures: HashMap, -) -> Result<(), BatchMintValidationError> { - let metadata_hash = - MetadataArgsHash::new(&rolled_mint.leaf_update, tree_key, &rolled_mint.mint_args); - - for creator in &rolled_mint.mint_args.creators { - if creator.verified { - if let Some(signature) = creator_signatures.get(&creator.address) { - if !verify_signature(&creator.address, &metadata_hash.get_message(), signature) { - return Err(BatchMintValidationError::FailedCreatorVerification( - creator.address.to_string(), - )); - } - } else { - return Err(BatchMintValidationError::MissingCreatorSignature( - creator.address.to_string(), - )); - } - } - } - - Ok(()) -} - -fn get_leaf_hash( - asset: &BatchedMintInstruction, - tree_id: &Pubkey, -) -> Result<[u8; 32], BatchMintValidationError> { - let asset_id = get_asset_id(tree_id, asset.leaf_update.nonce()); - if asset_id != asset.leaf_update.id() { - return Err(BatchMintValidationError::PDACheckFail( - asset_id.to_string(), - asset.leaf_update.id().to_string(), - )); - } - - // @dev: seller_fee_basis points is encoded twice so that it can be passed to marketplace - // instructions, without passing the entire, un-hashed MetadataArgs struct - let metadata_args_hash = keccak::hashv(&[asset.mint_args.try_to_vec()?.as_slice()]); - let data_hash = keccak::hashv(&[ - &metadata_args_hash.to_bytes(), - &asset.mint_args.seller_fee_basis_points.to_le_bytes(), - ]); - if asset.leaf_update.data_hash() != data_hash.to_bytes() { - return Err(BatchMintValidationError::InvalidDataHash( - data_hash.to_string(), - Hash::new(asset.leaf_update.data_hash().as_slice()).to_string(), - )); - } - - // Use the metadata auth to check whether we can allow `verified` to be set to true in the - // creator Vec. - let creator_data = asset - .mint_args - .creators - .iter() - .map(|c| [c.address.as_ref(), &[c.verified as u8], &[c.share]].concat()) - .collect::>(); - - // Calculate creator hash. - let creator_hash = keccak::hashv( - creator_data - .iter() - .map(|c| c.as_slice()) - .collect::>() - .as_ref(), - ); - if asset.leaf_update.creator_hash() != creator_hash.to_bytes() { - return Err(BatchMintValidationError::InvalidCreatorsHash( - creator_hash.to_string(), - Hash::new(asset.leaf_update.creator_hash().as_slice()).to_string(), - )); - } - - Ok(asset.leaf_update.hash()) -} - pub async fn store_batch_mint_update( slot: u64, signature: String, diff --git a/program_transformers/src/batch_minting/merkle_tree_wrapper.rs b/program_transformers/src/batch_minting/merkle_tree_wrapper.rs deleted file mode 100644 index 5fc903b4..00000000 --- a/program_transformers/src/batch_minting/merkle_tree_wrapper.rs +++ /dev/null @@ -1,299 +0,0 @@ -use crate::batch_minting::batch_mint_persister; -use crate::batch_minting::batch_mint_persister::BatchMint; -use crate::error::BatchMintValidationError; -use anchor_lang::solana_program::keccak::Hash; -use spl_account_compression::{ConcurrentMerkleTree, ConcurrentMerkleTreeError, Node}; -use spl_concurrent_merkle_tree::changelog::ChangeLog; -use std::slice::Iter; - -/// Interface that abstract [ConcurrentMerkleTree] -/// regardless const generic parameters. -pub trait ITree { - fn initialize(&mut self) -> Result; - fn append(&mut self, node: Node) -> Result; - fn active_index(&self) -> u64; - fn change_logs(&self, ind: usize) -> Box; - fn sequence_number(&self) -> u64; - fn get_root(&self) -> [u8; 32]; -} - -#[macro_export] -macro_rules! make_tree_impls { - ( $( ($x:literal, $y:literal) ),* ) => { - $( - impl ITree for ConcurrentMerkleTree<$x,$y> { - fn initialize(&mut self) -> Result { - self.initialize() - } - fn append(&mut self, node: Node) -> Result { - self.append(node) - } - fn active_index(&self) -> u64 { - self.active_index - } - fn change_logs(&self, ind: usize) -> Box { - Box::new(self.change_logs[ind]) - } - fn sequence_number(&self) -> u64 { - self.sequence_number - } - fn get_root(&self) -> [u8; 32] { - self.get_root() - } - } - )* - } -} - -// Building implementations of ITree -// for all possible instances of ConcurrentMerkleTreeError. -make_tree_impls!( - (3, 8), - (5, 8), - (6, 16), - (7, 16), - (8, 16), - (9, 16), - (10, 32), - (11, 32), - (12, 32), - (13, 32), - (14, 64), - (14, 256), - (14, 1024), - (14, 2048), - (15, 64), - (16, 64), - (17, 64), - (18, 64), - (19, 64), - (20, 64), - (20, 256), - (20, 1024), - (20, 2048), - (24, 64), - (24, 256), - (24, 512), - (24, 1024), - (24, 2048), - (26, 512), - (26, 1024), - (26, 2048), - (30, 512), - (30, 1024), - (30, 2048) -); - -/// An abstraction for [ChangeLog] -/// that abstracts over const generic parameter. -/// Similar to [ITree]. -pub trait IChangeLog { - fn index(&self) -> u32; - fn root(&self) -> [u8; 32]; - fn path_iter(&self) -> Iter; - fn path_slice(&self) -> &[Node]; - fn path_len(&self) -> u32; -} - -#[macro_export] -macro_rules! make_changelog_impls { - ( $( $x:literal ),* ) => { - $( - impl IChangeLog for ChangeLog<$x> { - fn index(&self) -> u32 { - self.index - } - - fn root(&self) -> [u8; 32] { - self.root - } - - fn path_iter(&self) -> Iter { - self.path.iter() - } - - fn path_slice(&self) -> &[Node] { - &self.path - } - - fn path_len(&self) -> u32 { - self.path.len() as u32 - } - } - )* - } -} - -#[macro_export] -macro_rules! make_tree_creator_funcs { - ( $( ($x:literal, $y:literal) ),* ) => { - $( - paste::item! { - #[inline(never)] - fn [< make_concurrent_merkle_tree_ $x _ $y >]() -> Box { - Box::new(ConcurrentMerkleTree::<$x,$y>::new()) - } - } - )* - } -} - -make_tree_creator_funcs!( - (3, 8), - (5, 8), - (6, 16), - (7, 16), - (8, 16), - (9, 16), - (10, 32), - (11, 32), - (12, 32), - (13, 32), - (14, 64), - (14, 256), - (14, 1024), - (14, 2048), - (15, 64), - (16, 64), - (17, 64), - (18, 64), - (19, 64), - (20, 64), - (20, 256), - (20, 1024), - (20, 2048), - (24, 64), - (24, 256), - (24, 512), - (24, 1024), - (24, 2048), - (26, 512), - (26, 1024), - (26, 2048), - (30, 512), - (30, 1024), - (30, 2048) -); - -pub fn make_concurrent_merkle_tree( - max_depth: u32, - max_buf_size: u32, -) -> Result, BatchMintValidationError> { - // Note: We do not create ConcurrentMerkleTree object right inside of match statement - // because of how Rust compiler reserves space for functions: - // the total size of function in memory (i.e. frame size) is as big as total size of - // all possible ConcurrentMerkleTree objects created in all branches. - // Because they are allocated on stack. - // And since these objects are quite big (up to 2MB), - // the total size of function becomes so big, it cannot fit in the thread stack. - // This all applies only for debug build, for release the compiler is smart enough - // to optimize this. - // Though, we need the debug to not fail with the stack overflow, - // that's why we had to move creation of an exact ConcurrentMerkleTree objects - // into separate function that return trait objects. - match (max_depth, max_buf_size) { - (3, 8) => Ok(make_concurrent_merkle_tree_3_8()), - (5, 8) => Ok(make_concurrent_merkle_tree_5_8()), - (6, 16) => Ok(make_concurrent_merkle_tree_6_16()), - (7, 16) => Ok(make_concurrent_merkle_tree_7_16()), - (8, 16) => Ok(make_concurrent_merkle_tree_8_16()), - (9, 16) => Ok(make_concurrent_merkle_tree_9_16()), - (10, 32) => Ok(make_concurrent_merkle_tree_10_32()), - (11, 32) => Ok(make_concurrent_merkle_tree_11_32()), - (12, 32) => Ok(make_concurrent_merkle_tree_12_32()), - (13, 32) => Ok(make_concurrent_merkle_tree_13_32()), - (14, 64) => Ok(make_concurrent_merkle_tree_14_64()), - (14, 256) => Ok(make_concurrent_merkle_tree_14_256()), - (14, 1024) => Ok(make_concurrent_merkle_tree_14_1024()), - (14, 2048) => Ok(make_concurrent_merkle_tree_14_2048()), - (15, 64) => Ok(make_concurrent_merkle_tree_15_64()), - (16, 64) => Ok(make_concurrent_merkle_tree_16_64()), - (17, 64) => Ok(make_concurrent_merkle_tree_17_64()), - (18, 64) => Ok(make_concurrent_merkle_tree_18_64()), - (19, 64) => Ok(make_concurrent_merkle_tree_19_64()), - (20, 64) => Ok(make_concurrent_merkle_tree_20_64()), - (20, 256) => Ok(make_concurrent_merkle_tree_20_256()), - (20, 1024) => Ok(make_concurrent_merkle_tree_20_1024()), - (20, 2048) => Ok(make_concurrent_merkle_tree_20_2048()), - (24, 64) => Ok(make_concurrent_merkle_tree_24_64()), - (24, 256) => Ok(make_concurrent_merkle_tree_24_256()), - (24, 512) => Ok(make_concurrent_merkle_tree_24_512()), - (24, 1024) => Ok(make_concurrent_merkle_tree_24_1024()), - (24, 2048) => Ok(make_concurrent_merkle_tree_24_2048()), - (26, 512) => Ok(make_concurrent_merkle_tree_26_512()), - (26, 1024) => Ok(make_concurrent_merkle_tree_26_1024()), - (26, 2048) => Ok(make_concurrent_merkle_tree_26_2048()), - (30, 512) => Ok(make_concurrent_merkle_tree_30_512()), - (30, 1024) => Ok(make_concurrent_merkle_tree_30_1024()), - (30, 2048) => Ok(make_concurrent_merkle_tree_30_2048()), - (d, s) => Err(BatchMintValidationError::UnexpectedTreeSize(d, s)), - } -} - -make_changelog_impls!(3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 24, 26, 30); - -pub fn validate_change_logs( - max_depth: u32, - max_buffer_size: u32, - leaves: &[[u8; 32]], - batch_mint: &BatchMint, -) -> Result<(), BatchMintValidationError> { - let mut tree = make_concurrent_merkle_tree(max_depth, max_buffer_size)?; - tree.initialize()?; - for (i, leaf_hash) in leaves.iter().enumerate() { - tree.append(*leaf_hash)?; - let changelog = tree.change_logs(tree.active_index() as usize); - let path_len = changelog.path_len() as u32; - let mut path: Vec = changelog - .path_iter() - .enumerate() - .map(|(lvl, n)| { - spl_account_compression::state::PathNode::new( - *n, - (1 << (path_len - lvl as u32)) + (changelog.index() >> lvl), - ) - }) - .collect(); - path.push(spl_account_compression::state::PathNode::new( - changelog.root(), - 1, - )); - - match batch_mint.batch_mints.get(i) { - Some(mint) => { - if mint.tree_update.path - != path - .into_iter() - .map(Into::::into) - .collect::>() - { - return Err(BatchMintValidationError::WrongAssetPath( - mint.leaf_update.id().to_string(), - )); - } - if mint.tree_update.id != batch_mint.tree_id { - return Err(BatchMintValidationError::WrongTreeIdForChangeLog( - mint.leaf_update.id().to_string(), - batch_mint.tree_id.to_string(), - mint.tree_update.id.to_string(), - )); - } - if mint.tree_update.index != changelog.index() { - return Err(BatchMintValidationError::WrongChangeLogIndex( - mint.leaf_update.id().to_string(), - changelog.index(), - mint.tree_update.index, - )); - } - } - None => return Err(BatchMintValidationError::NoRelevantRolledMint(i as u64)), - } - } - if tree.get_root() != batch_mint.merkle_root { - return Err(BatchMintValidationError::InvalidRoot( - Hash::new(tree.get_root().as_slice()).to_string(), - Hash::new(batch_mint.merkle_root.as_slice()).to_string(), - )); - } - Ok(()) -} diff --git a/program_transformers/src/batch_minting/mod.rs b/program_transformers/src/batch_minting/mod.rs index 95aac533..16302928 100644 --- a/program_transformers/src/batch_minting/mod.rs +++ b/program_transformers/src/batch_minting/mod.rs @@ -1,3 +1 @@ pub mod batch_mint_persister; -mod merkle_tree_wrapper; -pub mod tests; diff --git a/program_transformers/src/batch_minting/tests.rs b/program_transformers/src/batch_minting/tests.rs deleted file mode 100644 index 6fe0dcab..00000000 --- a/program_transformers/src/batch_minting/tests.rs +++ /dev/null @@ -1,311 +0,0 @@ -use crate::batch_minting::batch_mint_persister::{ - validate_batch_mint, BatchMint, BatchedMintInstruction, ChangeLogEventV1, PathNode, -}; -use crate::error::BatchMintValidationError; -use anchor_lang::AnchorSerialize; -use mpl_bubblegum::types::{LeafSchema, MetadataArgs}; -use rand::{thread_rng, Rng}; -use solana_sdk::keccak; -use solana_sdk::pubkey::Pubkey; -use spl_concurrent_merkle_tree::concurrent_merkle_tree::ConcurrentMerkleTree; -use std::collections::HashMap; -use std::str::FromStr; - -pub fn generate_batch_mint(size: usize, creators_verified: bool) -> BatchMint { - let authority = Pubkey::from_str("3VvLDXqJbw3heyRwFxv8MmurPznmDVUJS9gPMX2BDqfM").unwrap(); - let tree = Pubkey::from_str("HxhCw9g3kZvrdg9zZvctmh6qpSDg1FfsBXfFvRkbCHB7").unwrap(); - let mut mints = Vec::new(); - let mut merkle = ConcurrentMerkleTree::<10, 32>::new(); - merkle.initialize().unwrap(); - - let mut last_leaf_hash = [0u8; 32]; - for i in 0..size { - let mint_args = MetadataArgs { - name: thread_rng() - .sample_iter(rand::distributions::Alphanumeric) - .take(15) - .map(char::from) - .collect(), - symbol: thread_rng() - .sample_iter(rand::distributions::Alphanumeric) - .take(5) - .map(char::from) - .collect(), - uri: format!( - "https://arweave.net/{}", - thread_rng() - .sample_iter(rand::distributions::Alphanumeric) - .take(43) - .map(char::from) - .collect::() - ), - seller_fee_basis_points: thread_rng() - .sample(rand::distributions::Uniform::new(0, 10000)), - primary_sale_happened: thread_rng().gen_bool(0.5), - is_mutable: thread_rng().gen_bool(0.5), - edition_nonce: if thread_rng().gen_bool(0.5) { - None - } else { - Some(thread_rng().sample(rand::distributions::Uniform::new(0, 255))) - }, - token_standard: if thread_rng().gen_bool(0.5) { - None - } else { - Some(mpl_bubblegum::types::TokenStandard::NonFungible) - }, - collection: if thread_rng().gen_bool(0.5) { - None - } else { - Some(mpl_bubblegum::types::Collection { - verified: false, - key: Pubkey::new_unique(), - }) - }, - uses: None, // todo - token_program_version: mpl_bubblegum::types::TokenProgramVersion::Original, - creators: (0..thread_rng().sample(rand::distributions::Uniform::new(1, 5))) - .map(|_| mpl_bubblegum::types::Creator { - address: Pubkey::new_unique(), - verified: creators_verified, - share: thread_rng().sample(rand::distributions::Uniform::new(0, 100)), - }) - .collect(), - }; - let nonce = i as u64; - let id = mpl_bubblegum::utils::get_asset_id(&tree, nonce); - let owner = authority.clone(); - let delegate = authority.clone(); - - let metadata_args_hash = keccak::hashv(&[mint_args.try_to_vec().unwrap().as_slice()]); - let data_hash = keccak::hashv(&[ - &metadata_args_hash.to_bytes(), - &mint_args.seller_fee_basis_points.to_le_bytes(), - ]); - let creator_data = mint_args - .creators - .iter() - .map(|c| [c.address.as_ref(), &[c.verified as u8], &[c.share]].concat()) - .collect::>(); - let creator_hash = keccak::hashv( - creator_data - .iter() - .map(|c| c.as_slice()) - .collect::>() - .as_ref(), - ); - - let hashed_leaf = keccak::hashv(&[ - &[1], //self.version().to_bytes() - id.as_ref(), - owner.as_ref(), - delegate.as_ref(), - nonce.to_le_bytes().as_ref(), - data_hash.as_ref(), - creator_hash.as_ref(), - ]) - .to_bytes(); - merkle.append(hashed_leaf).unwrap(); - last_leaf_hash = hashed_leaf; - let changelog = merkle.change_logs[merkle.active_index as usize]; - let path_len = changelog.path.len() as u32; - let mut path: Vec = changelog - .path - .iter() - .enumerate() - .map(|(lvl, n)| { - spl_account_compression::state::PathNode::new( - *n, - (1 << (path_len - lvl as u32)) + (changelog.index >> lvl), - ) - }) - .collect(); - path.push(spl_account_compression::state::PathNode::new( - changelog.root, - 1, - )); - - let rolled_mint = BatchedMintInstruction { - tree_update: ChangeLogEventV1 { - id: tree, - path: path.into_iter().map(Into::into).collect::>(), - seq: merkle.sequence_number, - index: changelog.index, - }, - leaf_update: LeafSchema::V1 { - id, - owner, - delegate, - nonce, - data_hash: data_hash.to_bytes(), - creator_hash: creator_hash.to_bytes(), - }, - mint_args, - authority, - creator_signature: None, - }; - mints.push(rolled_mint); - } - let batch_mint = BatchMint { - tree_id: tree, - raw_metadata_map: HashMap::new(), - max_depth: 10, - batch_mints: mints, - merkle_root: merkle.get_root(), - last_leaf_hash, - max_buffer_size: 32, - }; - - batch_mint -} - -#[tokio::test] -async fn batch_mint_validation_test() { - let mut batch_mint = generate_batch_mint(1000, false); - - let validation_result = validate_batch_mint(&batch_mint, None).await; - assert_eq!(validation_result, Ok(())); - - let old_root = batch_mint.merkle_root; - let new_root = Pubkey::new_unique(); - batch_mint.merkle_root = new_root.to_bytes(); - - let validation_result = validate_batch_mint(&batch_mint, None).await; - assert_eq!( - validation_result, - Err(BatchMintValidationError::InvalidRoot( - Pubkey::from(old_root).to_string(), - new_root.to_string() - )) - ); - - batch_mint.merkle_root = old_root; - let leaf_idx = 111; - let old_leaf_data_hash = batch_mint.batch_mints[leaf_idx].leaf_update.data_hash(); - let new_leaf_data_hash = Pubkey::new_unique(); - batch_mint.batch_mints[leaf_idx].leaf_update = LeafSchema::V1 { - id: batch_mint.batch_mints[leaf_idx].leaf_update.id(), - owner: batch_mint.batch_mints[leaf_idx].leaf_update.owner(), - delegate: batch_mint.batch_mints[leaf_idx].leaf_update.delegate(), - nonce: batch_mint.batch_mints[leaf_idx].leaf_update.nonce(), - data_hash: new_leaf_data_hash.to_bytes(), - creator_hash: batch_mint.batch_mints[leaf_idx].leaf_update.creator_hash(), - }; - let validation_result = validate_batch_mint(&batch_mint, None).await; - - assert_eq!( - validation_result, - Err(BatchMintValidationError::InvalidDataHash( - Pubkey::from(old_leaf_data_hash).to_string(), - new_leaf_data_hash.to_string() - )) - ); - - batch_mint.batch_mints[leaf_idx].leaf_update = LeafSchema::V1 { - id: batch_mint.batch_mints[leaf_idx].leaf_update.id(), - owner: batch_mint.batch_mints[leaf_idx].leaf_update.owner(), - delegate: batch_mint.batch_mints[leaf_idx].leaf_update.delegate(), - nonce: batch_mint.batch_mints[leaf_idx].leaf_update.nonce(), - data_hash: old_leaf_data_hash, - creator_hash: batch_mint.batch_mints[leaf_idx].leaf_update.creator_hash(), - }; - let old_tree_depth = batch_mint.max_depth; - let new_tree_depth = 100; - batch_mint.max_depth = new_tree_depth; - let validation_result = validate_batch_mint(&batch_mint, None).await; - - assert_eq!( - validation_result, - Err(BatchMintValidationError::UnexpectedTreeSize( - new_tree_depth, - batch_mint.max_buffer_size - )) - ); - - batch_mint.max_depth = old_tree_depth; - let new_asset_id = Pubkey::new_unique(); - let old_asset_id = batch_mint.batch_mints[leaf_idx].leaf_update.id(); - batch_mint.batch_mints[leaf_idx].leaf_update = LeafSchema::V1 { - id: new_asset_id, - owner: batch_mint.batch_mints[leaf_idx].leaf_update.owner(), - delegate: batch_mint.batch_mints[leaf_idx].leaf_update.delegate(), - nonce: batch_mint.batch_mints[leaf_idx].leaf_update.nonce(), - data_hash: batch_mint.batch_mints[leaf_idx].leaf_update.data_hash(), - creator_hash: batch_mint.batch_mints[leaf_idx].leaf_update.creator_hash(), - }; - let validation_result = validate_batch_mint(&batch_mint, None).await; - - assert_eq!( - validation_result, - Err(BatchMintValidationError::PDACheckFail( - old_asset_id.to_string(), - new_asset_id.to_string() - )) - ); - - batch_mint.batch_mints[leaf_idx].leaf_update = LeafSchema::V1 { - id: old_asset_id, - owner: batch_mint.batch_mints[leaf_idx].leaf_update.owner(), - delegate: batch_mint.batch_mints[leaf_idx].leaf_update.delegate(), - nonce: batch_mint.batch_mints[leaf_idx].leaf_update.nonce(), - data_hash: batch_mint.batch_mints[leaf_idx].leaf_update.data_hash(), - creator_hash: batch_mint.batch_mints[leaf_idx].leaf_update.creator_hash(), - }; - let old_path = batch_mint.batch_mints[leaf_idx] - .tree_update - .path - .iter() - .map(|path| PathNode { - node: path.node, - index: path.index, - }) - .collect::>(); - let new_path = Vec::new(); - batch_mint.batch_mints[leaf_idx].tree_update.path = new_path; - let validation_result = validate_batch_mint(&batch_mint, None).await; - - assert_eq!( - validation_result, - Err(BatchMintValidationError::WrongAssetPath( - batch_mint.batch_mints[leaf_idx] - .leaf_update - .id() - .to_string() - )) - ); - - batch_mint.batch_mints[leaf_idx].tree_update.path = old_path; - let old_tree_id = batch_mint.batch_mints[leaf_idx].tree_update.id; - let new_tree_id = Pubkey::new_unique(); - batch_mint.batch_mints[leaf_idx].tree_update.id = new_tree_id; - let validation_result = validate_batch_mint(&batch_mint, None).await; - - assert_eq!( - validation_result, - Err(BatchMintValidationError::WrongTreeIdForChangeLog( - batch_mint.batch_mints[leaf_idx] - .leaf_update - .id() - .to_string(), - old_tree_id.to_string(), - new_tree_id.to_string() - )) - ); - - batch_mint.batch_mints[leaf_idx].tree_update.id = old_tree_id; - let old_index = batch_mint.batch_mints[leaf_idx].tree_update.index; - let new_index = 1; - batch_mint.batch_mints[leaf_idx].tree_update.index = new_index; - let validation_result = validate_batch_mint(&batch_mint, None).await; - - assert_eq!( - validation_result, - Err(BatchMintValidationError::WrongChangeLogIndex( - batch_mint.batch_mints[leaf_idx] - .leaf_update - .id() - .to_string(), - old_index, - new_index - )) - ); -}