Skip to content

Commit

Permalink
Check compression programs at runtime (#115)
Browse files Browse the repository at this point in the history
* Check compression programs at runtime

* Update Cargo.lock

* Regenerate IDL and SDKs

* Add test for new programs and update program IDs

* Use MPL-specific types instead of assuming spl-account-compression types are compatible

* Use mpl-account-compression published versions

Update spl-account-compression version

Use Node type from spl-concurrent-merkle-tree crate

* Add idl-build feature

* Remove Anchor idl-build feature and remove account compression dependencies that use it

* Temporarily skip different compression program test

* fix ts lint issue

* Download mpl-account-compression programs and reactivate test

* Add tests

* Remove log wrapper validation in decompress as it's unused

* Update to latest spl-account-compression

* Update program download script
  • Loading branch information
danenbm authored Oct 29, 2024
1 parent df5b5fe commit 14329d8
Show file tree
Hide file tree
Showing 30 changed files with 941 additions and 258 deletions.
17 changes: 13 additions & 4 deletions clients/js/src/createTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ export const createTree = async (
getMerkleTreeSize(input.maxDepth, input.maxBufferSize, input.canopyDepth);
const lamports = await context.rpc.getRent(space);

let programId;
if (input.compressionProgram) {
programId = Array.isArray(input.compressionProgram)
? input.compressionProgram[0]
: input.compressionProgram;
} else {
programId = context.programs.getPublicKey(
'splAccountCompression',
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID
);
}

return (
transactionBuilder()
// Create the empty Merkle tree account.
Expand All @@ -36,10 +48,7 @@ export const createTree = async (
newAccount: input.merkleTree,
lamports,
space,
programId: context.programs.getPublicKey(
'splAccountCompression',
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID
),
programId,
})
)
// Create the tree config.
Expand Down
26 changes: 26 additions & 0 deletions clients/js/src/generated/errors/mplBubblegum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,32 @@ export class InvalidCanopySizeError extends ProgramError {
codeToErrorMap.set(0x1799, InvalidCanopySizeError);
nameToErrorMap.set('InvalidCanopySize', InvalidCanopySizeError);

/** InvalidLogWrapper: Invalid log wrapper program */
export class InvalidLogWrapperError extends ProgramError {
readonly name: string = 'InvalidLogWrapper';

readonly code: number = 0x179a; // 6042

constructor(program: Program, cause?: Error) {
super('Invalid log wrapper program', program, cause);
}
}
codeToErrorMap.set(0x179a, InvalidLogWrapperError);
nameToErrorMap.set('InvalidLogWrapper', InvalidLogWrapperError);

/** InvalidCompressionProgram: Invalid compression program */
export class InvalidCompressionProgramError extends ProgramError {
readonly name: string = 'InvalidCompressionProgram';

readonly code: number = 0x179b; // 6043

constructor(program: Program, cause?: Error) {
super('Invalid compression program', program, cause);
}
}
codeToErrorMap.set(0x179b, InvalidCompressionProgramError);
nameToErrorMap.set('InvalidCompressionProgram', InvalidCompressionProgramError);

/**
* Attempts to resolve a custom program error from the provided error code.
* @category Errors
Expand Down
253 changes: 251 additions & 2 deletions clients/js/test/createTree.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,75 @@
import { generateSigner, publicKey } from '@metaplex-foundation/umi';
import { createAccount } from '@metaplex-foundation/mpl-toolbox';
import {
generateSigner,
publicKey,
Context,
Signer,
TransactionBuilder,
transactionBuilder,
PublicKey,
} from '@metaplex-foundation/umi';
import test from 'ava';
import { TreeConfig, createTree, fetchTreeConfigFromSeeds } from '../src';
import {
TreeConfig,
createTree,
createTreeConfig,
fetchTreeConfigFromSeeds,
safeFetchTreeConfigFromSeeds,
getMerkleTreeSize,
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
} from '../src';
import { createUmi } from './_setup';

const createTreeWithSpecificMerkleOwner = async (
context: Parameters<typeof createAccount>[0] &
Parameters<typeof createTreeConfig>[0] &
Pick<Context, 'rpc'>,
input: Omit<Parameters<typeof createTreeConfig>[1], 'merkleTree'> & {
merkleTree: Signer;
merkleTreeSize?: number;
canopyDepth?: number;
merkleTreeOwner?: PublicKey;
}
): Promise<TransactionBuilder> => {
const space =
input.merkleTreeSize ??
getMerkleTreeSize(input.maxDepth, input.maxBufferSize, input.canopyDepth);
const lamports = await context.rpc.getRent(space);

let programId;
if (input.compressionProgram) {
programId = Array.isArray(input.compressionProgram)
? input.compressionProgram[0]
: input.compressionProgram;
} else {
programId = context.programs.getPublicKey(
'splAccountCompression',
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID
);
}

return (
transactionBuilder()
// Create the empty Merkle tree account.
.add(
createAccount(context, {
payer: input.payer ?? context.payer,
newAccount: input.merkleTree,
lamports,
space,
programId: input.merkleTreeOwner ? input.merkleTreeOwner : programId,
})
)
// Create the tree config.
.add(
createTreeConfig(context, {
...input,
merkleTree: input.merkleTree.publicKey,
})
)
);
};

test('it can create a Bubblegum tree', async (t) => {
// Given a brand new merkle tree signer.
const umi = await createUmi();
Expand Down Expand Up @@ -60,3 +127,185 @@ test('it can create a Bubblegum tree using a newer size', async (t) => {
isPublic: false,
});
});

test('it can create a Bubblegum tree using mpl-account-compression and mpl-noop', async (t) => {
// Given a brand new merkle tree signer.
const umi = await createUmi();
const merkleTree = generateSigner(umi);

// When we create a tree at this address.
const builder = await createTree(umi, {
merkleTree,
maxDepth: 14,
maxBufferSize: 64,
logWrapper: publicKey('mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3'),
compressionProgram: publicKey(
'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW'
),
});
await builder.sendAndConfirm(umi);

// Then an account exists at the merkle tree address.
t.true(await umi.rpc.accountExists(merkleTree.publicKey));

// And a tree config was created with the correct data.
const treeConfig = await fetchTreeConfigFromSeeds(umi, {
merkleTree: merkleTree.publicKey,
});
t.like(treeConfig, <TreeConfig>{
treeCreator: publicKey(umi.identity),
treeDelegate: publicKey(umi.identity),
totalMintCapacity: 2n ** 14n,
numMinted: 0n,
isPublic: false,
});
});

test('it cannot create a Bubblegum tree using invalid logWrapper with spl-account-compression', async (t) => {
// Given a brand new merkle tree signer.
const umi = await createUmi();
const merkleTree = generateSigner(umi);

// When we create a tree at this address.
const builder = await createTree(umi, {
merkleTree,
maxDepth: 14,
maxBufferSize: 64,
logWrapper: generateSigner(umi).publicKey,
});

const promise = builder.sendAndConfirm(umi);

// Then we expect a program error.
await t.throwsAsync(promise, { name: 'InvalidLogWrapper' });

// And an account does not exist at the merkle tree address.
t.false(await umi.rpc.accountExists(merkleTree.publicKey));

// And a tree config was not created with the correct data.
const treeConfig = await safeFetchTreeConfigFromSeeds(umi, {
merkleTree: merkleTree.publicKey,
});
t.is(treeConfig, null);
});

test('it cannot create a Bubblegum tree using invalid logWrapper with mpl-account-compression', async (t) => {
// Given a brand new merkle tree signer.
const umi = await createUmi();
const merkleTree = generateSigner(umi);

// When we create a tree at this address.
const builder = await createTree(umi, {
merkleTree,
maxDepth: 14,
maxBufferSize: 64,
logWrapper: generateSigner(umi).publicKey,
compressionProgram: publicKey(
'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW'
),
});

const promise = builder.sendAndConfirm(umi);

// Then we expect a program error.
await t.throwsAsync(promise, { name: 'InvalidLogWrapper' });

// And an account does not exist at the merkle tree address.
t.false(await umi.rpc.accountExists(merkleTree.publicKey));

// And a tree config was not created with the correct data.
const treeConfig = await safeFetchTreeConfigFromSeeds(umi, {
merkleTree: merkleTree.publicKey,
});
t.is(treeConfig, null);
});

test('it cannot create a Bubblegum tree when compression program does not match tree owned by spl-account-compression', async (t) => {
// Given a brand new merkle tree signer.
const umi = await createUmi();
const merkleTree = generateSigner(umi);

// When we create a tree at this address.
const builder = await createTreeWithSpecificMerkleOwner(umi, {
merkleTree,
maxDepth: 14,
maxBufferSize: 64,
compressionProgram: generateSigner(umi).publicKey,
merkleTreeOwner: umi.programs.getPublicKey(
'splAccountCompression',
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID
),
});

const promise = builder.sendAndConfirm(umi);

// Then we expect a program error.
await t.throwsAsync(promise, { name: 'InvalidCompressionProgram' });

// And an account does not exist at the merkle tree address.
t.false(await umi.rpc.accountExists(merkleTree.publicKey));

// And a tree config was not created with the correct data.
const treeConfig = await safeFetchTreeConfigFromSeeds(umi, {
merkleTree: merkleTree.publicKey,
});
t.is(treeConfig, null);
});

test('it cannot create a Bubblegum tree when compression program does not match tree owned by mpl-account-compression', async (t) => {
// Given a brand new merkle tree signer.
const umi = await createUmi();
const merkleTree = generateSigner(umi);

// When we create a tree at this address.
const builder = await createTreeWithSpecificMerkleOwner(umi, {
merkleTree,
maxDepth: 14,
maxBufferSize: 64,
logWrapper: publicKey('mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3'),
compressionProgram: generateSigner(umi).publicKey,
merkleTreeOwner: publicKey('mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW'),
});

const promise = builder.sendAndConfirm(umi);

// Then we expect a program error.
await t.throwsAsync(promise, { name: 'InvalidCompressionProgram' });

// And an account does not exist at the merkle tree address.
t.false(await umi.rpc.accountExists(merkleTree.publicKey));

// And a tree config was not created with the correct data.
const treeConfig = await safeFetchTreeConfigFromSeeds(umi, {
merkleTree: merkleTree.publicKey,
});
t.is(treeConfig, null);
});

test('it cannot create a Bubblegum tree with incorrect Merkle tree owner', async (t) => {
// Given a brand new merkle tree signer.
const umi = await createUmi();
const merkleTree = generateSigner(umi);

// When we create a tree at this address.
const builder = await createTreeWithSpecificMerkleOwner(umi, {
merkleTree,
maxDepth: 14,
maxBufferSize: 64,
merkleTreeOwner: generateSigner(umi).publicKey,
});

const promise = builder.sendAndConfirm(umi);

// Then we expect a program error.
await t.throwsAsync(promise, { name: 'IncorrectOwner' });

// And an account does not exist at the merkle tree address.
t.false(await umi.rpc.accountExists(merkleTree.publicKey));

// And a tree config was not created with the correct data.
const treeConfig = await safeFetchTreeConfigFromSeeds(umi, {
merkleTree: merkleTree.publicKey,
});
t.is(treeConfig, null);
});
Loading

0 comments on commit 14329d8

Please sign in to comment.