Merge branch 'feat/nakamoto-block-inv' of https://github.com/stacks-network/stacks-core into feat/nakamoto-block-inv

This commit is contained in:
Jude Nelson
2024-02-01 23:46:56 -05:00
12 changed files with 822 additions and 20 deletions

View File

@@ -15,3 +15,15 @@ path = "../../stackslib/src/chainstate/stacks/boot/pox-4.clar"
depends_on = []
clarity = 2
epoch = 2.4
[contracts.signers]
path = "../../stackslib/src/chainstate/stacks/boot/signers.clar"
depends_on = []
clarity = 2
epoch = 2.4
[contracts.signers-voting]
path = "../../stackslib/src/chainstate/stacks/boot/signers-voting.clar"
depends_on = []
clarity = 2
epoch = 2.4

View File

@@ -0,0 +1,92 @@
import { Cl } from "@stacks/transactions";
import { beforeEach, describe, expect, it } from "vitest";
const accounts = simnet.getAccounts();
const alice = accounts.get("wallet_1")!;
const bob = accounts.get("wallet_2")!;
const charlie = accounts.get("wallet_3")!;
const ERR_SIGNER_INDEX_MISMATCH = 10000;
const ERR_INVALID_SIGNER_INDEX = 10001;
const ERR_OUT_OF_VOTING_WINDOW = 10002
const ERR_OLD_ROUND = 10003;
const ERR_ILL_FORMED_AGGREGATE_PUBLIC_KEY = 10004;
const ERR_DUPLICATE_AGGREGATE_PUBLIC_KEY = 10005;
const ERR_DUPLICATE_VOTE = 10006;
const ERR_INVALID_BURN_BLOCK_HEIGHT = 10007
const KEY_1 = "123456789a123456789a123456789a123456789a123456789a123456789a010203";
const KEY_2 = "123456789a123456789a123456789a123456789a123456789a123456789ab0b1b2";
const SIGNERS_VOTING = "signers-voting";
describe("test signers-voting contract voting rounds", () => {
describe("test pox-info", () => {
it("should return correct burn-height", () => {
const { result:result1 } = simnet.callReadOnlyFn(SIGNERS_VOTING,
"reward-cycle-to-burn-height",
[Cl.uint(1)],
alice)
expect(result1).toEqual(Cl.uint(1050))
const { result:result2 } = simnet.callReadOnlyFn(SIGNERS_VOTING,
"reward-cycle-to-burn-height",
[Cl.uint(2)],
alice)
expect(result2).toEqual(Cl.uint(2100))
})
it("should return correct reward-cycle", () => {
const { result: result1 } = simnet.callReadOnlyFn(SIGNERS_VOTING,
"burn-height-to-reward-cycle",
[Cl.uint(1)],
alice)
expect(result1).toEqual(Cl.uint(0))
const { result: result2000 } = simnet.callReadOnlyFn(SIGNERS_VOTING,
"burn-height-to-reward-cycle",
[Cl.uint(2000)],
alice)
expect(result2000).toEqual(Cl.uint(1))
})
it("should return true if in prepare phase", () => {
const { result:result999 } = simnet.callReadOnlyFn(SIGNERS_VOTING,
"is-in-prepare-phase",
[Cl.uint(999)],
alice)
expect(result999).toEqual(Cl.bool(false))
const { result } = simnet.callReadOnlyFn(SIGNERS_VOTING,
"is-in-prepare-phase",
[Cl.uint(1000)],
alice)
expect(result).toEqual(Cl.bool(true))
const { result: result1001 } = simnet.callReadOnlyFn(SIGNERS_VOTING,
"is-in-prepare-phase",
[Cl.uint(1001)],
alice)
expect(result1001).toEqual(Cl.bool(true))
const { result: result0 } = simnet.callReadOnlyFn(SIGNERS_VOTING,
"is-in-prepare-phase",
[Cl.uint(1049)],
alice)
expect(result0).toEqual(Cl.bool(true))
const { result: result1 } = simnet.callReadOnlyFn(SIGNERS_VOTING,
"is-in-prepare-phase",
[Cl.uint(1050)],
alice)
expect(result1).toEqual(Cl.bool(false))
const { result: result2 } = simnet.callReadOnlyFn(SIGNERS_VOTING,
"is-in-prepare-phase",
[Cl.uint(1051)],
alice)
expect(result2).toEqual(Cl.bool(false))
})
})
});

View File

@@ -127,7 +127,9 @@ fn create_event_info_data_code(
start-burn-height: {start_burn_height},
;; how long to lock, in burn blocks
;; equal to args[3]
lock-period: {lock_period}
lock-period: {lock_period},
;; equal to args[4]
signer-key: {signer_key}
}}
}}
"#,
@@ -135,6 +137,7 @@ fn create_event_info_data_code(
lock_period = &args[3],
pox_addr = &args[1],
start_burn_height = &args[2],
signer_key = &args.get(3).map_or("none".to_string(), |v| v.to_string()),
)
}
"delegate-stack-stx" => {
@@ -244,12 +247,15 @@ fn create_event_info_data_code(
;; equal to args[0]
extend-count: {extend_count},
;; new unlock burnchain block height
unlock-burn-height: new-unlock-ht
unlock-burn-height: new-unlock-ht,
;; equal to args[2]
signer-key: {signer_key}
}}
}})
"#,
extend_count = &args[0],
pox_addr = &args[1],
signer_key = &args.get(2).map_or("none".to_string(), |v| v.to_string()),
)
}
"delegate-stack-extend" => {
@@ -307,12 +313,15 @@ fn create_event_info_data_code(
(unwrap-panic (map-get? logged-partial-stacked-by-cycle
{{ pox-addr: {pox_addr}, sender: tx-sender, reward-cycle: {reward_cycle} }}))),
;; delegator (this is the caller)
delegator: tx-sender
delegator: tx-sender,
;; equal to args[2]
signer-key: {signer_key}
}}
}}
"#,
pox_addr = &args[0],
reward_cycle = &args[1]
reward_cycle = &args[1],
signer_key = &args.get(2).map_or("none".to_string(), |v| v.to_string()),
)
}
"delegate-stx" => {

View File

@@ -105,6 +105,7 @@ pub fn boot_nakamoto<'a>(
mut initial_balances: Vec<(PrincipalData, u64)>,
test_signers: &TestSigners,
test_stackers: Option<Vec<&TestStacker>>,
observer: Option<&'a TestEventObserver>,
) -> TestPeer<'a> {
let aggregate_public_key = test_signers.aggregate_public_key.clone();
let mut peer_config = TestPeerConfig::new(test_name, 0, 0);
@@ -135,7 +136,7 @@ pub fn boot_nakamoto<'a>(
(0..test_signers.num_keys)
.map(|index| {
let stacker_private_key = StacksPrivateKey::from_seed(&index.to_be_bytes());
let signer_private_key = StacksPrivateKey::from_seed(&(index + 1000).to_be_bytes());
let signer_private_key = StacksPrivateKey::from_seed(&index.to_be_bytes());
TestStacker {
stacker_private_key,
signer_private_key,
@@ -151,7 +152,7 @@ pub fn boot_nakamoto<'a>(
.map(|test_stacker| {
(
PrincipalData::from(key_to_stacks_addr(&test_stacker.stacker_private_key)),
u64::try_from(test_stacker.amount).expect("Stacking amount too large"),
u64::try_from(test_stacker.amount + 10000).expect("Stacking amount too large"),
)
})
.collect();
@@ -163,7 +164,7 @@ pub fn boot_nakamoto<'a>(
peer_config.burnchain.pox_constants.v3_unlock_height = 27;
peer_config.burnchain.pox_constants.pox_4_activation_height = 31;
peer_config.test_stackers = Some(test_stackers.clone());
let mut peer = TestPeer::new(peer_config);
let mut peer = TestPeer::new_with_observer(peer_config, observer);
advance_to_nakamoto(&mut peer, &test_signers, test_stackers);
@@ -296,7 +297,7 @@ fn replay_reward_cycle(
#[test]
fn test_simple_nakamoto_coordinator_bootup() {
let mut test_signers = TestSigners::default();
let mut peer = boot_nakamoto(function_name!(), vec![], &test_signers, None);
let mut peer = boot_nakamoto(function_name!(), vec![], &test_signers, None, None);
let (burn_ops, mut tenure_change, miner_key) =
peer.begin_nakamoto_tenure(TenureChangeCause::BlockFound);
@@ -357,6 +358,7 @@ fn test_simple_nakamoto_coordinator_1_tenure_10_blocks() {
vec![(addr.into(), 100_000_000)],
&test_signers,
None,
None,
);
let (burn_ops, mut tenure_change, miner_key) =
@@ -479,6 +481,7 @@ fn test_nakamoto_chainstate_getters() {
vec![(addr.into(), 100_000_000)],
&test_signers,
None,
None,
);
let sort_tip = {
@@ -968,6 +971,7 @@ pub fn simple_nakamoto_coordinator_10_tenures_10_sortitions<'a>() -> TestPeer<'a
vec![(addr.into(), 100_000_000)],
&test_signers,
None,
None,
);
let mut all_blocks = vec![];
@@ -1295,6 +1299,7 @@ pub fn simple_nakamoto_coordinator_2_tenures_3_sortitions<'a>() -> TestPeer<'a>
vec![(addr.into(), 100_000_000)],
&test_signers,
None,
None,
);
let mut rc_burn_ops = vec![];
@@ -1630,6 +1635,7 @@ pub fn simple_nakamoto_coordinator_10_extended_tenures_10_sortitions() -> TestPe
vec![(addr.into(), 100_000_000)],
&test_signers,
None,
None,
);
let mut all_blocks = vec![];

View File

@@ -1502,7 +1502,7 @@ fn make_fork_run_with_arrivals(
#[test]
pub fn test_get_highest_nakamoto_tenure() {
let test_signers = TestSigners::default();
let mut peer = boot_nakamoto(function_name!(), vec![], &test_signers, None);
let mut peer = boot_nakamoto(function_name!(), vec![], &test_signers, None, None);
// extract chainstate and sortdb -- we don't need the peer anymore
let chainstate = &mut peer.stacks_node.as_mut().unwrap().chainstate;
@@ -1644,7 +1644,7 @@ pub fn test_get_highest_nakamoto_tenure() {
#[test]
fn test_make_miners_stackerdb_config() {
let test_signers = TestSigners::default();
let mut peer = boot_nakamoto(function_name!(), vec![], &test_signers, None);
let mut peer = boot_nakamoto(function_name!(), vec![], &test_signers, None, None);
let naka_miner_hash160 = peer.miner.nakamoto_miner_hash160();
let miner_keys: Vec<_> = (0..10).map(|_| StacksPrivateKey::new()).collect();

View File

@@ -79,6 +79,7 @@ pub const POX_2_NAME: &'static str = "pox-2";
pub const POX_3_NAME: &'static str = "pox-3";
pub const POX_4_NAME: &'static str = "pox-4";
pub const SIGNERS_NAME: &'static str = "signers";
pub const SIGNERS_VOTING_NAME: &'static str = "signers-voting";
/// This is the name of a variable in the `.signers` contract which tracks the most recently updated
/// reward cycle number.
pub const SIGNERS_UPDATE_STATE: &'static str = "last-set-cycle";
@@ -89,6 +90,7 @@ const POX_2_BODY: &'static str = std::include_str!("pox-2.clar");
const POX_3_BODY: &'static str = std::include_str!("pox-3.clar");
const POX_4_BODY: &'static str = std::include_str!("pox-4.clar");
pub const SIGNERS_BODY: &'static str = std::include_str!("signers.clar");
const SIGNERS_VOTING_BODY: &'static str = std::include_str!("signers-voting.clar");
pub const COSTS_1_NAME: &'static str = "costs";
pub const COSTS_2_NAME: &'static str = "costs-2";
@@ -117,6 +119,7 @@ lazy_static! {
pub static ref POX_3_TESTNET_CODE: String =
format!("{}\n{}", BOOT_CODE_POX_TESTNET_CONSTS, POX_3_BODY);
pub static ref POX_4_CODE: String = format!("{}", POX_4_BODY);
pub static ref SIGNER_VOTING_CODE: String = format!("{}", SIGNERS_VOTING_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); 6] = [
("pox", &BOOT_CODE_POX_MAINNET),
@@ -1307,6 +1310,8 @@ pub mod pox_3_tests;
pub mod pox_4_tests;
#[cfg(test)]
mod signers_tests;
#[cfg(test)]
pub mod signers_voting_tests;
#[cfg(test)]
pub mod test {
@@ -1314,7 +1319,9 @@ pub mod test {
use std::convert::From;
use std::fs;
use clarity::boot_util::boot_code_addr;
use clarity::vm::contracts::Contract;
use clarity::vm::tests::symbols_from_values;
use clarity::vm::types::*;
use stacks_common::util::hash::to_hex;
use stacks_common::util::*;
@@ -1874,6 +1881,30 @@ pub mod test {
make_tx(key, nonce, 0, payload)
}
pub fn make_signers_vote_for_aggregate_public_key(
key: &StacksPrivateKey,
nonce: u64,
signer_index: u128,
aggregate_public_key: &Point,
round: u128,
) -> StacksTransaction {
let aggregate_public_key = Value::buff_from(aggregate_public_key.compress().data.to_vec())
.expect("Failed to serialize aggregate public key");
let payload = TransactionPayload::new_contract_call(
boot_code_test_addr(),
SIGNERS_VOTING_NAME,
"vote-for-aggregate-public-key",
vec![
Value::UInt(signer_index),
aggregate_public_key,
Value::UInt(round),
],
)
.unwrap();
// TODO set tx_fee back to 0 once these txs are free
make_tx(key, nonce, 1, payload)
}
pub fn make_pox_2_increase(
key: &StacksPrivateKey,
nonce: u64,

View File

@@ -82,7 +82,7 @@ const ERR_REUSED_SIGNER_KEY: i128 = 33;
/// Return the BlockSnapshot for the latest sortition in the provided
/// SortitionDB option-reference. Panics on any errors.
fn get_tip(sortdb: Option<&SortitionDB>) -> BlockSnapshot {
pub fn get_tip(sortdb: Option<&SortitionDB>) -> BlockSnapshot {
SortitionDB::get_canonical_burn_chain_tip(&sortdb.unwrap().conn()).unwrap()
}

View File

@@ -0,0 +1,105 @@
;;
;; @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), reward-slots: 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 and rounds
(define-map used-aggregate-public-keys (buff 33) {reward-cycle: uint, round: uint})
(define-constant err-signer-index-mismatch (err u10000))
(define-constant err-invalid-signer-index (err u10001))
(define-constant err-out-of-voting-window (err u10002))
(define-constant err-old-round (err u10003))
(define-constant err-ill-formed-aggregate-public-key (err u10004))
(define-constant err-duplicate-aggregate-public-key (err u10005))
(define-constant err-duplicate-vote (err u10006))
(define-constant err-invalid-burn-block-height (err u10007))
(define-constant pox-info
(unwrap-panic (contract-call? .pox-4 get-pox-info)))
;; maps reward-cycle ids to last round
(define-map rounds uint uint)
(define-data-var state-1 {reward-cycle: uint, round: uint, aggregate-public-key: (optional (buff 33)),
total-votes: uint} {reward-cycle: u0, round: u0, aggregate-public-key: none, total-votes: u0})
(define-data-var state-2 {reward-cycle: uint, round: uint, aggregate-public-key: (optional (buff 33)),
total-votes: uint} {reward-cycle: u0, round: u0, aggregate-public-key: none, total-votes: u0})
;; get voting info by burn block height
(define-read-only (get-info (height uint))
(ok (at-block (unwrap! (get-block-info? id-header-hash height) err-invalid-burn-block-height) (get-current-info))))
;; get current voting info
(define-read-only (get-current-info)
(var-get state-1))
(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-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-slots (signer-index uint) (reward-cycle uint))
(let ((height (reward-cycle-to-burn-height reward-cycle)))
(ok (at-block
(unwrap! (get-block-info? id-header-hash height) err-invalid-burn-block-height)
(get-current-signer-slots signer-index)))))
(define-read-only (get-current-signer-slots (signer-index uint))
(let ((details (unwrap! (unwrap-panic (contract-call? .signers stackerdb-get-signer-by-index signer-index)) err-invalid-signer-index)))
(asserts! (is-eq (get signer details) tx-sender) err-signer-index-mismatch)
(ok (get num-slots details))))
;; aggregate public key must be unique and can be used only in a single cycle-round pair
(define-read-only (is-valid-aggregated-public-key (key (buff 33)) (dkg-id {reward-cycle: uint, round: uint}))
(is-eq (default-to dkg-id (map-get? used-aggregate-public-keys key)) dkg-id))
(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)))
(define-private (is-in-voting-window (height uint) (reward-cycle uint))
(let ((last-cycle (unwrap-panic (contract-call? .signers stackerdb-get-last-set-cycle))))
(and (is-eq last-cycle reward-cycle)
(is-in-prepare-phase height))))
(define-public (vote-for-aggregate-public-key (signer-index uint) (key (buff 33)) (round uint))
(let ((reward-cycle (+ u1 (burn-height-to-reward-cycle burn-block-height)))
(tally-key {reward-cycle: reward-cycle, round: round, aggregate-public-key: key})
;; one slot, one vote
(num-slots (try! (get-current-signer-slots signer-index)))
(new-total (+ num-slots (default-to u0 (map-get? tally tally-key)))))
(asserts! (is-in-voting-window burn-block-height reward-cycle) err-out-of-voting-window)
(asserts! (>= round (default-to u0 (map-get? rounds reward-cycle))) err-old-round)
(asserts! (is-eq (len key) u33) err-ill-formed-aggregate-public-key)
(asserts! (is-valid-aggregated-public-key key {reward-cycle: reward-cycle, round: round}) err-duplicate-aggregate-public-key)
(asserts! (map-insert votes {reward-cycle: reward-cycle, round: round, signer: tx-sender} {aggregate-public-key: key, reward-slots: num-slots}) err-duplicate-vote)
(map-set tally tally-key new-total)
(map-set used-aggregate-public-keys key {reward-cycle: reward-cycle, round: round})
(update-last-round reward-cycle round)
(print "voted")
(ok true)))
(define-private (update-last-round (reward-cycle uint) (round uint))
(match (map-get? rounds reward-cycle)
last-round (and (> round last-round) (map-set rounds reward-cycle round))
(map-set rounds reward-cycle round)))

View File

@@ -13,6 +13,10 @@
(define-read-only (stackerdb-get-signer-slots)
(ok (var-get stackerdb-signer-slots)))
(define-read-only (stackerdb-get-signer-by-index (signer-index uint))
(ok (element-at (var-get stackerdb-signer-slots) signer-index))
)
(define-read-only (stackerdb-get-config)
(ok
{ chunk-size: CHUNK_SIZE,
@@ -21,3 +25,6 @@
max-neighbors: u32,
hint-replicas: (list) }
))
(define-read-only (stackerdb-get-last-set-cycle)
(ok (var-get last-set-cycle)))

View File

@@ -18,7 +18,9 @@ use clarity::vm::clarity::ClarityConnection;
use clarity::vm::contexts::OwnedEnvironment;
use clarity::vm::costs::LimitedCostTracker;
use clarity::vm::tests::symbols_from_values;
use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier, TupleData};
use clarity::vm::types::{
PrincipalData, QualifiedContractIdentifier, StacksAddressExtensions, TupleData,
};
use clarity::vm::Value::Principal;
use clarity::vm::{ClarityName, ClarityVersion, ContractName, Value};
use stacks_common::address::AddressHashMode;
@@ -44,7 +46,7 @@ use crate::chainstate::stacks::boot::pox_4_tests::{
use crate::chainstate::stacks::boot::test::{
instantiate_pox_peer_with_epoch, key_to_stacks_addr, make_pox_4_lockup, with_sortdb,
};
use crate::chainstate::stacks::boot::{NakamotoSignerEntry, SIGNERS_NAME};
use crate::chainstate::stacks::boot::{NakamotoSignerEntry, SIGNERS_NAME, SIGNERS_VOTING_NAME};
use crate::chainstate::stacks::db::StacksChainState;
use crate::chainstate::stacks::index::marf::MarfConnection;
use crate::chainstate::stacks::{
@@ -197,8 +199,12 @@ fn signers_get_signer_keys_from_stackerdb() {
let stacker_1 = TestStacker::from_seed(&[3, 4]);
let stacker_2 = TestStacker::from_seed(&[5, 6]);
let (mut peer, test_signers, latest_block_id) =
prepare_signers_test(function_name!(), Some(vec![&stacker_1, &stacker_2]));
let (mut peer, test_signers, latest_block_id, _) = prepare_signers_test(
function_name!(),
vec![],
Some(vec![&stacker_1, &stacker_2]),
None,
);
let private_key = peer.config.private_key.clone();
@@ -239,13 +245,21 @@ fn signers_get_signer_keys_from_stackerdb() {
assert_eq!(signers, expected_stackerdb_slots);
}
fn prepare_signers_test<'a>(
pub fn prepare_signers_test<'a>(
test_name: &str,
initial_balances: Vec<(PrincipalData, u64)>,
stackers: Option<Vec<&TestStacker>>,
) -> (TestPeer<'a>, TestSigners, StacksBlockId) {
observer: Option<&'a TestEventObserver>,
) -> (TestPeer<'a>, TestSigners, StacksBlockId, u128) {
let mut test_signers = TestSigners::default();
let mut peer = boot_nakamoto(test_name, vec![], &test_signers, stackers);
let mut peer = boot_nakamoto(
test_name,
initial_balances,
&test_signers,
stackers,
observer,
);
let (burn_ops, mut tenure_change, miner_key) =
peer.begin_nakamoto_tenure(TenureChangeCause::BlockFound);
@@ -269,7 +283,30 @@ fn prepare_signers_test<'a>(
);
let latest_block_id = blocks_and_sizes.last().unwrap().0.block_id();
(peer, test_signers, latest_block_id)
let current_reward_cycle = readonly_call(
&mut peer,
&latest_block_id,
SIGNERS_VOTING_NAME.into(),
"current-reward-cycle".into(),
vec![],
)
.expect_u128();
assert_eq!(current_reward_cycle, 7);
let last_set_cycle = readonly_call(
&mut peer,
&latest_block_id,
SIGNERS_NAME.into(),
"stackerdb-get-last-set-cycle".into(),
vec![],
)
.expect_result_ok()
.expect_u128();
assert_eq!(last_set_cycle, 7);
(peer, test_signers, latest_block_id, current_reward_cycle)
}
fn advance_blocks(
@@ -355,3 +392,33 @@ fn readonly_call(
})
.unwrap()
}
pub fn get_signer_index(
peer: &mut TestPeer<'_>,
latest_block_id: StacksBlockId,
signer_address: StacksAddress,
) -> u128 {
let signers = readonly_call(
peer,
&latest_block_id,
"signers".into(),
"stackerdb-get-signer-slots".into(),
vec![],
)
.expect_result_ok()
.expect_list();
signers
.iter()
.position(|value| {
value
.clone()
.expect_tuple()
.get("signer")
.unwrap()
.clone()
.expect_principal()
== signer_address.to_account_principal()
})
.expect("signer not found") as u128
}

View File

@@ -0,0 +1,435 @@
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
// Copyright (C) 2020-2024 Stacks Open Internet Foundation
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use std::collections::{HashMap, HashSet, VecDeque};
use std::convert::{TryFrom, TryInto};
use clarity::boot_util::boot_code_addr;
use clarity::vm::clarity::ClarityConnection;
use clarity::vm::contexts::OwnedEnvironment;
use clarity::vm::contracts::Contract;
use clarity::vm::costs::{CostOverflowingMath, LimitedCostTracker};
use clarity::vm::database::*;
use clarity::vm::errors::{
CheckErrors, Error, IncomparableError, InterpreterError, InterpreterResult, RuntimeErrorType,
};
use clarity::vm::eval;
use clarity::vm::events::StacksTransactionEvent;
use clarity::vm::representations::SymbolicExpression;
use clarity::vm::tests::{execute, is_committed, is_err_code, symbols_from_values};
use clarity::vm::types::Value::Response;
use clarity::vm::types::{
BuffData, OptionalData, PrincipalData, QualifiedContractIdentifier, ResponseData, SequenceData,
StacksAddressExtensions, StandardPrincipalData, TupleData, TupleTypeSignature, TypeSignature,
Value, NONE,
};
use stacks_common::address::AddressHashMode;
use stacks_common::types::chainstate::{
BlockHeaderHash, BurnchainHeaderHash, StacksAddress, StacksBlockId, VRFSeed,
};
use stacks_common::types::Address;
use stacks_common::util::hash::{hex_bytes, to_hex, Sha256Sum, Sha512Trunc256Sum};
use stacks_common::util::secp256k1::Secp256k1PrivateKey;
use wsts::curve::point::{Compressed, Point};
use super::test::*;
use super::RawRewardSetEntry;
use crate::burnchains::{Burnchain, PoxConstants};
use crate::chainstate::burn::db::sortdb::{self, SortitionDB};
use crate::chainstate::burn::operations::*;
use crate::chainstate::burn::{BlockSnapshot, ConsensusHash};
use crate::chainstate::nakamoto::coordinator::tests::make_token_transfer;
use crate::chainstate::nakamoto::tests::get_account;
use crate::chainstate::nakamoto::tests::node::{TestSigners, TestStacker};
use crate::chainstate::nakamoto::NakamotoBlock;
use crate::chainstate::stacks::address::{PoxAddress, PoxAddressType20, PoxAddressType32};
use crate::chainstate::stacks::boot::pox_2_tests::{
check_pox_print_event, generate_pox_clarity_value, get_reward_set_entries_at,
get_stacking_state_pox, get_stx_account_at, with_clarity_db_ro, PoxPrintFields,
StackingStateCheckData,
};
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::{
BOOT_CODE_COST_VOTING_TESTNET as BOOT_CODE_COST_VOTING, BOOT_CODE_POX_TESTNET, SIGNERS_NAME,
SIGNERS_VOTING_NAME,
};
use crate::chainstate::stacks::db::{
MinerPaymentSchedule, StacksChainState, StacksHeaderInfo, MINER_REWARD_MATURITY,
};
use crate::chainstate::stacks::events::{StacksTransactionReceipt, TransactionOrigin};
use crate::chainstate::stacks::index::marf::MarfConnection;
use crate::chainstate::stacks::index::MarfTrieId;
use crate::chainstate::stacks::tests::make_coinbase;
use crate::chainstate::{self, stacks::*};
use crate::clarity_vm::clarity::{ClarityBlockConnection, Error as ClarityError};
use crate::clarity_vm::database::marf::{MarfedKV, WritableMarfStore};
use crate::clarity_vm::database::HeadersDBConn;
use crate::core::*;
use crate::net::test::{TestEventObserver, TestPeer};
use crate::util_lib::boot::boot_code_id;
use crate::util_lib::db::{DBConn, FromRow};
pub fn prepare_pox4_test<'a>(
test_name: &str,
observer: Option<&'a TestEventObserver>,
) -> (
Burnchain,
TestPeer<'a>,
Vec<StacksPrivateKey>,
StacksBlockId,
u64,
usize,
) {
let (epochs, pox_constants) = make_test_epochs_pox();
let mut burnchain = Burnchain::default_unittest(
0,
&BurnchainHeaderHash::from_hex(BITCOIN_REGTEST_FIRST_BLOCK_HASH).unwrap(),
);
burnchain.pox_constants = pox_constants.clone();
let (mut peer, keys) =
instantiate_pox_peer_with_epoch(&burnchain, test_name, Some(epochs.clone()), observer);
assert_eq!(burnchain.pox_constants.reward_slots(), 6);
let mut coinbase_nonce = 0;
// Advance into pox4
let target_height = burnchain.pox_constants.pox_4_activation_height;
let mut latest_block = peer.tenure_with_txs(&[], &mut coinbase_nonce);
while get_tip(peer.sortdb.as_ref()).block_height < u64::from(target_height) {
latest_block = peer.tenure_with_txs(&[], &mut coinbase_nonce);
// if we reach epoch 2.1, perform the check
if get_tip(peer.sortdb.as_ref()).block_height > epochs[3].start_height {
assert_latest_was_burn(&mut peer);
}
}
let block_height = get_tip(peer.sortdb.as_ref()).block_height;
info!("Block height: {}", block_height);
(
burnchain,
peer,
keys,
latest_block,
block_height,
coinbase_nonce,
)
}
/// In this test case, Alice votes in the first block of the first tenure of the prepare phase.
/// Alice can vote successfully.
/// A second vote on the same key and round fails with "duplicate vote" error
#[test]
fn vote_for_aggregate_public_key_in_first_block() {
let stacker_1 = TestStacker::from_seed(&[3, 4]);
let stacker_2 = TestStacker::from_seed(&[5, 6]);
let observer = TestEventObserver::new();
let signer = key_to_stacks_addr(&stacker_1.signer_private_key).to_account_principal();
let (mut peer, mut test_signers, latest_block_id, current_reward_cycle) = prepare_signers_test(
function_name!(),
vec![(signer, 1000)],
Some(vec![&stacker_1, &stacker_2]),
Some(&observer),
);
// create vote txs
let signer_nonce = 0;
let signer_key = &stacker_1.signer_private_key;
let signer_address = key_to_stacks_addr(signer_key);
let signer_principal = PrincipalData::from(signer_address);
let cycle_id = current_reward_cycle;
let signer_index = get_signer_index(&mut peer, latest_block_id, signer_address);
let aggregate_public_key: Point = Point::new();
let aggreagte_public_key_value =
Value::buff_from(aggregate_public_key.compress().data.to_vec())
.expect("Failed to serialize aggregate public key");
let txs = vec![
// cast a vote for the aggregate public key
make_signers_vote_for_aggregate_public_key(
signer_key,
signer_nonce,
signer_index,
&aggregate_public_key,
0,
),
// cast the vote twice
make_signers_vote_for_aggregate_public_key(
signer_key,
signer_nonce + 1,
signer_index,
&aggregate_public_key,
0,
),
];
//
// vote in the first burn block of prepare phase
//
let blocks_and_sizes = nakamoto_tenure(&mut peer, &mut test_signers, vec![txs], signer_key);
// check the last two txs in the last block
let block = observer.get_blocks().last().unwrap().clone();
let receipts = block.receipts.as_slice();
assert_eq!(receipts.len(), 4);
// ignore tenure change tx
// ignore tenure coinbase tx
// first vote should succeed
let tx1 = &receipts[receipts.len() - 2];
assert_eq!(
tx1.result,
Value::Response(ResponseData {
committed: true,
data: Box::new(Value::Bool(true))
})
);
// second vote should fail with duplicate vote error
let tx2 = &receipts[receipts.len() - 1];
assert_eq!(
tx2.result,
Value::Response(ResponseData {
committed: false,
data: Box::new(Value::UInt(10006)) // err-duplicate-vote
})
);
}
/// In this test case, Alice votes in the first block of the last tenure of the prepare phase.
/// Bob votes in the second block of that tenure.
/// Alice can vote successfully.
/// Bob is out of the voting window.
#[test]
fn vote_for_aggregate_public_key_in_last_block() {
let stacker_1 = TestStacker::from_seed(&[3, 4]);
let stacker_2 = TestStacker::from_seed(&[5, 6]);
let observer = TestEventObserver::new();
let signer_1 = key_to_stacks_addr(&stacker_1.signer_private_key).to_account_principal();
let signer_2 = key_to_stacks_addr(&stacker_2.signer_private_key).to_account_principal();
let (mut peer, mut test_signers, latest_block_id, current_reward_cycle) = prepare_signers_test(
function_name!(),
vec![(signer_1, 1000), (signer_2, 1000)],
Some(vec![&stacker_1, &stacker_2]),
Some(&observer),
);
let mut stacker_1_nonce: u64 = 1;
let dummy_tx_1 = make_dummy_tx(
&mut peer,
&stacker_1.stacker_private_key,
&mut stacker_1_nonce,
);
let dummy_tx_2 = make_dummy_tx(
&mut peer,
&stacker_1.stacker_private_key,
&mut stacker_1_nonce,
);
let dummy_tx_3 = make_dummy_tx(
&mut peer,
&stacker_1.stacker_private_key,
&mut stacker_1_nonce,
);
let cycle_id: u128 = current_reward_cycle;
let aggregated_public_key: Point = Point::new();
// create vote txs for alice
let signer_1_nonce = 0;
let signer_1_key = &stacker_1.signer_private_key;
let signer_1_address = key_to_stacks_addr(signer_1_key);
let signer_1_principal = PrincipalData::from(signer_1_address);
let signer_1_index = get_signer_index(&mut peer, latest_block_id, signer_1_address);
let txs_1 = vec![
// cast a vote for the aggregate public key
make_signers_vote_for_aggregate_public_key(
signer_1_key,
signer_1_nonce,
signer_1_index,
&aggregated_public_key,
0,
),
// cast the vote twice
make_signers_vote_for_aggregate_public_key(
signer_1_key,
signer_1_nonce + 1,
signer_1_index,
&aggregated_public_key,
0,
),
];
// create vote txs for bob
let signer_2_nonce = 0;
let signer_2_key = &stacker_2.signer_private_key;
let signer_2_address = key_to_stacks_addr(signer_2_key);
let signer_2_principal = PrincipalData::from(signer_2_address);
let signer_2_index = get_signer_index(&mut peer, latest_block_id, signer_2_address);
let txs_2 = vec![
// cast a vote for the aggregate public key
make_signers_vote_for_aggregate_public_key(
signer_2_key,
signer_2_nonce,
signer_2_index,
&aggregated_public_key,
0,
),
];
//
// vote in the last burn block of prepare phase
//
nakamoto_tenure(
&mut peer,
&mut test_signers,
vec![vec![dummy_tx_1]],
signer_1_key,
);
nakamoto_tenure(
&mut peer,
&mut test_signers,
vec![vec![dummy_tx_2]],
signer_1_key,
);
// alice votes in first block of tenure
// bob votes in second block of tenure
let blocks_and_sizes = nakamoto_tenure(
&mut peer,
&mut test_signers,
vec![txs_1, txs_2],
signer_1_key,
);
// check alice's and bob's txs
let blocks = observer.get_blocks();
// alice's block
let block = &blocks[blocks.len() - 2].clone();
let receipts = &block.receipts;
assert_eq!(receipts.len(), 4);
// first vote should succeed
let tx1 = &receipts[receipts.len() - 2];
assert_eq!(
tx1.result,
Value::Response(ResponseData {
committed: true,
data: Box::new(Value::Bool(true))
})
);
// second vote should fail with duplicate vote error
let tx2 = &receipts[receipts.len() - 1];
assert_eq!(
tx2.result,
Value::Response(ResponseData {
committed: false,
data: Box::new(Value::UInt(10006)) // err-duplicate-vote
})
);
// bob's block
let block = blocks.last().unwrap().clone();
let receipts = block.receipts.as_slice();
assert_eq!(receipts.len(), 1);
// vote should succeed
let tx1 = &receipts[receipts.len() - 1];
assert_eq!(
tx1.result,
Value::Response(ResponseData {
committed: false,
data: Box::new(Value::UInt(10002)) // err-out-of-voting-window
})
);
}
fn nakamoto_tenure(
peer: &mut TestPeer,
test_signers: &mut TestSigners,
txs_of_blocks: Vec<Vec<StacksTransaction>>,
stacker_private_key: &StacksPrivateKey,
) -> Vec<(NakamotoBlock, u64, ExecutionCost)> {
let current_height = peer.get_burnchain_view().unwrap().burn_block_height;
info!("current height: {}", current_height);
let (burn_ops, mut tenure_change, miner_key) =
peer.begin_nakamoto_tenure(TenureChangeCause::BlockFound);
let (_, _, consensus_hash) = peer.next_burnchain_block(burn_ops);
let vrf_proof = peer.make_nakamoto_vrf_proof(miner_key);
tenure_change.tenure_consensus_hash = consensus_hash.clone();
tenure_change.burn_view_consensus_hash = consensus_hash.clone();
let tenure_change_tx = peer
.miner
.make_nakamoto_tenure_change(tenure_change.clone());
let coinbase_tx = peer.miner.make_nakamoto_coinbase(None, vrf_proof);
let recipient_addr = boot_code_addr(false);
let mut mutable_txs_of_blocks = txs_of_blocks.clone();
mutable_txs_of_blocks.reverse();
let blocks_and_sizes = peer.make_nakamoto_tenure(
tenure_change_tx,
coinbase_tx.clone(),
test_signers,
|miner, chainstate, sortdb, blocks| mutable_txs_of_blocks.pop().unwrap_or(vec![]),
);
info!("tenure length {}", blocks_and_sizes.len());
blocks_and_sizes
}
fn make_dummy_tx(
peer: &mut TestPeer,
private_key: &StacksPrivateKey,
nonce: &mut u64,
) -> StacksTransaction {
peer.with_db_state(|sortdb, chainstate, _, _| {
let addr = key_to_stacks_addr(&private_key);
let account = get_account(chainstate, sortdb, &addr);
let recipient_addr = boot_code_addr(false);
let stx_transfer = make_token_transfer(
chainstate,
sortdb,
&private_key,
*nonce,
1,
1,
&recipient_addr,
);
*nonce += 1;
Ok(stx_transfer)
})
.unwrap()
}

View File

@@ -49,6 +49,7 @@ use crate::chainstate::stacks::boot::{
BOOT_TEST_POX_4_AGG_KEY_CONTRACT, BOOT_TEST_POX_4_AGG_KEY_FNAME, COSTS_2_NAME, COSTS_3_NAME,
MINERS_NAME, POX_2_MAINNET_CODE, POX_2_NAME, POX_2_TESTNET_CODE, POX_3_MAINNET_CODE,
POX_3_NAME, POX_3_TESTNET_CODE, POX_4_CODE, POX_4_NAME, SIGNERS_BODY, SIGNERS_NAME,
SIGNERS_VOTING_NAME, SIGNER_VOTING_CODE,
};
use crate::chainstate::stacks::db::{StacksAccount, StacksChainState};
use crate::chainstate::stacks::events::{StacksTransactionEvent, StacksTransactionReceipt};
@@ -1436,7 +1437,7 @@ impl<'a, 'b> ClarityBlockConnection<'a, 'b> {
&boot_code_account,
ASTRules::PrecheckSize,
)
.expect("FATAL: Failed to process .miners contract initialization");
.expect("FATAL: Failed to process .signers contract initialization");
receipt
});
@@ -1449,6 +1450,43 @@ impl<'a, 'b> ClarityBlockConnection<'a, 'b> {
);
}
let signers_voting_code = &*SIGNER_VOTING_CODE;
let signers_voting_contract_id = boot_code_id(SIGNERS_VOTING_NAME, mainnet);
let payload = TransactionPayload::SmartContract(
TransactionSmartContract {
name: ContractName::try_from(SIGNERS_VOTING_NAME)
.expect("FATAL: invalid boot-code contract name"),
code_body: StacksString::from_str(signers_voting_code)
.expect("FATAL: invalid boot code body"),
},
Some(ClarityVersion::Clarity2),
);
let signers_contract_tx =
StacksTransaction::new(tx_version.clone(), boot_code_auth.clone(), payload);
let signers_voting_initialization_receipt = self.as_transaction(|tx_conn| {
// initialize with a synthetic transaction
debug!("Instantiate {} contract", &signers_voting_contract_id);
let receipt = StacksChainState::process_transaction_payload(
tx_conn,
&signers_contract_tx,
&boot_code_account,
ASTRules::PrecheckSize,
)
.expect("FATAL: Failed to process .signers-voting contract initialization");
receipt
});
if signers_voting_initialization_receipt.result != Value::okay_true()
|| signers_voting_initialization_receipt.post_condition_aborted
{
panic!(
"FATAL: Failure processing signers-voting contract initialization: {:#?}",
&signers_voting_initialization_receipt
);
}
debug!("Epoch 2.5 initialized");
(old_cost_tracker, Ok(vec![pox_4_initialization_receipt]))
})