Skip to content

Commit b78ce0e

Browse files
committed
fix(eligibility): calculate target hash for witnessing eligibility
1 parent c19179c commit b78ce0e

File tree

3 files changed

+137
-79
lines changed

3 files changed

+137
-79
lines changed

node/src/actors/chain_manager/mining.rs

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ impl ChainManager {
382382
collateral_age
383383
};
384384
let (target_hash, probability) = if protocol_version >= V2_0 {
385-
let eligibility = self
385+
let (eligibility, target_hash, probability) = self
386386
.chain_state
387387
.stakes
388388
.witnessing_eligibility(own_pkh, current_epoch, num_witnesses, round)
@@ -397,8 +397,8 @@ impl ChainManager {
397397
return Ok(());
398398
}
399399
}
400-
// Using a target hash of zero for V2_X essentially disables preliminary VRF checks
401-
(Hash::min(), 0f64)
400+
401+
(target_hash, probability)
402402
} else {
403403
calculate_reppoe_threshold(
404404
rep_eng,
@@ -474,36 +474,31 @@ impl ChainManager {
474474
)
475475
})
476476
.map(move |(vrf_proof, vrf_proof_hash)| {
477-
// This is where eligibility is verified for several protocol versions
478-
if protocol_version >= V2_0 {
479-
Ok(vrf_proof)
480-
} else {
481-
// invalid: vrf_hash > target_hash
482-
let proof_invalid = vrf_proof_hash > target_hash;
477+
// invalid: vrf_hash > target_hash
478+
let proof_invalid = vrf_proof_hash > target_hash;
483479

484-
log::debug!(
480+
log::debug!(
485481
"{} witnesses and {} backup witnesses",
486482
num_witnesses,
487483
num_backup_witnesses
488484
);
489-
log::debug!(
485+
log::debug!(
490486
"Probability to be eligible for this data request: {:.6}%",
491487
probability * 100.0
492488
);
493-
log::trace!("[DR] Target hash: {}", target_hash);
494-
log::trace!("[DR] Our proof: {}", vrf_proof_hash);
495-
if proof_invalid {
496-
log::debug!("No eligibility for data request {}", dr_pointer);
497-
Err(())
498-
} else {
499-
log::info!(
489+
log::trace!("[DR] Target hash: {}", target_hash);
490+
log::trace!("[DR] Our proof: {}", vrf_proof_hash);
491+
if proof_invalid {
492+
log::debug!("No eligibility for data request {}", dr_pointer);
493+
Err(())
494+
} else {
495+
log::info!(
500496
"{} Discovered eligibility for mining a data request {} for epoch #{}",
501497
Yellow.bold().paint("[Mining]"),
502498
Yellow.bold().paint(dr_pointer.to_string()),
503499
Yellow.bold().paint(current_epoch.to_string())
504500
);
505-
Ok(vrf_proof)
506-
}
501+
Ok(vrf_proof)
507502
}
508503
})
509504
)

validations/src/eligibility/current.rs

Lines changed: 100 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::{
44
ops::{Add, Div, Mul, Rem, Sub},
55
};
66

7-
use witnet_data_structures::{staking::prelude::*, wit::PrecisionLoss};
7+
use witnet_data_structures::{chain::Hash, staking::prelude::*, wit::PrecisionLoss};
88

99
const MINING_REPLICATION_FACTOR: usize = 4;
1010
const WITNESSING_MAX_ROUNDS: usize = 4;
@@ -71,7 +71,7 @@ where
7171
epoch: Epoch,
7272
witnesses: u16,
7373
round: u16,
74-
) -> StakesResult<Eligible, Address, Coins, Epoch>
74+
) -> StakesResult<(Eligible, Hash, f64), Address, Coins, Epoch>
7575
where
7676
ISK: Into<Address>;
7777

@@ -87,10 +87,10 @@ where
8787
where
8888
ISK: Into<Address>,
8989
{
90-
matches!(
91-
self.witnessing_eligibility(validator, epoch, witnesses, round),
92-
Ok(Eligible::Yes)
93-
)
90+
match self.witnessing_eligibility(validator, epoch, witnesses, round) {
91+
Ok((eligible, _, _)) => matches!(eligible, Eligible::Yes),
92+
Err(_) => false,
93+
}
9494
}
9595
}
9696

@@ -139,7 +139,7 @@ where
139139
+ From<u64>
140140
+ Sum
141141
+ Display,
142-
u64: From<Coins> + From<Power>,
142+
u64: From<Coins> + From<Power> + Mul<Power, Output = u64> + Div<Power, Output = u64>,
143143
{
144144
fn mining_eligibility<ISK>(
145145
&self,
@@ -180,7 +180,7 @@ where
180180
epoch: Epoch,
181181
witnesses: u16,
182182
round: u16,
183-
) -> StakesResult<Eligible, Address, Coins, Epoch>
183+
) -> StakesResult<(Eligible, Hash, f64), Address, Coins, Epoch>
184184
where
185185
ISK: Into<Address>,
186186
{
@@ -189,44 +189,51 @@ where
189189
Err(e) => {
190190
// Early exit if the stake key does not exist
191191
return match e {
192-
StakesError::EntryNotFound { .. } => Ok(IneligibilityReason::NotStaking.into()),
192+
StakesError::EntryNotFound { .. } => {
193+
Ok((IneligibilityReason::NotStaking.into(), Hash::min(), 0.0))
194+
}
193195
e => Err(e),
194196
};
195197
}
196198
};
197199

198-
let mut rank = self.rank(Capability::Mining, epoch);
199-
let rf = 2usize.pow(u32::from(round)) * witnesses as usize;
200+
let mut rank = self.rank(Capability::Witnessing, epoch);
201+
let (_, max_power) = rank.next().unwrap_or_default();
200202

201203
// Requirement no. 2 from the WIP:
202-
// "the witnessing power of the block proposer is in the `rf / stakers`th quantile among the witnessing powers
203-
// of all the stakers"
204-
let stakers = self.stakes_count();
205-
let quantile = stakers / MINING_REPLICATION_FACTOR;
206-
// TODO: verify if defaulting to 0 makes sense
207-
let (_, threshold) = rank.nth(quantile).unwrap_or_default();
208-
if power <= threshold {
209-
return Ok(IneligibilityReason::InsufficientPower.into());
204+
// "the mining power of the block proposer is in the `rf / stakers`th quantile among the witnessing powers of all
205+
// the stakers"
206+
let rf = 2usize.pow(u32::from(round)) * witnesses as usize;
207+
let (_, threshold_power) = rank.nth(rf - 2).unwrap_or_default();
208+
if power <= threshold_power {
209+
return Ok((
210+
IneligibilityReason::InsufficientPower.into(),
211+
Hash::min(),
212+
0.0,
213+
));
210214
}
211215

212216
// Requirement no. 3 from the WIP:
213217
// "the big-endian value of the VRF output is less than
214-
// `max_rounds * own_power / (max_power * (rf - max_rounds) - rf * threshold_power)`"
215-
// TODO: verify if defaulting to 0 makes sense
216-
let (_, max_power) = rank.next().unwrap_or_default();
217-
let stakers = self.stakes_count();
218-
let quantile = stakers / rf;
219-
// TODO: verify if defaulting to 0 makes sense
220-
let (_, threshold_power) = rank.nth(quantile).unwrap_or_default();
218+
// `max_rounds * own_power / (max_power * (rf - max_rounds) - rf * threshold_power)`"
221219
let dividend = Power::from(WITNESSING_MAX_ROUNDS as u64) * power;
222220
let divisor = max_power * Power::from((rf - WITNESSING_MAX_ROUNDS) as u64)
223221
- Power::from(rf as u64) * threshold_power;
224-
let threshold = dividend / divisor;
225-
if power <= threshold {
226-
return Ok(IneligibilityReason::InsufficientPower.into());
227-
}
222+
let target_hash = if divisor == Power::from(0) {
223+
Hash::with_first_u32(u32::MAX)
224+
} else {
225+
Hash::with_first_u32(
226+
(((u64::MAX / divisor).saturating_mul(dividend.into())) >> 32)
227+
.try_into()
228+
.unwrap(),
229+
)
230+
};
228231

229-
Ok(Eligible::Yes)
232+
Ok((
233+
Eligible::Yes,
234+
target_hash,
235+
(u64::from(dividend) / divisor) as f64,
236+
))
230237
}
231238
}
232239

@@ -274,16 +281,20 @@ mod tests {
274281
let stakes = <Stakes<String, _, _, _>>::with_minimum(100u64);
275282
let isk = "validator";
276283

277-
assert_eq!(
278-
stakes.witnessing_eligibility(isk, 0, 10, 0),
279-
Ok(Eligible::No(IneligibilityReason::NotStaking))
280-
);
284+
match stakes.witnessing_eligibility(isk, 0, 10, 0) {
285+
Ok((eligible, _, _)) => {
286+
assert_eq!(eligible, Eligible::No(IneligibilityReason::NotStaking));
287+
}
288+
Err(_) => assert!(false),
289+
}
281290
assert!(!stakes.witnessing_eligibility_bool(isk, 0, 10, 0));
282291

283-
assert_eq!(
284-
stakes.witnessing_eligibility(isk, 100, 10, 0),
285-
Ok(Eligible::No(IneligibilityReason::NotStaking))
286-
);
292+
match stakes.witnessing_eligibility(isk, 100, 10, 0) {
293+
Ok((eligible, _, _)) => {
294+
assert_eq!(eligible, Eligible::No(IneligibilityReason::NotStaking));
295+
}
296+
Err(_) => assert!(false),
297+
}
287298
assert!(!stakes.witnessing_eligibility_bool(isk, 100, 10, 0));
288299
}
289300

@@ -294,16 +305,58 @@ mod tests {
294305

295306
stakes.add_stake(isk, 1_000, 0).unwrap();
296307

297-
assert_eq!(
298-
stakes.witnessing_eligibility(isk, 0, 10, 0),
299-
Ok(Eligible::No(IneligibilityReason::InsufficientPower))
300-
);
308+
match stakes.witnessing_eligibility(isk, 0, 10, 0) {
309+
Ok((eligible, _, _)) => {
310+
assert_eq!(
311+
eligible,
312+
Eligible::No(IneligibilityReason::InsufficientPower)
313+
);
314+
}
315+
Err(_) => assert!(false),
316+
}
301317
assert!(!stakes.witnessing_eligibility_bool(isk, 0, 10, 0));
302318

303-
assert_eq!(
304-
stakes.witnessing_eligibility(isk, 100, 10, 0),
305-
Ok(Eligible::Yes)
306-
);
319+
match stakes.witnessing_eligibility(isk, 100, 10, 0) {
320+
Ok((eligible, _, _)) => {
321+
assert_eq!(eligible, Eligible::Yes);
322+
}
323+
Err(_) => assert!(false),
324+
}
307325
assert!(stakes.witnessing_eligibility_bool(isk, 100, 10, 0));
308326
}
327+
328+
#[test]
329+
fn test_witnessing_eligibility_target_hash() {
330+
let mut stakes = <Stakes<String, _, _, _>>::with_minimum(100u64);
331+
let isk_1 = "validator_1";
332+
let isk_2 = "validator_2";
333+
let isk_3 = "validator_3";
334+
let isk_4 = "validator_4";
335+
336+
stakes.add_stake(isk_1, 10_000_000_000, 0).unwrap();
337+
stakes.add_stake(isk_2, 20_000_000_000, 0).unwrap();
338+
stakes.add_stake(isk_3, 30_000_000_000, 0).unwrap();
339+
stakes.add_stake(isk_4, 40_000_000_000, 0).unwrap();
340+
341+
match stakes.witnessing_eligibility(isk_1, 0, 2, 0) {
342+
// TODO: verify target hash
343+
Ok((eligible, _target_hash, _)) => {
344+
assert_eq!(
345+
eligible,
346+
Eligible::No(IneligibilityReason::InsufficientPower)
347+
);
348+
}
349+
Err(_) => assert!(false),
350+
}
351+
assert!(!stakes.witnessing_eligibility_bool(isk_1, 0, 10, 0));
352+
353+
match stakes.witnessing_eligibility(isk_1, 100, 2, 0) {
354+
// TODO: verify target hash
355+
Ok((eligible, _target_hash, _)) => {
356+
assert_eq!(eligible, Eligible::No(IneligibilityReason::InsufficientPower));
357+
}
358+
Err(_) => assert!(false),
359+
}
360+
assert!(stakes.witnessing_eligibility_bool(isk_1, 100, 10, 0));
361+
}
309362
}

validations/src/validations.rs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -684,22 +684,32 @@ pub fn validate_commit_transaction(
684684
let proof_pkh = co_tx.body.proof.proof.pkh();
685685

686686
// Check if the commit transaction is from an eligible validator
687-
if protocol_version >= ProtocolVersion::V2_0 {
688-
let eligibility = stakes.witnessing_eligibility(
687+
let target_hash_wit2 = if protocol_version >= ProtocolVersion::V2_0 {
688+
let target_hash = match stakes.witnessing_eligibility(
689689
proof_pkh,
690690
epoch,
691691
dr_state.data_request.witnesses,
692692
dr_state.info.current_commit_round,
693-
);
694-
if eligibility == Ok(Eligible::No(InsufficientPower))
695-
|| eligibility == Ok(Eligible::No(NotStaking))
696-
{
697-
return Err(TransactionError::ValidatorNotEligible {
698-
validator: proof_pkh,
693+
) {
694+
Ok((eligibility, target_hash, _)) => {
695+
if eligibility == Eligible::No(InsufficientPower)
696+
|| eligibility == Eligible::No(NotStaking)
697+
{
698+
return Err(TransactionError::ValidatorNotEligible {
699+
validator: proof_pkh,
700+
}
701+
.into());
702+
}
703+
704+
target_hash
699705
}
700-
.into());
701-
}
702-
}
706+
Err(e) => return Err(e.into()),
707+
};
708+
709+
target_hash
710+
} else {
711+
Hash::min()
712+
};
703713

704714
// Commitment's output is only for change propose, so it only has to be one output and the
705715
// address has to be the same than the address which creates the commitment
@@ -780,7 +790,7 @@ pub fn validate_commit_transaction(
780790

781791
target_hash
782792
} else {
783-
Hash::max()
793+
target_hash_wit2
784794
};
785795
add_dr_vrf_signature_to_verify(
786796
signatures_to_verify,

0 commit comments

Comments
 (0)