Skip to content

Commit e338d27

Browse files
committed
refactor: relax output tree ordering wip
1 parent 3e4751b commit e338d27

File tree

6 files changed

+208
-90
lines changed

6 files changed

+208
-90
lines changed

program-tests/system-test/tests/test.rs

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -522,26 +522,26 @@ pub async fn failing_transaction_inputs_inner<R: Rpc>(
522522
.await
523523
.unwrap();
524524
}
525-
// output Merkle tree is not unique (we need at least 2 outputs for this test)
526-
if num_outputs > 1 {
527-
let mut inputs_struct = inputs_struct.clone();
528-
let mut remaining_accounts = remaining_accounts.clone();
529-
let remaining_mt_acc = remaining_accounts
530-
[inputs_struct.output_compressed_accounts[1].merkle_tree_index as usize]
531-
.clone();
532-
remaining_accounts.push(remaining_mt_acc);
533-
inputs_struct.output_compressed_accounts[1].merkle_tree_index =
534-
(remaining_accounts.len() - 1) as u8;
535-
create_instruction_and_failing_transaction(
536-
rpc,
537-
payer,
538-
inputs_struct,
539-
remaining_accounts.clone(),
540-
SystemProgramError::OutputMerkleTreeNotUnique.into(),
541-
)
542-
.await
543-
.unwrap();
544-
}
525+
// // output Merkle tree is not unique (we need at least 2 outputs for this test)
526+
// if num_outputs > 1 {
527+
// let mut inputs_struct = inputs_struct.clone();
528+
// let mut remaining_accounts = remaining_accounts.clone();
529+
// let remaining_mt_acc = remaining_accounts
530+
// [inputs_struct.output_compressed_accounts[1].merkle_tree_index as usize]
531+
// .clone();
532+
// remaining_accounts.push(remaining_mt_acc);
533+
// inputs_struct.output_compressed_accounts[1].merkle_tree_index =
534+
// (remaining_accounts.len() - 1) as u8;
535+
// create_instruction_and_failing_transaction(
536+
// rpc,
537+
// payer,
538+
// inputs_struct,
539+
// remaining_accounts.clone(),
540+
// SystemProgramError::OutputMerkleTreeNotUnique.into(),
541+
// )
542+
// .await
543+
// .unwrap();
544+
// }
545545
Ok(())
546546
}
547547

