Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batch minting #113

Open
wants to merge 91 commits into
base: main
Choose a base branch
from
Open

Batch minting #113

wants to merge 91 commits into from

Conversation

StanChe
Copy link

@StanChe StanChe commented Sep 20, 2024

A finalized version of the Bubblegum update for batch minting

Batch operations

As was observed, minting of assets constitutes over 90% of all operations pertaining to digital assets. In order to reduce the number of transactions and optimize the time and cost it takes to put your tree on-chain during the Solana heavy load events Metaplex has introduced the batch-mint operations. The batch extension to the Bubblegum program introduces offline Merkle tree creation, enabling users to prepare and manage Merkle trees offline before finalizing them on-chain. The resulting trees are fully compatible with the regular trees. The snapshot of the tree created is required to be stored off-chain so the replay of the tree creation is possible on any indexer.

Batch-Mint Operations

Introduction

The latest extension to the Bubblegum contract introduces batch-mint operations, allowing users to mint multiple cNFTs in just several transactions, which significantly reduces on-chain overhead and optimizes minting processes for large collections.

How It Works

With the batch-mint operations, users can prepare an entire set of NFTs off-chain, populate them within a Merkle tree structure, and then mint them to the tree in a small number of transactions. This process is designed to handle large-scale NFT collections more efficiently.

Steps to Perform a Batch-Mint

In order to simplify the Merkle tree creation and interactions we recommend using the SDK.

To understand the batch-mint flow, let's recall the structure of a tree data account:

+--------+----------------------+-----------------+
| Header |     Tree body        |     Canopy      |
+--------+----------------------+-----------------+
   56      depends on the tree    (2ⁿ⁺¹ - 2) * 32
  bytes    depth and buffer size       bytes

where n is the depth of the canopy.

  1. Prepare Tree
  • Invoke the prepare_tree method to initialize an account with a tree header and an empty tree body and empty canopy buffer.
  • The tree is set up with initial parameters (tree size and canopy if required) but remains empty, allowing offline filling.
  1. Fill Tree Offline
  • User populates the tree offline, ensuring all necessary leaves are in place.
  • If the canopy is required, then canopy leaf nodes are populated in the process of adding asset leaves to tree.
  • The final root, proof, and last leaf are prepared for submission.
  1. Serialize and Upload the Tree
  • The offline tree is serialized and uploaded to an IPFS-like service, such as Arweave, ensuring public access.
  1. Adding canopy (for a tree with a canopy)
  • To transfer canopy leaf nodes from offline tree to the solana tree data account the add_canopy method is invoked, tree body at this stage stays empty
  1. Finalize Tree
  • To finalize the tree the methods finalize_tree_with_root for a tree without verified collections or finalize_tree_with_root_and_collection for a tree with one verified collection are used. Signatures from both the tree owner and a designated staker are required.
  • The staker is responsible for ensuring the data's availability and consistency, verifying it before signing off.
  1. Manage the Tree
    • Once the batch minting process is complete, you can manage the tree and its NFTs using all the standard Bubblegum operations.
    • You can also mint additional assets into a batch-minted tree as if it's a regular tree.

📄 prepare_tree

Prepare a tree structure that will be used to hold multiple NFTs in a batch-mint operation. This step initializes the tree and allocates the necessary resources for subsequent operations.

Accounts
Name Writable Signer Optional Description
tree_authority The TreeConfig PDA account that is initialized by this instruction.
merkle_tree Unchecked account representing the Merkle tree, must be zero-initialized.
payer The account responsible for paying the transaction fees.
tree_creator The creator of the tree, who must sign the transaction.
log_wrapper The Solana Program Library Wrapper (spl-noop) program ID for logging.
compression_program The Solana Program Library spl-account-compression program ID.
system_program The Solana System Program ID.
Arguments
Argument Offset Size Description
max_depth 0 4 The maximum depth of the Merkle tree.
max_buffer_size 4 4 The maximum buffer size for the Merkle tree.
public 8 1 An optional boolean indicating if the tree is public.

