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

[account compression] Clean up tests #5847

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions account-compression/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,21 @@
"fmt": "prettier --write '{*,**/*}.{ts,tsx,js,jsx,json}'",
"pretty": "prettier --check '{,{src,test}/**/}*.{j,t}s'",
"pretty:fix": "prettier --write '{,{src,test}/**/}*.{j,t}s'",
"lint": "set -ex; npm run pretty; eslint . --ext .js,.ts",
"lint:fix": "npm run pretty:fix && eslint . --fix --ext .js,.ts",
"lint": "set -ex; pnpm run pretty; eslint . --ext .js,.ts",
"lint:fix": "pnpm run pretty:fix && eslint . --fix --ext .js,.ts",
"docs": "rm -rf docs/ && typedoc --out docs",
"deploy:docs": "yarn docs && gh-pages --dest account-compression/sdk --dist docs --dotfiles",
"deploy:docs": "pnpm docs && gh-pages --dest account-compression/sdk --dist docs --dotfiles",
"start-validator": "solana-test-validator --reset --quiet --bpf-program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK ../target/deploy/spl_account_compression.so --bpf-program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV ../target/deploy/spl_noop.so",
"run-tests": "jest tests --detectOpenHandles",
"run-tests:events": "jest tests/events --detectOpenHandles",
"run-tests:accounts": "jest tests/accounts --detectOpenHandles",
"run-tests:instructions": "jest tests/instructions --detectOpenHandles",
"run-tests:e2e": "jest accountCompression.test.ts --detectOpenHandles",
"test:events": "start-server-and-test start-validator http://127.0.0.1:8899/health run-tests:events",
"test:accounts": "start-server-and-test start-validator http://127.0.0.1:8899/health run-tests:accounts",
"test:e2e": "start-server-and-test start-validator http://127.0.0.1:8899/health run-tests:e2e",
"test:merkle-tree": "jest tests/merkleTree.test.ts --detectOpenHandles",
"test:instructions": "start-server-and-test start-validator http://127.0.0.1:8899/health run-tests:instructions",
"test": "start-server-and-test start-validator http://127.0.0.1:8899/health run-tests"
},
"dependencies": {
Expand All @@ -71,7 +73,8 @@
"@solana/prettier-config-solana": "^0.0.2",
"@types/bn.js": "^5.1.1",
"@types/chai": "^4.3.0",
"@types/jest": "^29.0.0",
"@types/jest": "^29.5.8",
"@types/mocha": "^10.0.4",
"@types/node-fetch": "^2.6.2",
"@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1",
Expand Down
317 changes: 16 additions & 301 deletions account-compression/sdk/tests/accountCompression.test.ts

Large diffs are not rendered by default.

204 changes: 204 additions & 0 deletions account-compression/sdk/tests/accounts/canopy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { AnchorProvider } from '@project-serum/anchor';
import NodeWallet from '@project-serum/anchor/dist/cjs/nodewallet';
import { Connection, Keypair, PublicKey, TransactionInstruction } from '@solana/web3.js';
import { assert } from 'chai';
import * as crypto from 'crypto';

import {
ConcurrentMerkleTreeAccount,
createAppendIx,
createReplaceIx,
createVerifyLeafIx,
ValidDepthSizePair,
} from '../../src';
import { MerkleTree } from '../../src/merkle-tree';
import { createTreeOnChain, execute } from '../utils';

describe(`Canopy test`, () => {
let offChainTree: MerkleTree;
let cmtKeypair: Keypair;
let cmt: PublicKey;
let payerKeypair: Keypair;
let payer: PublicKey;
let connection: Connection;
let provider: AnchorProvider;

const MAX_BUFFER_SIZE = 8;
const MAX_DEPTH = 5;
const DEPTH_SIZE_PAIR: ValidDepthSizePair = {
maxBufferSize: MAX_BUFFER_SIZE,
maxDepth: MAX_DEPTH,
};

beforeEach(async () => {
payerKeypair = Keypair.generate();
payer = payerKeypair.publicKey;
connection = new Connection('http://127.0.0.1:8899', {
commitment: 'confirmed',
});
const wallet = new NodeWallet(payerKeypair);
provider = new AnchorProvider(connection, wallet, {
commitment: connection.commitment,
skipPreflight: true,
});

await provider.connection.confirmTransaction(
await provider.connection.requestAirdrop(payer, 1e10),
'confirmed'
);
});

describe(`Unit test proof instructions`, () => {
beforeEach(async () => {
[cmtKeypair, offChainTree] = await createTreeOnChain(
provider,
payerKeypair,
2 ** MAX_DEPTH, // Fill up the tree
DEPTH_SIZE_PAIR,
MAX_DEPTH // Store full tree on chain
);
cmt = cmtKeypair.publicKey;
});
it(`VerifyLeaf works with no proof accounts`, async () => {
const splCMT = await ConcurrentMerkleTreeAccount.fromAccountAddress(connection, cmt, 'confirmed');

// Test that the entire state of the tree is stored properly
// by verifying every leaf in the tree.
// We use batches of 4 verify ixs / tx to speed up the test
let i = 0;
const stepSize = 4;
while (i < 2 ** MAX_DEPTH) {
const ixs: TransactionInstruction[] = [];
for (let j = 0; j < stepSize; j += 1) {
const leafIndex = i + j;
const leaf = offChainTree.leaves[leafIndex].node;
const verifyIx = createVerifyLeafIx(cmt, {
leaf,
leafIndex,
proof: [],
root: splCMT.getCurrentRoot(),
});
ixs.push(verifyIx);
}
i += stepSize;
await execute(provider, ixs, [payerKeypair], true);
}
});
it('ReplaceLeaf works with no proof accounts', async () => {
for (let i = 0; i < 2 ** MAX_DEPTH; i += 1) {
const proof = offChainTree.getProof(i);

// Replace the current leaf to random bytes, without any additional proof accounts
const newLeaf = crypto.randomBytes(32);
const replaceIx = createReplaceIx(cmt, payer, newLeaf, {
...proof,
proof: [],
});
offChainTree.updateLeaf(i, newLeaf);
await execute(provider, [replaceIx], [payerKeypair], true, false);

// Check that replaced leaf actually exists in new tree root
const splCMT = await ConcurrentMerkleTreeAccount.fromAccountAddress(connection, cmt, {
commitment: 'confirmed',
});
assert(splCMT.getCurrentRoot().equals(Buffer.from(offChainTree.root)), 'Roots do not match');
}
});
});

describe('Test integrated appends & replaces', () => {
beforeEach(async () => {
[cmtKeypair, offChainTree] = await createTreeOnChain(
provider,
payerKeypair,
0, // Start off with 0 leaves
DEPTH_SIZE_PAIR,
MAX_DEPTH // Store full tree on chain
);
cmt = cmtKeypair.publicKey;
});

it('Testing canopy for appends and replaces on a full on chain tree', async () => {
// Test that the canopy updates properly throughout multiple modifying instructions
// in the same transaction
const leaves: Array<number>[] = [];
let i = 0;
const stepSize = 4;
while (i < 2 ** MAX_DEPTH) {
const ixs: TransactionInstruction[] = [];
for (let j = 0; j < stepSize; ++j) {
const newLeaf = Array.from(Buffer.alloc(32, i + 1));
leaves.push(newLeaf);
const appendIx = createAppendIx(cmt, payer, newLeaf);
ixs.push(appendIx);
}
await execute(provider, ixs, [payerKeypair]);
i += stepSize;
console.log('Appended', i, 'leaves');
}

// Compare on-chain & off-chain roots
let ixs: TransactionInstruction[] = [];
const splCMT = await ConcurrentMerkleTreeAccount.fromAccountAddress(connection, cmt);
const root = splCMT.getCurrentRoot();

// Test that the entire state of the tree is stored properly
// by using the canopy to infer proofs to all of the leaves in the tree.
// We test that the canopy is updating properly by replacing all the leaves
// in the tree
const leafList = Array.from(leaves.entries());
leafList.sort(() => Math.random() - 0.5);
let replaces = 0;
const newLeaves: Record<number, Buffer> = {};
for (const [i, leaf] of leafList) {
const newLeaf = crypto.randomBytes(32);
newLeaves[i] = newLeaf;
const replaceIx = createReplaceIx(cmt, payer, newLeaf, {
leaf: Buffer.from(Uint8Array.from(leaf)),
leafIndex: i,
proof: [],
root, // No proof necessary
});
ixs.push(replaceIx);
if (ixs.length == stepSize) {
replaces++;
await execute(provider, ixs, [payerKeypair], true);
console.log('Replaced', replaces * stepSize, 'leaves');
ixs = [];
}
}

const newLeafList: Buffer[] = [];
for (let i = 0; i < 32; ++i) {
newLeafList.push(newLeaves[i]);
}

const tree = new MerkleTree(newLeafList);

for (let proofSize = 1; proofSize <= 5; ++proofSize) {
const newLeaf = crypto.randomBytes(32);
const i = Math.floor(Math.random() * 32);
const leaf = newLeaves[i];

let proof = tree.getProof(i);
const partialProof = proof.proof.slice(0, proofSize);

// Create an instruction to replace the leaf
const replaceIx = createReplaceIx(cmt, payer, newLeaf, {
...proof,
proof: partialProof,
});
tree.updateLeaf(i, newLeaf);

// Create an instruction to undo the previous replace, but using the now-outdated partialProof
proof = tree.getProof(i);
const replaceBackIx = createReplaceIx(cmt, payer, leaf, {
...proof,
proof: partialProof,
});
tree.updateLeaf(i, leaf);
await execute(provider, [replaceIx, replaceBackIx], [payerKeypair], true, false);
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ describe('ConcurrentMerkleTreeAccount tests', () => {
const maxDepth = 30;
const maxBufferSize = 2048;

for (let canopyDepth = 1; canopyDepth <= 14; canopyDepth++) {
for (let canopyDepth = 1; canopyDepth <= 17; canopyDepth++) {
// Airdrop enough SOL to cover tree creation
const size = getConcurrentMerkleTreeAccountSize(maxDepth, maxBufferSize, canopyDepth);
const rent = await connection.getMinimumBalanceForRentExemption(size, 'confirmed');
Expand Down
87 changes: 87 additions & 0 deletions account-compression/sdk/tests/instructions/appendLeaf.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { AnchorProvider } from '@project-serum/anchor';
import NodeWallet from '@project-serum/anchor/dist/cjs/nodewallet';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import { assert } from 'chai';
import * as crypto from 'crypto';

import { ConcurrentMerkleTreeAccount, createAppendIx, ValidDepthSizePair } from '../../src';
import { MerkleTree } from '../../src/merkle-tree';
import { createTreeOnChain, execute } from '../utils';

describe(`CloseEmptyTree instruction`, () => {
let offChainTree: MerkleTree;
let cmtKeypair: Keypair;
let cmt: PublicKey;
let payerKeypair: Keypair;
let payer: PublicKey;
let connection: Connection;
let provider: AnchorProvider;

const MAX_SIZE = 64;
const MAX_DEPTH = 14;
const DEPTH_SIZE_PAIR: ValidDepthSizePair = {
maxBufferSize: MAX_SIZE,
maxDepth: MAX_DEPTH,
};
beforeEach(async () => {
payerKeypair = Keypair.generate();
payer = payerKeypair.publicKey;
connection = new Connection('http://127.0.0.1:8899', {
commitment: 'confirmed',
});
const wallet = new NodeWallet(payerKeypair);
provider = new AnchorProvider(connection, wallet, {
commitment: connection.commitment,
skipPreflight: true,
});

await provider.connection.confirmTransaction(
await provider.connection.requestAirdrop(payer, 1e10),
'confirmed'
);
});
describe('Having created a tree with a single leaf', () => {
beforeEach(async () => {
[cmtKeypair, offChainTree] = await createTreeOnChain(provider, payerKeypair, 1, DEPTH_SIZE_PAIR);
cmt = cmtKeypair.publicKey;
});
it('Append single leaf', async () => {
const newLeaf = crypto.randomBytes(32);
const appendIx = createAppendIx(cmt, payer, newLeaf);

await execute(provider, [appendIx], [payerKeypair]);
offChainTree.updateLeaf(1, newLeaf);

const splCMT = await ConcurrentMerkleTreeAccount.fromAccountAddress(connection, cmt);
const onChainRoot = splCMT.getCurrentRoot();

assert(
Buffer.from(onChainRoot).equals(offChainTree.root),
'Updated on chain root matches root of updated off chain tree'
);
});
});
describe(`Having created a tree with 8 leaves`, () => {
beforeEach(async () => {
[cmtKeypair, offChainTree] = await createTreeOnChain(provider, payerKeypair, 2 ** 3, {
maxBufferSize: 8,
maxDepth: 3,
});
cmt = cmtKeypair.publicKey;
});
it(`Attempt to append a leaf to a full tree`, async () => {
// Ensure that this fails
const newLeaf = crypto.randomBytes(32);
const appendIx = createAppendIx(cmt, payer, newLeaf);

try {
await execute(provider, [appendIx], [payerKeypair]);
throw Error(
'This append instruction should have failed because there is no more space to append leaves'
);
} catch (_e) {
assert(true);
}
});
});
});
Loading