programs/system/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ pinocchio-pubkey = { workspace = true }
5656
solana-msg = { workspace = true }
5757
light-program-profiler = { workspace = true }
5858
light-heap = { workspace = true, optional = true }
59+
light-array-map = { workspace = true }
60+
arrayvec = { workspace = true }
5961
[dev-dependencies]
6062
rand = { workspace = true }
6163
light-compressed-account = { workspace = true, features = [

programs/system/src/errors.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use light_account_checks::error::AccountError;
2+
use light_array_map::ArrayMapError;
23
use light_batched_merkle_tree::errors::BatchedMerkleTreeError;
34
use light_concurrent_merkle_tree::errors::ConcurrentMerkleTreeError;
45
use light_indexed_merkle_tree::errors::IndexedMerkleTreeError;
@@ -140,6 +141,10 @@ pub enum SystemProgramError {
140141
PackedAccountIndexOutOfBounds,
141142
#[error("Unimplemented.")]
142143
Unimplemented,
144+
#[error("Too many output V2 queues (max 30).")]
145+
TooManyOutputV2Queues,
146+
#[error("Too many output V1 trees (max 30).")]
147+
TooManyOutputV1Trees,
143148
#[error("Batched Merkle tree error {0}")]
144149
BatchedMerkleTreeError(#[from] BatchedMerkleTreeError),
145150
#[error("Concurrent Merkle tree error {0}")]
@@ -223,6 +228,8 @@ impl From<SystemProgramError> for u32 {
223228
SystemProgramError::Unimplemented => 6063,
224229
SystemProgramError::CpiContextDeactivated => 6064,
225230
SystemProgramError::InputMerkleTreeIndexOutOfBounds => 6065,
231+
SystemProgramError::TooManyOutputV2Queues => 6066,
232+
SystemProgramError::TooManyOutputV1Trees => 6067,
226233
SystemProgramError::BatchedMerkleTreeError(e) => e.into(),
227234
SystemProgramError::IndexedMerkleTreeError(e) => e.into(),
228235
SystemProgramError::ConcurrentMerkleTreeError(e) => e.into(),
@@ -238,3 +245,12 @@ impl From<SystemProgramError> for ProgramError {
238245
ProgramError::Custom(e.into())
239246
}
240247
}
248+
249+
impl From<ArrayMapError> for SystemProgramError {
250+
fn from(e: ArrayMapError) -> Self {
251+
match e {
252+
ArrayMapError::CapacityExceeded => SystemProgramError::TooManyOutputV2Queues,
253+
ArrayMapError::IndexOutOfBounds => SystemProgramError::OutputMerkleTreeIndexOutOfBounds,
254+
}
255+
}
256+
}

programs/system/src/processor/create_outputs_cpi_data.rs

Lines changed: 94 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use light_array_map::ArrayMap;
12
use light_compressed_account::{
23
hash_to_bn254_field_size_be,
34
instruction_data::{
@@ -10,6 +11,7 @@ use light_hasher::{Hasher, Poseidon};
1011
use light_program_profiler::profile;
1112
use pinocchio::{account_info::AccountInfo, msg, program_error::ProgramError};
1213

14+
use super::tree_leaf_tracker_ext::TreeLeafTrackerTupleExt;
1315
use crate::{
1416
accounts::remaining_account_checks::AcpAccount,
1517
context::{SystemContext, WrappedInstructionData},
@@ -42,18 +44,15 @@ pub fn create_outputs_cpi_data<'a, 'info, T: InstructionData<'a>>(
4244
if inputs.output_len() == 0 {
4345
return Ok([0u8; 32]);
4446
}
45-
let mut current_index: i16 = -1;
46-
let mut num_leaves_in_tree: u32 = 0;
47-
let mut mt_next_index: u32 = 0;
4847
let mut hashed_merkle_tree = [0u8; 32];
4948
cpi_ix_data.start_output_appends = context.account_indices.len() as u8;
50-
let mut index_merkle_tree_account_account = cpi_ix_data.start_output_appends;
49+
// TODO: check correct index use and deduplicate if possible.
50+
let mut current_index: i16 = -1;
51+
let mut next_account_index = cpi_ix_data.start_output_appends;
5152
let mut index_merkle_tree_account = 0;
52-
let number_of_merkle_trees =
53-
inputs.output_accounts().last().unwrap().merkle_tree_index() as usize + 1;
5453

55-
let mut merkle_tree_pubkeys =
56-
Vec::<light_compressed_account::pubkey::Pubkey>::with_capacity(number_of_merkle_trees);
54+
// Track (tree_pubkey, (next_leaf_index, account_index)) for each unique tree
55+
let mut tree_leaf_tracker = ArrayMap::<[u8; 32], (u64, u8), 30>::new();
5756
let mut hash_chain = [0u8; 32];
5857
let mut rollover_fee = 0;
5958
let mut is_batched = true;
@@ -62,50 +61,95 @@ pub fn create_outputs_cpi_data<'a, 'info, T: InstructionData<'a>>(
6261
// if mt index == current index Merkle tree account info has already been added.
6362
// if mt index != current index, Merkle tree account info is new, add it.
6463
#[allow(clippy::comparison_chain)]
65-
if account.merkle_tree_index() as i16 == current_index {
66-
// Do nothing, but it is the most common case.
67-
} else if account.merkle_tree_index() as i16 > current_index {
64+
let (leaf_index, account_index) = if account.merkle_tree_index() as i16 == current_index {
65+
// Same tree as previous iteration - just increment leaf index
66+
tree_leaf_tracker.increment_current_tuple()?
67+
} else {
6868
current_index = account.merkle_tree_index().into();
69-
70-
let pubkey = match &accounts
69+
// Get tree/queue pubkey and metadata
70+
match &accounts
7171
.get(current_index as usize)
7272
.ok_or(SystemProgramError::OutputMerkleTreeIndexOutOfBounds)?
7373
{
7474
AcpAccount::OutputQueue(output_queue) => {
75-
context.set_network_fee(
76-
output_queue.metadata.rollover_metadata.network_fee,
77-
current_index as u8,
78-
);
75+
let initial_leaf_index = output_queue.batch_metadata.next_index;
76+
77+
// Get or insert tree entry - returns ((leaf_idx, account_idx), is_new)
78+
let ((leaf_idx, account_idx), is_new) = tree_leaf_tracker.get_or_insert_tuple(
79+
output_queue.pubkey().array_ref(),
80+
(initial_leaf_index, next_account_index),
81+
SystemProgramError::TooManyOutputV2Queues,
82+
)?;
7983

80-
hashed_merkle_tree = output_queue.hashed_merkle_tree_pubkey;
81-
rollover_fee = output_queue.metadata.rollover_metadata.rollover_fee;
82-
mt_next_index = output_queue.batch_metadata.next_index as u32;
83-
cpi_ix_data.output_sequence_numbers[index_merkle_tree_account as usize] =
84-
MerkleTreeSequenceNumber {
85-
tree_pubkey: output_queue.metadata.associated_merkle_tree,
86-
queue_pubkey: *output_queue.pubkey(),
87-
tree_type: (TreeType::StateV2 as u64).into(),
88-
seq: output_queue.batch_metadata.next_index.into(),
89-
};
90-
is_batched = true;
91-
*output_queue.pubkey()
84+
// Only set up metadata if this is a new tree (first time seeing this pubkey)
85+
if is_new {
86+
// TODO: depulicate logic
87+
context.set_network_fee(
88+
output_queue.metadata.rollover_metadata.network_fee,
89+
current_index as u8,
90+
);
91+
hashed_merkle_tree = output_queue.hashed_merkle_tree_pubkey;
92+
rollover_fee = output_queue.metadata.rollover_metadata.rollover_fee;
93+
is_batched = true;
94+
95+
cpi_ix_data.output_sequence_numbers[index_merkle_tree_account as usize] =
96+
MerkleTreeSequenceNumber {
97+
tree_pubkey: output_queue.metadata.associated_merkle_tree,
98+
queue_pubkey: *output_queue.pubkey(),
99+
tree_type: (TreeType::StateV2 as u64).into(),
100+
seq: initial_leaf_index.into(),
101+
};
102+
103+
context.get_index_or_insert(
104+
account.merkle_tree_index(),
105+
remaining_accounts,
106+
"Output queue for V2 state trees (Merkle tree for V1 state trees)",
107+
)?;
108+
109+
index_merkle_tree_account += 1;
110+
next_account_index += 1;
111+
}
112+
113+
(leaf_idx, account_idx)
92114
}
93115
AcpAccount::StateTree((pubkey, tree)) => {
94-
cpi_ix_data.output_sequence_numbers[index_merkle_tree_account as usize] =
95-
MerkleTreeSequenceNumber {
96-
tree_pubkey: *pubkey,
97-
queue_pubkey: *pubkey,
98-
tree_type: (TreeType::StateV1 as u64).into(),
99-
seq: (tree.sequence_number() as u64 + 1).into(),
100-
};
101-
let merkle_context = context
102-
.get_legacy_merkle_context(current_index as u8)
103-
.unwrap();
104-
hashed_merkle_tree = merkle_context.hashed_pubkey;
105-
rollover_fee = merkle_context.rollover_fee;
106-
mt_next_index = tree.next_index() as u32;
107-
is_batched = false;
108-
*pubkey
116+
let initial_leaf_index = tree.next_index() as u64;
117+
118+
// Get or insert tree entry - returns ((leaf_idx, account_idx), is_new)
119+
let ((leaf_idx, account_idx), is_new) = tree_leaf_tracker.get_or_insert_tuple(
120+
pubkey.array_ref(),
121+
(initial_leaf_index, next_account_index),
122+
SystemProgramError::TooManyOutputV1Trees,
123+
)?;
124+
125+
// Only set up metadata if this is a new tree (first time seeing this pubkey)
126+
if is_new {
127+
cpi_ix_data.output_sequence_numbers[index_merkle_tree_account as usize] =
128+
MerkleTreeSequenceNumber {
129+
tree_pubkey: *pubkey,
130+
queue_pubkey: *pubkey,
131+
tree_type: (TreeType::StateV1 as u64).into(),
132+
seq: (tree.sequence_number() as u64 + 1).into(),
133+
};
134+
135+
let merkle_context = context
136+
.get_legacy_merkle_context(current_index as u8)
137+
.unwrap();
138+
hashed_merkle_tree = merkle_context.hashed_pubkey;
139+
rollover_fee = merkle_context.rollover_fee;
140+
is_batched = false;
141+
142+
context.get_index_or_insert(
143+
account.merkle_tree_index(),
144+
remaining_accounts,
145+
"Output queue for V2 state trees (Merkle tree for V1 state trees)",
146+
)?;
147+
148+
index_merkle_tree_account += 1;
149+
next_account_index += 1;
150+
}
151+
152+
(leaf_idx, account_idx)
109153
}
110154
AcpAccount::Unknown() => {
111155
msg!(
@@ -144,30 +188,8 @@ pub fn create_outputs_cpi_data<'a, 'info, T: InstructionData<'a>>(
144188
SystemProgramError::StateMerkleTreeAccountDiscriminatorMismatch.into(),
145189
);
146190
}
147-
};
148-
// check Merkle tree uniqueness
149-
if merkle_tree_pubkeys.contains(&pubkey) {
150-
return Err(SystemProgramError::OutputMerkleTreeNotUnique.into());
151-
} else {
152-
merkle_tree_pubkeys.push(pubkey);
153191
}
154-
155-
context.get_index_or_insert(
156-
account.merkle_tree_index(),
157-
remaining_accounts,
158-
"Output queue for V2 state trees (Merkle tree for V1 state trees)",
159-
)?;
160-
num_leaves_in_tree = 0;
161-
index_merkle_tree_account += 1;
162-
index_merkle_tree_account_account += 1;
163-
} else {
164-
// Check 2.
165-
// Output Merkle tree indices must be in order since we use the
166-
// number of leaves in a Merkle tree to determine the correct leaf
167-
// index. Since the leaf index is part of the hash this is security
168-
// critical.
169-
return Err(SystemProgramError::OutputMerkleTreeIndicesNotInOrder.into());
170-
}
192+
};
171193

172194
// Check 3.
173195
if let Some(address) = account.address() {
@@ -183,9 +205,11 @@ pub fn create_outputs_cpi_data<'a, 'info, T: InstructionData<'a>>(
183205
return Err(SystemProgramError::InvalidAddress.into());
184206
}
185207
}
186-
cpi_ix_data.output_leaf_indices[j] = (mt_next_index + num_leaves_in_tree).into();
187208

188-
num_leaves_in_tree += 1;
209+
// Use the tracked leaf index from our ArrayVec
210+
cpi_ix_data.output_leaf_indices[j] = u32::try_from(leaf_index)
211+
.map_err(|_| SystemProgramError::PackedAccountIndexOutOfBounds)?
212+
.into();
189213
if account.has_data() && context.invoking_program_id.is_none() {
190214
msg!("Invoking program is not provided.");
191215
msg!("Only program owned compressed accounts can have data.");
@@ -214,7 +238,7 @@ pub fn create_outputs_cpi_data<'a, 'info, T: InstructionData<'a>>(
214238
is_batched,
215239
)
216240
.map_err(ProgramError::from)?;
217-
cpi_ix_data.leaves[j].account_index = index_merkle_tree_account_account - 1;
241+
cpi_ix_data.leaves[j].account_index = account_index;
218242

219243
if !cpi_ix_data.nullifiers.is_empty() {
220244
if j == 0 {

programs/system/src/processor/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ pub mod read_only_account;
77
pub mod read_only_address;
88
pub mod sol_compression;
99
pub mod sum_check;
10+
pub mod tree_leaf_tracker_ext;
1011
pub mod verify_proof;

0 commit comments

Comments
 (0)