From 3f9f706db25161c6adaa11e2da717457cfff39c0 Mon Sep 17 00:00:00 2001 From: Rahul Garg Date: Wed, 26 Nov 2025 16:56:10 +0400 Subject: [PATCH 1/3] feat(contributor-rewards): add read-rewards command --- .../src/calculator/ledger_operations.rs | 242 ++++++++++++++---- .../src/calculator/orchestrator.rs | 12 + crates/contributor-rewards/src/cli/rewards.rs | 46 +++- 3 files changed, 250 insertions(+), 50 deletions(-) diff --git a/crates/contributor-rewards/src/calculator/ledger_operations.rs b/crates/contributor-rewards/src/calculator/ledger_operations.rs index 91d4dc5c..dd085133 100644 --- a/crates/contributor-rewards/src/calculator/ledger_operations.rs +++ b/crates/contributor-rewards/src/calculator/ledger_operations.rs @@ -56,7 +56,7 @@ pub async fn get_rewards_accountant( .0; let rewards_accountant = program_config.rewards_accountant_key; - info!( + debug!( "Retrieved rewards_accountant from ProgramConfig: {}", rewards_accountant ); @@ -481,12 +481,25 @@ pub async fn read_reward_input( Ok(()) } +/// JSON output struct for check_contributor_reward +#[derive(serde::Serialize)] +pub struct CheckRewardOutput { + pub epoch: u64, + pub contributor: String, + pub unit_share: u32, + pub merkle_root: String, + pub total_contributors: usize, + pub total_units: u32, + pub verified: bool, +} + /// Check contributor reward and verify merkle proof dynamically pub async fn check_contributor_reward( settings: &Settings, contributor_pubkey: &Pubkey, epoch: u64, rewards_accountant: Option, + json_output: bool, ) -> Result<()> { let fetcher = Fetcher::from_settings(settings)?; @@ -519,56 +532,69 @@ pub async fn check_contributor_reward( .unwrap(); let verification_result = verification_root == computed_root; - #[derive(Tabled)] - struct RewardVerification { - #[tabled(rename = "Field")] - field: String, - #[tabled(rename = "Value")] - value: String, - } + if json_output { + let output = CheckRewardOutput { + epoch, + contributor: reward.contributor_key.to_string(), + unit_share: reward.unit_share, + merkle_root: format!("{computed_root:?}"), + total_contributors: shapley_storage.rewards.len(), + total_units: shapley_storage.total_unit_shares, + verified: verification_result, + }; + println!("{}", serde_json::to_string(&output)?); + } else { + #[derive(Tabled)] + struct RewardVerification { + #[tabled(rename = "Field")] + field: String, + #[tabled(rename = "Value")] + value: String, + } - let verification_data = vec![ - RewardVerification { - field: "Epoch".to_string(), - value: epoch.to_string(), - }, - RewardVerification { - field: "Contributor Pubkey".to_string(), - value: reward.contributor_key.to_string(), - }, - RewardVerification { - field: "Unit Share".to_string(), - value: format!("{}", reward.unit_share), - }, - RewardVerification { - field: "Merkle Root".to_string(), - value: format!("{computed_root:?}"), - }, - RewardVerification { - field: "Total Contributors".to_string(), - value: shapley_storage.rewards.len().to_string(), - }, - RewardVerification { - field: "Total Units".to_string(), - value: format!( - "{} (should be 1,000,000,000)", - shapley_storage.total_unit_shares - ), - }, - RewardVerification { - field: "Verification Status".to_string(), - value: if verification_result { - "[VALID] Proof verified successfully!".to_string() - } else { - "[INVALID] Proof verification failed!".to_string() + let verification_data = vec![ + RewardVerification { + field: "Epoch".to_string(), + value: epoch.to_string(), }, - }, - ]; + RewardVerification { + field: "Contributor Pubkey".to_string(), + value: reward.contributor_key.to_string(), + }, + RewardVerification { + field: "Unit Share".to_string(), + value: format!("{}", reward.unit_share), + }, + RewardVerification { + field: "Merkle Root".to_string(), + value: format!("{computed_root:?}"), + }, + RewardVerification { + field: "Total Contributors".to_string(), + value: shapley_storage.rewards.len().to_string(), + }, + RewardVerification { + field: "Total Units".to_string(), + value: format!( + "{} (should be 1,000,000,000)", + shapley_storage.total_unit_shares + ), + }, + RewardVerification { + field: "Verification Status".to_string(), + value: if verification_result { + "[VALID] Proof verified successfully!".to_string() + } else { + "[INVALID] Proof verification failed!".to_string() + }, + }, + ]; - println!( - "{}", - Table::new(verification_data).with(Style::psql().remove_horizontals()) - ); + println!( + "{}", + Table::new(verification_data).with(Style::psql().remove_horizontals()) + ); + } if !verification_result { bail!("Merkle proof verification failed"); @@ -577,6 +603,126 @@ pub async fn check_contributor_reward( Ok(()) } +/// JSON output struct for a single reward entry +#[derive(serde::Serialize)] +pub struct RewardEntry { + pub contributor: String, + pub unit_share: u32, +} + +/// JSON output struct for read_all_rewards +#[derive(serde::Serialize)] +pub struct AllRewardsOutput { + pub epoch: u64, + pub merkle_root: String, + pub total_contributors: usize, + pub total_units: u32, + pub rewards: Vec, +} + +/// Read all contributor rewards for an epoch +pub async fn read_all_rewards( + settings: &Settings, + epoch: u64, + rewards_accountant: Option, + json_output: bool, +) -> Result<()> { + let fetcher = Fetcher::from_settings(settings)?; + + // Auto-fetch rewards_accountant if not provided + let rewards_accountant = + get_rewards_accountant(&fetcher.solana_write_client, rewards_accountant).await?; + + let prefix = settings.get_contributor_rewards_prefix(); + + // Fetch the shapley output storage + let shapley_storage = + try_fetch_shapley_output(&fetcher.dz_rpc_client, &prefix, &rewards_accountant, epoch) + .await?; + + // Compute merkle root + let merkle_root = shapley_storage.compute_merkle_root()?; + + if json_output { + let rewards: Vec = shapley_storage + .rewards + .iter() + .map(|r| RewardEntry { + contributor: r.contributor_key.to_string(), + unit_share: r.unit_share, + }) + .collect(); + + let output = AllRewardsOutput { + epoch, + merkle_root: format!("{merkle_root:?}"), + total_contributors: shapley_storage.rewards.len(), + total_units: shapley_storage.total_unit_shares, + rewards, + }; + println!("{}", serde_json::to_string(&output)?); + } else { + // Print summary table + #[derive(Tabled)] + struct SummaryRow { + #[tabled(rename = "Field")] + field: String, + #[tabled(rename = "Value")] + value: String, + } + + let summary_data = vec![ + SummaryRow { + field: "Epoch".to_string(), + value: epoch.to_string(), + }, + SummaryRow { + field: "Merkle Root".to_string(), + value: format!("{merkle_root:?}"), + }, + SummaryRow { + field: "Total Contributors".to_string(), + value: shapley_storage.rewards.len().to_string(), + }, + SummaryRow { + field: "Total Units".to_string(), + value: shapley_storage.total_unit_shares.to_string(), + }, + ]; + + println!( + "{}", + Table::new(summary_data).with(Style::psql().remove_horizontals()) + ); + + // Print rewards table + #[derive(Tabled)] + struct RewardRow { + #[tabled(rename = "Contributor")] + contributor: String, + #[tabled(rename = "Unit Share")] + unit_share: u32, + } + + let reward_rows: Vec = shapley_storage + .rewards + .iter() + .map(|r| RewardRow { + contributor: r.contributor_key.to_string(), + unit_share: r.unit_share, + }) + .collect(); + + println!(); + println!( + "{}", + Table::new(reward_rows).with(Style::psql().remove_horizontals()) + ); + } + + Ok(()) +} + /// Read shapley output storage from the ledger pub async fn read_shapley_output( settings: &Settings, diff --git a/crates/contributor-rewards/src/calculator/orchestrator.rs b/crates/contributor-rewards/src/calculator/orchestrator.rs index 4882ac9b..ad708f68 100644 --- a/crates/contributor-rewards/src/calculator/orchestrator.rs +++ b/crates/contributor-rewards/src/calculator/orchestrator.rs @@ -477,16 +477,28 @@ impl Orchestrator { contributor: &Pubkey, epoch: u64, rewards_accountant: Option, + json_output: bool, ) -> Result<()> { ledger_operations::check_contributor_reward( &self.settings, contributor, epoch, rewards_accountant, + json_output, ) .await } + pub async fn read_all_rewards( + &self, + epoch: u64, + rewards_accountant: Option, + json_output: bool, + ) -> Result<()> { + ledger_operations::read_all_rewards(&self.settings, epoch, rewards_accountant, json_output) + .await + } + pub async fn read_reward_input( &self, epoch: u64, diff --git a/crates/contributor-rewards/src/cli/rewards.rs b/crates/contributor-rewards/src/cli/rewards.rs index 1bfa729b..28985f4c 100644 --- a/crates/contributor-rewards/src/cli/rewards.rs +++ b/crates/contributor-rewards/src/cli/rewards.rs @@ -118,7 +118,10 @@ pub enum RewardsCommands { check-reward --contributor 7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV --epoch 123 # Check with explicit rewards accountant - check-reward -c 7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV -e 123 -r "# + check-reward -c 7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV -e 123 -r + + # Output as JSON + check-reward -c 7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV -e 123 --json"# )] CheckReward { /// Contributor's public key (base58 encoded) @@ -132,6 +135,10 @@ pub enum RewardsCommands { /// Rewards accountant public key (auto-fetched from ProgramConfig if not provided) #[arg(short = 'r', long, value_name = "PUBKEY")] rewards_accountant: Option, + + /// Output as JSON instead of table + #[arg(long)] + json: bool, }, #[command( about = "Read and display the reward input configuration for an epoch", @@ -151,6 +158,31 @@ pub enum RewardsCommands { #[arg(short = 'r', long, value_name = "PUBKEY")] rewards_accountant: Option, }, + #[command( + about = "Read and display all contributor rewards for an epoch", + after_help = r#"Examples: + # Read all rewards for epoch 56 + read-rewards --epoch 56 + + # Read with JSON output + read-rewards --epoch 56 --json + + # Read with specific rewards accountant + read-rewards --epoch 56 --rewards-accountant "# + )] + ReadRewards { + /// DZ epoch number to read rewards from + #[arg(short, long, value_name = "EPOCH")] + epoch: u64, + + /// Rewards accountant public key (auto-fetched from ProgramConfig if not provided) + #[arg(short = 'r', long, value_name = "PUBKEY")] + rewards_accountant: Option, + + /// Output as JSON instead of table + #[arg(long)] + json: bool, + }, #[command( about = "Reallocate a record account to change its size", after_help = r#"Examples: @@ -370,9 +402,10 @@ pub async fn handle(orchestrator: &Orchestrator, cmd: RewardsCommands) -> Result contributor, epoch, rewards_accountant, + json, } => { orchestrator - .check_contributor_reward(&contributor, epoch, rewards_accountant) + .check_contributor_reward(&contributor, epoch, rewards_accountant, json) .await } RewardsCommands::ReadRewardInput { @@ -383,6 +416,15 @@ pub async fn handle(orchestrator: &Orchestrator, cmd: RewardsCommands) -> Result .read_reward_input(epoch, rewards_accountant) .await } + RewardsCommands::ReadRewards { + epoch, + rewards_accountant, + json, + } => { + orchestrator + .read_all_rewards(epoch, rewards_accountant, json) + .await + } RewardsCommands::ReallocRecord { r#type, epoch, From 3dcf39abae365e37fb78aa2180b692d51449717b Mon Sep 17 00:00:00 2001 From: Rahul Garg Date: Wed, 26 Nov 2025 22:53:37 +0400 Subject: [PATCH 2/3] chore(contributor-rewards): bump CHANGELOG --- crates/contributor-rewards/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/contributor-rewards/CHANGELOG.md b/crates/contributor-rewards/CHANGELOG.md index 712542f2..fe8984dd 100644 --- a/crates/contributor-rewards/CHANGELOG.md +++ b/crates/contributor-rewards/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- feat(contributor-rewards): add read-rewards command ([#212](https://github.com/doublezerofoundation/doublezero-offchain/pull/212) + +## [0.3.5](https://github.com/doublezerofoundation/doublezero-offchain/releases/tag/doublezero-contributor-rewards/v0.3.5) - 2025-11-24 + - feat(contributor-rewards): add snapshot flag to inspect shapley cmd ([#209](https://github.com/doublezerofoundation/doublezero-offchain/pull/209) - fix(contributor-rewards): track shapley output record address for slack notifications ([#208](https://github.com/doublezerofoundation/doublezero-offchain/pull/208) From d2c584b61efb4d6897b9853f53e796f8c26bbee3 Mon Sep 17 00:00:00 2001 From: Rahul Garg Date: Wed, 26 Nov 2025 23:34:10 +0400 Subject: [PATCH 3/3] fix(contributor-rewards): use computed_root.to_string directly --- .../contributor-rewards/src/calculator/ledger_operations.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/contributor-rewards/src/calculator/ledger_operations.rs b/crates/contributor-rewards/src/calculator/ledger_operations.rs index dd085133..7f48b95b 100644 --- a/crates/contributor-rewards/src/calculator/ledger_operations.rs +++ b/crates/contributor-rewards/src/calculator/ledger_operations.rs @@ -537,7 +537,7 @@ pub async fn check_contributor_reward( epoch, contributor: reward.contributor_key.to_string(), unit_share: reward.unit_share, - merkle_root: format!("{computed_root:?}"), + merkle_root: computed_root.to_string(), total_contributors: shapley_storage.rewards.len(), total_units: shapley_storage.total_unit_shares, verified: verification_result, @@ -567,7 +567,7 @@ pub async fn check_contributor_reward( }, RewardVerification { field: "Merkle Root".to_string(), - value: format!("{computed_root:?}"), + value: computed_root.to_string(), }, RewardVerification { field: "Total Contributors".to_string(),