Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit ebe5051

Browse files
committed
updated close account to allow closing prepared and not finalized accounts + updated comments on append_canopy_nodes to reflect the possibility to replace those
1 parent 757e84f commit ebe5051

File tree

5 files changed

+206
-12
lines changed

5 files changed

+206
-12
lines changed

account-compression/programs/account-compression/src/lib.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,17 @@ pub mod spl_account_compression {
229229
/// This instruction pre-initializes the canopy with the specified leaf nodes of the canopy.
230230
/// This is intended to be used after `prepare_tree` and in conjunction with the
231231
/// `finalize_merkle_tree_with_root` instruction that'll finalize the tree initialization.
232+
/// The canopy is used to cache the uppermost nodes of the tree, which allows for a smaller
233+
/// proof size when updating the tree. The canopy should be filled with the necessary nodes
234+
/// before calling `finalize_merkle_tree_with_root`. You may call this instruction multiple
235+
/// times to fill the canopy with the necessary nodes. The canopy may be filled with the
236+
/// nodes in any order. The already filled nodes may be replaced with new nodes before calling
237+
/// `finalize_merkle_tree_with_root` if the step was done in error.
238+
/// The canopy should be filled with all the nodes that are to the left of the rightmost
239+
/// leaf of the tree before calling `finalize_merkle_tree_with_root`. The canopy should not
240+
/// contain any nodes to the right of the rightmost leaf of the tree.
241+
/// This instruction calculates and filles in all the canopy nodes "above" the provided ones.
242+
/// The validation of the canopy is done in the `finalize_merkle_tree_with_root` instruction.
232243
pub fn append_canopy_nodes(
233244
ctx: Context<Modify>,
234245
start_index: u32,
@@ -264,8 +275,15 @@ pub mod spl_account_compression {
264275
)
265276
}
266277

267-
/// Initializes a prepared tree with a root and a rightmost leaf. The rightmost leaf is used to verify the canopy if the tree has it. Before calling this instruction, the tree should be prepared with `prepare_tree` and the canopy should be filled with the necessary nodes with `append_canopy_nodes` (if the canopy is used).
268-
/// This method should be used for rolluped creation of trees. The indexing of such rollups should be done off-chain. The programs calling this instruction should take care of ensuring the indexing is possible. For example, staking may be required to ensure the tree creator has some responsibility for what is being indexed. If indexing is not possible, there should be a mechanism to penalize the tree creator.
278+
/// Initializes a prepared tree with a root and a rightmost leaf. The rightmost leaf is used to
279+
/// verify the canopy if the tree has it. Before calling this instruction, the tree should be
280+
/// prepared with `prepare_tree` and the canopy should be filled with the necessary nodes with
281+
/// `append_canopy_nodes` (if the canopy is used). This method should be used for rolluped
282+
/// creation of trees. The indexing of such rollups should be done off-chain. The programs
283+
/// calling this instruction should take care of ensuring the indexing is possible. For example,
284+
/// staking may be required to ensure the tree creator has some responsibility for what is being
285+
/// indexed. If indexing is not possible, there should be a mechanism to penalize the tree
286+
/// creator.
269287
pub fn finalize_merkle_tree_with_root(
270288
ctx: Context<Modify>,
271289
root: [u8; 32],
@@ -546,8 +564,11 @@ pub mod spl_account_compression {
546564
let merkle_tree_size = merkle_tree_get_size(&header)?;
547565
let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);
548566

549-
let id = ctx.accounts.merkle_tree.key();
550-
merkle_tree_apply_fn_mut!(header, id, tree_bytes, prove_tree_is_empty,)?;
567+
// Check if the tree is either empty or is batch initialized and not finalized yet.
568+
if !header.get_is_batch_initialized() || !tree_bytes.iter().all(|&x| x == 0) {
569+
let id = ctx.accounts.merkle_tree.key();
570+
merkle_tree_apply_fn_mut!(header, id, tree_bytes, prove_tree_is_empty,)?;
571+
}
551572

552573
// Close merkle tree account
553574
// 1. Move lamports

account-compression/programs/account-compression/src/state/concurrent_merkle_tree_header.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ impl ConcurrentMerkleTreeHeader {
140140
}
141141
}
142142

143+
pub fn get_is_batch_initialized(&self) -> bool {
144+
match &self.header {
145+
ConcurrentMerkleTreeHeaderData::V1(header) => header.is_batch_initialized,
146+
}
147+
}
148+
143149
pub fn set_new_authority(&mut self, new_authority: &Pubkey) {
144150
match self.header {
145151
ConcurrentMerkleTreeHeaderData::V1(ref mut header) => {

account-compression/sdk/idl/spl_account_compression.json

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,18 @@
109109
"docs": [
110110
"This instruction pre-initializes the canopy with the specified leaf nodes of the canopy.",
111111
"This is intended to be used after `prepare_tree` and in conjunction with the",
112-
"`finalize_merkle_tree_with_root` instruction that'll finalize the tree initialization."
112+
"`finalize_merkle_tree_with_root` instruction that'll finalize the tree initialization.",
113+
"The canopy is used to cache the uppermost nodes of the tree, which allows for a smaller",
114+
"proof size when updating the tree. The canopy should be filled with the necessary nodes",
115+
"before calling `finalize_merkle_tree_with_root`. You may call this instruction multiple",
116+
"times to fill the canopy with the necessary nodes. The canopy may be filled with the",
117+
"nodes in any order. The already filled nodes may be replaced with new nodes before calling",
118+
"`finalize_merkle_tree_with_root` if the step was done in error.",
119+
"The canopy should be filled with all the nodes that are to the left of the rightmost",
120+
"leaf of the tree before calling `finalize_merkle_tree_with_root`. The canopy should not",
121+
"contain any nodes to the right of the rightmost leaf of the tree.",
122+
"This instruction calculates and filles in all the canopy nodes \"above\" the provided ones.",
123+
"The validation of the canopy is done in the `finalize_merkle_tree_with_root` instruction."
113124
],
114125
"accounts": [
115126
{
@@ -151,8 +162,15 @@
151162
{
152163
"name": "finalizeMerkleTreeWithRoot",
153164
"docs": [
154-
"Initializes a prepared tree with a root and a rightmost leaf. The rightmost leaf is used to verify the canopy if the tree has it. Before calling this instruction, the tree should be prepared with `prepare_tree` and the canopy should be filled with the necessary nodes with `append_canopy_nodes` (if the canopy is used).",
155-
"This method should be used for rolluped creation of trees. The indexing of such rollups should be done off-chain. The programs calling this instruction should take care of ensuring the indexing is possible. For example, staking may be required to ensure the tree creator has some responsibility for what is being indexed. If indexing is not possible, there should be a mechanism to penalize the tree creator."
165+
"Initializes a prepared tree with a root and a rightmost leaf. The rightmost leaf is used to",
166+
"verify the canopy if the tree has it. Before calling this instruction, the tree should be",
167+
"prepared with `prepare_tree` and the canopy should be filled with the necessary nodes with",
168+
"`append_canopy_nodes` (if the canopy is used). This method should be used for rolluped",
169+
"creation of trees. The indexing of such rollups should be done off-chain. The programs",
170+
"calling this instruction should take care of ensuring the indexing is possible. For example,",
171+
"staking may be required to ensure the tree creator has some responsibility for what is being",
172+
"indexed. If indexing is not possible, there should be a mechanism to penalize the tree",
173+
"creator."
156174
],
157175
"accounts": [
158176
{
@@ -727,11 +745,16 @@
727745
},
728746
{
729747
"code": 6011,
748+
"name": "BatchNotInitialized",
749+
"msg": "Tree header was not initialized for batch processing"
750+
},
751+
{
752+
"code": 6012,
730753
"name": "CanopyRootMismatch",
731754
"msg": "Canopy root does not match the root of the tree"
732755
},
733756
{
734-
"code": 6012,
757+
"code": 6013,
735758
"name": "CanopyRightmostLeafMismatch",
736759
"msg": "Canopy contains nodes to the right of the rightmost leaf of the tree"
737760
}

account-compression/sdk/src/generated/errors/index.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -234,14 +234,34 @@ export class TreeAlreadyInitializedError extends Error {
234234
createErrorFromCodeLookup.set(0x177a, () => new TreeAlreadyInitializedError());
235235
createErrorFromNameLookup.set('TreeAlreadyInitialized', () => new TreeAlreadyInitializedError());
236236

237+
/**
238+
* BatchNotInitialized: 'Tree header was not initialized for batch processing'
239+
*
240+
* @category Errors
241+
* @category generated
242+
*/
243+
export class BatchNotInitializedError extends Error {
244+
readonly code: number = 0x177b;
245+
readonly name: string = 'BatchNotInitialized';
246+
constructor() {
247+
super('Tree header was not initialized for batch processing');
248+
if (typeof Error.captureStackTrace === 'function') {
249+
Error.captureStackTrace(this, BatchNotInitializedError);
250+
}
251+
}
252+
}
253+
254+
createErrorFromCodeLookup.set(0x177b, () => new BatchNotInitializedError());
255+
createErrorFromNameLookup.set('BatchNotInitialized', () => new BatchNotInitializedError());
256+
237257
/**
238258
* CanopyRootMismatch: 'Canopy root does not match the root of the tree'
239259
*
240260
* @category Errors
241261
* @category generated
242262
*/
243263
export class CanopyRootMismatchError extends Error {
244-
readonly code: number = 0x177b;
264+
readonly code: number = 0x177c;
245265
readonly name: string = 'CanopyRootMismatch';
246266
constructor() {
247267
super('Canopy root does not match the root of the tree');
@@ -251,7 +271,7 @@ export class CanopyRootMismatchError extends Error {
251271
}
252272
}
253273

254-
createErrorFromCodeLookup.set(0x177b, () => new CanopyRootMismatchError());
274+
createErrorFromCodeLookup.set(0x177c, () => new CanopyRootMismatchError());
255275
createErrorFromNameLookup.set('CanopyRootMismatch', () => new CanopyRootMismatchError());
256276

257277
/**
@@ -261,7 +281,7 @@ createErrorFromNameLookup.set('CanopyRootMismatch', () => new CanopyRootMismatch
261281
* @category generated
262282
*/
263283
export class CanopyRightmostLeafMismatchError extends Error {
264-
readonly code: number = 0x177c;
284+
readonly code: number = 0x177d;
265285
readonly name: string = 'CanopyRightmostLeafMismatch';
266286
constructor() {
267287
super('Canopy contains nodes to the right of the rightmost leaf of the tree');
@@ -271,7 +291,7 @@ export class CanopyRightmostLeafMismatchError extends Error {
271291
}
272292
}
273293

274-
createErrorFromCodeLookup.set(0x177c, () => new CanopyRightmostLeafMismatchError());
294+
createErrorFromCodeLookup.set(0x177d, () => new CanopyRightmostLeafMismatchError());
275295
createErrorFromNameLookup.set('CanopyRightmostLeafMismatch', () => new CanopyRightmostLeafMismatchError());
276296

277297
/**

account-compression/sdk/tests/accountCompression.test.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
createAppendCanopyNodesIx,
1212
createAppendIx,
1313
createCloseEmptyTreeInstruction,
14+
createCloseEmptyTreeIx,
1415
createFinalizeMerkleTreeWithRootIx,
1516
createInitEmptyMerkleTreeIx,
1617
createReplaceIx,
@@ -177,6 +178,27 @@ describe('Account Compression', () => {
177178
assert(false, 'Double finalizing should have failed');
178179
} catch {}
179180
});
181+
182+
it('Should be able to close a prepared tree', async () => {
183+
let payerInfo = await provider.connection.getAccountInfo(payer, 'confirmed')!;
184+
let treeInfo = await provider.connection.getAccountInfo(cmt, 'confirmed')!;
185+
186+
const payerLamports = payerInfo!.lamports;
187+
const treeLamports = treeInfo!.lamports;
188+
189+
const closeIx = createCloseEmptyTreeIx(cmt, payer, payer);
190+
await execute(provider, [closeIx], [payerKeypair]);
191+
192+
payerInfo = await provider.connection.getAccountInfo(payer, 'confirmed')!;
193+
const finalLamports = payerInfo!.lamports;
194+
assert(
195+
finalLamports === payerLamports + treeLamports - 5000,
196+
'Expected payer to have received the lamports from the closed tree account'
197+
);
198+
199+
treeInfo = await provider.connection.getAccountInfo(cmt, 'confirmed');
200+
assert(treeInfo === null, 'Expected the merkle tree account info to be null');
201+
});
180202
});
181203
describe('Having prepared a tree with canopy', () => {
182204
const depth = 3;
@@ -474,6 +496,108 @@ describe('Account Compression', () => {
474496
assert(false, 'Initializing an empty tree after preparing a tree should have failed');
475497
} catch {}
476498
});
499+
it('Should be able to close a prepared tree after setting the canopy', async () => {
500+
const merkleTreeRaw = new MerkleTree(leaves);
501+
502+
const appendIx = createAppendCanopyNodesIx(
503+
cmt,
504+
payer,
505+
merkleTreeRaw.leaves
506+
.slice(0, leaves.length / 2)
507+
.filter((_, i) => i % 2 === 0)
508+
.map(leaf => leaf.parent!.node!),
509+
0,
510+
);
511+
await execute(provider, [appendIx], [payerKeypair]);
512+
let payerInfo = await provider.connection.getAccountInfo(payer, 'confirmed')!;
513+
let treeInfo = await provider.connection.getAccountInfo(cmt, 'confirmed')!;
514+
515+
const payerLamports = payerInfo!.lamports;
516+
const treeLamports = treeInfo!.lamports;
517+
518+
const closeIx = createCloseEmptyTreeIx(cmt, payer, payer);
519+
await execute(provider, [closeIx], [payerKeypair]);
520+
521+
payerInfo = await provider.connection.getAccountInfo(payer, 'confirmed')!;
522+
const finalLamports = payerInfo!.lamports;
523+
assert(
524+
finalLamports === payerLamports + treeLamports - 5000,
525+
'Expected payer to have received the lamports from the closed tree account'
526+
);
527+
528+
treeInfo = await provider.connection.getAccountInfo(cmt, 'confirmed');
529+
assert(treeInfo === null, 'Expected the merkle tree account info to be null');
530+
});
531+
});
532+
describe('Having prepared an empty tree with canopy', () => {
533+
const depth = 3;
534+
const size = 8;
535+
const canopyDepth = 2;
536+
// empty leaves represent the empty tree
537+
const leaves = [
538+
Buffer.alloc(32),
539+
Buffer.alloc(32),
540+
Buffer.alloc(32),
541+
Buffer.alloc(32),
542+
Buffer.alloc(32),
543+
Buffer.alloc(32),
544+
Buffer.alloc(32),
545+
Buffer.alloc(32),
546+
];
547+
let anotherKeyPair: Keypair;
548+
let another: PublicKey;
549+
beforeEach(async () => {
550+
const cmtKeypair = await prepareTree({
551+
canopyDepth,
552+
depthSizePair: {
553+
maxBufferSize: size,
554+
maxDepth: depth,
555+
},
556+
payer: payerKeypair,
557+
provider,
558+
});
559+
cmt = cmtKeypair.publicKey;
560+
anotherKeyPair = Keypair.generate();
561+
another = anotherKeyPair.publicKey;
562+
await provider.connection.confirmTransaction(
563+
await provider.connection.requestAirdrop(another, 1e10),
564+
'confirmed',
565+
);
566+
});
567+
568+
it('Should be able to finalize an empty tree with empty canopy and close it afterwards', async () => {
569+
const merkleTreeRaw = new MerkleTree(leaves);
570+
const root = merkleTreeRaw.root;
571+
const leaf = leaves[leaves.length - 1];
572+
573+
const finalize = createFinalizeMerkleTreeWithRootIx(
574+
cmt,
575+
payer,
576+
root,
577+
leaf,
578+
leaves.length - 1,
579+
merkleTreeRaw.getProof(leaves.length - 1).proof,
580+
);
581+
await execute(provider, [finalize], [payerKeypair]);
582+
let payerInfo = await provider.connection.getAccountInfo(payer, 'confirmed')!;
583+
let treeInfo = await provider.connection.getAccountInfo(cmt, 'confirmed')!;
584+
585+
const payerLamports = payerInfo!.lamports;
586+
const treeLamports = treeInfo!.lamports;
587+
588+
const closeIx = createCloseEmptyTreeIx(cmt, payer, payer);
589+
await execute(provider, [closeIx], [payerKeypair]);
590+
591+
payerInfo = await provider.connection.getAccountInfo(payer, 'confirmed')!;
592+
const finalLamports = payerInfo!.lamports;
593+
assert(
594+
finalLamports === payerLamports + treeLamports - 5000,
595+
'Expected payer to have received the lamports from the closed tree account'
596+
);
597+
598+
treeInfo = await provider.connection.getAccountInfo(cmt, 'confirmed');
599+
assert(treeInfo === null, 'Expected the merkle tree account info to be null');
600+
});
477601
});
478602

479603
describe('Having created a tree with a single leaf', () => {

0 commit comments

Comments
 (0)