Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
6d4720a
state update simplified
sergeytimoshin Nov 23, 2025
b7abf38
format
sergeytimoshin Nov 23, 2025
cd3242c
append+nullify works. epoch transition wip
sergeytimoshin Nov 23, 2025
ca787e8
wip
sergeytimoshin Nov 23, 2025
1ee207c
test green
sergeytimoshin Nov 24, 2025
d28d5f8
fetch all batches
sergeytimoshin Nov 24, 2025
d2ea7b4
reduce rpc calls
sergeytimoshin Nov 25, 2025
1e17478
bump photon hash
sergeytimoshin Nov 25, 2025
5e19233
reorg module
sergeytimoshin Nov 25, 2025
0a4dd78
cleanup
sergeytimoshin Nov 25, 2025
a91e376
cleanup
sergeytimoshin Nov 25, 2025
ea59891
cleanup
sergeytimoshin Nov 25, 2025
6c55aaa
cleanup
sergeytimoshin Nov 25, 2025
5e966bd
cleanup
sergeytimoshin Nov 25, 2025
8c1d1aa
cleanup
sergeytimoshin Nov 25, 2025
a58ac90
cleanup
sergeytimoshin Nov 25, 2025
074fbc2
cleanup
sergeytimoshin Nov 25, 2025
0c1fa8a
cleanup
sergeytimoshin Nov 25, 2025
1d251ad
cleanup
sergeytimoshin Nov 25, 2025
89110ca
cleanup
sergeytimoshin Nov 25, 2025
442daec
cleanup
sergeytimoshin Nov 25, 2025
dbdac0b
cleanup
sergeytimoshin Nov 25, 2025
0060bf3
cleanup
sergeytimoshin Nov 25, 2025
9067dc6
cleanup
sergeytimoshin Nov 25, 2025
6ad54fd
cleanup
sergeytimoshin Nov 25, 2025
bd3fccb
cleanup
sergeytimoshin Nov 25, 2025
a43a9b2
cleanup
sergeytimoshin Nov 25, 2025
7912b30
cleanup
sergeytimoshin Nov 25, 2025
947c250
cleanup
sergeytimoshin Nov 25, 2025
0d30616
cleanup
sergeytimoshin Nov 25, 2025
3784df4
cleanup
sergeytimoshin Nov 25, 2025
c464efe
cleanup
sergeytimoshin Nov 25, 2025
1386c3c
Update forester/src/processor/v2/state/helpers.rs
sergeytimoshin Nov 25, 2025
5d2b3c3
cleanup
sergeytimoshin Nov 25, 2025
80bb32d
cleanup
sergeytimoshin Nov 25, 2025
6fbf4a7
cleanup
sergeytimoshin Nov 25, 2025
f10ac8c
cleanup
sergeytimoshin Nov 26, 2025
1db6f3a
cleanup
sergeytimoshin Nov 26, 2025
e6ffd04
cleanup
sergeytimoshin Nov 26, 2025
891ba99
cleanup
sergeytimoshin Nov 26, 2025
e1373cb
cleanup
sergeytimoshin Nov 26, 2025
697dedb
cleanup
sergeytimoshin Nov 26, 2025
2af5e1b
cleanup
sergeytimoshin Nov 26, 2025
7fdc4da
feat: add prover polling interval and max wait time to config
sergeytimoshin Nov 26, 2025
6f06524
add prover polling interval and max wait time to test configuration
sergeytimoshin Nov 26, 2025
bb99b50
cleanup
sergeytimoshin Nov 26, 2025
7dcdc22
cleanup
sergeytimoshin Nov 26, 2025
e8ff2c0
cleanup
sergeytimoshin Nov 26, 2025
4b9614c
reduce slots stop threshold to 1
sergeytimoshin Nov 26, 2025
cbc00c6
refactor: simplify proof worker spawning
sergeytimoshin Nov 27, 2025
00cadc7
cleanup
sergeytimoshin Nov 27, 2025
162ea99
feat: add method to get end of consecutive eligible slots in TreeFore…
sergeytimoshin Nov 27, 2025
7b125f3
fix: handle tree height mismatch in node insertion
sergeytimoshin Nov 27, 2025
c48dd7c
fix: skip node insertion for root level to prevent tree height mismat…
sergeytimoshin Nov 27, 2025
a4044c5
addresses wip
sergeytimoshin Nov 27, 2025
f2c0a16
wip
sergeytimoshin Nov 27, 2025
1e9fd9d
e2e test green
sergeytimoshin Nov 27, 2025
a49ebfb
wip
sergeytimoshin Nov 27, 2025
f0d2190
wip
sergeytimoshin Nov 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[workspace]

