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

Commit 757e84f

Browse files
committed
extended the concurrent merkle tree header with a is_batch_initialized flag + comments fixed
1 parent 7d75b13 commit 757e84f

File tree

10 files changed

+124
-24
lines changed

10 files changed

+124
-24
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ pub enum AccountCompressionError {
5656
#[msg("Tree was already initialized")]
5757
TreeAlreadyInitialized,
5858

59+
/// The tree header was not initialized for batch processing
60+
#[msg("Tree header was not initialized for batch processing")]
61+
BatchNotInitialized,
62+
5963
/// The canopy root doesn't match the root of the tree
6064
#[msg("Canopy root does not match the root of the tree")]
6165
CanopyRootMismatch,

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,9 @@ pub mod spl_account_compression {
193193
/// expected flow is `init_empty_merkle_tree`. For the latter case, the canopy should be
194194
/// filled with the necessary nodes to render the tree usable. Thus we need to prefill the
195195
/// canopy with the necessary nodes. The expected flow for a tree without canopy is
196-
/// `prepare_tree` -> `init_merkle_tree_with_root`. The expected flow for a tree with canopy
196+
/// `prepare_tree` -> `finalize_merkle_tree_with_root`. The expected flow for a tree with canopy
197197
/// is `prepare_tree` -> `append_canopy_nodes` (multiple times until all of the canopy is
198-
/// filled) -> `init_merkle_tree_with_root`. This instruction initializes the tree header
198+
/// filled) -> `finalize_merkle_tree_with_root`. This instruction initializes the tree header
199199
/// while leaving the tree itself uninitialized. This allows distinguishing between an empty
200200
/// tree and a tree prepare to be initialized with a root.
201201
pub fn prepare_tree(
@@ -214,7 +214,7 @@ pub mod spl_account_compression {
214214
merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1);
215215

216216
let mut header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
217-
header.initialize(
217+
header.initialize_batched(
218218
max_depth,
219219
max_buffer_size,
220220
&ctx.accounts.authority.key(),
@@ -228,7 +228,7 @@ pub mod spl_account_compression {
228228

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
231-
/// `init_merkle_tree_with_root` instruction that'll finalize the tree initialization.
231+
/// `finalize_merkle_tree_with_root` instruction that'll finalize the tree initialization.
232232
pub fn append_canopy_nodes(
233233
ctx: Context<Modify>,
234234
start_index: u32,
@@ -246,6 +246,7 @@ pub mod spl_account_compression {
246246

247247
let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
248248
header.assert_valid_authority(&ctx.accounts.authority.key())?;
249+
header.assert_is_batch_initialized()?;
249250
// assert the tree is not initialized yet, we don't want to overwrite the canopy of an
250251
// initialized tree
251252
let merkle_tree_size = merkle_tree_get_size(&header)?;
@@ -283,6 +284,7 @@ pub mod spl_account_compression {
283284
// the header should already be initialized with prepare_tree
284285
let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
285286
header.assert_valid_authority(&ctx.accounts.authority.key())?;
287+
header.assert_is_batch_initialized()?;
286288
let merkle_tree_size = merkle_tree_get_size(&header)?;
287289
let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);
288290
// check the canopy root matches the tree root

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

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,14 @@ pub struct ConcurrentMerkleTreeHeaderDataV1 {
6565
/// Provides a lower-bound on what slot to start (re-)building a tree from.
6666
creation_slot: u64,
6767

68+
/// A flag indicating whether the tree has been initialized with a root.
69+
/// This field was added together with the `finalize_tree_with_root` instruction.
70+
/// It takes 1 byte of space taken from the previous padding for existing accounts.
71+
is_batch_initialized: bool,
72+
6873
/// Needs padding for the account to be 8-byte aligned
6974
/// 8-byte alignment is necessary to zero-copy the SPL ConcurrentMerkleTree
70-
_padding: [u8; 6],
75+
_padding: [u8; 5],
7176
}
7277

7378
#[repr(C)]
@@ -95,6 +100,24 @@ impl ConcurrentMerkleTreeHeader {
95100
header.max_depth = max_depth;
96101
header.authority = *authority;
97102
header.creation_slot = creation_slot;
103+
// is_batch_initialized is left false by default
104+
}
105+
}
106+
}
107+
108+
/// Initializes the header with the given parameters and sets the `is_batch_initialized` flag to
109+
/// true.
110+
pub fn initialize_batched(
111+
&mut self,
112+
max_depth: u32,
113+
max_buffer_size: u32,
114+
authority: &Pubkey,
115+
creation_slot: u64,
116+
) {
117+
self.initialize(max_depth, max_buffer_size, authority, creation_slot);
118+
match self.header {
119+
ConcurrentMerkleTreeHeaderData::V1(ref mut header) => {
120+
header.is_batch_initialized = true;
98121
}
99122
}
100123
}
@@ -155,6 +178,18 @@ impl ConcurrentMerkleTreeHeader {
155178
}
156179
Ok(())
157180
}
181+
182+
pub fn assert_is_batch_initialized(&self) -> Result<()> {
183+
match &self.header {
184+
ConcurrentMerkleTreeHeaderData::V1(header) => {
185+
require!(
186+
header.is_batch_initialized,
187+
AccountCompressionError::BatchNotInitialized
188+
);
189+
}
190+
}
191+
Ok(())
192+
}
158193
}
159194

160195
pub fn merkle_tree_get_size(header: &ConcurrentMerkleTreeHeader) -> Result<usize> {
@@ -166,10 +201,10 @@ pub fn merkle_tree_get_size(header: &ConcurrentMerkleTreeHeader) -> Result<usize
166201
(7, 16) => Ok(size_of::<ConcurrentMerkleTree<7, 16>>()),
167202
(8, 16) => Ok(size_of::<ConcurrentMerkleTree<8, 16>>()),
168203
(9, 16) => Ok(size_of::<ConcurrentMerkleTree<9, 16>>()),
169-
(10, 32)=> Ok(size_of::<ConcurrentMerkleTree<10, 32>>()),
170-
(11, 32)=> Ok(size_of::<ConcurrentMerkleTree<11, 32>>()),
171-
(12, 32)=> Ok(size_of::<ConcurrentMerkleTree<12, 32>>()),
172-
(13, 32)=> Ok(size_of::<ConcurrentMerkleTree<13, 32>>()),
204+
(10, 32) => Ok(size_of::<ConcurrentMerkleTree<10, 32>>()),
205+
(11, 32) => Ok(size_of::<ConcurrentMerkleTree<11, 32>>()),
206+
(12, 32) => Ok(size_of::<ConcurrentMerkleTree<12, 32>>()),
207+
(13, 32) => Ok(size_of::<ConcurrentMerkleTree<13, 32>>()),
173208
(14, 64) => Ok(size_of::<ConcurrentMerkleTree<14, 64>>()),
174209
(14, 256) => Ok(size_of::<ConcurrentMerkleTree<14, 256>>()),
175210
(14, 1024) => Ok(size_of::<ConcurrentMerkleTree<14, 1024>>()),

account-compression/sdk/idl/spl_account_compression.json

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@
6565
"expected flow is `init_empty_merkle_tree`. For the latter case, the canopy should be",
6666
"filled with the necessary nodes to render the tree usable. Thus we need to prefill the",
6767
"canopy with the necessary nodes. The expected flow for a tree without canopy is",
68-
"`prepare_tree` -> `init_merkle_tree_with_root`. The expected flow for a tree with canopy",
68+
"`prepare_tree` -> `finalize_merkle_tree_with_root`. The expected flow for a tree with canopy",
6969
"is `prepare_tree` -> `append_canopy_nodes` (multiple times until all of the canopy is",
70-
"filled) -> `init_merkle_tree_with_root`. This instruction initializes the tree header",
70+
"filled) -> `finalize_merkle_tree_with_root`. This instruction initializes the tree header",
7171
"while leaving the tree itself uninitialized. This allows distinguishing between an empty",
7272
"tree and a tree prepare to be initialized with a root."
7373
],
@@ -109,7 +109,7 @@
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-
"`init_merkle_tree_with_root` instruction that'll finalize the tree initialization."
112+
"`finalize_merkle_tree_with_root` instruction that'll finalize the tree initialization."
113113
],
114114
"accounts": [
115115
{
@@ -542,14 +542,23 @@
542542
],
543543
"type": "u64"
544544
},
545+
{
546+
"name": "isBatchInitialized",
547+
"docs": [
548+
"A flag indicating whether the tree has been initialized with a root.",
549+
"This field was added together with the `finalize_tree_with_root` instruction.",
550+
"It takes 1 byte of space taken from the previous padding for existing accounts."
551+
],
552+
"type": "bool"
553+
},
545554
{
546555
"name": "padding",
547556
"docs": [
548557
"Needs padding for the account to be 8-byte aligned",
549558
"8-byte alignment is necessary to zero-copy the SPL ConcurrentMerkleTree"
550559
],
551560
"type": {
552-
"array": ["u8", 6]
561+
"array": ["u8", 5]
553562
}
554563
}
555564
]

account-compression/sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"lint:fix": "npm run pretty:fix && eslint . --fix --ext .js,.ts",
4343
"docs": "rm -rf docs/ && typedoc --out docs",
4444
"deploy:docs": "npm run docs && gh-pages --dest account-compression/sdk --dist docs --dotfiles",
45-
"start-validator": "solana-test-validator --reset --quiet --bpf-program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK ../target/deploy/spl_account_compression.so --bpf-program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV ../target/deploy/spl_noop.so",
45+
"start-validator": "solana-test-validator --reset --quiet --bpf-program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK ../target/deploy/spl_account_compression.so --bpf-program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV ../target/deploy/spl_noop.so --account 27QMkDMpBoAhmWj6xxQNYdqXZL5nnC8tkZcEtkNxCqeX pre-batch-init-tree-account.json",
4646
"run-tests": "jest tests --detectOpenHandles",
4747
"run-tests:events": "jest tests/events --detectOpenHandles",
4848
"run-tests:accounts": "jest tests/accounts --detectOpenHandles",

account-compression/sdk/src/accounts/ConcurrentMerkleTreeAccount.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,14 @@ export class ConcurrentMerkleTreeAccount {
121121
getCanopyDepth(): number {
122122
return getCanopyDepth(this.canopy.canopyBytes.length);
123123
}
124+
125+
/**
126+
* Returns the flag that indicates if the tree has been batch initialized
127+
* @returns the flag
128+
*/
129+
getIsBatchInitialized(): boolean {
130+
return this.getHeaderV1().isBatchInitialized;
131+
}
124132
}
125133

126134
/**

account-compression/sdk/src/generated/types/ConcurrentMerkleTreeHeaderDataV1.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import * as web3 from '@solana/web3.js';
1111
export type ConcurrentMerkleTreeHeaderDataV1 = {
1212
authority: web3.PublicKey;
1313
creationSlot: beet.bignum;
14+
isBatchInitialized: boolean;
1415
maxBufferSize: number;
1516
maxDepth: number;
16-
padding: number[] /* size: 6 */;
17+
padding: number[] /* size: 5 */;
1718
};
1819

1920
/**
@@ -26,7 +27,8 @@ export const concurrentMerkleTreeHeaderDataV1Beet = new beet.BeetArgsStruct<Conc
2627
['maxDepth', beet.u32],
2728
['authority', beetSolana.publicKey],
2829
['creationSlot', beet.u64],
29-
['padding', beet.uniformFixedSizeArray(beet.u8, 6)],
30+
['isBatchInitialized', beet.bool],
31+
['padding', beet.uniformFixedSizeArray(beet.u8, 5)],
3032
],
3133
'ConcurrentMerkleTreeHeaderDataV1',
3234
);

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
ValidDepthSizePair,
2020
} from '../src';
2121
import { hash, MerkleTree } from '../src/merkle-tree';
22+
import { assertCMTProperties } from './accounts/concurrentMerkleTreeAccount.test';
2223
import { createTreeOnChain, execute, prepareTree } from './utils';
2324

2425
// eslint-disable-next-line no-empty
@@ -96,7 +97,7 @@ describe('Account Compression', () => {
9697
const merkleTreeRaw = new MerkleTree(leaves);
9798
const root = merkleTreeRaw.root;
9899
const leaf = leaves[leaves.length - 1];
99-
100+
const canopyDepth = 0;
100101
const finalize = createFinalizeMerkleTreeWithRootIx(
101102
cmt,
102103
payer,
@@ -109,11 +110,7 @@ describe('Account Compression', () => {
109110
await execute(provider, [finalize], [payerKeypair]);
110111

111112
const splCMT = await ConcurrentMerkleTreeAccount.fromAccountAddress(connection, cmt);
112-
assert(splCMT.getMaxBufferSize() === size, 'Buffer size does not match');
113-
assert(
114-
splCMT.getCanopyDepth() === canopyDepth,
115-
'Canopy depth does not match: expected ' + canopyDepth + ' but got ' + splCMT.getCanopyDepth(),
116-
);
113+
assertCMTProperties(splCMT, depth, size, payer, root, canopyDepth, true);
117114
assert(splCMT.getBufferSize() == 1, 'Buffer size does not match');
118115
});
119116
it('Should fail to append canopy node for a tree without canopy', async () => {
@@ -327,6 +324,8 @@ describe('Account Compression', () => {
327324
merkleTreeRaw.getProof(leaves.length - 1).proof,
328325
);
329326
await execute(provider, [finalize], [payerKeypair]);
327+
const splCMT = await ConcurrentMerkleTreeAccount.fromAccountAddress(connection, cmt);
328+
assertCMTProperties(splCMT, depth, size, payer, root, canopyDepth, true);
330329
});
331330
it('Should be able to setup canopy with several transactions', async () => {
332331
const merkleTreeRaw = new MerkleTree(leaves);

account-compression/sdk/tests/accounts/concurrentMerkleTreeAccount.test.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import { ALL_DEPTH_SIZE_PAIRS, ConcurrentMerkleTreeAccount, getConcurrentMerkleT
88
import { emptyNode, MerkleTree } from '../../src/merkle-tree';
99
import { createEmptyTreeOnChain, createTreeOnChain } from '../utils';
1010

11-
function assertCMTProperties(
11+
export function assertCMTProperties(
1212
onChainCMT: ConcurrentMerkleTreeAccount,
1313
expectedMaxDepth: number,
1414
expectedMaxBufferSize: number,
1515
expectedAuthority: PublicKey,
1616
expectedRoot: Buffer,
17-
expectedCanopyDepth?: number
17+
expectedCanopyDepth?: number,
18+
expectedIsBatchInitialized = false,
1819
) {
1920
assert(
2021
onChainCMT.getMaxDepth() === expectedMaxDepth,
@@ -32,6 +33,10 @@ function assertCMTProperties(
3233
'On chain canopy depth does not match expected canopy depth'
3334
);
3435
}
36+
assert(
37+
onChainCMT.getIsBatchInitialized() === expectedIsBatchInitialized,
38+
'On chain isBatchInitialized does not match expected value'
39+
);
3540
}
3641

3742
describe('ConcurrentMerkleTreeAccount tests', () => {
@@ -142,4 +147,26 @@ describe('ConcurrentMerkleTreeAccount tests', () => {
142147
}
143148
});
144149
});
150+
151+
describe('Can deserialize an existing CMTAccount from a real on-chain CMT created before the is_batch_initialized field was introduced inplace of the first byte of _padding', () => {
152+
it('Interpreted on-chain fields correctly', async () => {
153+
// The account data was generated by running:
154+
// $ solana account 27QMkDMpBoAhmWj6xxQNYdqXZL5nnC8tkZcEtkNxCqeX \
155+
// --output-file tests/fixtures/pre-batch-init-tree-account.json \
156+
// --output json
157+
const deployedAccount = new PublicKey('27QMkDMpBoAhmWj6xxQNYdqXZL5nnC8tkZcEtkNxCqeX');
158+
const cmt = await ConcurrentMerkleTreeAccount.fromAccountAddress(
159+
connection,
160+
deployedAccount,
161+
'confirmed'
162+
);
163+
const expectedMaxDepth = 10;
164+
const expectedMaxBufferSize = 32;
165+
const expectedCanopyDepth = 0;
166+
const expectedAuthority = new PublicKey('BFNT941iRwYPe2Js64dTJSoksGCptWAwrkKMaSN73XK2');
167+
const expectedRoot = new PublicKey('83UjseEuEgxyVyDTmrJCQ9QbeksdRZ7KPDZGQYc5cAgF').toBuffer();
168+
const expectedIsBatchInitialized = false;
169+
await assertCMTProperties(cmt, expectedMaxDepth, expectedMaxBufferSize, expectedAuthority, expectedRoot, expectedCanopyDepth, expectedIsBatchInitialized);
170+
});
171+
});
145172
});

account-compression/sdk/tests/fixtures/pre-batch-init-tree-account.json

Lines changed: 14 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)