📄 add_canopy

Add an optional canopy to the tree structure. A canopy is used to optimize the verification process for the tree, making it easier to validate NFT ownership.

Accounts
Name Writable Signer Optional Description
tree_authority The TreeConfig PDA account previously initialized by prepare_tree.
merkle_tree The account representing the Merkle tree to which the canopy is being added.
tree_delegate The delegate authorized to modify the tree.
log_wrapper The Solana Program Library Wrapper (spl-noop) program ID for logging.
compression_program The Solana Program Library spl-account-compression program ID.
system_program The Solana System Program ID.
Arguments
Argument Offset Size Description
start_index 0 4 The starting index for the canopy nodes being added.
canopy_nodes 4 ??? A vector of canopy nodes (32-byte arrays) to append to the Merkle tree.

📄 finalize_tree_with_root

Finalize the tree structure by setting the Merkle root, which represents the entire batch of NFTs. This operation completes the preparation phase and makes the tree ready for usage.

Accounts
Name Writable Signer Optional Description
tree_authority The TreeConfig PDA account previously initialized by prepare_tree.
merkle_tree The account containing the Merkle tree structure.
payer The account responsible for paying the transaction fees.
tree_delegate The delegate of the tree, responsible for finalizing it.
staker The account of the staker, required to have the minimal required stake to allow batch-minting.
registrar The account representing the registrar for managing stake accounts.
voter The account representing the voting account.
mining The account representing the mining account on rewards contract.
fee_receiver The account designated to receive protocol fees.
log_wrapper The Solana Program Library Wrapper (spl-noop) program ID for logging.
compression_program The Solana Program Library spl-account-compression program ID.
system_program The Solana System Program ID.
remaining accounts Pubkeys(s) that are 32-byte Keccak256 hash values that represent the nodes for this cNFT's Merkle proof.
Arguments
Argument Offset Size Description
root 0 32 The Merkle root hash for the tree.
rightmost_leaf 32 32 The hash of the rightmost leaf node in the tree.
rightmost_index 64 4 The index of the rightmost leaf node in the tree.
_metadata_url 68 ??? A string - URL for the uploaded batch-mint json, required by indexers to fetch the tree for initialization.
_metadata_hash ??? ??? A string representing a hex-encoded xxh3_128 hash of the uploaded batch-mint json-file.

📄 finalize_tree_with_root_and_collection

Finalize the tree structure by setting the Merkle root and associating it with a specific NFT collection. This operation allows having a verified collection for NFTs in the batch.

Accounts
Name Writable Signer Optional Description
tree_authority The TreeConfig PDA account previously initialized by prepare_tree.
merkle_tree The account containing the Merkle tree structure.
payer The account responsible for paying the transaction fees.
tree_delegate The delegate of the tree, responsible for finalizing it.
staker The account of the staker, required to have the minimal required stake to allow batch-minting.
collection_authority Either the collection update authority or a delegate.
registrar The account representing the registrar for managing stake accounts.
voter The account representing the voting account.
mining The account representing the mining authority.
fee_receiver The account designated to receive protocol fees.
collection_authority_record_pda Either a metadata delegate record PDA for a collection delegate, or a legacy collection authority record PDA.
collection_mint The account representing the collection mint.
collection_metadata Metadata account of the collection. Modified in the case of a sized collection.
edition_account The account representing the Master Edition account of the collection.
log_wrapper The Solana Program Library Wrapper (spl-noop) program ID for logging.
compression_program The Solana Program Library spl-account-compression program ID.
system_program The Solana System Program ID.
remaining accounts Pubkeys(s) that are 32-byte Keccak256 hash values that represent the nodes for this cNFT's Merkle proof.
Arguments
Argument Offset Size Description
root 0 32 The Merkle root hash for the tree.
rightmost_leaf 32 32 The hash of the rightmost leaf node in the tree.
rightmost_index 64 4 The index of the rightmost leaf node in the tree.
_metadata_url 68 ??? A string - URL for the uploaded batch-mint json, required by indexers to fetch the tree for initialization.
_metadata_hash ??? ??? A string representing a hex-encoded xxh3_128 hash of the uploaded batch-mint json-file.

n00m4d and others added 30 commits May 16, 2024 17:29
# Conflicts:
#	programs/bubblegum/program/src/processor/create_tree_with_root.rs
# Conflicts:
#	programs/bubblegum/program/src/processor/create_tree_with_root.rs
#	programs/bubblegum/program/tests/rollup.rs
#	programs/bubblegum/program/tests/utils/tree.rs
# Conflicts:
#	clients/js/src/generated/instructions/prepareTree.ts
#	clients/rust/src/generated/instructions/finalize_tree_with_root.rs
#	idls/bubblegum.json
#	programs/bubblegum/program/src/processor/finalize_tree_with_root.rs
#	programs/bubblegum/program/tests/rollup.rs
#	programs/bubblegum/program/tests/utils/tree.rs
# Conflicts:
#	programs/bubblegum/program/tests/rollup.rs
The reason for adding prep_tree are still unknown, but keeping it for now. As the next steps I'd suggest to get rid of it to keep the styling consistent across bubblegum methods
next steps:
- test are really shitty, we need to create test helpers to build this
- when the SDK is ready - use it in tests
RequescoS and others added 8 commits August 29, 2024 14:36
…cleanup

# Conflicts:
#	clients/rust/Cargo.lock
#	clients/rust/Cargo.toml
#	programs/bubblegum/Cargo.lock
#	programs/bubblegum/program/Cargo.toml
#	programs/bubblegum/program/src/processor/create_tree.rs
Copy link

vercel bot commented Sep 20, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
mpl-bubblegum-js-docs ✅ Ready (Inspect) Visit Preview 💬 Add feedback Sep 30, 2024 11:28am

Copy link
Contributor

@danenbm danenbm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice clean code change, looks good. Left a few comments. Still need to read through the tests though, will review those tomorrow morning.

codeToErrorMap.set(0x179c, StakingVoterMismatchError);
nameToErrorMap.set('StakingVoterMismatch', StakingVoterMismatchError);

/** FeeReceiverMismatch: Fee receiver mismatch */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: It would be nice if some of these new Bubblegum errors were a bit more descriptive. For example, if this one said something more like who the fee receiver is mismatched with.

Like some of the spl-account compression ones have a bit of wording that really helps with the clarity, i.e.:
BatchNotInitializedError: Tree header was not initialized for batch processing
CanopyRootMismatchError: Canopy root does not match the root of the tree

@@ -12,6 +12,21 @@ pub const VOUCHER_PREFIX: &str = "voucher";
pub const ASSET_PREFIX: &str = "asset";
pub const COLLECTION_CPI_PREFIX: &str = "collection_cpi";

pub const MAX_ACC_PROOFS_SIZE: u32 = 17;

// TODO: set real keys before mainnet deploy
Copy link
Contributor

@danenbm danenbm Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I see this looks like it is fixed in #114

version = "0.12.0"
version = "0.13.0"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we actually normally don't version the program package now given that we don't publish it ever, so you could leave this at 12 unless there's another reason.

)]
pub tree_authority: Account<'info, TreeConfig>,
#[account(mut)]
/// CHECK:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: no comment detail