members = [
"program-libs/account-checks",
"program-libs/array-map",
Expand Down
5 changes: 5 additions & 0 deletions forester-utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ light-indexed-merkle-tree = { workspace = true }
light-compressed-account = { workspace = true, features = ["std"] }
light-batched-merkle-tree = { workspace = true }
light-merkle-tree-metadata = { workspace = true }
light-merkle-tree-reference = { workspace = true }
light-sparse-merkle-tree = { workspace = true }
light-account-checks = { workspace = true }
light-sdk = { workspace = true }
Expand All @@ -39,10 +40,12 @@ anchor-lang = { workspace = true }
solana-sdk = { workspace = true }

thiserror = { workspace = true }
anyhow = { workspace = true }

tracing = { workspace = true }

num-traits = { workspace = true }
num-bigint = { workspace = true }

bb8 = { workspace = true }
async-trait = { workspace = true }
Expand All @@ -51,3 +54,5 @@ governor = { workspace = true }
[dev-dependencies]
tokio-postgres = "0.7"
bs58 = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
166 changes: 166 additions & 0 deletions forester-utils/src/address_staging_tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
use light_batched_merkle_tree::constants::DEFAULT_BATCH_ADDRESS_TREE_HEIGHT;
use light_hasher::Poseidon;
use light_prover_client::proof_types::batch_address_append::{
get_batch_address_append_circuit_inputs, BatchAddressAppendInputs,
};
use light_sparse_merkle_tree::{
changelog::ChangelogEntry, indexed_changelog::IndexedChangelogEntry, SparseMerkleTree,
};
use tracing::debug;

use crate::error::ForesterUtilsError;

const HEIGHT: usize = DEFAULT_BATCH_ADDRESS_TREE_HEIGHT as usize;

/// Result of processing a batch of address appends
#[derive(Clone, Debug)]
pub struct AddressBatchResult {
pub circuit_inputs: BatchAddressAppendInputs,
pub new_root: [u8; 32],
pub old_root: [u8; 32],
}

/// Staging tree for indexed (address) Merkle trees.
/// Uses SparseMerkleTree and changelogs to properly compute proofs
/// for batch address appends with concurrent updates.
#[derive(Clone, Debug)]
pub struct AddressStagingTree {
sparse_tree: SparseMerkleTree<Poseidon, HEIGHT>,
changelog: Vec<ChangelogEntry<HEIGHT>>,
indexed_changelog: Vec<IndexedChangelogEntry<usize, HEIGHT>>,
current_root: [u8; 32],
next_index: usize,
}

impl AddressStagingTree {
/// Creates a new AddressStagingTree from subtrees data.
///
/// # Arguments
/// * `subtrees` - Array of subtree hashes for SparseMerkleTree initialization
/// * `start_index` - The tree's next_index where new leaves will be appended
/// * `initial_root` - The current root of the tree
pub fn new(subtrees: [[u8; 32]; HEIGHT], start_index: usize, initial_root: [u8; 32]) -> Self {
debug!(
"AddressStagingTree::new: start_index={}, initial_root={:?}[..4]",
start_index,
&initial_root[..4]
);

Self {
sparse_tree: SparseMerkleTree::new(subtrees, start_index),
changelog: Vec::new(),
indexed_changelog: Vec::new(),
current_root: initial_root,
next_index: start_index,
}
}

/// Creates a new AddressStagingTree from a Vec of subtrees.
/// The subtrees Vec must have exactly HEIGHT elements.
pub fn from_subtrees_vec(
subtrees: Vec<[u8; 32]>,
start_index: usize,
initial_root: [u8; 32],
) -> Result<Self, ForesterUtilsError> {
let subtrees_array: [[u8; 32]; HEIGHT] =
subtrees.try_into().map_err(|v: Vec<[u8; 32]>| {
ForesterUtilsError::AddressStagingTree(format!(
"Invalid subtrees length: expected {}, got {}",
HEIGHT,
v.len()
))
})?;
Ok(Self::new(subtrees_array, start_index, initial_root))
}

/// Returns the current root of the tree.
pub fn current_root(&self) -> [u8; 32] {
self.current_root
}

/// Returns the current next_index of the tree.
pub fn next_index(&self) -> usize {
self.next_index
}

/// Processes a batch of address appends and returns the circuit inputs.
///
/// # Arguments
/// * `addresses` - The new addresses (element values) to append
/// * `low_element_values` - Values of low elements
/// * `low_element_next_values` - Next values of low elements
/// * `low_element_indices` - Indices of low elements
/// * `low_element_next_indices` - Next indices of low elements
/// * `low_element_proofs` - Merkle proofs for low elements
/// * `leaves_hashchain` - Pre-computed hash chain of the addresses
/// * `zkp_batch_size` - Number of addresses in this batch
#[allow(clippy::too_many_arguments)]
pub fn process_batch(
&mut self,
addresses: Vec<[u8; 32]>,
low_element_values: Vec<[u8; 32]>,
low_element_next_values: Vec<[u8; 32]>,
low_element_indices: Vec<usize>,
low_element_next_indices: Vec<usize>,
low_element_proofs: Vec<Vec<[u8; 32]>>,
leaves_hashchain: [u8; 32],
zkp_batch_size: usize,
) -> Result<AddressBatchResult, ForesterUtilsError> {
let old_root = self.current_root;
let start_index = self.next_index;

debug!(
"AddressStagingTree::process_batch: {} addresses, start_index={}, old_root={:?}[..4]",
addresses.len(),
start_index,
&old_root[..4]
);

let circuit_inputs = get_batch_address_append_circuit_inputs::<HEIGHT>(
start_index,
old_root,
low_element_values,
low_element_next_values,
low_element_indices,
low_element_next_indices,
low_element_proofs,
addresses,
&mut self.sparse_tree,
leaves_hashchain,
zkp_batch_size,
&mut self.changelog,
&mut self.indexed_changelog,
)
.map_err(|e| {
ForesterUtilsError::AddressStagingTree(format!("Circuit input error: {}", e))
})?;

// Update state
let new_root =
light_hasher::bigint::bigint_to_be_bytes_array::<32>(&circuit_inputs.new_root)
.map_err(|e| {
ForesterUtilsError::AddressStagingTree(format!("Root conversion error: {}", e))
})?;

self.current_root = new_root;
self.next_index += zkp_batch_size;

debug!(
"AddressStagingTree::process_batch complete: new_root={:?}[..4], next_index={}",
&new_root[..4],
self.next_index
);

Ok(AddressBatchResult {
circuit_inputs,
new_root,
old_root,
})
}

/// Clears the changelogs. Call this when resetting the staging tree.
pub fn clear_changelogs(&mut self) {
self.changelog.clear();
self.indexed_changelog.clear();
}
}
6 changes: 6 additions & 0 deletions forester-utils/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,10 @@ pub enum ForesterUtilsError {

#[error("pool error: {0}")]
Pool(#[from] PoolError),

#[error("error: {0}")]
StagingTree(String),

#[error("address staging tree error: {0}")]
AddressStagingTree(String),
}
17 changes: 17 additions & 0 deletions forester-utils/src/forester_epoch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,23 @@ impl TreeForesterSchedule {
pub fn is_eligible(&self, forester_slot: u64) -> bool {
self.slots[forester_slot as usize].is_some()
}

/// Returns the end solana slot of the last consecutive eligible slot
/// starting from the given light slot index.
pub fn get_consecutive_eligibility_end(&self, from_slot_idx: usize) -> Option<u64> {
let mut last_eligible_end = None;

for slot_opt in self.slots.iter().skip(from_slot_idx) {
match slot_opt {
Some(slot) => {
last_eligible_end = Some(slot.end_solana_slot);
}
None => break,
}
}

last_eligible_end
}
}

#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, Default, PartialEq, Eq)]
Expand Down
Loading