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

Commit 5504af4

Browse files
authored
Account Compression: Add CMT checks for out of bounds leaf indices and initialization (#3724)
* cmt: add check for leaf index OOB * ac: add checks for leaf index OOB * ac: add tests for leaf index OOB * nit: fix some poor logging & unused imports * cmt: add initialization checks before public methods * cmt: update LeafContentsModified error message * cmt: make tests easier to read, add PartialEq, Eq to CMTError * ac: make LeafOOB error the last error in the struct to prevent breaking changes * ac: fmt fix
1 parent 44d9f15 commit 5504af4

File tree

15 files changed

+191
-10
lines changed

15 files changed

+191
-10
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

account-compression/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

account-compression/programs/account-compression/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ default = []
2020
[dependencies]
2121
anchor-lang = "0.25.0"
2222
bytemuck = "1.8.0"
23-
spl-concurrent-merkle-tree = { version = "0.1.1", path="../../../libraries/concurrent-merkle-tree", features = [ "sol-log" ]}
23+
spl-concurrent-merkle-tree = { version="0.1.2", path="../../../libraries/concurrent-merkle-tree", features = [ "sol-log" ]}
2424
spl-noop = { version = "0.1.3", path="../noop", features = [ "no-entrypoint" ]}
2525

2626
[profile.release]

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ pub enum AccountCompressionError {
4242
/// Incorrect account type
4343
#[msg("Account provided has incorrect account type")]
4444
IncorrectAccountType,
45+
46+
/// Tree information cannot be processed because the provided leaf_index
47+
/// is out of bounds of tree's maximum leaf capacity
48+
#[msg("Leaf index of concurrent merkle tree is out of bounds")]
49+
LeafIndexOutOfBounds,
4550
}
4651

4752
impl From<&ConcurrentMerkleTreeError> for AccountCompressionError {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ pub mod spl_account_compression {
352352

353353
let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
354354
header.assert_valid_authority(&ctx.accounts.authority.key())?;
355+
header.assert_valid_leaf_index(index)?;
355356

356357
let merkle_tree_size = merkle_tree_get_size(&header)?;
357358
let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);
@@ -428,6 +429,7 @@ pub mod spl_account_compression {
428429

429430
let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
430431
header.assert_valid()?;
432+
header.assert_valid_leaf_index(index)?;
431433

432434
let merkle_tree_size = merkle_tree_get_size(&header)?;
433435
let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);
@@ -498,6 +500,7 @@ pub mod spl_account_compression {
498500

499501
let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?;
500502
header.assert_valid_authority(&ctx.accounts.authority.key())?;
503+
header.assert_valid_leaf_index(index)?;
501504

502505
let merkle_tree_size = merkle_tree_get_size(&header)?;
503506
let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size);

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,11 @@ impl ConcurrentMerkleTreeHeader {
139139
}
140140
Ok(())
141141
}
142+
143+
pub fn assert_valid_leaf_index(&self, leaf_index: u32) -> Result<()> {
144+
if leaf_index >= (1 << self.get_max_depth()) {
145+
return Err(AccountCompressionError::LeafIndexOutOfBounds.into());
146+
}
147+
Ok(())
148+
}
142149
}

account-compression/sdk/idl/spl_account_compression.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,11 @@
622622
"code": 6007,
623623
"name": "IncorrectAccountType",
624624
"msg": "Account provided has incorrect account type"
625+
},
626+
{
627+
"code": 6008,
628+
"name": "LeafIndexOutOfBounds",
629+
"msg": "Leaf index of concurrent merkle tree is out of bounds"
625630
}
626631
],
627632
"metadata": {

account-compression/sdk/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@
3838
"run-tests": "jest tests --detectOpenHandles",
3939
"run-tests:events": "jest tests/events --detectOpenHandles",
4040
"run-tests:accounts": "jest tests/accounts --detectOpenHandles",
41+
"run-tests:e2e": "jest accountCompression.test.ts --detectOpenHandles",
4142
"test:events": "start-server-and-test start-validator http://localhost:8899/health run-tests:events",
4243
"test:accounts": "start-server-and-test start-validator http://localhost:8899/health run-tests:accounts",
44+
"test:e2e": "start-server-and-test start-validator http://localhost:8899/health run-tests:e2e",
4345
"test": "start-server-and-test start-validator http://localhost:8899/health run-tests"
4446
},
4547
"dependencies": {

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,29 @@ createErrorFromNameLookup.set(
200200
() => new IncorrectAccountTypeError()
201201
)
202202

203+
/**
204+
* LeafIndexOutOfBounds: 'Leaf index of concurrent merkle tree is out of bounds'
205+
*
206+
* @category Errors
207+
* @category generated
208+
*/
209+
export class LeafIndexOutOfBoundsError extends Error {
210+
readonly code: number = 0x1778
211+
readonly name: string = 'LeafIndexOutOfBounds'
212+
constructor() {
213+
super('Leaf index of concurrent merkle tree is out of bounds')
214+
if (typeof Error.captureStackTrace === 'function') {
215+
Error.captureStackTrace(this, LeafIndexOutOfBoundsError)
216+
}
217+
}
218+
}
219+
220+
createErrorFromCodeLookup.set(0x1778, () => new LeafIndexOutOfBoundsError())
221+
createErrorFromNameLookup.set(
222+
'LeafIndexOutOfBounds',
223+
() => new LeafIndexOutOfBoundsError()
224+
)
225+
203226
/**
204227
* Attempts to resolve a custom program error from the provided error code.
205228
* @category Errors

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,4 +650,45 @@ describe("Account Compression", () => {
650650
}
651651
});
652652
});
653+
describe(`Having created a tree with 8 leaves`, () => {
654+
beforeEach(async () => {
655+
[cmtKeypair, offChainTree] = await createTreeOnChain(
656+
provider,
657+
payer,
658+
1 << 3,
659+
3,
660+
8,
661+
);
662+
});
663+
it(`Attempt to replace a leaf beyond the tree's capacity`, async () => {
664+
// Ensure that this fails
665+
let outOfBoundsIndex = 8;
666+
const index = outOfBoundsIndex;
667+
const newLeaf = hash(
668+
payer.publicKey.toBuffer(),
669+
Buffer.from(new BN(outOfBoundsIndex).toArray())
670+
);
671+
let proof;
672+
let node;
673+
node = offChainTree.leaves[outOfBoundsIndex - 1].node
674+
proof = getProofOfLeaf(offChainTree, index - 1);
675+
676+
const replaceIx = createReplaceIx(
677+
payer,
678+
cmtKeypair.publicKey,
679+
offChainTree.root,
680+
node,
681+
newLeaf,
682+
index,
683+
proof.map((treeNode) => {
684+
return treeNode.node;
685+
})
686+
);
687+
688+
try {
689+
await execute(provider, [replaceIx], [payer]);
690+
throw Error("This replace instruction should have failed because the leaf index is OOB");
691+
} catch (_e) { }
692+
});
693+
});
653694
});

0 commit comments

Comments
 (0)