use std::collections::BTreeMap;
use dig_protocol::Bytes32;
use serde::{Deserialize, Serialize};
use crate::appeal::envelope::{SlashAppeal, SlashAppealPayload};
use crate::appeal::ground::AttesterAppealGround;
use crate::appeal::verdict::{AppealSustainReason, AppealVerdict};
use crate::bonds::{BondError, BondEscrow, BondTag};
use crate::constants::{
APPELLANT_BOND_MOJOS, BOND_AWARD_TO_WINNER_BPS, BPS_DENOMINATOR, INVALID_BLOCK_BASE_BPS,
MIN_SLASHING_PENALTY_QUOTIENT, PROPOSER_REWARD_QUOTIENT, REPORTER_BOND_MOJOS,
WHISTLEBLOWER_REWARD_QUOTIENT,
};
use crate::pending::{AppealAttempt, AppealOutcome, PendingSlash, PendingSlashStatus};
use crate::traits::{
CollateralSlasher, EffectiveBalanceView, RewardClawback, RewardPayout, ValidatorView,
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AppealAdjudicationResult {
pub appeal_hash: Bytes32,
pub evidence_hash: Bytes32,
pub outcome: AppealOutcome,
pub reverted_stake_mojos: Vec<(u32, u64)>,
pub reverted_collateral_mojos: Vec<(u32, u64)>,
pub clawback_shortfall: u64,
pub reporter_bond_forfeited: u64,
pub appellant_award_mojos: u64,
pub reporter_penalty_mojos: u64,
pub appellant_bond_forfeited: u64,
pub reporter_award_mojos: u64,
pub burn_amount: u64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct ClawbackResult {
pub wb_amount: u64,
pub prop_amount: u64,
pub wb_clawed: u64,
pub prop_clawed: u64,
pub shortfall: u64,
}
#[must_use]
pub fn adjudicate_sustained_revert_base_slash(
pending: &PendingSlash,
appeal: &SlashAppeal,
verdict: &AppealVerdict,
validator_set: &mut dyn ValidatorView,
) -> Vec<u32> {
let reason = match verdict {
AppealVerdict::Sustained { reason } => *reason,
AppealVerdict::Rejected { .. } => return Vec::new(),
};
let named_index = if matches!(reason, AppealSustainReason::ValidatorNotInIntersection) {
named_validator_from_ground(appeal)
} else {
None
};
let mut reverted: Vec<u32> = Vec::new();
for slash in &pending.base_slash_per_validator {
if let Some(named) = named_index
&& slash.validator_index != named
{
continue;
}
if let Some(entry) = validator_set.get_mut(slash.validator_index) {
entry.credit_stake(slash.base_slash_amount);
reverted.push(slash.validator_index);
}
}
reverted
}
#[must_use]
pub fn adjudicate_sustained_restore_status(
pending: &PendingSlash,
appeal: &SlashAppeal,
verdict: &AppealVerdict,
validator_set: &mut dyn ValidatorView,
) -> Vec<u32> {
let reason = match verdict {
AppealVerdict::Sustained { reason } => *reason,
AppealVerdict::Rejected { .. } => return Vec::new(),
};
let named_index = if matches!(reason, AppealSustainReason::ValidatorNotInIntersection) {
named_validator_from_ground(appeal)
} else {
None
};
let mut restored: Vec<u32> = Vec::new();
for slash in &pending.base_slash_per_validator {
if let Some(named) = named_index
&& slash.validator_index != named
{
continue;
}
if let Some(entry) = validator_set.get_mut(slash.validator_index)
&& entry.restore_status()
{
restored.push(slash.validator_index);
}
}
restored
}
#[must_use]
pub fn adjudicate_sustained_revert_collateral(
pending: &PendingSlash,
appeal: &SlashAppeal,
verdict: &AppealVerdict,
collateral: Option<&mut dyn CollateralSlasher>,
) -> Vec<u32> {
let reason = match verdict {
AppealVerdict::Sustained { reason } => *reason,
AppealVerdict::Rejected { .. } => return Vec::new(),
};
let Some(slasher) = collateral else {
return Vec::new();
};
let named_index = if matches!(reason, AppealSustainReason::ValidatorNotInIntersection) {
named_validator_from_ground(appeal)
} else {
None
};
let mut credited: Vec<u32> = Vec::new();
for slash in &pending.base_slash_per_validator {
if let Some(named) = named_index
&& slash.validator_index != named
{
continue;
}
if slash.collateral_slashed == 0 {
continue;
}
slasher.credit(slash.validator_index, slash.collateral_slashed);
credited.push(slash.validator_index);
}
credited
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct BondSplitResult {
pub forfeited: u64,
pub winner_award: u64,
pub burn: u64,
}
pub fn adjudicate_sustained_forfeit_reporter_bond(
pending: &PendingSlash,
appeal: &SlashAppeal,
verdict: &AppealVerdict,
bond_escrow: &mut dyn BondEscrow,
reward_payout: &mut dyn RewardPayout,
) -> Result<BondSplitResult, BondError> {
if matches!(verdict, AppealVerdict::Rejected { .. }) {
return Ok(BondSplitResult {
forfeited: 0,
winner_award: 0,
burn: 0,
});
}
let forfeited = bond_escrow.forfeit(
pending.evidence.reporter_validator_index,
REPORTER_BOND_MOJOS,
BondTag::Reporter(pending.evidence_hash),
)?;
let winner_award = forfeited * BOND_AWARD_TO_WINNER_BPS / BPS_DENOMINATOR;
let burn = forfeited - winner_award;
reward_payout.pay(appeal.appellant_puzzle_hash, winner_award);
Ok(BondSplitResult {
forfeited,
winner_award,
burn,
})
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct ReporterPenalty {
pub reporter_index: u32,
pub effective_balance_at_slash: u64,
pub penalty_mojos: u64,
}
pub fn adjudicate_sustained_reporter_penalty(
pending: &PendingSlash,
verdict: &AppealVerdict,
validator_set: &mut dyn ValidatorView,
effective_balances: &dyn EffectiveBalanceView,
slashed_in_window: &mut BTreeMap<(u64, u32), u64>,
current_epoch: u64,
) -> Option<ReporterPenalty> {
if matches!(verdict, AppealVerdict::Rejected { .. }) {
return None;
}
let reporter_index = pending.evidence.reporter_validator_index;
let eff_bal = effective_balances.get(reporter_index);
let bps_term = eff_bal * u64::from(INVALID_BLOCK_BASE_BPS) / BPS_DENOMINATOR;
let floor_term = eff_bal / MIN_SLASHING_PENALTY_QUOTIENT;
let penalty_mojos = std::cmp::max(bps_term, floor_term);
let entry = validator_set.get_mut(reporter_index)?;
entry.slash_absolute(penalty_mojos, current_epoch);
slashed_in_window.insert((current_epoch, reporter_index), eff_bal);
Some(ReporterPenalty {
reporter_index,
effective_balance_at_slash: eff_bal,
penalty_mojos,
})
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct ShortfallAbsorption {
pub clawback_shortfall: u64,
pub original_burn: u64,
pub final_burn: u64,
pub residue: u64,
}
#[must_use]
pub fn adjudicate_absorb_clawback_shortfall(
clawback: &ClawbackResult,
bond_split: &BondSplitResult,
) -> ShortfallAbsorption {
let clawback_shortfall = clawback.shortfall;
let original_burn = bond_split.burn;
let final_burn = original_burn.saturating_add(clawback_shortfall);
let residue = final_burn.saturating_sub(bond_split.forfeited);
ShortfallAbsorption {
clawback_shortfall,
original_burn,
final_burn,
residue,
}
}
pub fn adjudicate_rejected_challenge_open(
pending: &mut PendingSlash,
appeal: &SlashAppeal,
verdict: &AppealVerdict,
reason_hash: Bytes32,
) {
if matches!(verdict, AppealVerdict::Sustained { .. }) {
return;
}
let new_status = match pending.status {
PendingSlashStatus::Accepted => PendingSlashStatus::ChallengeOpen {
first_appeal_filed_epoch: appeal.filed_epoch,
appeal_count: 1,
},
PendingSlashStatus::ChallengeOpen {
first_appeal_filed_epoch,
appeal_count,
} => PendingSlashStatus::ChallengeOpen {
first_appeal_filed_epoch,
appeal_count: appeal_count.saturating_add(1),
},
PendingSlashStatus::Reverted { .. } | PendingSlashStatus::Finalised { .. } => {
pending.status
}
};
pending.status = new_status;
pending.appeal_history.push(AppealAttempt {
appeal_hash: appeal.hash(),
appellant_index: appeal.appellant_index,
filed_epoch: appeal.filed_epoch,
outcome: AppealOutcome::Lost { reason_hash },
bond_mojos: APPELLANT_BOND_MOJOS,
});
}
pub fn adjudicate_rejected_forfeit_appellant_bond(
pending: &PendingSlash,
appeal: &SlashAppeal,
verdict: &AppealVerdict,
bond_escrow: &mut dyn BondEscrow,
reward_payout: &mut dyn RewardPayout,
) -> Result<BondSplitResult, BondError> {
if matches!(verdict, AppealVerdict::Sustained { .. }) {
return Ok(BondSplitResult {
forfeited: 0,
winner_award: 0,
burn: 0,
});
}
let forfeited = bond_escrow.forfeit(
appeal.appellant_index,
APPELLANT_BOND_MOJOS,
BondTag::Appellant(appeal.hash()),
)?;
let winner_award = forfeited * BOND_AWARD_TO_WINNER_BPS / BPS_DENOMINATOR;
let burn = forfeited - winner_award;
reward_payout.pay(pending.evidence.reporter_puzzle_hash, winner_award);
Ok(BondSplitResult {
forfeited,
winner_award,
burn,
})
}
#[must_use]
pub fn adjudicate_sustained_clawback_rewards(
pending: &PendingSlash,
verdict: &AppealVerdict,
reward_clawback: &mut dyn RewardClawback,
proposer_puzzle_hash: Bytes32,
) -> ClawbackResult {
if matches!(verdict, AppealVerdict::Rejected { .. }) {
return ClawbackResult {
wb_amount: 0,
prop_amount: 0,
wb_clawed: 0,
prop_clawed: 0,
shortfall: 0,
};
}
let total_eff_bal: u64 = pending
.base_slash_per_validator
.iter()
.map(|p| p.effective_balance_at_slash)
.sum();
let wb_amount = total_eff_bal / WHISTLEBLOWER_REWARD_QUOTIENT;
let prop_amount = wb_amount / PROPOSER_REWARD_QUOTIENT;
let wb_clawed = reward_clawback.claw_back(pending.evidence.reporter_puzzle_hash, wb_amount);
let prop_clawed = reward_clawback.claw_back(proposer_puzzle_hash, prop_amount);
let expected = wb_amount + prop_amount;
let got = wb_clawed + prop_clawed;
let shortfall = expected.saturating_sub(got);
ClawbackResult {
wb_amount,
prop_amount,
wb_clawed,
prop_clawed,
shortfall,
}
}
pub fn adjudicate_sustained_status_reverted(
pending: &mut PendingSlash,
appeal: &SlashAppeal,
verdict: &AppealVerdict,
current_epoch: u64,
) {
if matches!(verdict, AppealVerdict::Rejected { .. }) {
return;
}
let appeal_hash = appeal.hash();
pending.appeal_history.push(AppealAttempt {
appeal_hash,
appellant_index: appeal.appellant_index,
filed_epoch: appeal.filed_epoch,
outcome: AppealOutcome::Won,
bond_mojos: APPELLANT_BOND_MOJOS,
});
pending.status = PendingSlashStatus::Reverted {
winning_appeal_hash: appeal_hash,
reverted_at_epoch: current_epoch,
};
}
#[allow(clippy::too_many_arguments)]
pub fn adjudicate_appeal(
verdict: AppealVerdict,
pending: &mut PendingSlash,
appeal: &SlashAppeal,
validator_set: &mut dyn ValidatorView,
effective_balances: &dyn EffectiveBalanceView,
collateral: Option<&mut dyn CollateralSlasher>,
bond_escrow: &mut dyn BondEscrow,
reward_payout: &mut dyn RewardPayout,
reward_clawback: &mut dyn RewardClawback,
slashed_in_window: &mut BTreeMap<(u64, u32), u64>,
proposer_puzzle_hash: Bytes32,
reason_hash: Bytes32,
current_epoch: u64,
) -> Result<crate::appeal::adjudicator::AppealAdjudicationResult, BondError> {
let outcome = verdict.to_appeal_outcome();
let appeal_hash = appeal.hash();
let evidence_hash = pending.evidence_hash;
match verdict {
AppealVerdict::Sustained { .. } => {
let reverted_idx =
adjudicate_sustained_revert_base_slash(pending, appeal, &verdict, validator_set);
let reverted_stake_mojos: Vec<(u32, u64)> = pending
.base_slash_per_validator
.iter()
.filter(|p| reverted_idx.contains(&p.validator_index))
.map(|p| (p.validator_index, p.base_slash_amount))
.collect();
let collateral_idx =
adjudicate_sustained_revert_collateral(pending, appeal, &verdict, collateral);
let reverted_collateral_mojos: Vec<(u32, u64)> = pending
.base_slash_per_validator
.iter()
.filter(|p| collateral_idx.contains(&p.validator_index))
.map(|p| (p.validator_index, p.collateral_slashed))
.collect();
let _restored =
adjudicate_sustained_restore_status(pending, appeal, &verdict, validator_set);
let clawback = adjudicate_sustained_clawback_rewards(
pending,
&verdict,
reward_clawback,
proposer_puzzle_hash,
);
let bond_split = adjudicate_sustained_forfeit_reporter_bond(
pending,
appeal,
&verdict,
bond_escrow,
reward_payout,
)?;
let absorb = adjudicate_absorb_clawback_shortfall(&clawback, &bond_split);
let rp = adjudicate_sustained_reporter_penalty(
pending,
&verdict,
validator_set,
effective_balances,
slashed_in_window,
current_epoch,
);
let reporter_penalty_mojos = rp.map(|r| r.penalty_mojos).unwrap_or(0);
adjudicate_sustained_status_reverted(pending, appeal, &verdict, current_epoch);
Ok(AppealAdjudicationResult {
appeal_hash,
evidence_hash,
outcome,
reverted_stake_mojos,
reverted_collateral_mojos,
clawback_shortfall: clawback.shortfall,
reporter_bond_forfeited: bond_split.forfeited,
appellant_award_mojos: bond_split.winner_award,
reporter_penalty_mojos,
appellant_bond_forfeited: 0,
reporter_award_mojos: 0,
burn_amount: absorb.final_burn,
})
}
AppealVerdict::Rejected { .. } => {
let bond_split = adjudicate_rejected_forfeit_appellant_bond(
pending,
appeal,
&verdict,
bond_escrow,
reward_payout,
)?;
adjudicate_rejected_challenge_open(pending, appeal, &verdict, reason_hash);
Ok(AppealAdjudicationResult {
appeal_hash,
evidence_hash,
outcome,
reverted_stake_mojos: Vec::new(),
reverted_collateral_mojos: Vec::new(),
clawback_shortfall: 0,
reporter_bond_forfeited: 0,
appellant_award_mojos: 0,
reporter_penalty_mojos: 0,
appellant_bond_forfeited: bond_split.forfeited,
reporter_award_mojos: bond_split.winner_award,
burn_amount: bond_split.burn,
})
}
}
}
fn named_validator_from_ground(appeal: &SlashAppeal) -> Option<u32> {
match &appeal.payload {
SlashAppealPayload::Attester(a) => match a.ground {
AttesterAppealGround::ValidatorNotInIntersection { validator_index } => {
Some(validator_index)
}
_ => None,
},
SlashAppealPayload::Proposer(_) | SlashAppealPayload::InvalidBlock(_) => None,
}
}