Skip to content

Commit

Permalink
Feat(stateless-validation): Rewards for stateless validators (#11121)
Browse files Browse the repository at this point in the history
The goal of this PR is to give rewards to stateless validators
(validators which do not produce blocks or chunks but do validate chunks
by producing endorsements).

This means updating `EpochInfoAggregator` to track the expected number
of endorsements for each validator as well as how many were actually
produced. For the MVP we are not actually tracking endorsements
directly, instead we assume that if a chunk was included in a block then
all stateless validators for that shard should be rewarded (because
there must have been a sufficient number of endorsements for the block
producer to include the chunk). However, this logic will need to be
iterated on in future versions of the stateless validation protocol.
Additionally, the reward calculator required updating to use the new
endorsement stats as part of the uptime calculation which forms the
basis of the rewards.

Both of those changes are relatively minor. The technical snag in all of
this is that the `EpochInfoAggregator` and other structures related to
validator stats are persisted in the node state via borsh serialization.
~To avoid a large database migration, I have made this update backwards
compatible with the old format without the endorsement stats. The logic
for this is in `core/primitives/src/types/chunk_validator_stats.rs`. The
legacy variant is distinguished from the new one in the serialization by
serializing the one's compliment of the `u64` values for the new
variant. During deserialization we check if the first `u64` is larger
than 2^32 - 1 or not. If it is larger then we assume the value is
actually a one's compliment and proceed with the new variant
deserialization, and otherwise we do the legacy deserialization.~

~This means that the serialized values can be interpreted improperly if
2^32 (4.29 billion) or more chunks are produced by a single validator
during one epoch. I do not expect this to be an issue, but it is a
limitation worth documenting.~

@Longarithm told me that the DB migration is actually fine here because
it's not that much data. So the DB migration of the `EpochValidatorInfo`
column is also implemented.
  • Loading branch information
birchmd committed Apr 23, 2024
1 parent 7e530dc commit f95087b
Show file tree
Hide file tree
Showing 12 changed files with 615 additions and 106 deletions.
59 changes: 55 additions & 4 deletions chain/chain/src/runtime/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -788,13 +788,28 @@ fn test_get_validator_info() {
let staking_transaction = stake(1, &signer, &block_producers[0], 0);
let mut expected_blocks = [0, 0];
let mut expected_chunks = [0, 0];
let mut expected_endorsements = [0, 0];
let update_validator_stats =
|env: &mut TestEnv, expected_blocks: &mut [u64], expected_chunks: &mut [u64]| {
|env: &mut TestEnv,
expected_blocks: &mut [u64; 2],
expected_chunks: &mut [u64; 2],
expected_endorsements: &mut [u64; 2]| {
let epoch_id = env.head.epoch_id.clone();
let height = env.head.height;
let em = env.runtime.epoch_manager.read();
let bp = em.get_block_producer_info(&epoch_id, height).unwrap();
let cp = em.get_chunk_producer_info(&epoch_id, height, 0).unwrap();
let stateless_validators =
em.get_chunk_validator_assignments(&epoch_id, 0, height).ok();

if let Some(vs) = stateless_validators {
if vs.contains(&validators[0]) {
expected_endorsements[0] += 1;
}
if vs.contains(&validators[1]) {
expected_endorsements[1] += 1;
}
}

if bp.account_id() == "test1" {
expected_blocks[0] += 1;
Expand All @@ -809,13 +824,23 @@ fn test_get_validator_info() {
}
};
env.step_default(vec![staking_transaction]);
update_validator_stats(&mut env, &mut expected_blocks, &mut expected_chunks);
update_validator_stats(
&mut env,
&mut expected_blocks,
&mut expected_chunks,
&mut expected_endorsements,
);
assert!(env
.epoch_manager
.get_validator_info(ValidatorInfoIdentifier::EpochId(env.head.epoch_id.clone()))
.is_err());
env.step_default(vec![]);
update_validator_stats(&mut env, &mut expected_blocks, &mut expected_chunks);
update_validator_stats(
&mut env,
&mut expected_blocks,
&mut expected_chunks,
&mut expected_endorsements,
);
let mut current_epoch_validator_info = vec![
CurrentEpochValidatorInfo {
account_id: "test1".parse().unwrap(),
Expand All @@ -829,6 +854,10 @@ fn test_get_validator_info() {
num_expected_chunks: expected_chunks[0],
num_produced_chunks_per_shard: vec![expected_chunks[0]],
num_expected_chunks_per_shard: vec![expected_chunks[0]],
num_produced_endorsements: expected_endorsements[0],
num_expected_endorsements: expected_endorsements[0],
num_expected_endorsements_per_shard: vec![expected_endorsements[0]],
num_produced_endorsements_per_shard: vec![expected_endorsements[0]],
},
CurrentEpochValidatorInfo {
account_id: "test2".parse().unwrap(),
Expand All @@ -842,6 +871,10 @@ fn test_get_validator_info() {
num_expected_chunks: expected_chunks[1],
num_produced_chunks_per_shard: vec![expected_chunks[1]],
num_expected_chunks_per_shard: vec![expected_chunks[1]],
num_produced_endorsements: expected_endorsements[1],
num_expected_endorsements: expected_endorsements[1],
num_expected_endorsements_per_shard: vec![expected_endorsements[1]],
num_produced_endorsements_per_shard: vec![expected_endorsements[1]],
},
];
let next_epoch_validator_info = vec![
Expand Down Expand Up @@ -882,8 +915,14 @@ fn test_get_validator_info() {
);
expected_blocks = [0, 0];
expected_chunks = [0, 0];
expected_endorsements = [0, 0];
env.step_default(vec![]);
update_validator_stats(&mut env, &mut expected_blocks, &mut expected_chunks);
update_validator_stats(
&mut env,
&mut expected_blocks,
&mut expected_chunks,
&mut expected_endorsements,
);
let response = env
.epoch_manager
.get_validator_info(ValidatorInfoIdentifier::BlockHash(env.head.last_block_hash))
Expand All @@ -895,12 +934,24 @@ fn test_get_validator_info() {
current_epoch_validator_info[0].num_expected_chunks = expected_chunks[0];
current_epoch_validator_info[0].num_produced_chunks_per_shard = vec![expected_chunks[0]];
current_epoch_validator_info[0].num_expected_chunks_per_shard = vec![expected_chunks[0]];
current_epoch_validator_info[0].num_produced_endorsements = expected_endorsements[0];
current_epoch_validator_info[0].num_expected_endorsements = expected_endorsements[0];
current_epoch_validator_info[0].num_produced_endorsements_per_shard =
vec![expected_endorsements[0]];
current_epoch_validator_info[0].num_expected_endorsements_per_shard =
vec![expected_endorsements[0]];
current_epoch_validator_info[1].num_produced_blocks = expected_blocks[1];
current_epoch_validator_info[1].num_expected_blocks = expected_blocks[1];
current_epoch_validator_info[1].num_produced_chunks = expected_chunks[1];
current_epoch_validator_info[1].num_expected_chunks = expected_chunks[1];
current_epoch_validator_info[1].num_produced_chunks_per_shard = vec![expected_chunks[1]];
current_epoch_validator_info[1].num_expected_chunks_per_shard = vec![expected_chunks[1]];
current_epoch_validator_info[1].num_produced_endorsements = expected_endorsements[1];
current_epoch_validator_info[1].num_expected_endorsements = expected_endorsements[1];
current_epoch_validator_info[1].num_produced_endorsements_per_shard =
vec![expected_endorsements[1]];
current_epoch_validator_info[1].num_expected_endorsements_per_shard =
vec![expected_endorsements[1]];
assert_eq!(response.current_validators, current_epoch_validator_info);
assert_eq!(
response.next_validators,
Expand Down
116 changes: 83 additions & 33 deletions chain/epoch-manager/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ use near_primitives::shard_layout::ShardLayout;
use near_primitives::stateless_validation::ChunkValidatorAssignments;
use near_primitives::types::validator_stake::ValidatorStake;
use near_primitives::types::{
AccountId, ApprovalStake, Balance, BlockChunkValidatorStats, BlockHeight, EpochId,
EpochInfoProvider, NumBlocks, NumSeats, ShardId, ValidatorId, ValidatorInfoIdentifier,
AccountId, ApprovalStake, Balance, BlockChunkValidatorStats, BlockHeight, ChunkValidatorStats,
EpochId, EpochInfoProvider, NumSeats, ShardId, ValidatorId, ValidatorInfoIdentifier,
ValidatorKickoutReason, ValidatorStats,
};
use near_primitives::version::{ProtocolVersion, UPGRADABILITY_FIX_PROTOCOL_VERSION};
Expand Down Expand Up @@ -399,22 +399,22 @@ impl EpochManager {
.iter()
.map(|(account, stats)| {
let production_ratio =
if stats.block_stats.expected == 0 && stats.chunk_stats.expected == 0 {
if stats.block_stats.expected == 0 && stats.chunk_stats.expected() == 0 {
Rational64::from_integer(1)
} else if stats.block_stats.expected == 0 {
Rational64::new(
stats.chunk_stats.produced as i64,
stats.chunk_stats.expected as i64,
stats.chunk_stats.produced() as i64,
stats.chunk_stats.expected() as i64,
)
} else if stats.chunk_stats.expected == 0 {
} else if stats.chunk_stats.expected() == 0 {
Rational64::new(
stats.block_stats.produced as i64,
stats.block_stats.expected as i64,
)
} else {
(Rational64::new(
stats.chunk_stats.produced as i64,
stats.chunk_stats.expected as i64,
stats.chunk_stats.produced() as i64,
stats.chunk_stats.expected() as i64,
) + Rational64::new(
stats.block_stats.produced as i64,
stats.block_stats.expected as i64,
Expand Down Expand Up @@ -463,7 +463,7 @@ impl EpochManager {
config: &EpochConfig,
epoch_info: &EpochInfo,
block_validator_tracker: &HashMap<ValidatorId, ValidatorStats>,
chunk_validator_tracker: &HashMap<ShardId, HashMap<ValidatorId, ValidatorStats>>,
chunk_validator_tracker: &HashMap<ShardId, HashMap<ValidatorId, ChunkValidatorStats>>,
slashed: &HashMap<AccountId, SlashState>,
prev_validator_kickout: &HashMap<AccountId, ValidatorKickoutReason>,
) -> (HashMap<AccountId, ValidatorKickoutReason>, HashMap<AccountId, BlockChunkValidatorStats>)
Expand All @@ -484,11 +484,11 @@ impl EpochManager {
.get(&(i as u64))
.unwrap_or(&ValidatorStats { expected: 0, produced: 0 })
.clone();
let mut chunk_stats = ValidatorStats { produced: 0, expected: 0 };
let mut chunk_stats = ChunkValidatorStats::default();
for (_, tracker) in chunk_validator_tracker.iter() {
if let Some(stat) = tracker.get(&(i as u64)) {
chunk_stats.expected += stat.expected;
chunk_stats.produced += stat.produced;
*chunk_stats.expected_mut() += stat.expected();
*chunk_stats.produced_mut() += stat.produced();
}
}
total_stake += v.stake();
Expand Down Expand Up @@ -530,13 +530,13 @@ impl EpochManager {
},
);
}
if stats.chunk_stats.produced * 100
< u64::from(chunk_producer_kickout_threshold) * stats.chunk_stats.expected
if stats.chunk_stats.produced() * 100
< u64::from(chunk_producer_kickout_threshold) * stats.chunk_stats.expected()
{
validator_kickout.entry(account_id.clone()).or_insert_with(|| {
ValidatorKickoutReason::NotEnoughChunks {
produced: stats.chunk_stats.produced,
expected: stats.chunk_stats.expected,
produced: stats.chunk_stats.produced(),
expected: stats.chunk_stats.expected(),
}
});
}
Expand Down Expand Up @@ -1346,7 +1346,10 @@ impl EpochManager {
.get(info.account_id())
.unwrap_or(&BlockChunkValidatorStats {
block_stats: ValidatorStats { produced: 0, expected: 0 },
chunk_stats: ValidatorStats { produced: 0, expected: 0 },
chunk_stats: ChunkValidatorStats {
production: ValidatorStats { produced: 0, expected: 0 },
endorsement: ValidatorStats { produced: 0, expected: 0 },
},
});
let mut shards = validator_to_shard[validator_id]
.iter()
Expand All @@ -1365,8 +1368,19 @@ impl EpochManager {
shards,
num_produced_blocks: validator_stats.block_stats.produced,
num_expected_blocks: validator_stats.block_stats.expected,
num_produced_chunks: validator_stats.chunk_stats.produced,
num_expected_chunks: validator_stats.chunk_stats.expected,
num_produced_chunks: validator_stats.chunk_stats.produced(),
num_expected_chunks: validator_stats.chunk_stats.expected(),
num_produced_endorsements: validator_stats
.chunk_stats
.endorsement_stats()
.produced,
num_expected_endorsements: validator_stats
.chunk_stats
.endorsement_stats()
.expected,
// Same TODO as above for `num_produced_chunks_per_shard`
num_produced_endorsements_per_shard: Vec::new(),
num_expected_endorsements_per_shard: Vec::new(),
})
})
.collect::<Result<Vec<CurrentEpochValidatorInfo>, EpochError>>()?;
Expand All @@ -1390,19 +1404,29 @@ impl EpochManager {
.unwrap_or(&ValidatorStats { produced: 0, expected: 0 })
.clone();

let mut chunks_produced_by_shard: HashMap<ShardId, NumBlocks> =
let mut chunks_stats_by_shard: HashMap<ShardId, ChunkValidatorStats> =
HashMap::new();
let mut chunks_expected_by_shard: HashMap<ShardId, NumBlocks> =
HashMap::new();
let mut chunk_stats = ValidatorStats { produced: 0, expected: 0 };
let mut chunk_stats = ChunkValidatorStats::default();
for (shard, tracker) in aggregator.shard_tracker.iter() {
if let Some(stats) = tracker.get(&(validator_id as u64)) {
chunk_stats.produced += stats.produced;
chunk_stats.expected += stats.expected;
*chunks_produced_by_shard.entry(*shard).or_insert(0) +=
stats.produced;
*chunks_expected_by_shard.entry(*shard).or_insert(0) +=
stats.expected;
let produced = stats.produced();
let expected = stats.expected();
let endorsement_stats = stats.endorsement_stats();

*chunk_stats.produced_mut() += produced;
*chunk_stats.expected_mut() += expected;
chunk_stats.endorsement_stats_mut().produced +=
endorsement_stats.produced;
chunk_stats.endorsement_stats_mut().expected +=
endorsement_stats.expected;

let shard_stats = chunks_stats_by_shard.entry(*shard).or_default();
*shard_stats.produced_mut() += produced;
*shard_stats.expected_mut() += expected;
shard_stats.endorsement_stats_mut().produced +=
endorsement_stats.produced;
shard_stats.endorsement_stats_mut().expected +=
endorsement_stats.expected;
}
}
let mut shards = validator_to_shard[validator_id]
Expand All @@ -1419,15 +1443,41 @@ impl EpochManager {
shards: shards.clone(),
num_produced_blocks: block_stats.produced,
num_expected_blocks: block_stats.expected,
num_produced_chunks: chunk_stats.produced,
num_expected_chunks: chunk_stats.expected,
num_produced_chunks: chunk_stats.produced(),
num_expected_chunks: chunk_stats.expected(),
num_produced_chunks_per_shard: shards
.iter()
.map(|shard| *chunks_produced_by_shard.entry(*shard).or_default())
.map(|shard| {
chunks_stats_by_shard
.get(shard)
.map_or(0, |stats| stats.produced())
})
.collect(),
num_expected_chunks_per_shard: shards
.iter()
.map(|shard| *chunks_expected_by_shard.entry(*shard).or_default())
.map(|shard| {
chunks_stats_by_shard
.get(shard)
.map_or(0, |stats| stats.expected())
})
.collect(),
num_produced_endorsements: chunk_stats.endorsement_stats().produced,
num_expected_endorsements: chunk_stats.endorsement_stats().expected,
num_produced_endorsements_per_shard: shards
.iter()
.map(|shard| {
chunks_stats_by_shard
.get(shard)
.map_or(0, |stats| stats.endorsement_stats().produced)
})
.collect(),
num_expected_endorsements_per_shard: shards
.iter()
.map(|shard| {
chunks_stats_by_shard
.get(shard)
.map_or(0, |stats| stats.endorsement_stats().expected)
})
.collect(),
})
})
Expand Down
Loading

0 comments on commit f95087b

Please sign in to comment.