Skip to content

Conversation

@ss-es
Copy link
Contributor

@ss-es ss-es commented Jan 26, 2026

  • persistence for catchup and for reward claims are separated (and neither uses the old database)
  • we serve merkle proofs at the old reward claim interface, but only for the latest height in the reward claim contract. these are served from the database, similar to before (slightly simplified)
  • we serve catchup requests at a new endpoint, reward-state-v2/reward-merkle-tree-v2/:height. this is a GET endpoint that returns the serialized (account, balance) pairs or (if something went really wrong) the full serialized tree as a fallback. there is a wrapper enum RewardMerkleTreeV2Data that handles these cases. for reference, at 500 delegators/node on demo-native (so 2500 total), the endpoint returns ~438KB of data (key/value pairs). the data here also comes directly from storageoverall,
  • we no longer persist anything in memory. but this is a bit misleading since we need to reconstruct a historical merkle tree whenever the light client's block height is updated. I think typically, however, we shouldn't hold more than 2-3 merkle trees in memory at a time (and we'll soon have improvements for this)

@gemini-code-assist
Copy link

Summary of Changes

Hello @ss-es, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly refactors the storage and access mechanism for RewardMerkleTreeV2 data. The core change involves moving the storage of these Merkle trees from the SQL database to an in-memory, thread-safe registry. This change aims to improve performance by providing faster access to reward account proofs, as the data will be readily available in memory rather than requiring database lookups. The in-memory trees are populated during state updates for relevant epoch versions, and a configurable history of these trees can be maintained.

Highlights

  • In-memory storage for RewardMerkleTreeV2: RewardMerkleTreeV2 instances are now stored in a global, thread-safe, in-memory registry (a HashMap protected by RwLock and initialized with OnceLock) for faster access.
  • Optimized proof retrieval: The load_v2_reward_account_proof method now directly retrieves proofs from the in-memory RewardMerkleTreeV2 registry, eliminating the need for database queries for these proofs.
  • Database persistence removed: The previous logic for storing RewardMerkleTreeV2 proofs in the SQL database has been removed, streamlining the data flow.
  • Dynamic history retention: The in-memory registry for RewardMerkleTreeV2 can retain a configurable history of trees based on the REWARD_MERKLE_TREE_V2_RETAIN_HISTORY environment variable, allowing for flexible memory management.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the storage mechanism for RewardMerkleTreeV2, moving it from persistent database storage to an in-memory registry. This change simplifies the data access logic for RewardMerkleTreeV2 proofs and allows for more efficient retrieval. However, there are a couple of areas that could be improved regarding default retention policy and potential performance implications for batch operations.

Comment on lines 1347 to 1350
let n = std::env::var("REWARD_MERKLE_TREE_V2_RETAIN_HISTORY")
.ok()
.and_then(|n| n.parse::<u64>().ok())
.unwrap_or(0);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The environment variable REWARD_MERKLE_TREE_V2_RETAIN_HISTORY defaults to 0 if not set or unparseable. This means that by default, no historical RewardMerkleTreeV2 instances will be retained in memory. This might lead to unexpected behavior for historical queries if not explicitly configured, as the name "retain history" suggests some level of retention. Consider setting a more sensible default (e.g., 1 or a small number) or adding a clear warning in the documentation about this default behavior.

            let n = std::env::var("REWARD_MERKLE_TREE_V2_RETAIN_HISTORY")
                .ok()
                .and_then(|n| n.parse::<u64>().ok())
                .unwrap_or(10); // Default to retaining 10 historical trees

Comment on lines 733 to 743
for account in accounts {
// Clone things we will need in the closure
let db_clone = db.clone();
let account_clone = *account;
let header_height = header.height();

// Create the closure that will get the path for the account
let func = async move {
// Open a new transaction
let mut tx = db_clone
.read()
.await
.with_context(|| "failed to open read transaction")?;

// Get the path for the account
let proof = tx
.get_path(
Snapshot::<SeqTypes, RewardMerkleTreeV2, { RewardMerkleTreeV2::ARITY }>::Index(
header_height,
),
account_clone,
)
.await
.with_context(|| {
format!(
"failed to get path for v2 reward account {account_clone:?}; height \
{height}"
)
})?;

Ok::<_, anyhow::Error>(proof)
};

// Spawn the task
let id = join_set.spawn(func).id();

// Add the task ID to the account map
task_id_to_account.insert(id, account);
}

// Wait for each task to complete
while let Some(result) = join_set.join_next_with_id().await {
// Get the inner result (past the join error)
let (id, result) = result.with_context(|| "failed to join task")?;

// Get the proof from the result
let proof = result?;

// Get the account from the task ID to account map
let account = task_id_to_account
.remove(&id)
.with_context(|| "task ID for spawned task not found")?;

match proof.proof.first().with_context(|| {
format!("empty proof for v2 reward account {account:?}; height {height}")
})? {
MerkleNode::Leaf { pos, elem, .. } => {
snapshot.remember(*pos, *elem, proof)?;
},
MerkleNode::Empty => {
snapshot.non_membership_remember(*account, proof)?;
},
_ => {
bail!("invalid proof for v2 reward account {account:?}; height {height}");
match db
.load_v2_reward_account_proof(height, *account)
.await?
.proof
.proof
{
RewardMerkleProofV2::Presence(pf) | RewardMerkleProofV2::Absence(pf) => {
snapshot.insert_path(*account, &pf)?;
},
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The previous implementation of load_v2_reward_accounts used a BoundedJoinSet to fetch account paths concurrently. The new implementation iterates and awaits each load_v2_reward_account_proof call sequentially. If the accounts slice can be large, this sequential processing might introduce a performance bottleneck. Consider reintroducing concurrency (e.g., using futures::stream::iter().map().buffer_unordered()) to maintain performance for batch operations.

    for account in accounts {
        let (proof, _) = db
            .load_v2_reward_account_proof(height, *account)
            .await?
            .proof;
        match proof {
            RewardMerkleProofV2::Presence(pf) | RewardMerkleProofV2::Absence(pf) => {
                snapshot.insert_path(*account, &pf)?;
            }
        }
    }

Comment on lines 1254 to 1258
#[derive(Serialize, Deserialize)]
pub(crate) enum RewardMerkleTreeV2Data {
Tree(RewardMerkleTreeV2),
Balances(Vec<(RewardAccountV2, RewardAmount)>),
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use some comments explaining why we have the two separate cases

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.persist_tree(height, serialization).await?;
}

if height.is_multiple_of(30) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make 30 a configurable parameter?

Copy link
Contributor Author

@ss-es ss-es left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: proof generation is now decoupled from the state storage loop. specifically, no part of the logic which generates and persists proofs can cause update_state_storage to fail

because the reward merkle proofs are transient and only serve the reward claim contract, it's not consequential if this fails. we end up retrying it as part of the normal state update loop within a minute or so anyway

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants