Skip to content

Commit b77bc58

Browse files
authored
feat(rpc): return rewards as part of validators RPC response (#15617)
Adds `validator_reward_paid_prev_epoch` to the `validators` RPC response, a per-account map of rewards earned two epochs ago and delivered at the start of the previous epoch. test-loop verifies the RPC output against `ValidatorAccountsUpdate` state changes at the epoch boundary. This allows users to figure out the validator rewards for each epoch via a single RPC call instead of having to do a diff between the balance of previous block and state changes of the epoch's first block.
1 parent 5e7114d commit b77bc58

6 files changed

Lines changed: 107 additions & 1 deletion

File tree

chain/chain/src/runtime/tests.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,10 @@ fn test_get_validator_info() {
11091109
prev_epoch_kickout: Default::default(),
11101110
epoch_start_height: 1,
11111111
epoch_height: 1,
1112+
validator_reward_paid_prev_epoch: HashMap::from([(
1113+
"near".parse().unwrap(),
1114+
Balance::ZERO,
1115+
)]),
11121116
}
11131117
);
11141118
expected_blocks = [0, 0];

chain/epoch-manager/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,6 +1507,7 @@ impl EpochManager {
15071507
prev_epoch_kickout,
15081508
epoch_start_height,
15091509
epoch_height,
1510+
validator_reward_paid_prev_epoch: cur_epoch_info.validator_reward().clone(),
15101511
})
15111512
}
15121513

chain/jsonrpc/openapi/openapi.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17618,6 +17618,14 @@
1761817618
"$ref": "#/components/schemas/ValidatorKickoutView"
1761917619
},
1762017620
"type": "array"
17621+
},
17622+
"validator_reward_paid_prev_epoch": {
17623+
"additionalProperties": {
17624+
"$ref": "#/components/schemas/NearToken"
17625+
},
17626+
"default": {},
17627+
"description": "Per-validator rewards paid out at the start of the previous epoch.\nFor epoch E, this contains the rewards earned in epoch E-2 that were\nadded to validator and treasury balances at the first block of epoch\nE-1 (via `ValidatorAccountsUpdate`).",
17628+
"type": "object"
1762117629
}
1762217630
},
1762317631
"required": [

chain/jsonrpc/openapi/openrpc.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9821,6 +9821,14 @@
98219821
"$ref": "#/components/schemas/ValidatorKickoutView"
98229822
},
98239823
"type": "array"
9824+
},
9825+
"validator_reward_paid_prev_epoch": {
9826+
"additionalProperties": {
9827+
"$ref": "#/components/schemas/NearToken"
9828+
},
9829+
"default": {},
9830+
"description": "Per-validator rewards paid out at the start of the previous epoch.\nFor epoch E, this contains the rewards earned in epoch E-2 that were\nadded to validator and treasury balances at the first block of epoch\nE-1 (via `ValidatorAccountsUpdate`).",
9831+
"type": "object"
98249832
}
98259833
},
98269834
"required": [

core/primitives/src/views.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2502,6 +2502,12 @@ pub struct EpochValidatorInfo {
25022502
pub epoch_start_height: BlockHeight,
25032503
/// Epoch height
25042504
pub epoch_height: EpochHeight,
2505+
/// Per-validator rewards paid out at the start of the previous epoch.
2506+
/// For epoch E, this contains the rewards earned in epoch E-2 that were
2507+
/// added to validator and treasury balances at the first block of epoch
2508+
/// E-1 (via `ValidatorAccountsUpdate`).
2509+
#[serde(default)]
2510+
pub validator_reward_paid_prev_epoch: HashMap<AccountId, Balance>,
25052511
}
25062512

25072513
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]

