feat: add batched state Merkle tree support #265
Merged
pmantica11 merged 121 commits intohelius-labs:mainfrom Jun 5, 2025
Merged
feat: add batched state Merkle tree support #265pmantica11 merged 121 commits intohelius-labs:mainfrom
pmantica11 merged 121 commits intohelius-labs:mainfrom
Conversation
Update src/dao/generated/accounts.rs Co-authored-by: ananas-block <58553958+ananas-block@users.noreply.github.com> Update src/ingester/parser/batch_event_parser.rs Co-authored-by: ananas-block <58553958+ananas-block@users.noreply.github.com> refactor: remove getSubtrees method and related API documentation Update src/ingester/parser/mod.rs Co-authored-by: ananas-block <58553958+ananas-block@users.noreply.github.com> Refactor struct fields to use camelCase naming convention replace the `calculate_two_inputs_hash_chain` function with the `create_two_inputs_hash_chain` method from the `light_compressed_account` crate. Refactor error handling in `parse_public_transaction_event_v2` format Remove obsolete and commented-out account update code Move `node_index_to_leaf_index` function to the appropriate location and remove dead code Add comments to clarify nullifier field usage Add comments to clarify tx_hash field usage in account struct Add comments to clarify seq field usage in account struct Add comments to clarify nullifier_queue_index field usage in account struct Refactor get_compressed_accounts_by_owner module and add common utilities for account filtering add validity proof v2 Add get_validity_proof_v2 and update API specifications remove unnecessary logging fix: mock tests fix for getValidityProof (v1) refactor: remove unused tree height parameters and add getValidityProofV2 method fix: update method name from address_from_bytes to state_from_bytes in mod.rs consistency. fix indexed_accounts query fix get_compressed_account_by_owner v1 refactor: simplify account handling and improve code consistency in transaction processing refactor: remove queue_position handling and update related queries and indexes upd .gitignore cargo fmt and fixed tests
…ccount_data function
wip e2e test make get_compressed_accounts_by_owner_v2 return AccountV2 primitive add mock_tests.rs add prove_by_index to AccountV2 refactor: validate heights of inclusion and non-inclusion proofs refactor: implement AccountWithContext constructor and remove parse_account_data function refactor: move spend_input_accounts_batched function to spend_batch module refactor: rename spend_batch module to spend and move spend_input_accounts function refactor: streamline transaction parsing refactor: restructure ingester/parser module refactor: restructure ingester/persist module Co-authored-by: ananas-block <58553958+ananas-block@users.noreply.github.com> Co-authored-by: ananas-block <58553958+ananas-block@users.noreply.github.com> Co-authored-by: ananas-block <58553958+ananas-block@users.noreply.github.com> test: add compressed token in batched tree test
…ounts and transactions
… instruction index check
… node persistence
…mprovements by setting `Validate::No`. 2. removed commented-out unused variables in the prover logic.
* chore: update dependencies and clean up unused packages * refactor: consolidate PublicTransactionEvent structs and update references * refactor: adjust root_seq assignment and add Debug trait to struct definitions * refactor: proof compression and error handling in prover * refactor: simplify AddressProofInputs, rootIndex: u16 * refactor: extract proof generation for empty tree into separate function
2800b7f to
8812f95
Compare
…mit, num_elements -> limit
23a41fd to
852100d
Compare
pmantica11
approved these changes
Jun 5, 2025
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
This pr adds support for V2 (batched) state and address Merkle tree types. Existing V1 state and address Merkle trees are concurrent Merkle trees. Changes include the indexing logic, and new api endpoints.
Motivation for the V2 update are costs associated with V1 Merkle trees:
V1 (concurrent) Merkle trees are expensive for users in terms of CU, and for the forester in terms of number of transactions.
V2 (batched) Merkle trees reduce the number of forester transactions by 1250x by updating Merkle tree in batches with zero knowledge proofs. Users transactions are cheaper because compressed account hashes and nullifiers are both inserted into queues (in V1 only nullifiers were inserted into queues).
Additionally, the V2 update includes CU performance optimizations in the system program, ie removal of noop event cpi, use of pinocchio instead of anchor, and zero copies instead of borsh for instruction data serialization.
No breaking changes for existing programs.
Life cycle Concurrent Merkle tree:
Lifecycle batched Merkle tree:
Note, a created account can be spent before step 2 is completed. The order of steps can be any of the following combinations:
Changes:
Api
get_compressed_account_v2returns
AccountV2get_compressed_account_proof_v2returns
GetCompressedAccountProofResponseValueV2which includes:2.1.
prove_by_indexis set to true if account isin_output_queue2.2.
tree_context(tree pubkey, queue pubkey, tree type, cpi context pubkey (placeholder))get_multiple_compressed_account_proofs_v2returns
GetCompressedAccountProofResponseValueV2get_compressed_token_accounts_by_owner_v2returns
PaginatedAccountListV2ofAccountV2get_compressed_token_accounts_by_delegate_v2returns
TokenAccountListResponseV2, containsAccountV2get_queue_elements(See notion doc for more details.)
get_compressed_accounts_by_owner_v2return TokenAccountListV2 which contains AccountV2
get_multiple_compressed_accounts_v2return
AccountListV2which containsAccountV2get_transaction_with_compression_info_v2returns CompressionInfoV2 which includes opened and closed accounts v2
get_validity_proof_v2get_batch_address_update_infoAccount Model
src/dao/generated/accounts.rsChanged Properties:
seq: i64->seq: Option<i64>In concurrent trees seq is always some since account hashes are directly appended to the Merkle tree by the user transaction.
Accounts that are inserted into batched trees are first inserted into the trees output queue, at that time the account has no sequence number.
New Fields:
queueQueue pubkey, for batched trees the output queue pubkey, for concurrent trees the nullifier queue pubkey.
(For batched trees the nullifier queue is part of the Merkle tree account.)
We added the output queue property so that we can return the queue efficiently in
get_compressed_account_v2.We need the output queue to spend accounts in batched trees.
in_output_queueIf true, account is in output queue and not in the tree yet.
Once the account is inserted into the tree by a forester event.
Note, the account can be in the nullifier queue or nullified in the Merkle tree and still be in the output queue.
nullifier_queue_indexIndex of value in a batched nullifier queue.
Used by
get_queue_elements.nullified_in_treetrue-> account is nullified in tree ->nullifier_queue_indexis Noneis a separate field for // could we just say
nullifier_queue_index.is_none && spent?tree_typeEither concurrent or batched.
Clients need to know which tree type an account is in to deserialize accounts correctly.
nullifierOnly used to nullify accounts in batched trees.
Accounts in concurrent trees are nullified by updating the leaf the account hash is stored in with
zero(the nullifier is constant for all account hashes).
For accounts in batched trees the nullifier is H(account_hash, leaf_index, tx_hash). This allows to do a zk proof about
the transaction an account was spent in.
tx_hashUnique per public transaction event.
tx_hash=H(hash_chain_input_account_hashes, hash_chain_output_account_hashes, slot)Tx hash is only created when a transaction has input compressed accounts.
Parser
src/ingester/parser/indexer_events.rs1.1.
MerkleTreeEventAdded events emitted by forester transactions updating batched Merkle trees.
1.1.1.
BatchAppend(BatchEvent)1.1.2.
BatchNullify(BatchEvent)1.1.3.
BatchAddressAppend(BatchEvent)1.2.
BatchPublicTransactionEventParsed from user transactions system program and account compression program instructions.
Wraps
PublicTransactionEvent.src/ingester/parser/merkle_tree_events_parser.rsExtracts logic to parse Merkle tree events from
parser/mod.rs2.1.
parse_merkle_tree_eventDeserializes event from an instruction.
2.2.
parse_legacy_nullifier_eventParses legacy (concurrent) nullifier event.
2.3.
parse_indexed_merkle_tree_updateParses an indexed Merkle tree event.
src/ingester/parser/mod.rs3.1. add if branch to
parse_public_transaction_event_v23.2. refactor existing event parsing logic into
parse_legacy_public_transaction_eventandparse_merkle_tree_event3.3. move helper functions unmodified into separate files
src/ingester/parser/state_update.rs4.1.
pub out_accounts: Vec<Account>->Vec<AccountWithContext>4.2.
pub batch_events: IndexedBatchEvents,Contains both batch append and batch nullify events.
4.3.
pub input_context: Vec<BatchNullifyContext>Contains context to insert accounts into the batched input queue.
4.4.
merge_updatesinclude added fieldssrc/ingester/parser/tree_info.rsAdds global hash set to map
tree pubkey->queue pubkeyandqueue pubkey->tree pubkeyWe need the mapping to determine the tree pubkey for accounts created in a batched Merkle tree since these account hashes are inserted into the output queue -> the event doesn't contain the tree pubkey.
As the number of trees grows this should likely become a separate table instead of a hash map that needs to be filled manually.
src/ingester/parser/tx_event_parser.rsContains parsing logic moved from
mod.rsto parsePublicTransactionEventfrom legacy transactions.src/ingester/parser/tx_event_parser_v2.rsContains new parsing logic to parse events from a light transaction (one or multiple system program instructions and one account compression program instruction) without the event emitted in an explicit noop cpi.
7.1.
event_from_light_transactiondoes the parsing and returnsVec<BatchPublicTransactionEvent>Persist
src/ingester/persist/mod.rs1.1. remove hardcoded tree height
1.2. add logic to insert into input queues
spend_input_accounts_batched1.3. add logic to persist batched events
persist_batch_events1.4.
append_output_accountsadapted to fill all new fields1.5. move
spend_input_accountsunmodified tosrc/ingester/persist/spend.rssrc/ingester/persist/leaf_node.rsunchanged logic moved fromsrc/ingester/persist/persisted_state_tree.rssrc/ingester/persist/leaf_node_proof.rsunchanged logic moved fromsrc/ingester/persist/persisted_state_tree.rs3.1. proof validation is commented because it makes new tests 20x slower
src/ingester/persist/merkle_proof_with_context.rsContains
MerkleProofWithContextandvalidatemethodsrc/ingester/persist/persisted_batch_event.rsPersists both batch append and batch nullify events.
src/ingester/persist/persisted_indexed_merkle_tree.rs6.1. adapt filling
seqtoOption<i64>src/ingester/persist/persisted_state_tree.rs7.1.
get_proof_nodesinclude empty leaves in proof needed forget_queue_elementssrc/ingester/persist/spend.rs8.1. unchanged
spend_input_accountsmoved frommod.rs8.2.
spend_input_accounts_batchedUpdate the nullifier queue index and nullifier of the input accounts in batched trees.
Migrations
Added migration from current to new account model.
Tests
tests/integration_tests/batched_state_tree_tests.rsContains 3 new tests, test data is generated with export-photon-test-transactions.sh in light-protocol.
1.1.
test_batched_tree_transactionsTested API calls with data in a batched state tree:
- get compressed account by owner
- get compressed account proofs
- correct root update after batch append and batch nullify events
-
get_validity_proof_v2-
get_queue_elements(for input and output queues)Test data:
- 50 active compressed accounts with 1_000_000 each owned by
Pubkey::new_unique()- 50 nullified compressed accounts
- All accounts are inserted into the batched Merkle tree
- 10 append events and 5 nullify events (zero knowledge proof size 10)
- Queues are empty once all transactions are indexed
1.2.
test_batched_tree_token_transactionsTest correct indexing of token accounts in a batched state Merkle tree.
Data:
- 4 recipients with 1 token account each
- 1 sender with 3 token accounts
Asserts:
- Sender has 3 token accounts with 12341 balance each.
- Recipients have 1 token account each with 9255, 9255, 9255, 9258 balance.
- Sender's token balances are correct.
- Recipients' token balances are correct.
1.3.
test_four_cpi_eventsTest indexes a transaction which creates 4 compressed accounts in 4 CPIs that create a transaction event each in one outer instruction.
tests/integration_tests/e2e_tests.rs(asserted with assert_json_snapshot)
2.1.
get_compressed_token_accounts_by_owner_v22.2.
get_validity_proof_v22.3.
get_transaction_helper_v2tests/integration_tests/mock_tests.rs(V1 tests already existed.)
3.1.
test_multiple_accountsTest V1 accounts with V1 and V2 endpoints:
-
get_compressed_accounts_by_owner-
get_compressed_accounts_by_owner_v2-
get_multiple_compressed_accounts-
get_multiple_compressed_accounts_v2-
get_compressed_account-
get_compressed_account_v23.2.
test_persist_token_dataTest V1 token accounts with V1 and V2 endpoints:
-
get_compressed_token_accounts_by_owner-
get_compressed_token_accounts_by_owner_v2-
get_compressed_token_balances_by_owner-
get_compressed_token_balances_by_owner_v2-
get_compressed_token_account_balance-
get_compressed_token_accounts_by_delegate-
get_compressed_token_accounts_by_delegate_v2Notes
QUEUE_TREE_MAPPINGshould probably be a temporary solution and be replaced in a different pr, for example by a tree metadata table.