diff --git a/components/clarity-repl/src/repl/boot/mod.rs b/components/clarity-repl/src/repl/boot/mod.rs index fb7a7893d..c1031a09c 100644 --- a/components/clarity-repl/src/repl/boot/mod.rs +++ b/components/clarity-repl/src/repl/boot/mod.rs @@ -16,29 +16,25 @@ // This code is copied from stacks-blockchain/src/chainstate/atacks/boot/mod.rs -const BOOT_CODE_POX_BODY: &str = std::include_str!("pox.clar"); -const BOOT_CODE_POX_TESTNET_CONSTS: &str = std::include_str!("pox-testnet.clar"); -const BOOT_CODE_POX_MAINNET_CONSTS: &str = std::include_str!("pox-mainnet.clar"); +const BOOT_CODE_GENESIS: &str = std::include_str!("genesis.clar"); +const BOOT_CODE_BNS: &str = std::include_str!("bns.clar"); const BOOT_CODE_LOCKUP: &str = std::include_str!("lockup.clar"); + pub const BOOT_CODE_COSTS: &str = std::include_str!("costs.clar"); pub const BOOT_CODE_COSTS_2: &str = std::include_str!("costs-2.clar"); -pub const BOOT_CODE_COSTS_3: &str = std::include_str!("costs-3.clar"); pub const BOOT_CODE_COSTS_2_TESTNET: &str = std::include_str!("costs-2-testnet.clar"); -pub const BOOT_CODE_COSTS_3_TESTNET: &str = std::include_str!("costs-3.clar"); +pub const BOOT_CODE_COSTS_3: &str = std::include_str!("costs-3.clar"); const BOOT_CODE_COST_VOTING_MAINNET: &str = std::include_str!("cost-voting.clar"); -const BOOT_CODE_BNS: &str = std::include_str!("bns.clar"); -const BOOT_CODE_GENESIS: &str = std::include_str!("genesis.clar"); -pub const POX_1_NAME: &str = "pox"; -pub const POX_2_NAME: &str = "pox-2"; -pub const POX_3_NAME: &str = "pox-3"; -pub const POX_4_NAME: &str = "pox-4"; +const BOOT_CODE_POX_TESTNET_CONSTS: &str = std::include_str!("pox-testnet.clar"); +const BOOT_CODE_POX_MAINNET_CONSTS: &str = std::include_str!("pox-mainnet.clar"); +const BOOT_CODE_POX_BODY: &str = std::include_str!("pox.clar"); const POX_2_BODY: &str = std::include_str!("pox-2.clar"); const POX_3_BODY: &str = std::include_str!("pox-3.clar"); const POX_4_BODY: &str = std::include_str!("pox-4.clar"); -pub const COSTS_1_NAME: &str = "costs"; -pub const COSTS_2_NAME: &str = "costs-2"; +pub const BOOT_CODE_SIGNERS: &str = std::include_str!("signers.clar"); +pub const BOOT_CODE_SIGNERS_VOTING: &str = std::include_str!("signers-voting.clar"); lazy_static! { pub static ref BOOT_CODE_POX_MAINNET: String = @@ -54,7 +50,7 @@ lazy_static! { pub static ref POX_3_TESTNET_CODE: String = format!("{}\n{}", BOOT_CODE_POX_TESTNET_CONSTS, POX_3_BODY); pub static ref BOOT_CODE_COST_VOTING_TESTNET: String = make_testnet_cost_voting(); - pub static ref STACKS_BOOT_CODE_MAINNET: [(&'static str, &'static str); 11] = [ + pub static ref STACKS_BOOT_CODE_MAINNET: [(&'static str, &'static str); 13] = [ ("pox", &BOOT_CODE_POX_MAINNET), ("lockup", &BOOT_CODE_LOCKUP), ("costs", &BOOT_CODE_COSTS), @@ -66,8 +62,10 @@ lazy_static! { ("costs-3", &BOOT_CODE_COSTS_3), ("pox-3", &POX_3_MAINNET_CODE), ("pox-4", &POX_4_BODY), + ("signers", &BOOT_CODE_SIGNERS), + ("signers-voting", &BOOT_CODE_SIGNERS_VOTING), ]; - pub static ref STACKS_BOOT_CODE_TESTNET: [(&'static str, &'static str); 11] = [ + pub static ref STACKS_BOOT_CODE_TESTNET: [(&'static str, &'static str); 13] = [ ("pox", &BOOT_CODE_POX_TESTNET), ("lockup", &BOOT_CODE_LOCKUP), ("costs", &BOOT_CODE_COSTS), @@ -76,9 +74,11 @@ lazy_static! { ("genesis", &BOOT_CODE_GENESIS), ("costs-2", &BOOT_CODE_COSTS_2_TESTNET), ("pox-2", &POX_2_TESTNET_CODE), - ("costs-3", &BOOT_CODE_COSTS_3_TESTNET), + ("costs-3", &BOOT_CODE_COSTS_3), ("pox-3", &POX_3_TESTNET_CODE), ("pox-4", &POX_4_BODY), + ("signers", &BOOT_CODE_SIGNERS), + ("signers-voting", &BOOT_CODE_SIGNERS_VOTING), ]; } diff --git a/components/clarity-repl/src/repl/boot/signers-voting.clar b/components/clarity-repl/src/repl/boot/signers-voting.clar new file mode 100644 index 000000000..89f308a6e --- /dev/null +++ b/components/clarity-repl/src/repl/boot/signers-voting.clar @@ -0,0 +1,199 @@ +;; +;; @contract voting for the aggregate public key +;; + +;; maps dkg round and signer to proposed aggregate public key +(define-map votes {reward-cycle: uint, round: uint, signer: principal} {aggregate-public-key: (buff 33), signer-weight: uint}) +;; maps dkg round and aggregate public key to weights of signers supporting this key so far +(define-map tally {reward-cycle: uint, round: uint, aggregate-public-key: (buff 33)} uint) +;; maps aggregate public keys to rewards cycles +(define-map used-aggregate-public-keys (buff 33) uint) + +;; Error codes +;; 1 - 9 are reserved for use in the .signers contract, which can be returned +;; through this contract) +(define-constant ERR_SIGNER_INDEX_MISMATCH u10) +(define-constant ERR_INVALID_SIGNER_INDEX u11) +(define-constant ERR_OUT_OF_VOTING_WINDOW u12) +(define-constant ERR_ILL_FORMED_AGGREGATE_PUBLIC_KEY u13) +(define-constant ERR_DUPLICATE_AGGREGATE_PUBLIC_KEY u14) +(define-constant ERR_DUPLICATE_VOTE u15) +(define-constant ERR_FAILED_TO_RETRIEVE_SIGNERS u16) +(define-constant ERR_INVALID_ROUND u17) + +(define-constant pox-info + (unwrap-panic (contract-call? .pox-4 get-pox-info))) + +;; 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) + +;; Maps reward-cycle ids to aggregate public key. +(define-map aggregate-public-keys uint (buff 33)) + +;; Maps reward-cycle id to the total weight of signers. This map is used to +;; cache the total weight of signers for a given reward cycle, so it is not +;; 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))) + +(define-read-only (reward-cycle-to-burn-height (reward-cycle uint)) + (+ (* reward-cycle (get reward-cycle-length pox-info)) (get first-burnchain-block-height pox-info))) + +(define-read-only (current-reward-cycle) + (burn-height-to-reward-cycle burn-block-height)) + +(define-read-only (get-last-round (reward-cycle uint)) + (map-get? rounds reward-cycle)) + +(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)}) + +(define-read-only (get-tally (reward-cycle uint) (round uint) (aggregate-public-key (buff 33))) + (map-get? tally {reward-cycle: reward-cycle, round: round, aggregate-public-key: aggregate-public-key})) + +(define-read-only (get-signer-weight (signer-index uint) (reward-cycle uint)) + (let ((details (unwrap! (try! (contract-call? .signers get-signer-by-index reward-cycle signer-index)) (err ERR_INVALID_SIGNER_INDEX)))) + (asserts! (is-eq (get signer details) tx-sender) (err ERR_SIGNER_INDEX_MISMATCH)) + (ok (get weight details)))) + +;; aggregate public key must be unique and can be used only in a single cycle +(define-read-only (is-novel-aggregate-public-key (key (buff 33)) (reward-cycle uint)) + (is-eq (default-to reward-cycle (map-get? used-aggregate-public-keys key)) reward-cycle)) + +(define-read-only (is-in-prepare-phase (height uint)) + (< (mod (+ (- height (get first-burnchain-block-height pox-info)) + (get prepare-cycle-length pox-info)) + (get reward-cycle-length pox-info) + ) + (get prepare-cycle-length pox-info))) + +;; get the aggregate public key for the given reward cycle (or none) +(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) + (is-in-prepare-phase height)))) + +(define-private (sum-weights (signer { signer: principal, weight: uint }) (acc uint)) + (+ acc (get weight signer))) + +(define-private (get-and-cache-total-weight (reward-cycle uint)) + (match (map-get? cycle-total-weight reward-cycle) + total (ok total) + (let ((signers (unwrap! (contract-call? .signers get-signers reward-cycle) (err ERR_FAILED_TO_RETRIEVE_SIGNERS))) + (total (fold sum-weights signers u0))) + (map-set cycle-total-weight reward-cycle total) + (ok total)))) + +;; If the round is not set, or the new round is greater than the last round, +;; update the last round. +;; Returns: +;; * `(ok true)` if this is the first round for the reward cycle +;; * `(ok false)` if this is a new last round for the reward cycle +;; * `(err ERR_INVALID_ROUND)` if the round is incremented by more than 1 +(define-private (update-last-round (reward-cycle uint) (round uint)) + (ok (match (map-get? rounds reward-cycle) + last-round (begin + (asserts! (<= round (+ last-round u1)) (err ERR_INVALID_ROUND)) + (if (> round last-round) (map-set rounds reward-cycle round) false)) + (map-set rounds reward-cycle round)))) + +;; Signer vote for the aggregate public key of the next reward cycle +;; Each signer votes for the aggregate public key for the next reward cycle. +;; This vote must happen after the list of signers has been set by the node, +;; which occurs in the first block of the prepare phase. The vote is concluded +;; when the threshold of `threshold-consensus / 1000` is reached for a +;; specific aggregate public key. The vote is weighted by the amount of +;; reward slots that the signer controls in the next reward cycle. The vote +;; may require multiple rounds to reach consensus, but once consensus is +;; reached, later rounds will be ignored. +;; +;; Arguments: +;; * signer-index: the index of the calling signer in the signer set (from +;; `get-signers` in the .signers contract) +;; * key: the aggregate public key that this vote is in support of +;; * round: the voting round for which this vote is intended +;; * reward-cycle: the reward cycle for which this vote is intended +;; Returns: +;; * `(ok true)` if the vote was successful +;; * `(err )` if the vote was not successful (see errors above) +(define-public (vote-for-aggregate-public-key (signer-index uint) (key (buff 33)) (round uint) (reward-cycle uint)) + (let ((tally-key {reward-cycle: reward-cycle, round: round, aggregate-public-key: key}) + ;; 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)))) + (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 + (asserts! (is-eq (len key) u33) (err ERR_ILL_FORMED_AGGREGATE_PUBLIC_KEY)) + ;; Check that aggregate public key has not been used in a previous reward cycle + (asserts! (is-novel-aggregate-public-key key reward-cycle) (err ERR_DUPLICATE_AGGREGATE_PUBLIC_KEY)) + ;; Check that signer hasn't voted in this reward-cycle & round + (asserts! (map-insert votes {reward-cycle: reward-cycle, round: round, signer: tx-sender} {aggregate-public-key: key, signer-weight: signer-weight}) (err ERR_DUPLICATE_VOTE)) + ;; Check that the round is incremented by at most 1 + (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 { + event: "voted", + signer: tx-sender, + reward-cycle: reward-cycle, + round: round, + key: key, + new-total: new-total, + }) + ;; If the new total weight is greater than or equal to the 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. + (if (map-insert aggregate-public-keys reward-cycle key) + (begin + ;; Create an event for the approved aggregate public key + (print { + event: "approved-aggregate-public-key", + reward-cycle: reward-cycle, + round: round, + key: key, + }) + true) + false + ) + false + ) + (ok true))) diff --git a/components/clarity-repl/src/repl/boot/signers.clar b/components/clarity-repl/src/repl/boot/signers.clar new file mode 100644 index 000000000..62b5f76a0 --- /dev/null +++ b/components/clarity-repl/src/repl/boot/signers.clar @@ -0,0 +1,62 @@ +(define-data-var last-set-cycle uint u0) +(define-data-var stackerdb-signer-slots-0 (list 4000 { signer: principal, num-slots: uint }) (list)) +(define-data-var stackerdb-signer-slots-1 (list 4000 { signer: principal, num-slots: uint }) (list)) +(define-map cycle-set-height uint uint) +(define-constant MAX_WRITES u4294967295) +(define-constant CHUNK_SIZE (* u2 u1024 u1024)) +(define-constant ERR_NO_SUCH_PAGE u1) +(define-constant ERR_CYCLE_NOT_SET u2) + +(define-map cycle-signer-set uint (list 4000 { signer: principal, weight: uint })) + +;; Called internally by the Stacks node. +;; Stores the stackerdb signer slots for a given reward cycle. +;; Since there is one stackerdb per signer message, the `num-slots` field will always be u1. +(define-private (stackerdb-set-signer-slots + (signer-slots (list 4000 { signer: principal, num-slots: uint })) + (reward-cycle uint) + (set-at-height uint)) + (let ((cycle-mod (mod reward-cycle u2))) + (map-set cycle-set-height reward-cycle set-at-height) + (var-set last-set-cycle reward-cycle) + (if (is-eq cycle-mod u0) + (ok (var-set stackerdb-signer-slots-0 signer-slots)) + (ok (var-set stackerdb-signer-slots-1 signer-slots))))) + +;; Called internally by the Stacks node. +;; Sets the list of signers and weights for a given reward cycle. +(define-private (set-signers + (reward-cycle uint) + (signers (list 4000 { signer: principal, weight: uint }))) + (begin + (asserts! (is-eq (var-get last-set-cycle) reward-cycle) (err ERR_CYCLE_NOT_SET)) + (ok (map-set cycle-signer-set reward-cycle signers)))) + +;; Get the list of signers and weights for a given reward cycle. +(define-read-only (get-signers (cycle uint)) + (map-get? cycle-signer-set cycle)) + +;; called by .signers-(0|1)-xxx contracts to get the signers for their respective signing sets +(define-read-only (stackerdb-get-signer-slots-page (page uint)) + (if (is-eq page u0) (ok (var-get stackerdb-signer-slots-0)) + (if (is-eq page u1) (ok (var-get stackerdb-signer-slots-1)) + (err ERR_NO_SUCH_PAGE)))) + +;; Get a signer's signing weight by a given index. +;; Used by other contracts (e.g. the voting contract) +(define-read-only (get-signer-by-index (cycle uint) (signer-index uint)) + (ok (element-at (unwrap! (map-get? cycle-signer-set cycle) (err ERR_CYCLE_NOT_SET)) signer-index))) + +;; called by .signers-(0|1)-xxx contracts +;; NOTE: the node may ignore `write-freq`, since not all stackerdbs will be needed at a given time +(define-read-only (stackerdb-get-config) + (ok + { chunk-size: CHUNK_SIZE, + write-freq: u0, + max-writes: MAX_WRITES, + max-neighbors: u32, + hint-replicas: (list ) } + )) + +(define-read-only (get-last-set-cycle) + (ok (var-get last-set-cycle))) diff --git a/components/clarity-repl/src/repl/interpreter.rs b/components/clarity-repl/src/repl/interpreter.rs index 4916b441d..a04fffc87 100644 --- a/components/clarity-repl/src/repl/interpreter.rs +++ b/components/clarity-repl/src/repl/interpreter.rs @@ -1779,9 +1779,15 @@ mod tests { let boot_contracts_data = BOOT_CONTRACTS_DATA.clone(); for (_, (boot_contract, ast)) in boot_contracts_data { + if boot_contract.name == "signers-voting" { + continue; + } let res = interpreter .run(&boot_contract, &mut Some(ast), false, None) - .expect("failed to interprete boot contract"); + .expect(&format!( + "failed to interpret {} boot contract", + &boot_contract.name + )); assert!(res.diagnostics.is_empty()); } diff --git a/components/clarity-repl/src/repl/session.rs b/components/clarity-repl/src/repl/session.rs index 39b3deb99..ea6af6b0a 100644 --- a/components/clarity-repl/src/repl/session.rs +++ b/components/clarity-repl/src/repl/session.rs @@ -48,7 +48,7 @@ lazy_static! { PrincipalData::parse_standard_principal(BOOT_MAINNET_ADDRESS).unwrap(); pub static ref BOOT_CONTRACTS_DATA: BTreeMap = { let mut result = BTreeMap::new(); - let deploy: [(&StandardPrincipalData, [(&str, &str); 11]); 2] = [ + let deploy: [(&StandardPrincipalData, [(&str, &str); 13]); 2] = [ (&*BOOT_TESTNET_PRINCIPAL, *STACKS_BOOT_CODE_TESTNET), (&*BOOT_MAINNET_PRINCIPAL, *STACKS_BOOT_CODE_MAINNET), ]; @@ -58,7 +58,9 @@ lazy_static! { for (deployer, boot_code) in deploy.iter() { for (name, code) in boot_code.iter() { let (epoch, clarity_version) = match *name { - "pox-4" => (StacksEpochId::Epoch25, ClarityVersion::Clarity2), + "pox-4" | "signers" | "signers-voting" => { + (StacksEpochId::Epoch25, ClarityVersion::Clarity2) + } "pox-3" => (StacksEpochId::Epoch24, ClarityVersion::Clarity2), "pox-2" | "costs-3" => (StacksEpochId::Epoch21, ClarityVersion::Clarity2), "cost-2" => (StacksEpochId::Epoch2_05, ClarityVersion::Clarity1),