diff --git a/rpc-client-api/src/custom_error.rs b/rpc-client-api/src/custom_error.rs index 62857b1ee55c16..2e54e8edd22e02 100644 --- a/rpc-client-api/src/custom_error.rs +++ b/rpc-client-api/src/custom_error.rs @@ -25,6 +25,7 @@ pub const JSON_RPC_SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET: i64 = -32014; pub const JSON_RPC_SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION: i64 = -32015; pub const JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED: i64 = -32016; pub const JSON_RPC_SERVER_ERROR_EPOCH_REWARDS_PERIOD_ACTIVE: i64 = -32017; +pub const JSON_RPC_SERVER_ERROR_SLOT_NOT_EPOCH_BOUNDARY: i64 = -32018; #[derive(Error, Debug)] pub enum RpcCustomError { @@ -72,6 +73,8 @@ pub enum RpcCustomError { current_block_height: u64, rewards_complete_block_height: u64, }, + #[error("SlotNotEpochBoundary")] + SlotNotEpochBoundary { slot: Slot }, } #[derive(Debug, Serialize, Deserialize)] @@ -228,6 +231,14 @@ impl From for Error { rewards_complete_block_height, })), }, + RpcCustomError::SlotNotEpochBoundary { slot } => Self { + code: ErrorCode::ServerError(JSON_RPC_SERVER_ERROR_SLOT_NOT_EPOCH_BOUNDARY), + message: format!( + "Rewards cannot be found because slot {slot} is not the epoch boundary. This \ + may be due to gap in the queried node's local ledger or long-term storage" + ), + data: None, + }, } } } diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index d62a61ec81fe00..be0679c352eea1 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -633,6 +633,20 @@ impl JsonRpcRequestProcessor { .into()); }; + // If there is a gap in blockstore or long-term historical storage that + // includes the epoch boundary, the `get_blocks_with_limit()` call above + // will return the slot of the block at the end of that gap, not a + // legitimate epoch-boundary block. Therefore, verify that the parent of + // `epoch_boundary_block` occurred before the `first_slot_in_epoch`. If + // it didn't, return an error; it will be impossible to locate + // rewards properly. + if epoch_boundary_block.parent_slot >= first_slot_in_epoch { + return Err(RpcCustomError::SlotNotEpochBoundary { + slot: first_confirmed_block_in_epoch, + } + .into()); + } + // Collect rewards from first block in the epoch if partitioned epoch // rewards not enabled, or address is a vote account let mut reward_map: HashMap = {