Comment on lines +30 to +35
// incoming_tree_delegate is the tree owner as set in prepare tree, it's required to do any modificaitons to the tree,
// including the canopy setup
require!(
incoming_tree_delegate == authority.tree_delegate,
BubblegumError::TreeAuthorityIncorrect,
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why doesn't this allow authority.tree_creator in addition to authority.tree_delegate?

Comment on lines +9 to +22
export * from './addCanopy';
export * from './appendCanopyNodes';
export * from './burn';
export * from './cancelRedeem';
export * from './createTreeConfig';
export * from './decompressV1';
export * from './delegate';
export * from './finalizeTreeWithRoot';
export * from './finalizeTreeWithRootAndCollection';
export * from './initPreparedTreeWithRoot';
export * from './mintToCollectionV1';
export * from './mintV1';
export * from './prepareBatchMerkleTree';
export * from './prepareTree';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these spl-account-compression instructions such as appendCanopyNodes do not need to be included in our JS and Rust clients. They are added by default and can be removed in kinobi. See kinobi.cjs where other spl-account-compression instructions such as append and closeEmptyTree are marked for deletion.

version = "1.4.0"
version = "1.5.0"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: you don't need to update this version as cargo release does this as part of our client publish CI.

Comment on lines -25 to +26
solana-program = "^1.14"
thiserror = "^1.0"
solana-program = "~1.18.11"
thiserror = "1.0.63"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Rust client is designed to not require regular Solana program dependency updates. Can it be left at ^1.14 in the client?

RPC="https://api.mainnet-beta.solana.com"
RPC="https://api.devnet.solana.com"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm I think in general we want to be testing with mainnet-beta programs. But I think spl-account-compression you must use the devnet version for this. We did this in mpl-core with the oracle-test-program on devnet, I think we just cloned the script. Can you do that here as well?

@@ -276,6 +273,26 @@ async fn test_cannot_create_tree_needing_too_many_proofs_with_too_small_canopy()
async fn test_cannot_create_tree_needing_too_many_proofs_with_no_canopy() {
let context = BubblegumTestContext::new().await.unwrap();

let tree_create_result = context.create_tree_with_canopy::<19, 64>(1, true).await;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be called with 0 correct?

Copy link
Contributor

@danenbm danenbm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple last comments. There's also some CI failures that need to be fixed. Overall code change looks great. As mentioned before, this is a clean code change. Good documentation and good testing as well.

@@ -72,6 +77,52 @@ Compressed NFTs support transferring ownership, delegating authority, and burnin

Redeeming a cNFT removes the leaf from the Merkle tree and creates a voucher PDA account. The voucher account can be sent to the `decompress_v1` instruction to decompress the cNFT into a Token Metadata NFT. As mentioned above this will cost rent for the Metadata and Master Edition `token-metadata` accounts that are created during the decompression process. Note that after a cNFT is redeemed but before it is decompressed, the process can be reversed using `cancel_redeem`. This puts the cNFT back into the Merkle tree.

## Batch-Mint Operations
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great README additions!

Comment on lines +254 to +262
// TODO: good luck trying to make it work with those allignment requirements of the WrappedMining struct,
// let account_type:u8 = mplx_rewards::state::AccountType::Mining.into();
// mining_acc_data[0] = account_type;
// let mining_acc = mplx_rewards::state::WrappedMining::from_bytes_mut(&mut mining_acc_data)
// .expect("Failed to create mining account");
// mining_acc.mining.owner = voter_authority;
// mining_acc.mining.stake_from_others = 0;
// so here is a hacky way to set the owner of the mining account directly
mining_acc_data[32..64].copy_from_slice(&voter_authority.to_bytes());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think this should be resolved or cleaned up with however we want to solve it for the medium/long term, before going into main.

Comment on lines +32 to +33
mplx-staking-states = { git = "https://github.com/metaplex-foundation/aura-staking.git" }
mplx-rewards = { git = "https://github.com/metaplex-foundation/aura-rewards.git", features = ["no-entrypoint"] }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these should be published now right?

* feat: use constants from external crate

* feat: change after change in imported crate

* chore: drop useless error and use into() for type convertion

* chore: move keys cast out of runtime

* chore: drop unused import
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants