diff --git a/stackslib/src/chainstate/stacks/boot/signers-voting.clar b/stackslib/src/chainstate/stacks/boot/signers-voting.clar index a5ca200304..1fea756e93 100644 --- a/stackslib/src/chainstate/stacks/boot/signers-voting.clar +++ b/stackslib/src/chainstate/stacks/boot/signers-voting.clar @@ -24,9 +24,9 @@ (define-constant pox-info (unwrap-panic (contract-call? .pox-4 get-pox-info))) -;; Threshold consensus, expressed as parts-per-thousand to allow for integer -;; division with higher precision (e.g. 700 for 70%). -(define-constant threshold-consensus u700) +;; Threshold consensus, expressed as parts-per-hundred to allow for integer +;; division with higher precision (e.g. 70 for 70%). +(define-constant threshold-consensus u70) ;; Maps reward-cycle ids to last round (define-map rounds uint uint) @@ -39,6 +39,9 @@ ;; necessary to recalculate it on every vote. (define-map cycle-total-weight uint uint) +;; Maps voting data (count, current weight) per reward cycle & round +(define-map round-data {reward-cycle: uint, round: uint} {votes-count: uint, votes-weight: uint}) + (define-read-only (burn-height-to-reward-cycle (height uint)) (/ (- height (get first-burnchain-block-height pox-info)) (get reward-cycle-length pox-info))) @@ -54,6 +57,9 @@ (define-read-only (get-vote (reward-cycle uint) (round uint) (signer principal)) (map-get? votes {reward-cycle: reward-cycle, round: round, signer: signer})) +(define-read-only (get-round-info (reward-cycle uint) (round uint)) + (map-get? round-data {reward-cycle: reward-cycle, round: round})) + (define-read-only (get-candidate-info (reward-cycle uint) (round uint) (candidate (buff 33))) {candidate-weight: (default-to u0 (map-get? tally {reward-cycle: reward-cycle, round: round, aggregate-public-key: candidate})), total-weight: (map-get? cycle-total-weight reward-cycle)}) @@ -81,6 +87,11 @@ (define-read-only (get-approved-aggregate-key (reward-cycle uint)) (map-get? aggregate-public-keys reward-cycle)) +;; get the weight required for consensus threshold +(define-read-only (get-threshold-weight (reward-cycle uint)) + (let ((total-weight (default-to u0 (map-get? cycle-total-weight reward-cycle)))) + (/ (+ (* total-weight threshold-consensus) u99) u100))) + (define-private (is-in-voting-window (height uint) (reward-cycle uint)) (let ((last-cycle (unwrap-panic (contract-call? .signers get-last-set-cycle)))) (and (is-eq last-cycle reward-cycle) @@ -134,7 +145,12 @@ ;; vote by signer weight (signer-weight (try! (get-signer-weight signer-index reward-cycle))) (new-total (+ signer-weight (default-to u0 (map-get? tally tally-key)))) - (total-weight (try! (get-and-cache-total-weight reward-cycle)))) + (cached-weight (try! (get-and-cache-total-weight reward-cycle))) + (threshold-weight (get-threshold-weight reward-cycle)) + (current-round (default-to { + votes-count: u0, + votes-weight: u0} (map-get? round-data {reward-cycle: reward-cycle, round: round}))) + ) ;; Check that the key has not yet been set for this reward cycle (asserts! (is-none (map-get? aggregate-public-keys reward-cycle)) (err ERR_OUT_OF_VOTING_WINDOW)) ;; Check that the aggregate public key is the correct length @@ -147,6 +163,10 @@ (try! (update-last-round reward-cycle round)) ;; Update the tally for this aggregate public key candidate (map-set tally tally-key new-total) + ;; Update the current round data + (map-set round-data {reward-cycle: reward-cycle, round: round} { + votes-count: (+ (get votes-count current-round) u1), + votes-weight: (+ (get votes-weight current-round) signer-weight)}) ;; Update used aggregate public keys (map-set used-aggregate-public-keys key reward-cycle) (print { @@ -158,7 +178,7 @@ new-total: new-total, }) ;; If the new total weight is greater than or equal to the threshold consensus - (if (>= (/ (* new-total u1000) total-weight) threshold-consensus) + (if (>= new-total threshold-weight) ;; Save this approved aggregate public key for this reward cycle. ;; If there is not already a key for this cycle, the insert will ;; return true and an event will be created. diff --git a/stackslib/src/chainstate/stacks/boot/signers_voting_tests.rs b/stackslib/src/chainstate/stacks/boot/signers_voting_tests.rs index 5ac7d461c2..aef41ef4a5 100644 --- a/stackslib/src/chainstate/stacks/boot/signers_voting_tests.rs +++ b/stackslib/src/chainstate/stacks/boot/signers_voting_tests.rs @@ -64,7 +64,9 @@ use crate::chainstate::stacks::boot::pox_2_tests::{ use crate::chainstate::stacks::boot::pox_4_tests::{ assert_latest_was_burn, get_last_block_sender_transactions, get_tip, make_test_epochs_pox, }; -use crate::chainstate::stacks::boot::signers_tests::{get_signer_index, prepare_signers_test}; +use crate::chainstate::stacks::boot::signers_tests::{ + get_signer_index, prepare_signers_test, readonly_call, +}; use crate::chainstate::stacks::boot::{ BOOT_CODE_COST_VOTING_TESTNET as BOOT_CODE_COST_VOTING, BOOT_CODE_POX_TESTNET, SIGNERS_NAME, SIGNERS_VOTING_NAME, @@ -2048,6 +2050,123 @@ fn vote_for_aggregate_public_key_mixed_rounds() { assert_eq!(alice_vote_tx.events.len(), 0); } +// In this test case, Alice & Bob advance through setup & check +// the round info from the very first reward cycle & round. +#[test] +fn test_get_round_info() { + // Test setup + let alice = TestStacker::from_seed(&[3, 4]); + let bob = TestStacker::from_seed(&[5, 6]); + let observer = TestEventObserver::new(); + + // Alice - Signer 1 + let alice_key = &alice.signer_private_key; + let alice_address = key_to_stacks_addr(alice_key); + let alice_principal = PrincipalData::from(alice_address); + + // Bob - Signer 2 + let bob_key = &bob.signer_private_key; + let bob_address = key_to_stacks_addr(bob_key); + let bob_principal = PrincipalData::from(bob_address); + + let (mut peer, test_signers, latest_block_id, current_reward_cycle) = prepare_signers_test( + function_name!(), + vec![ + (alice_principal.clone(), 1000), + (bob_principal.clone(), 1000), + ], + &[alice.clone(), bob.clone()], + Some(&observer), + ); + + // Get the current creward cycle + let cycle_id = current_reward_cycle; + + let round_info = get_round_info(&mut peer, latest_block_id, cycle_id, 0) + .unwrap() + .expect_tuple() + .unwrap(); + let votes_count = round_info.get("votes-count").unwrap(); + let votes_weight = round_info.get("votes-weight").unwrap(); + + assert_eq!(votes_count, &Value::UInt(2)); + assert_eq!(votes_weight, &Value::UInt(4)); +} + +pub fn get_round_info( + peer: &mut TestPeer<'_>, + latest_block_id: StacksBlockId, + reward_cycle: u128, + round: u128, +) -> Option { + let round_tuple = readonly_call( + peer, + &latest_block_id, + "signers-voting".into(), + "get-round-info".into(), + vec![Value::UInt(reward_cycle), Value::UInt(round)], + ) + .expect_optional() + .unwrap(); + round_tuple +} + +// In this test case, Alice & Bob advance through setup & check +// the weight threshold info from the very first reward cycle & round. +#[test] +fn test_get_threshold_weight() { + // Test setup + let alice = TestStacker::from_seed(&[3, 4]); + let bob = TestStacker::from_seed(&[5, 6]); + let observer = TestEventObserver::new(); + + // Alice - Signer 1 + let alice_key = &alice.signer_private_key; + let alice_address = key_to_stacks_addr(alice_key); + let alice_principal = PrincipalData::from(alice_address); + + // Bob - Signer 2 + let bob_key = &bob.signer_private_key; + let bob_address = key_to_stacks_addr(bob_key); + let bob_principal = PrincipalData::from(bob_address); + + let (mut peer, test_signers, latest_block_id, current_reward_cycle) = prepare_signers_test( + function_name!(), + vec![ + (alice_principal.clone(), 1000), + (bob_principal.clone(), 1000), + ], + &[alice.clone(), bob.clone()], + Some(&observer), + ); + + // Get the current creward cycle + let cycle_id = current_reward_cycle; + + // Call get-threshold-weight + let threshold_weight: u128 = get_threshold_weight(&mut peer, latest_block_id, cycle_id); + + // Since there are four votes, the threshold weight should be 3 (75% of 4) + assert_eq!(threshold_weight, 3); +} + +pub fn get_threshold_weight( + peer: &mut TestPeer<'_>, + latest_block_id: StacksBlockId, + reward_cycle: u128, +) -> u128 { + let threshold_weight = readonly_call( + peer, + &latest_block_id, + "signers-voting".into(), + "get-threshold-weight".into(), + vec![Value::UInt(reward_cycle)], + ) + .expect_u128() + .unwrap(); + threshold_weight +} + fn nakamoto_tenure( peer: &mut TestPeer, test_signers: &mut TestSigners,