Skip to content

Commit

Permalink
feat: add signers and signers-voting boot contracts (#1386)
Browse files Browse the repository at this point in the history
* feat: add signers and signers-voting boot contracts

* tests: skip signers-voting test in wasm for now

* fix: typo

Co-authored-by: Micaiah Reid <micaiahreid@gmail.com>

---------

Co-authored-by: Micaiah Reid <micaiahreid@gmail.com>
  • Loading branch information
hugocaillard and MicaiahReid authored Mar 22, 2024
1 parent 5aacd89 commit 05c6a57
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 19 deletions.
32 changes: 16 additions & 16 deletions components/clarity-repl/src/repl/boot/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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),
Expand All @@ -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),
Expand All @@ -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),
];
}

Expand Down
199 changes: 199 additions & 0 deletions components/clarity-repl/src/repl/boot/signers-voting.clar
Original file line number Diff line number Diff line change
@@ -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 <code>)` 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)))
62 changes: 62 additions & 0 deletions components/clarity-repl/src/repl/boot/signers.clar
Original file line number Diff line number Diff line change
@@ -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)))
8 changes: 7 additions & 1 deletion components/clarity-repl/src/repl/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
6 changes: 4 additions & 2 deletions components/clarity-repl/src/repl/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ lazy_static! {
PrincipalData::parse_standard_principal(BOOT_MAINNET_ADDRESS).unwrap();
pub static ref BOOT_CONTRACTS_DATA: BTreeMap<QualifiedContractIdentifier, (ClarityContract, ContractAST)> = {
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),
];
Expand All @@ -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),
Expand Down

0 comments on commit 05c6a57

Please sign in to comment.