From 0a06487694e2990f6d70385eaffd52eee249b1ef Mon Sep 17 00:00:00 2001 From: requesco Date: Wed, 12 Jun 2024 10:16:22 +0300 Subject: [PATCH] rollup validation test --- Cargo.lock | 1 + program_transformers/Cargo.toml | 1 + program_transformers/src/error.rs | 2 - program_transformers/src/rollups/mod.rs | 1 + .../src/rollups/rollup_persister.rs | 2 +- program_transformers/src/rollups/tests.rs | 301 ++++++++++++++++++ 6 files changed, 305 insertions(+), 3 deletions(-) create mode 100644 program_transformers/src/rollups/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 25d44176f..e775d54e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4583,6 +4583,7 @@ dependencies = [ "mpl-bubblegum", "num-traits", "paste", + "rand 0.8.5", "reqwest", "sea-orm", "serde", diff --git a/program_transformers/Cargo.toml b/program_transformers/Cargo.toml index b2472a8c5..664e23f9d 100644 --- a/program_transformers/Cargo.toml +++ b/program_transformers/Cargo.toml @@ -38,6 +38,7 @@ xxhash-rust = { workspace = true } serde = { workspace = true } serde_with = { workspace = true } bincode = { workspace = true } +rand = { workspace = true } [lints] workspace = true diff --git a/program_transformers/src/error.rs b/program_transformers/src/error.rs index 48fa48757..4abf80042 100644 --- a/program_transformers/src/error.rs +++ b/program_transformers/src/error.rs @@ -48,8 +48,6 @@ pub enum RollupValidationError { InvalidCreatorsHash(String, String), #[error("InvalidRoot: expected: {0}, got: {1}")] InvalidRoot(String, String), - #[error("CannotCreateMerkleTree: depth [{0}], size [{1}]")] - CannotCreateMerkleTree(u32, u32), #[error("NoRelevantRolledMint: index {0}")] NoRelevantRolledMint(u64), #[error("WrongAssetPath: id {0}")] diff --git a/program_transformers/src/rollups/mod.rs b/program_transformers/src/rollups/mod.rs index 2f9a50430..54e92f651 100644 --- a/program_transformers/src/rollups/mod.rs +++ b/program_transformers/src/rollups/mod.rs @@ -1,2 +1,3 @@ mod merkle_tree_wrapper; pub mod rollup_persister; +mod tests; diff --git a/program_transformers/src/rollups/rollup_persister.rs b/program_transformers/src/rollups/rollup_persister.rs index cf7e120c2..4c70870e6 100644 --- a/program_transformers/src/rollups/rollup_persister.rs +++ b/program_transformers/src/rollups/rollup_persister.rs @@ -461,7 +461,7 @@ impl RollupPersister } } -async fn validate_rollup(rollup: &Rollup) -> Result<(), RollupValidationError> { +pub async fn validate_rollup(rollup: &Rollup) -> Result<(), RollupValidationError> { let mut leaf_hashes = Vec::new(); for asset in rollup.rolled_mints.iter() { let leaf_hash = match get_leaf_hash(asset, &rollup.tree_id) { diff --git a/program_transformers/src/rollups/tests.rs b/program_transformers/src/rollups/tests.rs new file mode 100644 index 000000000..2f0a9c0b5 --- /dev/null +++ b/program_transformers/src/rollups/tests.rs @@ -0,0 +1,301 @@ +use crate::error::RollupValidationError; +use crate::rollups::rollup_persister::{ + validate_rollup, ChangeLogEventV1, PathNode, RolledMintInstruction, Rollup, +}; +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; + +fn generate_rollup(size: usize) -> Rollup { + 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: false, + 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 = RolledMintInstruction { + 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, + }; + mints.push(rolled_mint); + } + let rollup = Rollup { + tree_id: tree, + raw_metadata_map: HashMap::new(), + max_depth: 10, + rolled_mints: mints, + merkle_root: merkle.get_root(), + last_leaf_hash, + max_buffer_size: 32, + }; + + rollup +} + +#[tokio::test] +async fn rollup_validation_test() { + let mut rollup = generate_rollup(1000); + + let validation_result = validate_rollup(&rollup).await; + assert_eq!(validation_result, Ok(())); + + let old_root = rollup.merkle_root; + let new_root = Pubkey::new_unique(); + rollup.merkle_root = new_root.to_bytes(); + + let validation_result = validate_rollup(&rollup).await; + assert_eq!( + validation_result, + Err(RollupValidationError::InvalidRoot( + Pubkey::from(old_root).to_string(), + new_root.to_string() + )) + ); + + rollup.merkle_root = old_root; + let leaf_idx = 111; + let old_leaf_data_hash = rollup.rolled_mints[leaf_idx].leaf_update.data_hash(); + let new_leaf_data_hash = Pubkey::new_unique(); + rollup.rolled_mints[leaf_idx].leaf_update = LeafSchema::V1 { + id: rollup.rolled_mints[leaf_idx].leaf_update.id(), + owner: rollup.rolled_mints[leaf_idx].leaf_update.owner(), + delegate: rollup.rolled_mints[leaf_idx].leaf_update.delegate(), + nonce: rollup.rolled_mints[leaf_idx].leaf_update.nonce(), + data_hash: new_leaf_data_hash.to_bytes(), + creator_hash: rollup.rolled_mints[leaf_idx].leaf_update.creator_hash(), + }; + let validation_result = validate_rollup(&rollup).await; + + assert_eq!( + validation_result, + Err(RollupValidationError::InvalidDataHash( + Pubkey::from(old_leaf_data_hash).to_string(), + new_leaf_data_hash.to_string() + )) + ); + + rollup.rolled_mints[leaf_idx].leaf_update = LeafSchema::V1 { + id: rollup.rolled_mints[leaf_idx].leaf_update.id(), + owner: rollup.rolled_mints[leaf_idx].leaf_update.owner(), + delegate: rollup.rolled_mints[leaf_idx].leaf_update.delegate(), + nonce: rollup.rolled_mints[leaf_idx].leaf_update.nonce(), + data_hash: old_leaf_data_hash, + creator_hash: rollup.rolled_mints[leaf_idx].leaf_update.creator_hash(), + }; + let old_tree_depth = rollup.max_depth; + let new_tree_depth = 100; + rollup.max_depth = new_tree_depth; + let validation_result = validate_rollup(&rollup).await; + + assert_eq!( + validation_result, + Err(RollupValidationError::UnexpectedTreeSize( + new_tree_depth, + rollup.max_buffer_size + )) + ); + + rollup.max_depth = old_tree_depth; + let new_asset_id = Pubkey::new_unique(); + let old_asset_id = rollup.rolled_mints[leaf_idx].leaf_update.id(); + rollup.rolled_mints[leaf_idx].leaf_update = LeafSchema::V1 { + id: new_asset_id, + owner: rollup.rolled_mints[leaf_idx].leaf_update.owner(), + delegate: rollup.rolled_mints[leaf_idx].leaf_update.delegate(), + nonce: rollup.rolled_mints[leaf_idx].leaf_update.nonce(), + data_hash: rollup.rolled_mints[leaf_idx].leaf_update.data_hash(), + creator_hash: rollup.rolled_mints[leaf_idx].leaf_update.creator_hash(), + }; + let validation_result = validate_rollup(&rollup).await; + + assert_eq!( + validation_result, + Err(RollupValidationError::PDACheckFail( + old_asset_id.to_string(), + new_asset_id.to_string() + )) + ); + + rollup.rolled_mints[leaf_idx].leaf_update = LeafSchema::V1 { + id: old_asset_id, + owner: rollup.rolled_mints[leaf_idx].leaf_update.owner(), + delegate: rollup.rolled_mints[leaf_idx].leaf_update.delegate(), + nonce: rollup.rolled_mints[leaf_idx].leaf_update.nonce(), + data_hash: rollup.rolled_mints[leaf_idx].leaf_update.data_hash(), + creator_hash: rollup.rolled_mints[leaf_idx].leaf_update.creator_hash(), + }; + let old_path = rollup.rolled_mints[leaf_idx] + .tree_update + .path + .iter() + .map(|path| PathNode { + node: path.node, + index: path.index, + }) + .collect::>(); + let new_path = Vec::new(); + rollup.rolled_mints[leaf_idx].tree_update.path = new_path; + let validation_result = validate_rollup(&rollup).await; + + assert_eq!( + validation_result, + Err(RollupValidationError::WrongAssetPath( + rollup.rolled_mints[leaf_idx].leaf_update.id().to_string() + )) + ); + + rollup.rolled_mints[leaf_idx].tree_update.path = old_path; + let old_tree_id = rollup.rolled_mints[leaf_idx].tree_update.id; + let new_tree_id = Pubkey::new_unique(); + rollup.rolled_mints[leaf_idx].tree_update.id = new_tree_id; + let validation_result = validate_rollup(&rollup).await; + + assert_eq!( + validation_result, + Err(RollupValidationError::WrongTreeIdForChangeLog( + rollup.rolled_mints[leaf_idx].leaf_update.id().to_string(), + old_tree_id.to_string(), + new_tree_id.to_string() + )) + ); + + rollup.rolled_mints[leaf_idx].tree_update.id = old_tree_id; + let old_index = rollup.rolled_mints[leaf_idx].tree_update.index; + let new_index = 1; + rollup.rolled_mints[leaf_idx].tree_update.index = new_index; + let validation_result = validate_rollup(&rollup).await; + + assert_eq!( + validation_result, + Err(RollupValidationError::WrongChangeLogIndex( + rollup.rolled_mints[leaf_idx].leaf_update.id().to_string(), + old_index, + new_index + )) + ); +}