test-loop-tests/src/tests/stake_nodes.rs

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,28 @@ use crate::utils::account::{
55
};
66
use crate::utils::transactions::{get_shared_block_hash, run_tx, run_txs_parallel};
77
use crate::utils::validators::get_epoch_all_validators_sorted;
8+
use near_async::messaging::Handler;
89
use near_async::time::Duration;
910
use near_chain_configs::TrackedShardsConfig;
1011
use near_chain_configs::test_genesis::ValidatorsSpec;
1112
use near_chain_configs::test_utils::{TESTING_INIT_BALANCE, TESTING_INIT_STAKE};
13+
use near_client::{GetStateChanges, GetValidatorInfo, Query};
1214
use near_o11y::testonly::init_test_logger;
1315
use near_primitives::action::{Action, StakeAction};
1416
use near_primitives::num_rational::Rational32;
1517
use near_primitives::test_utils::{create_test_signer, create_user_test_signer};
1618
use near_primitives::transaction::SignedTransaction;
17-
use near_primitives::types::{AccountInfo, Balance, ShardId};
19+
use near_primitives::types::{
20+
AccountId, AccountInfo, Balance, BlockId, BlockReference, EpochReference, ShardId,
21+
};
22+
use near_primitives::views::{
23+
QueryRequest, QueryResponseKind, StateChangeCauseView, StateChangeValueView,
24+
StateChangesRequestView,
25+
};
1826
use primitive_types::U256;
1927
use rand::rngs::StdRng;
2028
use rand::{Rng, SeedableRng};
29+
use std::collections::HashMap;
2130
/// Runs one validator network, sends staking transaction for the second node and
2231
/// waits until it becomes a validator.
2332
#[test]
@@ -592,3 +601,73 @@ fn test_inflation() {
592601
);
593602
}
594603
}
604+
605+
/// Verifies that `validator_reward_paid_prev_epoch` returned by the
606+
/// validators RPC matches the per-account reward deltas from the on-chain
607+
/// `ValidatorAccountsUpdate` state changes at the epoch boundary.
608+
#[test]
609+
fn test_validator_reward_in_get_validator_info() {
610+
init_test_logger();
611+
612+
let validators_spec = create_validators_spec(2, 0);
613+
let accounts = validators_spec_clients(&validators_spec);
614+
let genesis = TestLoopBuilder::new_genesis_builder()
615+
.validators_spec(validators_spec)
616+
.protocol_reward_rate(Rational32::new(1, 10))
617+
.build();
618+
let mut env = TestLoopBuilder::new()
619+
.genesis(genesis)
620+
.epoch_config_store_from_genesis()
621+
.clients(accounts.clone())
622+
.build();
623+
624+
env.node_runner(0).run_until_new_epoch();
625+
let boundary_block = env.node(0).client().chain.get_head_block().unwrap();
626+
env.node_runner(0).run_until_new_epoch();
627+
628+
let mut all_ids = accounts;
629+
all_ids.push("near".parse().unwrap()); // protocol treasury
630+
let boundary_hash = *boundary_block.hash();
631+
let prev_hash = *boundary_block.header().prev_hash();
632+
633+
let mut node = env.node_mut(0);
634+
let view_client = node.view_client_actor();
635+
636+
// Query pre-reward balances at the block before the epoch boundary.
637+
let mut pre_balances: HashMap<AccountId, Balance> = HashMap::new();
638+
for id in &all_ids {
639+
let query = Query::new(
640+
BlockReference::from(BlockId::Hash(prev_hash)),
641+
QueryRequest::ViewAccount { account_id: id.clone() },
642+
);
643+
if let Ok(response) = view_client.handle_query(query) {
644+
if let QueryResponseKind::ViewAccount(view) = response.kind {
645+
pre_balances.insert(id.clone(), view.locked.checked_add(view.amount).unwrap());
646+
}
647+
}
648+
}
649+
650+
// Compute expected per-account rewards from ValidatorAccountsUpdate
651+
// state changes: reward = post_balance - pre_balance.
652+
let state_changes = view_client
653+
.handle(GetStateChanges {
654+
block_hash: boundary_hash,
655+
state_changes_request: StateChangesRequestView::AccountChanges { account_ids: all_ids },
656+
})
657+
.unwrap();
658+
let mut expected: HashMap<AccountId, Balance> = HashMap::new();
659+
for change in &state_changes {
660+
if matches!(&change.cause, StateChangeCauseView::ValidatorAccountsUpdate) {
661+
if let StateChangeValueView::AccountUpdate { account_id, account } = &change.value {
662+
let post = account.locked.checked_add(account.amount).unwrap();
663+
let pre = pre_balances.get(account_id).copied().unwrap_or(Balance::ZERO);
664+
expected.insert(account_id.clone(), post.checked_sub(pre).unwrap());
665+
}
666+
}
667+
}
668+
assert!(!expected.is_empty(), "should find ValidatorAccountsUpdate changes");
669+
670+
let info =
671+
view_client.handle(GetValidatorInfo { epoch_reference: EpochReference::Latest }).unwrap();
672+
assert_eq!(info.validator_reward_paid_prev_epoch, expected);
673+
}

0 commit comments

Comments
 (0)