diff --git a/.github/workflows/bitcoin-tests.yml b/.github/workflows/bitcoin-tests.yml index ae465bbd4..e1e4fff76 100644 --- a/.github/workflows/bitcoin-tests.yml +++ b/.github/workflows/bitcoin-tests.yml @@ -76,8 +76,11 @@ jobs: - tests::nakamoto_integrations::block_proposal_api_endpoint - tests::nakamoto_integrations::miner_writes_proposed_block_to_stackerdb - tests::nakamoto_integrations::correct_burn_outs - - tests::signer::stackerdb_dkg_sign + - tests::signer::stackerdb_dkg + - tests::signer::stackerdb_sign - tests::signer::stackerdb_block_proposal + - tests::signer::stackerdb_filter_bad_transactions + - tests::signer::stackerdb_mine_2_nakamoto_reward_cycles steps: ## Setup test environment - name: Setup Test Environment diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb010862f..434c977a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,9 @@ jobs: - name: Rustfmt id: rustfmt - uses: actions-rust-lang/rustfmt@2d1d4e9f72379428552fa1def0b898733fb8472d # v1.1.0 + uses: stacks-network/actions/rustfmt@main + with: + alias: "fmt-stacks" ###################################################################################### ## Create a tagged github release @@ -148,6 +150,7 @@ jobs: ) || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || + github.event_name == 'merge_group' || ( contains(' refs/heads/master diff --git a/.github/workflows/pr-differences-mutants.yml b/.github/workflows/pr-differences-mutants.yml index 041db9759..fc4a72568 100644 --- a/.github/workflows/pr-differences-mutants.yml +++ b/.github/workflows/pr-differences-mutants.yml @@ -8,7 +8,7 @@ on: - synchronize - ready_for_review paths: - - "**.rs" + - '**.rs' concurrency: group: pr-differences-${{ github.head_ref || github.ref || github.run_id }} @@ -23,10 +23,13 @@ jobs: runs-on: ubuntu-latest outputs: - run_big_packages: ${{ steps.check_packages_and_shards.outputs.run_big_packages }} - big_packages_with_shards: ${{ steps.check_packages_and_shards.outputs.big_packages_with_shards }} + run_stackslib: ${{ steps.check_packages_and_shards.outputs.run_stackslib }} + stackslib_with_shards: ${{ steps.check_packages_and_shards.outputs.stackslib_with_shards }} + run_stacks_node: ${{ steps.check_packages_and_shards.outputs.run_stacks_node }} + stacks_node_with_shards: ${{ steps.check_packages_and_shards.outputs.stacks_node_with_shards }} run_small_packages: ${{ steps.check_packages_and_shards.outputs.run_small_packages }} small_packages_with_shards: ${{ steps.check_packages_and_shards.outputs.small_packages_with_shards }} + run_stacks_signer: ${{ steps.check_packages_and_shards.outputs.run_stacks_signer }} steps: - id: check_packages_and_shards @@ -38,7 +41,9 @@ jobs: needs: check-big-packages-and-shards - if: ${{ needs.check-big-packages-and-shards.outputs.run_small_packages == 'true' && needs.check-big-packages-and-shards.outputs.small_packages_with_shards == 'false' }} + if: | + needs.check-big-packages-and-shards.outputs.run_small_packages == 'true' && + needs.check-big-packages-and-shards.outputs.small_packages_with_shards == 'false' runs-on: ubuntu-latest @@ -46,7 +51,7 @@ jobs: - name: Run mutants on diffs uses: stacks-network/actions/stacks-core/mutation-testing/pr-differences@main with: - package-dimension: "small" + package: 'small' # Mutation testing - Execute on PR on small packages that have functions modified (run with strategy matrix shards) pr-differences-mutants-small-shards: @@ -54,7 +59,9 @@ jobs: needs: check-big-packages-and-shards - if: ${{ needs.check-big-packages-and-shards.outputs.run_small_packages == 'true' && needs.check-big-packages-and-shards.outputs.small_packages_with_shards == 'true' }} + if: | + needs.check-big-packages-and-shards.outputs.run_small_packages == 'true' && + needs.check-big-packages-and-shards.outputs.small_packages_with_shards == 'true' runs-on: ubuntu-latest @@ -68,15 +75,17 @@ jobs: uses: stacks-network/actions/stacks-core/mutation-testing/pr-differences@main with: shard: ${{ matrix.shard }} - package-dimension: "small" + package: 'small' - # Mutation testing - Execute on PR on big packages that have functions modified (normal run, no shards) - pr-differences-mutants-big-normal: - name: Mutation Testing - Normal, Big + # Mutation testing - Execute on PR on stackslib package (normal run, no shards) + pr-differences-mutants-stackslib-normal: + name: Mutation Testing - Normal, Stackslib needs: check-big-packages-and-shards - if: ${{ needs.check-big-packages-and-shards.outputs.run_big_packages == 'true' && needs.check-big-packages-and-shards.outputs.big_packages_with_shards == 'false' }} + if: | + needs.check-big-packages-and-shards.outputs.run_stackslib == 'true' && + needs.check-big-packages-and-shards.outputs.stackslib_with_shards == 'false' runs-on: ubuntu-latest @@ -87,15 +96,17 @@ jobs: RUST_BACKTRACE: full uses: stacks-network/actions/stacks-core/mutation-testing/pr-differences@main with: - package-dimension: "big" + package: 'stackslib' - # Mutation testing - Execute on PR on big packages that have functions modified (run with strategy matrix shards) - pr-differences-mutants-big-shards: - name: Mutation Testing - Shards, Big + # Mutation testing - Execute on PR on stackslib package (run with strategy matrix shards) + pr-differences-mutants-stackslib-shards: + name: Mutation Testing - Shards, Stackslib needs: check-big-packages-and-shards - if: ${{ needs.check-big-packages-and-shards.outputs.run_big_packages == 'true' && needs.check-big-packages-and-shards.outputs.big_packages_with_shards == 'true' }} + if: | + needs.check-big-packages-and-shards.outputs.run_stackslib == 'true' && + needs.check-big-packages-and-shards.outputs.stackslib_with_shards == 'true' runs-on: ubuntu-latest @@ -112,7 +123,72 @@ jobs: uses: stacks-network/actions/stacks-core/mutation-testing/pr-differences@main with: shard: ${{ matrix.shard }} - package-dimension: "big" + package: 'stackslib' + + # Mutation testing - Execute on PR on stacks-node package (normal run, no shards) + pr-differences-mutants-stacks-node-normal: + name: Mutation Testing - Normal, Stacks Node + + needs: check-big-packages-and-shards + + if: | + needs.check-big-packages-and-shards.outputs.run_stacks_node == 'true' && + needs.check-big-packages-and-shards.outputs.stacks_node_with_shards == 'false' + + runs-on: ubuntu-latest + + steps: + - name: Run Run mutants on diffs + env: + BITCOIND_TEST: 1 + RUST_BACKTRACE: full + uses: stacks-network/actions/stacks-core/mutation-testing/pr-differences@main + with: + package: 'stacks-node' + + # Mutation testing - Execute on PR on stacks-node package (run with strategy matrix shards) + pr-differences-mutants-stacks-node-shards: + name: Mutation Testing - Shards, Stacks Node + + needs: check-big-packages-and-shards + + if: | + needs.check-big-packages-and-shards.outputs.run_stacks_node == 'true' && + needs.check-big-packages-and-shards.outputs.stacks_node_with_shards == 'true' + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + shard: [0, 1, 2, 3] + + steps: + - name: Run mutants on diffs + env: + BITCOIND_TEST: 1 + RUST_BACKTRACE: full + uses: stacks-network/actions/stacks-core/mutation-testing/pr-differences@main + with: + shard: ${{ matrix.shard }} + package: 'stacks-node' + + # Mutation testing - Execute on PR on stacks-signer package (normal run, no shards) + pr-differences-mutants-stacks-signer-normal: + name: Mutation Testing - Normal, Stacks Signer + + needs: check-big-packages-and-shards + + if: | + needs.check-big-packages-and-shards.outputs.run_stacks_signer == 'true' + + runs-on: ubuntu-latest + + steps: + - name: Run mutants on diffs + uses: stacks-network/actions/stacks-core/mutation-testing/pr-differences@main + with: + package: 'stacks-signer' # Output the mutants and fail the workflow if there are missed/timeout/unviable mutants output-mutants: @@ -120,20 +196,27 @@ jobs: runs-on: ubuntu-latest + if: always() needs: [ check-big-packages-and-shards, pr-differences-mutants-small-normal, pr-differences-mutants-small-shards, - pr-differences-mutants-big-normal, - pr-differences-mutants-big-shards, + pr-differences-mutants-stackslib-normal, + pr-differences-mutants-stackslib-shards, + pr-differences-mutants-stacks-node-normal, + pr-differences-mutants-stacks-node-shards, + pr-differences-mutants-stacks-signer-normal, ] steps: - name: Output Mutants uses: stacks-network/actions/stacks-core/mutation-testing/output-pr-mutants@main with: - big_packages: ${{ needs.check-big-packages-and-shards.outputs.run_big_packages }} - shards_for_big_packages: ${{ needs.check-big-packages-and-shards.outputs.big_packages_with_shards }} + stackslib_package: ${{ needs.check-big-packages-and-shards.outputs.run_stackslib }} + shards_for_stackslib_package: ${{ needs.check-big-packages-and-shards.outputs.stackslib_with_shards }} + stacks_node_package: ${{ needs.check-big-packages-and-shards.outputs.run_stacks_node }} + shards_for_stacks_node_package: ${{ needs.check-big-packages-and-shards.outputs.stacks_node_with_shards }} small_packages: ${{ needs.check-big-packages-and-shards.outputs.run_small_packages }} shards_for_small_packages: ${{ needs.check-big-packages-and-shards.outputs.small_packages_with_shards }} + stacks_signer: ${{ needs.check-big-packages-and-shards.outputs.run_stacks_signer }} diff --git a/.github/workflows/stacks-core-tests.yml b/.github/workflows/stacks-core-tests.yml index 1e883d3d9..5105c6535 100644 --- a/.github/workflows/stacks-core-tests.yml +++ b/.github/workflows/stacks-core-tests.yml @@ -160,9 +160,11 @@ jobs: id: codecov uses: stacks-network/actions/codecov@main with: + # We'd like to uncomment the below line once the codecov upload is working + # fail_ci_if_error: true test-name: ${{ matrix.test-name }} upload-only: true - filename: ./lcov.info + filename: ./contrib/core-contract-tests/lcov.info # Core contract tests on Clarinet v1 # Check for false positives/negatives diff --git a/Cargo.lock b/Cargo.lock index 74c4a5efc..4aa6412fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -730,7 +730,6 @@ dependencies = [ "serde_derive", "serde_json", "serde_stacker", - "sha2-asm 0.5.5", "slog", "stacks-common", "time 0.2.27", @@ -989,19 +988,6 @@ dependencies = [ "syn 2.0.48", ] -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if 1.0.0", - "hashbrown 0.14.3", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "data-encoding" version = "2.5.0" @@ -1990,16 +1976,6 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" -[[package]] -name = "lock_api" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" -dependencies = [ - "autocfg", - "scopeguard", -] - [[package]] name = "log" version = "0.4.20" @@ -2118,6 +2094,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "mutants" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0287524726960e07b119cebd01678f852f147742ae0d925e6a520dca956126" + [[package]] name = "net2" version = "0.2.39" @@ -2273,29 +2255,6 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.48.5", -] - [[package]] name = "percent-encoding" version = "2.3.1" @@ -3062,12 +3021,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "sct" version = "0.7.1" @@ -3219,31 +3172,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serial_test" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ad9342b3aaca7cb43c45c097dd008d4907070394bd0751a0aa8817e5a018d" -dependencies = [ - "dashmap", - "futures", - "lazy_static", - "log", - "parking_lot", - "serial_test_derive", -] - -[[package]] -name = "serial_test_derive" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93fb4adc70021ac1b47f7d45e8cc4169baaa7ea58483bc5b721d19a26202212" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.48", -] - [[package]] name = "sha1" version = "0.6.1" @@ -3292,16 +3220,7 @@ dependencies = [ "cfg-if 1.0.0", "cpufeatures", "digest 0.10.7", - "sha2-asm 0.6.3", -] - -[[package]] -name = "sha2-asm" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7c2f225be6502f2134e6bbb35bb5e2957e41ffa0495ed08bce2e2b4ca885da4" -dependencies = [ - "cc", + "sha2-asm", ] [[package]] @@ -3497,6 +3416,7 @@ dependencies = [ "lazy_static", "libc", "libsigner", + "mutants", "pico-args", "rand 0.8.5", "rand_core 0.6.4", @@ -3539,7 +3459,6 @@ dependencies = [ "serde_derive", "serde_json", "serde_stacker", - "serial_test", "slog", "slog-json", "slog-term", @@ -3549,6 +3468,7 @@ dependencies = [ "toml 0.5.11", "tracing", "tracing-subscriber", + "url", "wsts", ] @@ -3568,6 +3488,7 @@ dependencies = [ "libc", "libstackerdb", "mio 0.6.23", + "mutants", "nix", "percent-encoding", "pox-locking", diff --git a/clarity/Cargo.toml b/clarity/Cargo.toml index c0b82a7fd..4d51cf3e4 100644 --- a/clarity/Cargo.toml +++ b/clarity/Cargo.toml @@ -56,6 +56,3 @@ developer-mode = [] slog_json = ["stacks_common/slog_json"] testing = [] -[target.'cfg(all(target_arch = "x86_64", not(target_env = "msvc")))'.dependencies] -sha2-asm = "0.5.3" - diff --git a/clarity/src/libclarity.rs b/clarity/src/libclarity.rs index 4540d15e8..daae7dcfd 100644 --- a/clarity/src/libclarity.rs +++ b/clarity/src/libclarity.rs @@ -51,7 +51,6 @@ pub use stacks_common::{ pub mod vm; pub mod boot_util { - use std::convert::TryFrom; use stacks_common::types::chainstate::StacksAddress; diff --git a/clarity/src/vm/analysis/contract_interface_builder/mod.rs b/clarity/src/vm/analysis/contract_interface_builder/mod.rs index c9bc3c71c..63cf11993 100644 --- a/clarity/src/vm/analysis/contract_interface_builder/mod.rs +++ b/clarity/src/vm/analysis/contract_interface_builder/mod.rs @@ -237,14 +237,13 @@ pub struct ContractInterfaceFunctionArg { impl ContractInterfaceFunctionArg { pub fn from_function_args(fnArgs: &[FunctionArg]) -> Vec { - let mut args: Vec = Vec::new(); - for fnArg in fnArgs.iter() { - args.push(ContractInterfaceFunctionArg { + fnArgs + .iter() + .map(|fnArg| ContractInterfaceFunctionArg { name: fnArg.name.to_string(), type_f: ContractInterfaceAtomType::from_type_signature(&fnArg.signature), - }); - } - args + }) + .collect() } } diff --git a/clarity/src/vm/analysis/type_checker/v2_05/mod.rs b/clarity/src/vm/analysis/type_checker/v2_05/mod.rs index cc6d7e906..286e2e11f 100644 --- a/clarity/src/vm/analysis/type_checker/v2_05/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_05/mod.rs @@ -19,7 +19,6 @@ pub mod contexts; pub mod natives; use std::collections::BTreeMap; -use std::convert::TryInto; use hashbrown::HashMap; use stacks_common::types::StacksEpochId; @@ -526,7 +525,7 @@ impl<'a, 'b> TypeChecker<'a, 'b> { args: &[SymbolicExpression], context: &TypingContext, ) -> CheckResult> { - let mut result = Vec::new(); + let mut result = Vec::with_capacity(args.len()); for arg in args.iter() { // don't use map here, since type_check has side-effects. result.push(self.type_check(arg, context)?) diff --git a/clarity/src/vm/analysis/type_checker/v2_05/natives/mod.rs b/clarity/src/vm/analysis/type_checker/v2_05/natives/mod.rs index 18663329f..b38cfd0d1 100644 --- a/clarity/src/vm/analysis/type_checker/v2_05/natives/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_05/natives/mod.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryFrom; - use stacks_common::types::StacksEpochId; use super::{ @@ -187,14 +185,11 @@ pub fn check_special_tuple_cons( context: &TypingContext, ) -> TypeResult { check_arguments_at_least(1, args)?; + let len = args.len(); - let mut tuple_type_data = Vec::new(); + runtime_cost(ClarityCostFunction::AnalysisCheckTupleCons, checker, len)?; - runtime_cost( - ClarityCostFunction::AnalysisCheckTupleCons, - checker, - args.len(), - )?; + let mut tuple_type_data = Vec::with_capacity(len); handle_binding_list(args, |var_name, var_sexp| { checker.type_check(var_sexp, context).and_then(|var_type| { diff --git a/clarity/src/vm/analysis/type_checker/v2_05/natives/sequences.rs b/clarity/src/vm/analysis/type_checker/v2_05/natives/sequences.rs index 49a29e8d3..bed885d14 100644 --- a/clarity/src/vm/analysis/type_checker/v2_05/natives/sequences.rs +++ b/clarity/src/vm/analysis/type_checker/v2_05/natives/sequences.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::{TryFrom, TryInto}; - use stacks_common::types::StacksEpochId; use super::{SimpleNativeFunction, TypedNativeFunction}; @@ -74,9 +72,10 @@ pub fn check_special_map( args.len(), )?; - let mut func_args = vec![]; + let iter = args[1..].iter(); + let mut func_args = Vec::with_capacity(iter.len()); let mut min_args = u32::MAX; - for arg in args[1..].iter() { + for arg in iter { let argument_type = checker.type_check(arg, context)?; let entry_type = match argument_type { TypeSignature::SequenceType(sequence) => { diff --git a/clarity/src/vm/analysis/type_checker/v2_05/tests/assets.rs b/clarity/src/vm/analysis/type_checker/v2_05/tests/assets.rs index 918e09967..df9c35ed0 100644 --- a/clarity/src/vm/analysis/type_checker/v2_05/tests/assets.rs +++ b/clarity/src/vm/analysis/type_checker/v2_05/tests/assets.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryInto; - use stacks_common::types::StacksEpochId; use crate::vm::analysis::errors::CheckErrors; diff --git a/clarity/src/vm/analysis/type_checker/v2_05/tests/mod.rs b/clarity/src/vm/analysis/type_checker/v2_05/tests/mod.rs index 6529b859f..1830caf7c 100644 --- a/clarity/src/vm/analysis/type_checker/v2_05/tests/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_05/tests/mod.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryInto; - use stacks_common::types::StacksEpochId; use crate::vm::analysis::errors::CheckErrors; diff --git a/clarity/src/vm/analysis/type_checker/v2_1/mod.rs b/clarity/src/vm/analysis/type_checker/v2_1/mod.rs index 38cf3c767..a0937e84c 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/mod.rs @@ -18,7 +18,6 @@ pub mod contexts; pub mod natives; use std::collections::BTreeMap; -use std::convert::TryInto; use hashbrown::HashMap; use stacks_common::types::StacksEpochId; @@ -459,7 +458,7 @@ impl FunctionType { } } } else { - let mut arg_types = Vec::new(); + let mut arg_types = Vec::with_capacity(func_args.len()); for arg in func_args { arg_types.push(self.principal_to_callable_type(arg, 1, clarity_version)?); } @@ -1028,7 +1027,7 @@ impl<'a, 'b> TypeChecker<'a, 'b> { args: &[SymbolicExpression], context: &TypingContext, ) -> CheckResult> { - let mut result = Vec::new(); + let mut result = Vec::with_capacity(args.len()); for arg in args.iter() { // don't use map here, since type_check has side-effects. result.push(self.type_check(arg, context)?) diff --git a/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs b/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs index fd1f45604..9fdc8c704 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryFrom; - use stacks_common::types::StacksEpochId; use super::{ @@ -195,7 +193,7 @@ pub fn check_special_tuple_cons( ) -> TypeResult { check_arguments_at_least(1, args)?; - let mut tuple_type_data = Vec::new(); + let mut tuple_type_data = Vec::with_capacity(args.len()); runtime_cost( ClarityCostFunction::AnalysisCheckTupleCons, diff --git a/clarity/src/vm/analysis/type_checker/v2_1/natives/sequences.rs b/clarity/src/vm/analysis/type_checker/v2_1/natives/sequences.rs index 9eb2ae17c..090b259a2 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/natives/sequences.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/natives/sequences.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::{TryFrom, TryInto}; - use stacks_common::types::StacksEpochId; use super::{SimpleNativeFunction, TypedNativeFunction}; @@ -74,9 +72,10 @@ pub fn check_special_map( args.len(), )?; - let mut func_args = vec![]; + let iter = args[1..].iter(); + let mut func_args = Vec::with_capacity(iter.len()); let mut min_args = u32::MAX; - for arg in args[1..].iter() { + for arg in iter { let argument_type = checker.type_check(arg, context)?; let entry_type = match argument_type { TypeSignature::SequenceType(sequence) => { diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/assets.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/assets.rs index 8989fb295..c870fdbab 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/tests/assets.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/assets.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryInto; - #[cfg(test)] use rstest::rstest; #[cfg(test)] diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs index 25e0eb054..2f023dcf4 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryFrom; use std::fs::read_to_string; use assert_json_diff::assert_json_eq; diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/mod.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/mod.rs index d8733cfab..85a6b39ea 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/tests/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/mod.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::{TryFrom, TryInto}; - #[cfg(test)] use rstest::rstest; #[cfg(test)] diff --git a/clarity/src/vm/ast/definition_sorter/mod.rs b/clarity/src/vm/ast/definition_sorter/mod.rs index 36dfb2166..eee662531 100644 --- a/clarity/src/vm/ast/definition_sorter/mod.rs +++ b/clarity/src/vm/ast/definition_sorter/mod.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::iter::FromIterator; - use hashbrown::{HashMap, HashSet}; use crate::vm::ast::errors::{ParseError, ParseErrors, ParseResult}; @@ -91,14 +89,14 @@ impl DefinitionSorter { let sorted_indexes = walker.get_sorted_dependencies(&self.graph)?; if let Some(deps) = walker.get_cycling_dependencies(&self.graph, &sorted_indexes) { - let mut deps_props = vec![]; - for i in deps.iter() { - let exp = &contract_ast.pre_expressions[*i]; - if let Some(def) = self.find_expression_definition(exp) { - deps_props.push(def); - } - } - let functions_names = deps_props.iter().map(|i| i.0.to_string()).collect(); + let functions_names = deps + .into_iter() + .filter_map(|i| { + let exp = &contract_ast.pre_expressions[i]; + self.find_expression_definition(exp) + }) + .map(|i| i.0.to_string()) + .collect::>(); let error = ParseError::new(ParseErrors::CircularReference(functions_names)); return Err(error); diff --git a/clarity/src/vm/ast/parser/v1.rs b/clarity/src/vm/ast/parser/v1.rs index d4dbdcffc..4cdea6e27 100644 --- a/clarity/src/vm/ast/parser/v1.rs +++ b/clarity/src/vm/ast/parser/v1.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::cmp; -use std::convert::TryInto; use lazy_static::lazy_static; use regex::{Captures, Regex}; diff --git a/clarity/src/vm/ast/parser/v2/mod.rs b/clarity/src/vm/ast/parser/v2/mod.rs index 75a622a7c..addbba1c5 100644 --- a/clarity/src/vm/ast/parser/v2/mod.rs +++ b/clarity/src/vm/ast/parser/v2/mod.rs @@ -1,6 +1,5 @@ pub mod lexer; -use std::convert::TryFrom; use std::num::ParseIntError; use stacks_common::util::hash::hex_bytes; @@ -894,12 +893,14 @@ impl<'a> Parser<'a> { Some(expr) } Token::Utf8String(s) => { - let mut data: Vec> = Vec::new(); - for ch in s.chars() { - let mut bytes = vec![0; ch.len_utf8()]; - ch.encode_utf8(&mut bytes); - data.push(bytes); - } + let data: Vec> = s + .chars() + .map(|ch| { + let mut bytes = vec![0; ch.len_utf8()]; + ch.encode_utf8(&mut bytes); + bytes + }) + .collect(); let val = Value::Sequence(SequenceData::String(CharType::UTF8(UTF8Data { data, diff --git a/clarity/src/vm/ast/sugar_expander/mod.rs b/clarity/src/vm/ast/sugar_expander/mod.rs index 481e1039d..7fc6064b8 100644 --- a/clarity/src/vm/ast/sugar_expander/mod.rs +++ b/clarity/src/vm/ast/sugar_expander/mod.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryInto; - use hashbrown::{HashMap, HashSet}; use crate::vm::ast::errors::{ParseError, ParseErrors, ParseResult}; @@ -65,7 +63,7 @@ impl SugarExpander { pre_exprs_iter: PreExpressionsDrain, contract_ast: &mut ContractAST, ) -> ParseResult> { - let mut expressions: Vec = Vec::new(); + let mut expressions: Vec = Vec::with_capacity(pre_exprs_iter.len()); #[cfg(feature = "developer-mode")] let mut comments = Vec::new(); diff --git a/clarity/src/vm/ast/traits_resolver/mod.rs b/clarity/src/vm/ast/traits_resolver/mod.rs index 7c4cdbf95..4cdb2f54a 100644 --- a/clarity/src/vm/ast/traits_resolver/mod.rs +++ b/clarity/src/vm/ast/traits_resolver/mod.rs @@ -46,14 +46,12 @@ impl TraitsResolver { } pub fn run(&mut self, contract_ast: &mut ContractAST) -> ParseResult<()> { - let exprs = contract_ast.pre_expressions[..].to_vec(); let mut referenced_traits = HashMap::new(); - for exp in exprs.iter() { + for exp in contract_ast.pre_expressions.iter() { // Top-level comment nodes have been filtered from `args` by `try_parse_pre_expr`. - let (define_type, args) = match self.try_parse_pre_expr(exp) { - Some(x) => x, - None => continue, + let Some((define_type, args)) = self.try_parse_pre_expr(exp) else { + continue; }; match define_type { diff --git a/clarity/src/vm/ast/types.rs b/clarity/src/vm/ast/types.rs index e8183220a..aedd31eae 100644 --- a/clarity/src/vm/ast/types.rs +++ b/clarity/src/vm/ast/types.rs @@ -79,10 +79,7 @@ pub struct PreExpressionsDrain { impl PreExpressionsDrain { pub fn new(pre_exprs_drain: Drain, sorting: Option>) -> Self { - let mut pre_expressions = HashMap::new(); - for (index, pre_expr) in pre_exprs_drain.enumerate() { - pre_expressions.insert(index, pre_expr); - } + let pre_expressions: HashMap<_, _> = pre_exprs_drain.enumerate().collect(); let sorting = match sorting { Some(sorting) if !sorting.is_empty() => Some(sorting), @@ -95,6 +92,10 @@ impl PreExpressionsDrain { index: 0, } } + + pub fn len(&self) -> usize { + self.len + } } impl Iterator for PreExpressionsDrain { diff --git a/clarity/src/vm/callables.rs b/clarity/src/vm/callables.rs index 597dbab35..32e7d0551 100644 --- a/clarity/src/vm/callables.rs +++ b/clarity/src/vm/callables.rs @@ -15,9 +15,7 @@ // along with this program. If not, see . use std::collections::BTreeMap; -use std::convert::TryInto; use std::fmt; -use std::iter::FromIterator; use stacks_common::types::StacksEpochId; diff --git a/clarity/src/vm/contexts.rs b/clarity/src/vm/contexts.rs index 104adbab1..de7b07036 100644 --- a/clarity/src/vm/contexts.rs +++ b/clarity/src/vm/contexts.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::collections::{BTreeMap, BTreeSet}; -use std::convert::TryInto; use std::fmt; use std::mem::replace; @@ -307,10 +306,7 @@ impl AssetMap { asset: AssetIdentifier, transfered: Value, ) { - let principal_map = self - .asset_map - .entry(principal.clone()) - .or_insert_with(|| HashMap::new()); + let principal_map = self.asset_map.entry(principal.clone()).or_default(); if let Some(map_entry) = principal_map.get_mut(&asset) { map_entry.push(transfered); @@ -327,10 +323,7 @@ impl AssetMap { ) -> Result<()> { let next_amount = self.get_next_amount(principal, &asset, amount)?; - let principal_map = self - .token_map - .entry(principal.clone()) - .or_insert_with(|| HashMap::new()); + let principal_map = self.token_map.entry(principal.clone()).or_default(); principal_map.insert(asset, next_amount); Ok(()) @@ -363,10 +356,7 @@ impl AssetMap { // After this point, this function will not fail. for (principal, mut principal_map) in other.asset_map.drain() { for (asset, mut transfers) in principal_map.drain() { - let landing_map = self - .asset_map - .entry(principal.clone()) - .or_insert_with(|| HashMap::new()); + let landing_map = self.asset_map.entry(principal.clone()).or_default(); if let Some(landing_vec) = landing_map.get_mut(&asset) { landing_vec.append(&mut transfers); } else { @@ -384,10 +374,7 @@ impl AssetMap { } for (principal, asset, amount) in to_add.into_iter() { - let principal_map = self - .token_map - .entry(principal) - .or_insert_with(|| HashMap::new()); + let principal_map = self.token_map.entry(principal).or_default(); principal_map.insert(asset, amount); } @@ -395,9 +382,9 @@ impl AssetMap { } pub fn to_table(mut self) -> HashMap> { - let mut map = HashMap::new(); + let mut map = HashMap::with_capacity(self.token_map.len()); for (principal, mut principal_map) in self.token_map.drain() { - let mut output_map = HashMap::new(); + let mut output_map = HashMap::with_capacity(principal_map.len()); for (asset, amount) in principal_map.drain() { output_map.insert(asset, AssetMapEntry::Token(amount)); } @@ -405,9 +392,7 @@ impl AssetMap { } for (principal, stx_amount) in self.stx_map.drain() { - let output_map = map - .entry(principal.clone()) - .or_insert_with(|| HashMap::new()); + let output_map = map.entry(principal.clone()).or_default(); output_map.insert( AssetIdentifier::STX(), AssetMapEntry::STX(stx_amount as u128), @@ -415,9 +400,7 @@ impl AssetMap { } for (principal, stx_burned_amount) in self.burn_map.drain() { - let output_map = map - .entry(principal.clone()) - .or_insert_with(|| HashMap::new()); + let output_map = map.entry(principal.clone()).or_default(); output_map.insert( AssetIdentifier::STX_burned(), AssetMapEntry::Burn(stx_burned_amount as u128), @@ -425,9 +408,7 @@ impl AssetMap { } for (principal, mut principal_map) in self.asset_map.drain() { - let output_map = map - .entry(principal.clone()) - .or_insert_with(|| HashMap::new()); + let output_map = map.entry(principal.clone()).or_default(); for (asset, transfers) in principal_map.drain() { output_map.insert(asset, AssetMapEntry::Asset(transfers)); } @@ -437,17 +418,11 @@ impl AssetMap { } pub fn get_stx(&self, principal: &PrincipalData) -> Option { - match self.stx_map.get(principal) { - Some(value) => Some(*value), - None => None, - } + self.stx_map.get(principal).copied() } pub fn get_stx_burned(&self, principal: &PrincipalData) -> Option { - match self.burn_map.get(principal) { - Some(value) => Some(*value), - None => None, - } + self.burn_map.get(principal).copied() } pub fn get_stx_burned_total(&self) -> Result { diff --git a/clarity/src/vm/contracts.rs b/clarity/src/vm/contracts.rs index 0019106ef..1982665ae 100644 --- a/clarity/src/vm/contracts.rs +++ b/clarity/src/vm/contracts.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryInto; - use stacks_common::types::StacksEpochId; use crate::vm::ast::ContractAST; diff --git a/clarity/src/vm/costs/mod.rs b/clarity/src/vm/costs/mod.rs index 2222aa4c1..f70fbe699 100644 --- a/clarity/src/vm/costs/mod.rs +++ b/clarity/src/vm/costs/mod.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::collections::BTreeMap; -use std::convert::{TryFrom, TryInto}; use std::{cmp, fmt}; use hashbrown::HashMap; @@ -812,9 +811,11 @@ impl TrackerData { self.contract_call_circuits = contract_call_circuits; - let mut cost_contracts = HashMap::new(); - let mut m = HashMap::new(); - for f in ClarityCostFunction::ALL.iter() { + let iter = ClarityCostFunction::ALL.iter(); + let iter_len = iter.len(); + let mut cost_contracts = HashMap::with_capacity(iter_len); + let mut m = HashMap::with_capacity(iter_len); + for f in iter { let cost_function_ref = cost_function_references.remove(f).unwrap_or_else(|| { ClarityCostFunctionReference::new(boot_costs_id.clone(), f.get_name()) }); diff --git a/clarity/src/vm/coverage.rs b/clarity/src/vm/coverage.rs index 6f2de9f5c..be8a647e9 100644 --- a/clarity/src/vm/coverage.rs +++ b/clarity/src/vm/coverage.rs @@ -72,12 +72,14 @@ impl CoverageReporter { pub fn to_file + Copy>(&self, filename: P) -> std::io::Result<()> { let f = File::create(filename)?; - let mut coverage = HashMap::new(); - for (contract, execution_map) in self.executed_lines.iter() { - let mut executed_lines = vec![]; - for (line, count) in execution_map.iter() { - executed_lines.push((*line, *count)); - } + let iter = self.executed_lines.iter(); + let mut coverage = HashMap::with_capacity(iter.len()); + for (contract, execution_map) in iter { + let mut executed_lines = execution_map + .iter() + .map(|(line, count)| (*line, *count)) + .collect::>(); + executed_lines.sort_by_key(|f| f.0); coverage.insert(contract.to_string(), executed_lines); diff --git a/clarity/src/vm/database/clarity_db.rs b/clarity/src/vm/database/clarity_db.rs index 5ef6c458d..4388e88e5 100644 --- a/clarity/src/vm/database/clarity_db.rs +++ b/clarity/src/vm/database/clarity_db.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::{TryFrom, TryInto}; - use serde_json; use stacks_common::address::AddressHashMode; use stacks_common::consts::{ diff --git a/clarity/src/vm/database/clarity_store.rs b/clarity/src/vm/database/clarity_store.rs index f3d9d2bb0..f093c5a3c 100644 --- a/clarity/src/vm/database/clarity_store.rs +++ b/clarity/src/vm/database/clarity_store.rs @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryInto; use std::path::PathBuf; use rusqlite::Connection; diff --git a/clarity/src/vm/database/key_value_wrapper.rs b/clarity/src/vm/database/key_value_wrapper.rs index bc4b85a9b..65de1adce 100644 --- a/clarity/src/vm/database/key_value_wrapper.rs +++ b/clarity/src/vm/database/key_value_wrapper.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::clone::Clone; -use std::cmp::Eq; use std::hash::Hash; use hashbrown::HashMap; diff --git a/clarity/src/vm/database/structures.rs b/clarity/src/vm/database/structures.rs index 53c7fbd68..937eda2bd 100644 --- a/clarity/src/vm/database/structures.rs +++ b/clarity/src/vm/database/structures.rs @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryInto; use std::io::Write; use serde::Deserialize; diff --git a/clarity/src/vm/docs/contracts.rs b/clarity/src/vm/docs/contracts.rs index 37d1452a1..7426be796 100644 --- a/clarity/src/vm/docs/contracts.rs +++ b/clarity/src/vm/docs/contracts.rs @@ -1,5 +1,4 @@ use std::collections::BTreeMap; -use std::iter::FromIterator; use hashbrown::{HashMap, HashSet}; use stacks_common::consts::CHAIN_ID_TESTNET; diff --git a/clarity/src/vm/docs/mod.rs b/clarity/src/vm/docs/mod.rs index 50ca695e4..205fdc617 100644 --- a/clarity/src/vm/docs/mod.rs +++ b/clarity/src/vm/docs/mod.rs @@ -2609,20 +2609,18 @@ pub fn make_define_reference(define_type: &DefineFunctions) -> FunctionAPI { fn make_all_api_reference() -> ReferenceAPIs { let mut functions: Vec<_> = NativeFunctions::ALL .iter() - .map(|x| make_api_reference(x)) + .map(make_api_reference) .collect(); for data_type in DefineFunctions::ALL.iter() { functions.push(make_define_reference(data_type)) } functions.sort_by(|x, y| x.name.cmp(&y.name)); - let mut keywords = Vec::new(); - for variable in NativeVariables::ALL.iter() { - let output = make_keyword_reference(variable); - if let Some(api_ref) = output { - keywords.push(api_ref) - } - } + let mut keywords: Vec<_> = NativeVariables::ALL + .iter() + .filter_map(make_keyword_reference) + .collect(); + keywords.sort_by(|x, y| x.name.cmp(&y.name)); ReferenceAPIs { diff --git a/clarity/src/vm/functions/arithmetic.rs b/clarity/src/vm/functions/arithmetic.rs index bd0edbf5e..1d52ae439 100644 --- a/clarity/src/vm/functions/arithmetic.rs +++ b/clarity/src/vm/functions/arithmetic.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::cmp; -use std::convert::TryFrom; use integer_sqrt::IntegerSquareRoot; diff --git a/clarity/src/vm/functions/assets.rs b/clarity/src/vm/functions/assets.rs index 3e926f2cc..0d004a846 100644 --- a/clarity/src/vm/functions/assets.rs +++ b/clarity/src/vm/functions/assets.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::{TryFrom, TryInto}; - use stacks_common::types::StacksEpochId; use crate::vm::costs::cost_functions::ClarityCostFunction; diff --git a/clarity/src/vm/functions/conversions.rs b/clarity/src/vm/functions/conversions.rs index b788455f9..090f0d210 100644 --- a/clarity/src/vm/functions/conversions.rs +++ b/clarity/src/vm/functions/conversions.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryFrom; - use stacks_common::codec::StacksMessageCodec; use stacks_common::types::StacksEpochId; diff --git a/clarity/src/vm/functions/database.rs b/clarity/src/vm/functions/database.rs index d036dd27c..b047faf68 100644 --- a/clarity/src/vm/functions/database.rs +++ b/clarity/src/vm/functions/database.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::cmp; -use std::convert::{TryFrom, TryInto}; use stacks_common::types::chainstate::StacksBlockId; use stacks_common::types::StacksEpochId; @@ -75,9 +74,11 @@ pub fn special_contract_call( runtime_cost(ClarityCostFunction::ContractCall, env, 0)?; let function_name = args[1].match_atom().ok_or(CheckErrors::ExpectedName)?; - let mut rest_args = vec![]; - let mut rest_args_sizes = vec![]; - for arg in args[2..].iter() { + let rest_args_slice = &args[2..]; + let rest_args_len = rest_args_slice.len(); + let mut rest_args = Vec::with_capacity(rest_args_len); + let mut rest_args_sizes = Vec::with_capacity(rest_args_len); + for arg in rest_args_slice.iter() { let evaluated_arg = eval(arg, env, context)?; rest_args_sizes.push(evaluated_arg.size()? as u64); rest_args.push(SymbolicExpression::atom_value(evaluated_arg)); diff --git a/clarity/src/vm/functions/mod.rs b/clarity/src/vm/functions/mod.rs index a523db864..7c3647c2f 100644 --- a/clarity/src/vm/functions/mod.rs +++ b/clarity/src/vm/functions/mod.rs @@ -701,7 +701,7 @@ pub fn parse_eval_bindings( env: &mut Environment, context: &LocalContext, ) -> Result> { - let mut result = Vec::new(); + let mut result = Vec::with_capacity(bindings.len()); handle_binding_list(bindings, |var_name, var_sexp| { eval(var_sexp, env, context).map(|value| result.push((var_name.clone(), value))) })?; diff --git a/clarity/src/vm/functions/principals.rs b/clarity/src/vm/functions/principals.rs index 426fa4f70..99246019d 100644 --- a/clarity/src/vm/functions/principals.rs +++ b/clarity/src/vm/functions/principals.rs @@ -1,5 +1,3 @@ -use std::convert::TryFrom; - use stacks_common::address::{ C32_ADDRESS_VERSION_MAINNET_MULTISIG, C32_ADDRESS_VERSION_MAINNET_SINGLESIG, C32_ADDRESS_VERSION_TESTNET_MULTISIG, C32_ADDRESS_VERSION_TESTNET_SINGLESIG, diff --git a/clarity/src/vm/functions/sequences.rs b/clarity/src/vm/functions/sequences.rs index 029e62484..60445f963 100644 --- a/clarity/src/vm/functions/sequences.rs +++ b/clarity/src/vm/functions/sequences.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::cmp; -use std::convert::{TryFrom, TryInto}; use stacks_common::types::StacksEpochId; diff --git a/clarity/src/vm/mod.rs b/clarity/src/vm/mod.rs index 7231ad584..9d74fae5d 100644 --- a/clarity/src/vm/mod.rs +++ b/clarity/src/vm/mod.rs @@ -50,7 +50,6 @@ pub mod test_util; pub mod clarity; use std::collections::BTreeMap; -use std::convert::{TryFrom, TryInto}; use serde_json; use stacks_common::types::StacksEpochId; @@ -257,7 +256,7 @@ pub fn apply( resp } else { let mut used_memory = 0; - let mut evaluated_args = vec![]; + let mut evaluated_args = Vec::with_capacity(args.len()); env.call_stack.incr_apply_depth(); for arg_x in args.iter() { let arg_value = match eval(arg_x, env, context) { diff --git a/clarity/src/vm/representations.rs b/clarity/src/vm/representations.rs index 580e9a51c..15e674eb1 100644 --- a/clarity/src/vm/representations.rs +++ b/clarity/src/vm/representations.rs @@ -16,7 +16,6 @@ use std::borrow::Borrow; use std::cmp::Ordering; -use std::convert::TryFrom; use std::fmt; use std::io::{Read, Write}; use std::ops::Deref; diff --git a/clarity/src/vm/tests/conversions.rs b/clarity/src/vm/tests/conversions.rs index 0d5e55975..dbe45eb72 100644 --- a/clarity/src/vm/tests/conversions.rs +++ b/clarity/src/vm/tests/conversions.rs @@ -15,15 +15,13 @@ // along with this program. If not, see . pub use crate::vm::analysis::errors::{CheckError, CheckErrors}; -use crate::vm::execute_v2; use crate::vm::types::SequenceSubtype::{BufferType, StringType}; use crate::vm::types::StringSubtype::ASCII; use crate::vm::types::TypeSignature::SequenceType; use crate::vm::types::{ ASCIIData, BuffData, BufferLength, CharType, SequenceData, TypeSignature, UTF8Data, Value, }; -use crate::vm::ClarityVersion; -use std::convert::TryFrom; +use crate::vm::{execute_v2, ClarityVersion}; #[test] fn test_simple_buff_to_int_le() { diff --git a/clarity/src/vm/tests/datamaps.rs b/clarity/src/vm/tests/datamaps.rs index 87f5dbcf3..828de608e 100644 --- a/clarity/src/vm/tests/datamaps.rs +++ b/clarity/src/vm/tests/datamaps.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::{From, TryFrom}; - use crate::vm::errors::{CheckErrors, Error, ShortReturnType}; use crate::vm::types::{ ListData, SequenceData, TupleData, TupleTypeSignature, TypeSignature, Value, diff --git a/clarity/src/vm/tests/sequences.rs b/clarity/src/vm/tests/sequences.rs index 51de0e402..e252f917e 100644 --- a/clarity/src/vm/tests/sequences.rs +++ b/clarity/src/vm/tests/sequences.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::{TryFrom, TryInto}; - use rstest::rstest; use rstest_reuse::{self, *}; use stacks_common::types::StacksEpochId; diff --git a/clarity/src/vm/tests/traits.rs b/clarity/src/vm/tests/traits.rs index 250ebc341..97c4292b0 100644 --- a/clarity/src/vm/tests/traits.rs +++ b/clarity/src/vm/tests/traits.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryInto; - use stacks_common::types::StacksEpochId; use super::MemoryEnvironmentGenerator; diff --git a/clarity/src/vm/types/mod.rs b/clarity/src/vm/types/mod.rs index 06eea0896..1c25e1c38 100644 --- a/clarity/src/vm/types/mod.rs +++ b/clarity/src/vm/types/mod.rs @@ -20,7 +20,6 @@ pub mod serialization; pub mod signatures; use std::collections::BTreeMap; -use std::convert::{TryFrom, TryInto}; use std::{char, cmp, fmt, str}; use regex::Regex; @@ -1034,12 +1033,14 @@ impl Value { Ok(string) => string, _ => return Err(CheckErrors::InvalidCharactersDetected.into()), }; - let mut data = vec![]; - for char in validated_utf8_str.chars() { - let mut encoded_char: Vec = vec![0; char.len_utf8()]; - char.encode_utf8(&mut encoded_char[..]); - data.push(encoded_char); - } + let data = validated_utf8_str + .chars() + .map(|char| { + let mut encoded_char = vec![0u8; char.len_utf8()]; + char.encode_utf8(&mut encoded_char); + encoded_char + }) + .collect::>(); // check the string size StringUTF8Length::try_from(data.len())?; diff --git a/clarity/src/vm/types/serialization.rs b/clarity/src/vm/types/serialization.rs index 32cf9cf9f..c7a92203b 100644 --- a/clarity/src/vm/types/serialization.rs +++ b/clarity/src/vm/types/serialization.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::borrow::Borrow; -use std::convert::{TryFrom, TryInto}; use std::io::{Read, Write}; use std::{cmp, error, fmt, str}; @@ -1249,7 +1247,7 @@ impl Value { if l.len().ok()? > lt.get_max_len() { return None; } - let mut sanitized_items = vec![]; + let mut sanitized_items = Vec::with_capacity(l.data.len()); let mut did_sanitize_children = false; for item in l.data.into_iter() { let (sanitized_item, did_sanitize) = @@ -1266,11 +1264,12 @@ impl Value { TypeSignature::TupleType(tt) => tt, _ => return None, }; - let mut sanitized_tuple_entries = vec![]; + let type_map = tt.get_type_map(); + let mut sanitized_tuple_entries = Vec::with_capacity(type_map.len()); let original_tuple_len = tuple_data.len(); let mut tuple_data_map = tuple_data.data_map; let mut did_sanitize_children = false; - for (key, expect_key_type) in tt.get_type_map().iter() { + for (key, expect_key_type) in type_map.iter() { let field_data = tuple_data_map.remove(key)?; let (sanitized_field, did_sanitize) = Self::sanitize_value(epoch, expect_key_type, field_data)?; diff --git a/clarity/src/vm/types/signatures.rs b/clarity/src/vm/types/signatures.rs index f906ada18..29445d249 100644 --- a/clarity/src/vm/types/signatures.rs +++ b/clarity/src/vm/types/signatures.rs @@ -16,7 +16,6 @@ use std::collections::btree_map::Entry; use std::collections::BTreeMap; -use std::convert::{TryFrom, TryInto}; use std::hash::{Hash, Hasher}; use std::{cmp, fmt}; @@ -981,10 +980,12 @@ impl FunctionSignature { } pub fn canonicalize(&self, epoch: &StacksEpochId) -> FunctionSignature { - let mut canonicalized_args = vec![]; - for arg in &self.args { - canonicalized_args.push(arg.canonicalize(epoch)); - } + let canonicalized_args = self + .args + .iter() + .map(|arg| arg.canonicalize(epoch)) + .collect(); + FunctionSignature { args: canonicalized_args, returns: self.returns.canonicalize(epoch), @@ -1644,8 +1645,8 @@ impl TypeSignature { let fn_args_exprs = args[1] .match_list() .ok_or(CheckErrors::DefineTraitBadSignature)?; - let mut fn_args = vec![]; - for arg_type in fn_args_exprs.iter() { + let mut fn_args = Vec::with_capacity(fn_args_exprs.len()); + for arg_type in fn_args_exprs.into_iter() { let arg_t = TypeSignature::parse_type_repr(epoch, arg_type, accounting)?; fn_args.push(arg_t); } @@ -2064,8 +2065,9 @@ mod test { // set k = 4033 let first_tuple = TypeSignature::from_string("(tuple (a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 bool))", version, epoch); - let mut keys = vec![]; - for i in 0..4033 { + let len = 4033; + let mut keys = Vec::with_capacity(len); + for i in 0..len { let key_name = ClarityName::try_from(format!("a{:0127}", i)).unwrap(); let key_val = first_tuple.clone(); keys.push((key_name, key_val)); diff --git a/clarity/src/vm/variables.rs b/clarity/src/vm/variables.rs index 66de0f3b6..539e14c39 100644 --- a/clarity/src/vm/variables.rs +++ b/clarity/src/vm/variables.rs @@ -13,7 +13,6 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryFrom; use super::errors::InterpreterError; use crate::vm::contexts::{Environment, LocalContext}; diff --git a/contrib/core-contract-tests/.gitignore b/contrib/core-contract-tests/.gitignore index 39b70c2f7..393158bd1 100644 --- a/contrib/core-contract-tests/.gitignore +++ b/contrib/core-contract-tests/.gitignore @@ -4,4 +4,5 @@ npm-debug.log* coverage *.info costs-reports.json -node_modules \ No newline at end of file +node_modules +history.txt diff --git a/contrib/core-contract-tests/Clarinet.toml b/contrib/core-contract-tests/Clarinet.toml index 7bd70e474..605ce3989 100644 --- a/contrib/core-contract-tests/Clarinet.toml +++ b/contrib/core-contract-tests/Clarinet.toml @@ -10,12 +10,6 @@ path = "../../stackslib/src/chainstate/stacks/boot/bns.clar" depends_on = [] epoch = 2.4 -[contracts.pox-4] -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 = [] @@ -27,3 +21,25 @@ path = "../../stackslib/src/chainstate/stacks/boot/signers-voting.clar" depends_on = [] clarity = 2 epoch = 2.4 + +[contracts.pox-4] +path = "../../stackslib/src/chainstate/stacks/boot/pox-4.clar" +clarity_version = 2 +epoch = 2.4 +depends_on = ["pox-mainnet"] + +[contracts.pox-mainnet] +path = "../../stackslib/src/chainstate/stacks/boot/pox-mainnet.clar" +clarity_version = 2 +epoch = 2.4 +depends_on = [] + +[contracts.bns_test] +path = "./tests/bns_test.clar" +clarity_version = 2 +epoch = 2.4 + +[contracts.pox_4_test] +path = "./tests/pox_4_test.clar" +clarity_version = 2 +epoch = 2.4 diff --git a/contrib/core-contract-tests/package-lock.json b/contrib/core-contract-tests/package-lock.json index e5c3e22e1..cb7cba8a4 100644 --- a/contrib/core-contract-tests/package-lock.json +++ b/contrib/core-contract-tests/package-lock.json @@ -10,57 +10,22 @@ "license": "ISC", "dependencies": { "@hirosystems/clarinet-sdk": "^1.1.0", - "@stacks/transactions": "^6.9.0", + "@stacks/clarunit": "0.0.1", + "@stacks/transactions": "^6.12.0", "chokidar-cli": "^3.0.0", + "fast-check": "^3.15.1", "typescript": "^5.2.2", "vite": "^4.4.9", "vitest": "^0.34.4", "vitest-environment-clarinet": "^1.0.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "node": ">=0.10.0" } }, "node_modules/@esbuild/darwin-arm64": { @@ -78,286 +43,68 @@ "node": ">=12" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, "engines": { - "node": ">=12" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "engines": { - "node": ">=12" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, "engines": { - "node": ">=12" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@hirosystems/clarinet-sdk": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@hirosystems/clarinet-sdk/-/clarinet-sdk-1.1.0.tgz", - "integrity": "sha512-O4iP+eqc2jtbCJcndC22l12ODIi8GxwUcWhWaltvnPBn+PXqCLxDqNU78C6iDCfPp/Ro2fcJy9z27KNqnu+A9g==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@hirosystems/clarinet-sdk/-/clarinet-sdk-1.2.0.tgz", + "integrity": "sha512-O0Gyh3pwwOVJTbLlxHG6vSB/KXr+U/nZzd2kpubQO4Qqxjn5/vo8l8J+/fwKOxhzM4QOa42M1sCaVZSB/PkTFg==", "dependencies": { - "@hirosystems/clarinet-sdk-wasm": "^1.1.0", + "@hirosystems/clarinet-sdk-wasm": "^1.2.0", "@stacks/transactions": "^6.9.0", "kolorist": "^1.8.0", "prompts": "^2.4.2", - "vitest": "^0.34.5", + "vitest": "^1.0.4", "yargs": "^17.7.2" }, "bin": { @@ -368,9 +115,359 @@ } }, "node_modules/@hirosystems/clarinet-sdk-wasm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@hirosystems/clarinet-sdk-wasm/-/clarinet-sdk-wasm-1.2.0.tgz", + "integrity": "sha512-TnJ243lEgIqHSIeMdEHi1hJceFBJ5mWfjfXv86GKaoyVOS6yX1vGL2a6ZuVO9FfWPNxsiSvaQV/FndVuansAVQ==" + }, + "node_modules/@hirosystems/clarinet-sdk/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.10.tgz", + "integrity": "sha512-YSRRs2zOpwypck+6GL3wGXx2gNP7DXzetmo5pHXLrY/VIMsS59yKfjPizQ4lLt5vEI80M41gjm2BxrGZ5U+VMA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@hirosystems/clarinet-sdk/node_modules/@vitest/expect": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@hirosystems/clarinet-sdk-wasm/-/clarinet-sdk-wasm-1.1.0.tgz", - "integrity": "sha512-hGf2Ib6qYVnhV2+idW1GuOsh1Fom4fhp+QYjxHmfGQvx9ptSb037/4YVlep+jbO4hKXHHF2uQJgKMRPwVrtN2g==" + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.1.0.tgz", + "integrity": "sha512-9IE2WWkcJo2BR9eqtY5MIo3TPmS50Pnwpm66A6neb2hvk/QSLfPXBz2qdiwUOQkwyFuuXEUj5380CbwfzW4+/w==", + "dependencies": { + "@vitest/spy": "1.1.0", + "@vitest/utils": "1.1.0", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@hirosystems/clarinet-sdk/node_modules/@vitest/runner": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.1.0.tgz", + "integrity": "sha512-zdNLJ00pm5z/uhbWF6aeIJCGMSyTyWImy3Fcp9piRGvueERFlQFbUwCpzVce79OLm2UHk9iwaMSOaU9jVHgNVw==", + "dependencies": { + "@vitest/utils": "1.1.0", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@hirosystems/clarinet-sdk/node_modules/@vitest/snapshot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.1.0.tgz", + "integrity": "sha512-5O/wyZg09V5qmNmAlUgCBqflvn2ylgsWJRRuPrnHEfDNT6tQpQ8O1isNGgo+VxofISHqz961SG3iVvt3SPK/QQ==", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@hirosystems/clarinet-sdk/node_modules/@vitest/spy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.1.0.tgz", + "integrity": "sha512-sNOVSU/GE+7+P76qYo+VXdXhXffzWZcYIPQfmkiRxaNCSPiLANvQx5Mx6ZURJ/ndtEkUJEpvKLXqAYTKEY+lTg==", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@hirosystems/clarinet-sdk/node_modules/@vitest/utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.1.0.tgz", + "integrity": "sha512-z+s510fKmYz4Y41XhNs3vcuFTFhcij2YF7F8VQfMEYAAUfqQh0Zfg7+w9xdgFGhPf3tX3TicAe+8BDITk6ampQ==", + "dependencies": { + "diff-sequences": "^29.6.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@hirosystems/clarinet-sdk/node_modules/esbuild": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.10.tgz", + "integrity": "sha512-S1Y27QGt/snkNYrRcswgRFqZjaTG5a5xM3EQo97uNBnH505pdzSNe/HLBq1v0RO7iK/ngdbhJB6mDAp0OK+iUA==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.10", + "@esbuild/android-arm": "0.19.10", + "@esbuild/android-arm64": "0.19.10", + "@esbuild/android-x64": "0.19.10", + "@esbuild/darwin-arm64": "0.19.10", + "@esbuild/darwin-x64": "0.19.10", + "@esbuild/freebsd-arm64": "0.19.10", + "@esbuild/freebsd-x64": "0.19.10", + "@esbuild/linux-arm": "0.19.10", + "@esbuild/linux-arm64": "0.19.10", + "@esbuild/linux-ia32": "0.19.10", + "@esbuild/linux-loong64": "0.19.10", + "@esbuild/linux-mips64el": "0.19.10", + "@esbuild/linux-ppc64": "0.19.10", + "@esbuild/linux-riscv64": "0.19.10", + "@esbuild/linux-s390x": "0.19.10", + "@esbuild/linux-x64": "0.19.10", + "@esbuild/netbsd-x64": "0.19.10", + "@esbuild/openbsd-x64": "0.19.10", + "@esbuild/sunos-x64": "0.19.10", + "@esbuild/win32-arm64": "0.19.10", + "@esbuild/win32-ia32": "0.19.10", + "@esbuild/win32-x64": "0.19.10" + } + }, + "node_modules/@hirosystems/clarinet-sdk/node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@hirosystems/clarinet-sdk/node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@hirosystems/clarinet-sdk/node_modules/rollup": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.1.tgz", + "integrity": "sha512-pgPO9DWzLoW/vIhlSoDByCzcpX92bKEorbgXuZrqxByte3JFk2xSW2JEeAcyLc9Ru9pqcNNW+Ob7ntsk2oT/Xw==", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.9.1", + "@rollup/rollup-android-arm64": "4.9.1", + "@rollup/rollup-darwin-arm64": "4.9.1", + "@rollup/rollup-darwin-x64": "4.9.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.9.1", + "@rollup/rollup-linux-arm64-gnu": "4.9.1", + "@rollup/rollup-linux-arm64-musl": "4.9.1", + "@rollup/rollup-linux-riscv64-gnu": "4.9.1", + "@rollup/rollup-linux-x64-gnu": "4.9.1", + "@rollup/rollup-linux-x64-musl": "4.9.1", + "@rollup/rollup-win32-arm64-msvc": "4.9.1", + "@rollup/rollup-win32-ia32-msvc": "4.9.1", + "@rollup/rollup-win32-x64-msvc": "4.9.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/@hirosystems/clarinet-sdk/node_modules/tinypool": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.1.tgz", + "integrity": "sha512-zBTCK0cCgRROxvs9c0CGK838sPkeokNGdQVUUwHAbynHFlmyJYj825f/oRs528HaIJ97lo0pLIlDUzwN+IorWg==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hirosystems/clarinet-sdk/node_modules/vite": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.10.tgz", + "integrity": "sha512-2P8J7WWgmc355HUMlFrwofacvr98DAjoE52BfdbwQtyLH06XKwaL/FMnmKM2crF0iX4MpmMKoDlNCB1ok7zHCw==", + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.32", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/@hirosystems/clarinet-sdk/node_modules/vite-node": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.1.0.tgz", + "integrity": "sha512-jV48DDUxGLEBdHCQvxL1mEh7+naVy+nhUUUaPAZLd3FJgXuxQiewHcfeZebbJ6onDqNGkP4r3MhQ342PRlG81Q==", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@hirosystems/clarinet-sdk/node_modules/vitest": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.1.0.tgz", + "integrity": "sha512-oDFiCrw7dd3Jf06HoMtSRARivvyjHJaTxikFxuqJjO76U436PqlVw1uLn7a8OSPrhSfMGVaRakKpA2lePdw79A==", + "dependencies": { + "@vitest/expect": "1.1.0", + "@vitest/runner": "1.1.0", + "@vitest/snapshot": "1.1.0", + "@vitest/spy": "1.1.0", + "@vitest/utils": "1.1.0", + "acorn-walk": "^8.3.0", + "cac": "^6.7.14", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^1.3.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.1", + "vite": "^5.0.0", + "vite-node": "1.1.0", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "^1.0.0", + "@vitest/ui": "^1.0.0", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==" }, "node_modules/@jest/schemas": { "version": "29.6.3", @@ -410,46 +507,509 @@ } ] }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.1.tgz", + "integrity": "sha512-LtYcLNM+bhsaKAIGwVkh5IOWhaZhjTfNOkGzGqdHvhiCUVuJDalvDxEdSnhFzAn+g23wgsycmZk1vbnaibZwwA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" }, + "node_modules/@stacks/clarunit": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@stacks/clarunit/-/clarunit-0.0.1.tgz", + "integrity": "sha512-AKf14ycQJjyUWL6yfvXU+yMqvkCfUy2NarHbAmXx6tXfv/fyXueGkjTZTh8+0r20+XoxEvhJTnBfoAA74VLNtg==", + "dependencies": { + "@hirosystems/clarinet-sdk": "^1.2.0", + "@stacks/transactions": "^6.11.0", + "chokidar-cli": "^3.0.0", + "eslint": "^8.56.0", + "path": "^0.12.7", + "typescript": "^5.2.2", + "vite": "^4.4.9", + "vitest": "^1.1.0", + "vitest-environment-clarinet": "^1.0.0" + }, + "bin": { + "clarunit": "src/cli.ts" + } + }, + "node_modules/@stacks/clarunit/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@stacks/clarunit/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz", + "integrity": "sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@stacks/clarunit/node_modules/@vitest/expect": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz", + "integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==", + "dependencies": { + "@vitest/spy": "1.3.1", + "@vitest/utils": "1.3.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@stacks/clarunit/node_modules/@vitest/runner": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz", + "integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==", + "dependencies": { + "@vitest/utils": "1.3.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@stacks/clarunit/node_modules/@vitest/snapshot": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz", + "integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@stacks/clarunit/node_modules/@vitest/spy": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz", + "integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@stacks/clarunit/node_modules/@vitest/utils": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz", + "integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@stacks/clarunit/node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/@stacks/clarunit/node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@stacks/clarunit/node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stacks/clarunit/node_modules/rollup": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.0.tgz", + "integrity": "sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==", + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.12.0", + "@rollup/rollup-android-arm64": "4.12.0", + "@rollup/rollup-darwin-arm64": "4.12.0", + "@rollup/rollup-darwin-x64": "4.12.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.12.0", + "@rollup/rollup-linux-arm64-gnu": "4.12.0", + "@rollup/rollup-linux-arm64-musl": "4.12.0", + "@rollup/rollup-linux-riscv64-gnu": "4.12.0", + "@rollup/rollup-linux-x64-gnu": "4.12.0", + "@rollup/rollup-linux-x64-musl": "4.12.0", + "@rollup/rollup-win32-arm64-msvc": "4.12.0", + "@rollup/rollup-win32-ia32-msvc": "4.12.0", + "@rollup/rollup-win32-x64-msvc": "4.12.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/@stacks/clarunit/node_modules/strip-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz", + "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==", + "dependencies": { + "js-tokens": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@stacks/clarunit/node_modules/tinypool": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz", + "integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@stacks/clarunit/node_modules/vite-node": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz", + "integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@stacks/clarunit/node_modules/vite-node/node_modules/vite": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz", + "integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==", + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.35", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/@stacks/clarunit/node_modules/vitest": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz", + "integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==", + "dependencies": { + "@vitest/expect": "1.3.1", + "@vitest/runner": "1.3.1", + "@vitest/snapshot": "1.3.1", + "@vitest/spy": "1.3.1", + "@vitest/utils": "1.3.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.2", + "vite": "^5.0.0", + "vite-node": "1.3.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.3.1", + "@vitest/ui": "1.3.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/@stacks/clarunit/node_modules/vitest/node_modules/vite": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz", + "integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==", + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.35", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, "node_modules/@stacks/common": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.8.1.tgz", - "integrity": "sha512-ewL9GLZNQYa5a/3K4xSHlHIgHkD4rwWW/QEaPId8zQIaL+1O9qCaF4LX9orNQeOmEk8kvG0x2xGV54fXKCZeWQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.10.0.tgz", + "integrity": "sha512-6x5Z7AKd9/kj3+DYE9xIDIkFLHihBH614i2wqrZIjN02WxVo063hWSjIlUxlx8P4gl6olVzlOy5LzhLJD9OP0A==", "dependencies": { "@types/bn.js": "^5.1.0", "@types/node": "^18.0.4" } }, "node_modules/@stacks/network": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.8.1.tgz", - "integrity": "sha512-n8M25pPbLqpSBctabtsLOTBlmPvm9EPQpTI//x7HLdt5lEjDXxauEQt0XGSvDUZwecrmztqt9xNxlciiGApRBw==", + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.11.3.tgz", + "integrity": "sha512-c4ClCU/QUwuu8NbHtDKPJNa0M5YxauLN3vYaR0+S4awbhVIKFQSxirm9Q9ckV1WBh7FtD6u2S0x+tDQGAODjNg==", "dependencies": { - "@stacks/common": "^6.8.1", + "@stacks/common": "^6.10.0", "cross-fetch": "^3.1.5" } }, "node_modules/@stacks/transactions": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.9.0.tgz", - "integrity": "sha512-hSs9+0Ew++GwMZMgPObOx0iVCQRxkiCqI+DHdPEikAmg2utpyLh2/txHOjfSIkQHvcBfJJ6O5KphmxDP4gUqiA==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.12.0.tgz", + "integrity": "sha512-gRP3SfTaAIoTdjMvOiLrMZb/senqB8JQlT5Y4C3/CiHhiprYwTx7TbOCSa7WsNOU99H4aNfHvatmymuggXQVkA==", "dependencies": { "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.1", - "@stacks/common": "^6.8.1", - "@stacks/network": "^6.8.1", + "@stacks/common": "^6.10.0", + "@stacks/network": "^6.11.3", "c32check": "^2.0.0", "lodash.clonedeep": "^4.5.0" } }, "node_modules/@types/bn.js": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.4.tgz", - "integrity": "sha512-ZtBd9L8hVtoBpPMSWfbwjC4dhQtJdlPS+e1A0Rydb7vg7bDcUwiRklPx24sMYtXcmAMST/k0Wze7JLbNU/5SkA==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.5.tgz", + "integrity": "sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==", "dependencies": { "@types/node": "*" } @@ -467,6 +1027,11 @@ "@types/chai": "*" } }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + }, "node_modules/@types/node": { "version": "18.18.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.8.tgz", @@ -475,6 +1040,11 @@ "undici-types": "~5.26.4" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, "node_modules/@vitest/expect": { "version": "0.34.6", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", @@ -549,14 +1119,37 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", - "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", "engines": { "node": ">=0.4.0" } }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -588,6 +1181,11 @@ "node": ">= 8" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -596,6 +1194,11 @@ "node": "*" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, "node_modules/base-x": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", @@ -609,6 +1212,15 @@ "node": ">=8" } }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", @@ -640,6 +1252,14 @@ "node": ">=8" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -665,6 +1285,51 @@ "node": ">=4" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/chalk/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, "node_modules/check-error": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", @@ -855,6 +1520,11 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, "node_modules/cross-fetch": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", @@ -863,6 +1533,19 @@ "node-fetch": "^2.6.12" } }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -898,6 +1581,11 @@ "node": ">=6" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -906,6 +1594,17 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -955,6 +1654,323 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-check": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.15.1.tgz", + "integrity": "sha512-GutOXZ+SCxGaFWfHe0Pbeq8PrkpGtPxA9/hdkI3s9YzqeMlrq5RdJ+QfYZ/S93jMX+tAyqgW0z5c9ppD+vkGUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "dependencies": { + "pure-rand": "^6.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -977,6 +1993,29 @@ "node": ">=6" } }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1006,6 +2045,36 @@ "node": "*" } }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -1017,6 +2086,86 @@ "node": ">= 6" } }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1063,11 +2212,74 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/js-tokens": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", + "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + }, "node_modules/jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -1081,6 +2293,18 @@ "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==" }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/local-pkg": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", @@ -1114,6 +2338,11 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, "node_modules/lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", @@ -1138,6 +2367,33 @@ "node": ">=12" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/mlly": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", @@ -1155,9 +2411,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", @@ -1171,6 +2427,11 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -1198,6 +2459,69 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", + "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/p-limit": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", @@ -1245,6 +2569,26 @@ "node": ">=6" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, "node_modules/path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -1253,6 +2597,22 @@ "node": ">=4" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", @@ -1293,9 +2653,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "funding": [ { "type": "opencollective", @@ -1311,7 +2671,7 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -1319,6 +2679,14 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -1332,6 +2700,14 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -1344,6 +2720,48 @@ "node": ">= 6" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", + "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -1373,6 +2791,37 @@ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "3.29.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", @@ -1388,16 +2837,68 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -1417,9 +2918,9 @@ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==" }, "node_modules/std-env": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.4.3.tgz", - "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==" + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==" }, "node_modules/string-width": { "version": "4.2.3", @@ -1445,6 +2946,28 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-literal": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", @@ -1456,6 +2979,22 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + }, "node_modules/tinybench": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.1.tgz", @@ -1493,6 +3032,17 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -1501,6 +3051,17 @@ "node": ">=4" } }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -1523,6 +3084,22 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dependencies": { + "inherits": "2.0.3" + } + }, "node_modules/vite": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", @@ -1698,6 +3275,20 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/which-module": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", @@ -1764,6 +3355,11 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/contrib/core-contract-tests/package.json b/contrib/core-contract-tests/package.json index 2f11d8736..774870d8f 100644 --- a/contrib/core-contract-tests/package.json +++ b/contrib/core-contract-tests/package.json @@ -4,17 +4,20 @@ "description": "Run unit tests on this project.", "private": true, "scripts": { - "test": "vitest run -- --coverage" + "test": "vitest run -- --coverage", + "genhtml": "genhtml lcov.info --branch-coverage -o coverage/" }, "author": "", "license": "ISC", "dependencies": { "@hirosystems/clarinet-sdk": "^1.1.0", - "@stacks/transactions": "^6.9.0", + "@stacks/transactions": "^6.12.0", "chokidar-cli": "^3.0.0", + "@stacks/clarunit": "0.0.1", + "fast-check": "^3.15.1", "typescript": "^5.2.2", "vite": "^4.4.9", "vitest": "^0.34.4", "vitest-environment-clarinet": "^1.0.0" } -} +} \ No newline at end of file diff --git a/contrib/core-contract-tests/tests/bns_test.clar b/contrib/core-contract-tests/tests/bns_test.clar new file mode 100644 index 000000000..8f0d12589 --- /dev/null +++ b/contrib/core-contract-tests/tests/bns_test.clar @@ -0,0 +1,9 @@ +(define-public (test-can-receive-name-none) + (begin + (asserts! + (is-eq (ok true) (contract-call? .bns can-receive-name tx-sender)) + (err "Should be able to receive a name") + ) + (ok true) + ) +) \ No newline at end of file diff --git a/contrib/core-contract-tests/tests/clarunit.test.ts b/contrib/core-contract-tests/tests/clarunit.test.ts new file mode 100644 index 000000000..5e4fa0da4 --- /dev/null +++ b/contrib/core-contract-tests/tests/clarunit.test.ts @@ -0,0 +1,2 @@ +import { clarunit } from "@stacks/clarunit"; +clarunit(simnet); diff --git a/contrib/core-contract-tests/tests/pox-4/signers-voting.prop.test.ts b/contrib/core-contract-tests/tests/pox-4/signers-voting.prop.test.ts new file mode 100644 index 000000000..0dc8ea217 --- /dev/null +++ b/contrib/core-contract-tests/tests/pox-4/signers-voting.prop.test.ts @@ -0,0 +1,130 @@ +import fc from "fast-check"; +import { assert, expect, it } from "vitest"; +import { Cl, ClarityType, isClarityType } from "@stacks/transactions"; + +it("should return correct reward-cycle-to-burn-height", () => { + fc.assert( + fc.property( + fc.constantFrom(...simnet.getAccounts().values()), + fc.nat(), + (account: string, reward_cycle: number) => { + // Arrange + const { result: pox_4_info } = simnet.callReadOnlyFn( + "pox-4", + "get-pox-info", + [], + account, + ); + assert(isClarityType(pox_4_info, ClarityType.ResponseOk)); + assert(isClarityType(pox_4_info.value, ClarityType.Tuple)); + const first_burnchain_block_height = + pox_4_info.value.data["first-burnchain-block-height"]; + const reward_cycle_length = + pox_4_info.value.data["reward-cycle-length"]; + + // Act + const { result: actual } = simnet.callReadOnlyFn( + "signers-voting", + "reward-cycle-to-burn-height", + [Cl.uint(reward_cycle)], + account, + ); + + // Assert + assert(isClarityType(reward_cycle_length, ClarityType.UInt)); + assert(isClarityType(first_burnchain_block_height, ClarityType.UInt)); + const expected = (reward_cycle * Number(reward_cycle_length.value)) + + Number(first_burnchain_block_height.value); + expect(actual).toBeUint(expected); + }, + ), + { numRuns: 250 }, + ); +}); + +it("should return correct burn-height-to-reward-cycle", () => { + fc.assert( + fc.property( + fc.constantFrom(...simnet.getAccounts().values()), + fc.nat(), + (account: string, height: number) => { + // Arrange + const { result: pox_4_info } = simnet.callReadOnlyFn( + "pox-4", + "get-pox-info", + [], + account, + ); + assert(isClarityType(pox_4_info, ClarityType.ResponseOk)); + assert(isClarityType(pox_4_info.value, ClarityType.Tuple)); + const first_burnchain_block_height = + pox_4_info.value.data["first-burnchain-block-height"]; + const reward_cycle_length = + pox_4_info.value.data["reward-cycle-length"]; + + // Act + const { result: actual } = simnet.callReadOnlyFn( + "signers-voting", + "burn-height-to-reward-cycle", + [Cl.uint(height)], + account, + ); + + // Assert + assert(isClarityType(first_burnchain_block_height, ClarityType.UInt)); + assert(isClarityType(reward_cycle_length, ClarityType.UInt)); + const expected = Math.floor( + (height - Number(first_burnchain_block_height.value)) / + Number(reward_cycle_length.value), + ); + expect(actual).toBeUint(expected); + }, + ), + { numRuns: 250 }, + ); +}); + +it("should return correct is-in-prepare-phase", () => { + fc.assert( + fc.property( + fc.constantFrom(...simnet.getAccounts().values()), + fc.nat(), + (account: string, height: number) => { + // Arrange + const { result: pox_4_info } = simnet.callReadOnlyFn( + "pox-4", + "get-pox-info", + [], + account, + ); + assert(isClarityType(pox_4_info, ClarityType.ResponseOk)); + assert(isClarityType(pox_4_info.value, ClarityType.Tuple)); + const first_burnchain_block_height = + pox_4_info.value.data["first-burnchain-block-height"]; + const prepare_cycle_length = + pox_4_info.value.data["prepare-cycle-length"]; + const reward_cycle_length = + pox_4_info.value.data["reward-cycle-length"]; + + // Act + const { result: actual } = simnet.callReadOnlyFn( + "signers-voting", + "is-in-prepare-phase", + [Cl.uint(height)], + account, + ); + + // Assert + assert(isClarityType(first_burnchain_block_height, ClarityType.UInt)); + assert(isClarityType(prepare_cycle_length, ClarityType.UInt)); + assert(isClarityType(reward_cycle_length, ClarityType.UInt)); + const expected = ((height - Number(first_burnchain_block_height.value) + + Number(prepare_cycle_length.value)) % + Number(reward_cycle_length.value)) < + Number(prepare_cycle_length.value); + expect(actual).toBeBool(expected); + }, + ), + { numRuns: 250 }, + ); +}); diff --git a/contrib/core-contract-tests/tests/pox_4_test.clar b/contrib/core-contract-tests/tests/pox_4_test.clar new file mode 100644 index 000000000..a158d9033 --- /dev/null +++ b/contrib/core-contract-tests/tests/pox_4_test.clar @@ -0,0 +1,135 @@ +(define-public (test-burn-height-to-reward-cycle) + (begin + (asserts! (is-eq u2 (contract-call? .pox-4 burn-height-to-reward-cycle u2100)) (err "Burn height 2100 should have been reward cycle 2")) + (asserts! (is-eq u3 (contract-call? .pox-4 burn-height-to-reward-cycle u3150)) (err "Burn height 3150 should have been reward cycle 2")) + (ok true) + ) +) + +(define-public (test-reward-cycle-to-burn-height) + (begin + (asserts! (is-eq u10500 (contract-call? .pox-4 reward-cycle-to-burn-height u10)) (err "Cycle 10 height should have been at burn height 10500")) + (asserts! (is-eq u18900 (contract-call? .pox-4 reward-cycle-to-burn-height u18)) (err "Cycle 18 height should have been at burn height 18900")) + (ok true) + ) +) + +(define-public (test-get-stacker-info-none) + (begin + (asserts! (is-none (contract-call? .pox-4 get-stacker-info tx-sender)) (err "By default, tx-sender should not have stacker info")) + (ok true) + ) +) + + +(define-private (check-pox-addr-version-iter (input (buff 1))) + (contract-call? .pox-4 check-pox-addr-version input) +) + +(define-public (test-check-pox-addr-version) + (begin + (asserts! (is-eq (map check-pox-addr-version-iter byte-list) + (list + true true true true true true true false false false false false false false false false + false false false false false false false false false false false false false false false false + false false false false false false false false false false false false false false false false + false false false false false false false false false false false false false false false false + false false false false false false false false false false false false false false false false + false false false false false false false false false false false false false false false false + false false false false false false false false false false false false false false false false + false false false false false false false false false false false false false false false false + false false false false false false false false false false false false false false false false + false false false false false false false false false false false false false false false false + false false false false false false false false false false false false false false false false + false false false false false false false false false false false false false false false false + false false false false false false false false false false false false false false false false + false false false false false false false false false false false false false false false false + false false false false false false false false false false false false false false false false + false false false false false false false false false false false false false false false false + )) + (err "Only the first 6 versions should be valid") + ) + (ok true) + ) +) + +(define-private (check-pox-addr-hashbytes-iter (test-length uint) (version (buff 1))) + (contract-call? .pox-4 check-pox-addr-hashbytes version (unwrap-panic (as-max-len? (unwrap-panic (slice? byte-list u0 test-length)) u32))) +) + +(define-public (test-invalid-pox-addr-hashbytes-length) + (let ( + (test-lengths (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11 u12 u13 u14 u15 u16 u17 u18 u19 u20 u21 u22 u23 u24 u25 u26 u27 u28 u29 u30 u31 u32)) + (length-20-valid (list + false false false false false false false false false false false false false false false false + false false false false true false false false false false false false false false false false false + )) + (length-32-valid (list + false false false false false false false false false false false false false false false false + false false false false false false false false false false false false false false false false true + )) + (length-all-invalid (list + false false false false false false false false false false false false false false false false + false false false false false false false false false false false false false false false false false + )) + ) + (asserts! (is-eq (map check-pox-addr-hashbytes-iter test-lengths (buff-repeat 0x00 (len test-lengths))) length-20-valid) + (err "Only length 20 should be valid for version 0x00") + ) + (asserts! (is-eq (map check-pox-addr-hashbytes-iter test-lengths (buff-repeat 0x01 (len test-lengths))) length-20-valid) + (err "Only length 20 should be valid for version 0x01") + ) + (asserts! (is-eq (map check-pox-addr-hashbytes-iter test-lengths (buff-repeat 0x02 (len test-lengths))) length-20-valid) + (err "Only length 20 should be valid for version 0x02") + ) + (asserts! (is-eq (map check-pox-addr-hashbytes-iter test-lengths (buff-repeat 0x03 (len test-lengths))) length-20-valid) + (err "Only length 20 should be valid for version 0x03") + ) + (asserts! (is-eq (map check-pox-addr-hashbytes-iter test-lengths (buff-repeat 0x04 (len test-lengths))) length-20-valid) + (err "Only length 20 should be valid for version 0x04") + ) + (asserts! (is-eq (map check-pox-addr-hashbytes-iter test-lengths (buff-repeat 0x05 (len test-lengths))) length-32-valid) + (err "Only length 32 should be valid for version 0x05") + ) + (asserts! (is-eq (map check-pox-addr-hashbytes-iter test-lengths (buff-repeat 0x06 (len test-lengths))) length-32-valid) + (err "Only length 32 should be valid for version 0x06") + ) + (asserts! (is-eq (map check-pox-addr-hashbytes-iter test-lengths (buff-repeat 0x07 (len test-lengths))) length-all-invalid) + (err "No length should be valid for version 0x07") + ) + (ok true) + ) +) + +(define-private (check-pox-lock-period-iter (period uint)) + (contract-call? .pox-4 check-pox-lock-period period) +) + +(define-public (test-check-pox-lock-period) + (let ((actual (map check-pox-lock-period-iter (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11 u12 u13)))) + (asserts! (is-eq + actual + (list false true true true true true true true true true true true true false)) + (err {err: "Expected only lock periods 1 to 12 to be valid", actual: actual}) + ) + (ok true) + ) +) + +(define-public (test-get-total-ustx-stacked) + (begin + (asserts! (is-eq (contract-call? .pox-4 get-total-ustx-stacked u1) u0) (err "Total ustx stacked should be 0")) + (ok true) + ) +) + + +(define-private (repeat-iter (a (buff 1)) (repeat {i: (buff 1), o: (buff 33)})) + {i: (get i repeat), o: (unwrap-panic (as-max-len? (concat (get i repeat) (get o repeat)) u33))} +) + +(define-read-only (buff-repeat (repeat (buff 1)) (times uint)) + (get o (fold repeat-iter (unwrap-panic (slice? byte-list u0 times)) {i: repeat, o: 0x})) +) + +(define-constant byte-list 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff) \ No newline at end of file diff --git a/libsigner/src/events.rs b/libsigner/src/events.rs index 588d02583..94bb17a85 100644 --- a/libsigner/src/events.rs +++ b/libsigner/src/events.rs @@ -56,7 +56,8 @@ pub enum SignerEvent { /// The miner proposed blocks for signers to observe and sign ProposedBlocks(Vec), /// The signer messages for other signers and miners to observe - SignerMessages(Vec), + /// The u32 is the signer set to which the message belongs (either 0 or 1) + SignerMessages(u32, Vec), /// A new block proposal validation response from the node BlockValidationResponse(BlockValidateResponse), /// Status endpoint request @@ -126,8 +127,6 @@ pub trait EventReceiver { /// Event receiver for Signer events pub struct SignerEventReceiver { - /// stacker db contracts we're listening for - pub stackerdb_contract_ids: Vec, /// Address we bind to local_addr: Option, /// server socket that listens for HTTP POSTs from the node @@ -143,12 +142,8 @@ pub struct SignerEventReceiver { impl SignerEventReceiver { /// Make a new Signer event receiver, and return both the receiver and the read end of a /// channel into which node-received data can be obtained. - pub fn new( - contract_ids: Vec, - is_mainnet: bool, - ) -> SignerEventReceiver { + pub fn new(is_mainnet: bool) -> SignerEventReceiver { SignerEventReceiver { - stackerdb_contract_ids: contract_ids, http_server: None, local_addr: None, out_channels: vec![], @@ -349,13 +344,18 @@ fn process_stackerdb_event( } else if event.contract_id.name.to_string().starts_with(SIGNERS_NAME) && event.contract_id.issuer.1 == [0u8; 20] { + let Some((signer_set, _)) = + get_signers_db_signer_set_message_id(event.contract_id.name.as_str()) + else { + return Err(EventError::UnrecognizedStackerDBContract(event.contract_id)); + }; // signer-XXX-YYY boot contract let signer_messages: Vec = event .modified_slots .iter() .filter_map(|chunk| read_next::(&mut &chunk.data[..]).ok()) .collect(); - SignerEvent::SignerMessages(signer_messages) + SignerEvent::SignerMessages(signer_set, signer_messages) } else { info!( "[{:?}] next_event got event from an unexpected contract id {}, return OK so other side doesn't keep sending this", @@ -400,3 +400,36 @@ fn process_proposal_response(mut request: HttpRequest) -> Result Option<(u32, u32)> { + // Splitting the string by '-' + let parts: Vec<&str> = name.split('-').collect(); + if parts.len() != 3 { + return None; + } + // Extracting message ID and slot ID + let signer_set = parts[1].parse::().ok()?; + let message_id = parts[2].parse::().ok()?; + Some((signer_set, message_id)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_signers_db_signer_set_message_id() { + let name = "signer-1-1"; + let (signer_set, message_id) = get_signers_db_signer_set_message_id(name).unwrap(); + assert_eq!(signer_set, 1); + assert_eq!(message_id, 1); + + let name = "signer-0-2"; + let (signer_set, message_id) = get_signers_db_signer_set_message_id(name).unwrap(); + assert_eq!(signer_set, 0); + assert_eq!(message_id, 2); + + let name = "signer--2"; + assert!(get_signers_db_signer_set_message_id(name).is_none()); + } +} diff --git a/libsigner/src/http.rs b/libsigner/src/http.rs index f5ba9bb2b..95f2e2b3c 100644 --- a/libsigner/src/http.rs +++ b/libsigner/src/http.rs @@ -224,7 +224,7 @@ pub fn decode_http_body(headers: &HashMap, mut buf: &[u8]) -> io /// Return the HTTP reply, decoded if it was chunked pub fn run_http_request( sock: &mut S, - host: &SocketAddr, + host: &str, verb: &str, path: &str, content_type: Option<&str>, diff --git a/libsigner/src/messages.rs b/libsigner/src/messages.rs index 7ac4b9e46..6135312a8 100644 --- a/libsigner/src/messages.rs +++ b/libsigner/src/messages.rs @@ -49,7 +49,7 @@ use wsts::net::{ SignatureShareRequest, SignatureShareResponse, }; use wsts::schnorr::ID; -use wsts::state_machine::signer; +use wsts::state_machine::{signer, SignError}; use crate::http::{decode_http_body, decode_http_request}; use crate::EventError; @@ -139,7 +139,9 @@ define_u8_enum!(RejectCodeTypePrefix{ SignedRejection = 1, InsufficientSigners = 2, MissingTransactions = 3, - ConnectivityIssues = 4 + ConnectivityIssues = 4, + NonceTimeout = 5, + AggregatorError = 6 }); impl TryFrom for RejectCodeTypePrefix { @@ -159,6 +161,8 @@ impl From<&RejectCode> for RejectCodeTypePrefix { RejectCode::InsufficientSigners(_) => RejectCodeTypePrefix::InsufficientSigners, RejectCode::MissingTransactions(_) => RejectCodeTypePrefix::MissingTransactions, RejectCode::ConnectivityIssues => RejectCodeTypePrefix::ConnectivityIssues, + RejectCode::NonceTimeout(_) => RejectCodeTypePrefix::NonceTimeout, + RejectCode::AggregatorError(_) => RejectCodeTypePrefix::AggregatorError, } } } @@ -177,7 +181,7 @@ pub enum SignerMessage { impl SignerMessage { /// Helper function to determine the slot ID for the provided stacker-db writer id pub fn msg_id(&self) -> u32 { - let msg_id = match self { + match self { Self::Packet(packet) => match packet.msg { Message::DkgBegin(_) => DKG_BEGIN_MSG_ID, Message::DkgPrivateBegin(_) => DKG_PRIVATE_BEGIN_MSG_ID, @@ -192,8 +196,7 @@ impl SignerMessage { }, Self::BlockResponse(_) => BLOCK_MSG_ID, Self::Transactions(_) => TRANSACTIONS_MSG_ID, - }; - msg_id + } } } @@ -259,10 +262,7 @@ impl StacksMessageCodecExtensions for Point { let compressed_bytes: Vec = read_next(fd)?; let compressed = Compressed::try_from(compressed_bytes.as_slice()) .map_err(|e| CodecError::DeserializeError(e.to_string()))?; - Ok( - Point::try_from(&compressed) - .map_err(|e| CodecError::DeserializeError(e.to_string()))?, - ) + Point::try_from(&compressed).map_err(|e| CodecError::DeserializeError(e.to_string())) } } @@ -938,26 +938,44 @@ pub enum RejectCode { ValidationFailed(ValidateRejectCode), /// Signers signed a block rejection SignedRejection(ThresholdSignature), + /// Nonce timeout was reached + NonceTimeout(Vec), /// Insufficient signers agreed to sign the block InsufficientSigners(Vec), + /// An internal error occurred in the signer when aggregating the signaure + AggregatorError(String), /// Missing the following expected transactions MissingTransactions(Vec), /// The block was rejected due to connectivity issues with the signer ConnectivityIssues, } +impl From<&SignError> for RejectCode { + fn from(err: &SignError) -> Self { + match err { + SignError::NonceTimeout(_valid_signers, malicious_signers) => { + Self::NonceTimeout(malicious_signers.clone()) + } + SignError::InsufficientSigners(malicious_signers) => { + Self::InsufficientSigners(malicious_signers.clone()) + } + SignError::Aggregator(e) => Self::AggregatorError(e.to_string()), + } + } +} + impl StacksMessageCodec for RejectCode { fn consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { write_next(fd, &(RejectCodeTypePrefix::from(self) as u8))?; match self { - RejectCode::ValidationFailed(code) => write_next(fd, &(code.clone() as u8))?, + RejectCode::ValidationFailed(code) => write_next(fd, &(*code as u8))?, RejectCode::SignedRejection(sig) => write_next(fd, sig)?, - RejectCode::InsufficientSigners(malicious_signers) => { - write_next(fd, malicious_signers)? - } + RejectCode::InsufficientSigners(malicious_signers) + | RejectCode::NonceTimeout(malicious_signers) => write_next(fd, malicious_signers)?, RejectCode::MissingTransactions(missing_transactions) => { write_next(fd, missing_transactions)? } + RejectCode::AggregatorError(reason) => write_next(fd, &reason.as_bytes().to_vec())?, RejectCode::ConnectivityIssues => write_next(fd, &4u8)?, }; Ok(()) @@ -984,7 +1002,20 @@ impl StacksMessageCodec for RejectCode { RejectCodeTypePrefix::MissingTransactions => { RejectCode::MissingTransactions(read_next::, _>(fd)?) } + RejectCodeTypePrefix::NonceTimeout => { + RejectCode::NonceTimeout(read_next::, _>(fd)?) + } RejectCodeTypePrefix::ConnectivityIssues => RejectCode::ConnectivityIssues, + RejectCodeTypePrefix::AggregatorError => { + let reason_bytes = read_next::, _>(fd)?; + let reason = String::from_utf8(reason_bytes).map_err(|e| { + CodecError::DeserializeError(format!( + "Failed to decode reason string: {:?}", + &e + )) + })?; + RejectCode::AggregatorError(reason) + } }; Ok(code) } @@ -1002,6 +1033,11 @@ impl std::fmt::Display for RejectCode { "Insufficient signers agreed to sign the block. The following signers are malicious: {:?}", malicious_signers ), + RejectCode::NonceTimeout(malicious_signers) => write!( + f, + "Nonce timeout occurred signers. The following signers are malicious: {:?}", + malicious_signers + ), RejectCode::MissingTransactions(missing_transactions) => write!( f, "Missing the following expected transactions: {:?}", @@ -1011,6 +1047,11 @@ impl std::fmt::Display for RejectCode { f, "The block was rejected due to connectivity issues with the signer." ), + RejectCode::AggregatorError(reason) => write!( + f, + "An internal error occurred in the signer when aggregating the signaure: {:?}", + reason + ), } } } @@ -1074,6 +1115,18 @@ mod test { .expect("Failed to deserialize RejectCode"); assert_eq!(code, deserialized_code); + let code = RejectCode::NonceTimeout(vec![0, 1, 2]); + let serialized_code = code.serialize_to_vec(); + let deserialized_code = read_next::(&mut &serialized_code[..]) + .expect("Failed to deserialize RejectCode"); + assert_eq!(code, deserialized_code); + + let code = RejectCode::AggregatorError("Test Error".into()); + let serialized_code = code.serialize_to_vec(); + let deserialized_code = read_next::(&mut &serialized_code[..]) + .expect("Failed to deserialize RejectCode"); + assert_eq!(code, deserialized_code); + let sk = StacksPrivateKey::new(); let tx = StacksTransaction { version: TransactionVersion::Testnet, @@ -1131,6 +1184,24 @@ mod test { let deserialized_rejection = read_next::(&mut &serialized_rejection[..]) .expect("Failed to deserialize BlockRejection"); assert_eq!(rejection, deserialized_rejection); + + let rejection = BlockRejection::new( + Sha512Trunc256Sum([2u8; 32]), + RejectCode::NonceTimeout(vec![0, 1, 2]), + ); + let serialized_rejection = rejection.serialize_to_vec(); + let deserialized_rejection = read_next::(&mut &serialized_rejection[..]) + .expect("Failed to deserialize BlockRejection"); + assert_eq!(rejection, deserialized_rejection); + + let rejection = BlockRejection::new( + Sha512Trunc256Sum([2u8; 32]), + RejectCode::AggregatorError("Test Error".into()), + ); + let serialized_rejection = rejection.serialize_to_vec(); + let deserialized_rejection = read_next::(&mut &serialized_rejection[..]) + .expect("Failed to deserialize BlockRejection"); + assert_eq!(rejection, deserialized_rejection); } #[test] diff --git a/libsigner/src/session.rs b/libsigner/src/session.rs index e5dbd67f3..30966f897 100644 --- a/libsigner/src/session.rs +++ b/libsigner/src/session.rs @@ -22,6 +22,7 @@ use libstackerdb::{ stackerdb_get_chunk_path, stackerdb_get_metadata_path, stackerdb_post_chunk_path, SlotMetadata, StackerDBChunkAckData, StackerDBChunkData, }; +use stacks_common::codec::StacksMessageCodec; use crate::error::RPCError; use crate::http::run_http_request; @@ -31,7 +32,7 @@ pub trait SignerSession { /// connect to the replica fn connect( &mut self, - host: SocketAddr, + host: String, stackerdb_contract_id: QualifiedContractIdentifier, ) -> Result<(), RPCError>; /// query the replica for a list of chunks @@ -51,7 +52,14 @@ pub trait SignerSession { /// Returns Ok(None) if the chunk with the given version does not exist /// Returns Err(..) on transport error fn get_chunk(&mut self, slot_id: u32, version: u32) -> Result>, RPCError> { - Ok(self.get_chunks(&[(slot_id, version)])?[0].clone()) + let mut chunks = self.get_chunks(&[(slot_id, version)])?; + // check if chunks is empty because [0] and remove(0) panic on out-of-bounds + if chunks.is_empty() { + return Ok(None); + } + // swap_remove breaks the ordering of latest_chunks, but we don't care because we + // only want the first element anyways. + Ok(chunks.swap_remove(0)) } /// Get a single latest chunk. @@ -59,14 +67,36 @@ pub trait SignerSession { /// Returns Ok(None) if not /// Returns Err(..) on transport error fn get_latest_chunk(&mut self, slot_id: u32) -> Result>, RPCError> { - Ok(self.get_latest_chunks(&[(slot_id)])?[0].clone()) + let mut latest_chunks = self.get_latest_chunks(&[slot_id])?; + // check if latest_chunks is empty because [0] and remove(0) panic on out-of-bounds + if latest_chunks.is_empty() { + return Ok(None); + } + // swap_remove breaks the ordering of latest_chunks, but we don't care because we + // only want the first element anyways. + Ok(latest_chunks.swap_remove(0)) + } + + /// Get a single latest chunk from the StackerDB and deserialize into `T` using the + /// StacksMessageCodec. + fn get_latest(&mut self, slot_id: u32) -> Result, RPCError> { + let Some(latest_bytes) = self.get_latest_chunk(slot_id)? else { + return Ok(None); + }; + Some( + T::consensus_deserialize(&mut latest_bytes.as_slice()).map_err(|e| { + let msg = format!("StacksMessageCodec::consensus_deserialize failure: {e}"); + RPCError::Deserialize(msg) + }), + ) + .transpose() } } /// signer session for a stackerdb instance pub struct StackerDBSession { /// host we're talking to - pub host: SocketAddr, + pub host: String, /// contract we're talking to pub stackerdb_contract_id: QualifiedContractIdentifier, /// connection to the replica @@ -75,12 +105,9 @@ pub struct StackerDBSession { impl StackerDBSession { /// instantiate but don't connect - pub fn new( - host: SocketAddr, - stackerdb_contract_id: QualifiedContractIdentifier, - ) -> StackerDBSession { + pub fn new(host: &str, stackerdb_contract_id: QualifiedContractIdentifier) -> StackerDBSession { StackerDBSession { - host, + host: host.to_owned(), stackerdb_contract_id, sock: None, } @@ -89,7 +116,7 @@ impl StackerDBSession { /// connect or reconnect to the node fn connect_or_reconnect(&mut self) -> Result<(), RPCError> { debug!("connect to {}", &self.host); - self.sock = Some(TcpStream::connect(self.host)?); + self.sock = Some(TcpStream::connect(&self.host)?); Ok(()) } @@ -134,7 +161,7 @@ impl SignerSession for StackerDBSession { /// connect to the replica fn connect( &mut self, - host: SocketAddr, + host: String, stackerdb_contract_id: QualifiedContractIdentifier, ) -> Result<(), RPCError> { self.host = host; diff --git a/libsigner/src/tests/http.rs b/libsigner/src/tests/http.rs index 3fac7c337..d2b052fae 100644 --- a/libsigner/src/tests/http.rs +++ b/libsigner/src/tests/http.rs @@ -264,7 +264,7 @@ fn test_run_http_request_with_body() { let result_chunked = run_http_request( &mut msock_chunked, - &"127.0.0.1:20443".parse().unwrap(), + &"127.0.0.1:20443", verb, path, content_type, @@ -275,7 +275,7 @@ fn test_run_http_request_with_body() { let result_plain = run_http_request( &mut msock_plain, - &"127.0.0.1:20443".parse().unwrap(), + &"127.0.0.1:20443", verb, path, content_type, @@ -321,7 +321,7 @@ fn test_run_http_request_no_body() { let result_chunked = run_http_request( &mut msock_chunked, - &"127.0.0.1:20443".parse().unwrap(), + &"127.0.0.1:20443", verb, path, content_type, @@ -330,7 +330,7 @@ fn test_run_http_request_no_body() { .unwrap(); let result_plain = run_http_request( &mut msock_plain, - &"127.0.0.1:20443".parse().unwrap(), + &"127.0.0.1:20443", verb, path, content_type, diff --git a/libsigner/src/tests/mod.rs b/libsigner/src/tests/mod.rs index 970cc3a5c..9f320b42f 100644 --- a/libsigner/src/tests/mod.rs +++ b/libsigner/src/tests/mod.rs @@ -22,6 +22,7 @@ use std::sync::mpsc::{channel, Receiver, Sender}; use std::time::Duration; use std::{mem, thread}; +use blockstack_lib::chainstate::nakamoto::signer_set::NakamotoSigners; use blockstack_lib::chainstate::stacks::boot::SIGNERS_NAME; use blockstack_lib::chainstate::stacks::events::StackerDBChunksEvent; use blockstack_lib::util_lib::boot::boot_code_id; @@ -95,8 +96,8 @@ impl SignerRunLoop, Command> for SimpleRunLoop { /// and the signer runloop. #[test] fn test_simple_signer() { - let contract_id = boot_code_id(SIGNERS_NAME, false); - let ev = SignerEventReceiver::new(vec![contract_id.clone()], false); + let contract_id = NakamotoSigners::make_signers_db_contract_id(0, 0, false); + let ev = SignerEventReceiver::new(false); let (_cmd_send, cmd_recv) = channel(); let (res_send, _res_recv) = channel(); let max_events = 5; @@ -160,7 +161,7 @@ fn test_simple_signer() { .map(|chunk| { let msg = chunk.modified_slots[0].data.clone(); let signer_message = read_next::(&mut &msg[..]).unwrap(); - SignerEvent::SignerMessages(vec![signer_message]) + SignerEvent::SignerMessages(0, vec![signer_message]) }) .collect(); @@ -170,10 +171,7 @@ fn test_simple_signer() { #[test] fn test_status_endpoint() { - let contract_id = - QualifiedContractIdentifier::parse("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.signers") - .unwrap(); // TODO: change to boot_code_id(SIGNERS_NAME, false) when .signers is deployed - let ev = SignerEventReceiver::new(vec![contract_id], false); + let ev = SignerEventReceiver::new(false); let (_cmd_send, cmd_recv) = channel(); let (res_send, _res_recv) = channel(); let max_events = 1; diff --git a/stacks-common/src/address/c32.rs b/stacks-common/src/address/c32.rs index 20eb64e11..a8c26632f 100644 --- a/stacks-common/src/address/c32.rs +++ b/stacks-common/src/address/c32.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryFrom; - use sha2::{Digest, Sha256}; use super::Error; @@ -180,7 +178,10 @@ const C32_CHARACTERS_MAP: [Option; 128] = [ ]; fn c32_encode(input_bytes: &[u8]) -> String { - let mut result = vec![]; + // ASCII characters are 8-bits and c32-encoding encodes 5-bits per + // character, so the c32-encoded size should be ceil((ascii size) * 8 / 5) + let size = input_bytes.len().saturating_mul(8).div_ceil(5); + let mut result = Vec::with_capacity(size); let mut carry = 0; let mut carry_bits = 0; @@ -234,10 +235,6 @@ fn c32_decode(input_str: &str) -> Result, Error> { } fn c32_decode_ascii(input_str: &str) -> Result, Error> { - let mut result = vec![]; - let mut carry: u16 = 0; - let mut carry_bits = 0; // can be up to 5 - let mut iter_c32_digits = Vec::::with_capacity(input_str.len()); for x in input_str.as_bytes().iter().rev() { @@ -251,6 +248,14 @@ fn c32_decode_ascii(input_str: &str) -> Result, Error> { return Err(Error::InvalidCrockford32); } + // c32-encoding encodes 5 bits into each character, while ASCII encodes + // 8-bits into each character. So, the ASCII-encoded size should be + // ceil((c32 size) * 5 / 8) + let size = iter_c32_digits.len().saturating_mul(5).div_ceil(8); + let mut result = Vec::with_capacity(size); + let mut carry: u16 = 0; + let mut carry_bits = 0; // can be up to 5 + for current_5bit in &iter_c32_digits { carry += (*current_5bit as u16) << carry_bits; carry_bits += 5; diff --git a/stacks-common/src/address/mod.rs b/stacks-common/src/address/mod.rs index b4bcb936c..381456f66 100644 --- a/stacks-common/src/address/mod.rs +++ b/stacks-common/src/address/mod.rs @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryFrom; use std::{error, fmt}; use sha2::{Digest, Sha256}; diff --git a/stacks-common/src/deps_common/bech32/mod.rs b/stacks-common/src/deps_common/bech32/mod.rs index 515126796..99f95e9cd 100644 --- a/stacks-common/src/deps_common/bech32/mod.rs +++ b/stacks-common/src/deps_common/bech32/mod.rs @@ -599,7 +599,8 @@ fn verify_checksum(hrp: &[u8], data: &[u5]) -> Option { } fn hrp_expand(hrp: &[u8]) -> Vec { - let mut v: Vec = Vec::new(); + let size = (hrp.len() * 2) + 1; + let mut v: Vec = Vec::with_capacity(size); for b in hrp { v.push(u5::try_from_u8(*b >> 5).expect("can't be out of range, max. 7")); } diff --git a/stacks-common/src/deps_common/bitcoin/blockdata/constants.rs b/stacks-common/src/deps_common/bitcoin/blockdata/constants.rs index 4a9cfddef..7d3c7d1b4 100644 --- a/stacks-common/src/deps_common/bitcoin/blockdata/constants.rs +++ b/stacks-common/src/deps_common/bitcoin/blockdata/constants.rs @@ -19,8 +19,6 @@ //! single transaction //! -use std::default::Default; - use crate::deps_common::bitcoin::blockdata::block::{Block, BlockHeader}; use crate::deps_common::bitcoin::blockdata::transaction::{OutPoint, Transaction, TxIn, TxOut}; use crate::deps_common::bitcoin::blockdata::{opcodes, script}; @@ -139,7 +137,6 @@ pub fn genesis_block(network: Network) -> Block { #[cfg(test)] mod test { - use std::default::Default; use crate::deps_common::bitcoin::blockdata::constants::{ bitcoin_genesis_tx, genesis_block, COIN_VALUE, MAX_SEQUENCE, diff --git a/stacks-common/src/deps_common/bitcoin/blockdata/script.rs b/stacks-common/src/deps_common/bitcoin/blockdata/script.rs index 748f9e2ee..8b08ab998 100644 --- a/stacks-common/src/deps_common/bitcoin/blockdata/script.rs +++ b/stacks-common/src/deps_common/bitcoin/blockdata/script.rs @@ -24,7 +24,7 @@ //! This module provides the structures and functions needed to support scripts. //! -use std::default::Default; +use std::mem::size_of; use std::{error, fmt}; use serde; @@ -201,8 +201,8 @@ fn build_scriptint(n: i64) -> Vec { let neg = n < 0; - let mut abs = if neg { -n } else { n } as usize; - let mut v = vec![]; + let mut abs = n.abs() as usize; + let mut v = Vec::with_capacity(size_of::() + 1); while abs > 0xFF { v.push((abs & 0xFF) as u8); abs >>= 8; diff --git a/stacks-common/src/deps_common/bitcoin/blockdata/transaction.rs b/stacks-common/src/deps_common/bitcoin/blockdata/transaction.rs index 87708334f..1ece07c51 100644 --- a/stacks-common/src/deps_common/bitcoin/blockdata/transaction.rs +++ b/stacks-common/src/deps_common/bitcoin/blockdata/transaction.rs @@ -23,7 +23,6 @@ //! This module provides the structures and functions needed to support transactions. //! -use std::default::Default; use std::fmt; use std::io::Write; @@ -313,7 +312,7 @@ impl Transaction { // sha256d of the concatenation of the nSequences let mut raw_vec = vec![]; for inp in self.input.iter() { - raw_vec.append(&mut inp.sequence.to_le_bytes().to_vec()); + raw_vec.extend_from_slice(&inp.sequence.to_le_bytes()); } Sha256dHash::from_data(&raw_vec) } @@ -323,26 +322,27 @@ impl Transaction { /// does not work for codeseparator fn segwit_script_pubkey_bytes(&self, script: &Script) -> Vec { // bizarrely, if this is a p2wpkh, we have to convert it into a p2pkh - let script_bytes = script.clone().into_bytes(); + let script_bytes = script.as_bytes(); if script_bytes.len() == 22 && script_bytes[0..2] == [0x00, 0x14] { // p2wpkh --> length-prefixed p2pkh - let mut converted_script_bytes = vec![]; - converted_script_bytes.append(&mut vec![0x19, 0x76, 0xa9, 0x14]); - converted_script_bytes.append(&mut script_bytes[2..22].to_vec()); - converted_script_bytes.append(&mut vec![0x88, 0xac]); + let mut converted_script_bytes = Vec::with_capacity(26); + converted_script_bytes.extend_from_slice(&[0x19, 0x76, 0xa9, 0x14]); + converted_script_bytes.extend_from_slice(&script_bytes[2..22]); + converted_script_bytes.extend_from_slice(&[0x88, 0xac]); converted_script_bytes } else { // p2wsh or p2tr // codeseparator is not supported // prefix the script bytes with a varint length - let mut length_script = vec![]; - let mut script_bytes = script.clone().into_bytes(); - let script_len = VarInt(script_bytes.len() as u64); + let script_bytes_len = script_bytes.len(); + let script_len = VarInt(script_bytes_len as u64); + let mut length_script = + Vec::with_capacity(script_len.encoded_length() as usize + script_bytes_len); let mut script_len_bytes = serialize(&script_len).expect("FATAL: failed to encode varint"); length_script.append(&mut script_len_bytes); - length_script.append(&mut script_bytes); + length_script.extend_from_slice(&script_bytes); length_script } } @@ -353,28 +353,31 @@ impl Transaction { // hash of all output amounts and scriptpubkeys let mut raw_vec = vec![]; for outp in self.output.iter() { - raw_vec.append(&mut outp.value.to_le_bytes().to_vec()); + raw_vec.extend_from_slice(&outp.value.to_le_bytes()); - let mut script_bytes = outp.script_pubkey.clone().into_bytes(); + let script_bytes = outp.script_pubkey.as_bytes(); let script_len = VarInt(script_bytes.len() as u64); let mut script_len_bytes = serialize(&script_len).expect("FATAL: failed to encode varint"); raw_vec.append(&mut script_len_bytes); - raw_vec.append(&mut script_bytes); + raw_vec.extend_from_slice(&script_bytes); } Sha256dHash::from_data(&raw_vec) } else if sighash_type == SigHashType::Single && input_index < self.output.len() { // hash of just the output indexed by the input index - let mut raw_vec = vec![]; - let mut script_bytes = self.output[input_index].script_pubkey.clone().into_bytes(); - let script_len = VarInt(script_bytes.len() as u64); + let script_bytes = self.output[input_index].script_pubkey.as_bytes(); + let script_bytes_len = script_bytes.len(); + let script_len = VarInt(script_bytes_len as u64); + + let mut raw_vec = + Vec::with_capacity(script_len.encoded_length() as usize + script_bytes_len); let mut script_len_bytes = serialize(&script_len).expect("FATAL: failed to encode varint"); raw_vec.append(&mut script_len_bytes); - raw_vec.append(&mut script_bytes); + raw_vec.extend_from_slice(script_bytes); Sha256dHash::from_data(&raw_vec) } else { Sha256dHash([0u8; 32]) @@ -406,15 +409,15 @@ impl Transaction { SigHashType::from_u32(sighash_u32).split_anyonecanpay_flag(); // nVersion - raw_vec.append(&mut self.version.to_le_bytes().to_vec()); + raw_vec.extend_from_slice(&self.version.to_le_bytes()); // hashPrevouts let prevouts_hash = self.segwit_prevouts_hash(anyone_can_pay); - raw_vec.append(&mut prevouts_hash.as_bytes().to_vec()); + raw_vec.extend_from_slice(prevouts_hash.as_bytes()); // hashSequence let hash_sequence = self.segwit_sequence_hash(sighash, anyone_can_pay); - raw_vec.append(&mut hash_sequence.as_bytes().to_vec()); + raw_vec.extend_from_slice(hash_sequence.as_bytes()); // outpoint in question let mut outpoint_to_sign = serialize(&self.input[input_index].previous_output) @@ -426,20 +429,20 @@ impl Transaction { raw_vec.append(&mut script_code); // value sent - raw_vec.append(&mut amount.to_le_bytes().to_vec()); + raw_vec.extend_from_slice(&amount.to_le_bytes()); // input sequence - raw_vec.append(&mut self.input[input_index].sequence.to_le_bytes().to_vec()); + raw_vec.extend_from_slice(&self.input[input_index].sequence.to_le_bytes()); // hashed outputs let outputs_hash = self.segwit_outputs_hash(input_index, sighash); - raw_vec.append(&mut outputs_hash.as_bytes().to_vec()); + raw_vec.extend_from_slice(outputs_hash.as_bytes()); // locktime - raw_vec.append(&mut self.lock_time.to_le_bytes().to_vec()); + raw_vec.extend_from_slice(&self.lock_time.to_le_bytes()); // sighash - raw_vec.append(&mut sighash_u32.to_le_bytes().to_vec()); + raw_vec.extend_from_slice(&sighash_u32.to_le_bytes()); Sha256dHash::from_data(&raw_vec) } diff --git a/stacks-common/src/deps_common/bitcoin/network/message_blockdata.rs b/stacks-common/src/deps_common/bitcoin/network/message_blockdata.rs index 099acca55..419b405a1 100644 --- a/stacks-common/src/deps_common/bitcoin/network/message_blockdata.rs +++ b/stacks-common/src/deps_common/bitcoin/network/message_blockdata.rs @@ -135,7 +135,6 @@ impl ConsensusDecodable for Inventory { #[cfg(test)] mod tests { - use std::default::Default; use super::{GetBlocksMessage, GetHeadersMessage}; use crate::deps_common::bitcoin::network::serialize::{deserialize, serialize}; diff --git a/stacks-common/src/deps_common/bitcoin/util/hash.rs b/stacks-common/src/deps_common/bitcoin/util/hash.rs index 2c64dca01..3e9186bd9 100644 --- a/stacks-common/src/deps_common/bitcoin/util/hash.rs +++ b/stacks-common/src/deps_common/bitcoin/util/hash.rs @@ -17,7 +17,6 @@ use std::char::from_digit; use std::cmp::min; -use std::default::Default; use std::io::{Cursor, Write}; use std::{error, fmt, mem}; @@ -419,8 +418,9 @@ pub fn bitcoin_merkle_root(data: Vec) -> Sha256dHash { return data[0]; } // Recursion - let mut next = vec![]; - for idx in 0..((data.len() + 1) / 2) { + let iterations = (data.len() + 1) / 2; + let mut next = Vec::with_capacity(iterations); + for idx in 0..iterations { let idx1 = 2 * idx; let idx2 = min(idx1 + 1, data.len() - 1); let mut encoder = RawEncoder::new(Cursor::new(vec![])); diff --git a/stacks-common/src/types/chainstate.rs b/stacks-common/src/types/chainstate.rs index ac6849dfc..d41e21225 100644 --- a/stacks-common/src/types/chainstate.rs +++ b/stacks-common/src/types/chainstate.rs @@ -190,14 +190,12 @@ impl PoxId { impl FromStr for PoxId { type Err = &'static str; fn from_str(s: &str) -> Result { - let mut result = vec![]; - for i in s.chars() { - if i == '1' { - result.push(true); - } else if i == '0' { - result.push(false); - } else { - return Err("Unexpected character in PoX ID serialization"); + let mut result = Vec::with_capacity(s.len()); + for c in s.chars() { + match c { + '0' => result.push(false), + '1' => result.push(true), + _ => return Err("Unexpected character in PoX ID serialization"), } } Ok(PoxId::new(result)) @@ -363,9 +361,7 @@ impl BlockHeaderHash { pub fn from_serialized_header(buf: &[u8]) -> BlockHeaderHash { let h = Sha512Trunc256Sum::from_data(buf); - let mut b = [0u8; 32]; - b.copy_from_slice(h.as_bytes()); - BlockHeaderHash(b) + BlockHeaderHash(h.to_bytes()) } } @@ -379,8 +375,7 @@ impl BurnchainHeaderHash { } pub fn to_bitcoin_hash(&self) -> Sha256dHash { - let mut bytes = self.0.to_vec(); - bytes.reverse(); + let bytes = self.0.iter().rev().copied().collect::>(); let mut buf = [0u8; 32]; buf.copy_from_slice(&bytes[0..32]); Sha256dHash(buf) @@ -401,10 +396,7 @@ impl BurnchainHeaderHash { bytes.extend_from_slice(index_root.as_bytes()); bytes.extend_from_slice(&noise.to_be_bytes()); let h = DoubleSha256::from_data(&bytes[..]); - let mut hb = [0u8; 32]; - hb.copy_from_slice(h.as_bytes()); - - BurnchainHeaderHash(hb) + BurnchainHeaderHash(h.to_bytes()) } } diff --git a/stacks-common/src/types/mod.rs b/stacks-common/src/types/mod.rs index 998edda48..cf5603dba 100644 --- a/stacks-common/src/types/mod.rs +++ b/stacks-common/src/types/mod.rs @@ -1,5 +1,4 @@ use std::cmp::Ordering; -use std::convert::TryFrom; use std::fmt; use crate::address::c32::{c32_address, c32_address_decode}; diff --git a/stacks-common/src/util/hash.rs b/stacks-common/src/util/hash.rs index c1d538f35..a5e4341b6 100644 --- a/stacks-common/src/util/hash.rs +++ b/stacks-common/src/util/hash.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::char::from_digit; -use std::convert::TryInto; use std::fmt::Write; use std::{fmt, mem}; @@ -451,10 +450,10 @@ where /// Get a non-leaf hash pub fn get_node_hash(left: &H, right: &H) -> H { - let mut buf = vec![]; - buf.extend_from_slice(left.bits()); - buf.extend_from_slice(right.bits()); - H::from_tagged_data(MERKLE_PATH_NODE_TAG, &buf[..]) + let iter = left.bits().iter(); + let iter = iter.chain(right.bits().iter()); + let buf = iter.copied().collect::>(); + H::from_tagged_data(MERKLE_PATH_NODE_TAG, &buf) } /// Find a given hash in a merkle tree row diff --git a/stacks-common/src/util/vrf.rs b/stacks-common/src/util/vrf.rs index 410c4a07e..ddfdedfaa 100644 --- a/stacks-common/src/util/vrf.rs +++ b/stacks-common/src/util/vrf.rs @@ -17,8 +17,7 @@ #![allow(non_camel_case_types)] #![allow(non_snake_case)] -use std::clone::Clone; -use std::cmp::{Eq, Ord, Ordering, PartialEq}; +use std::cmp::Ordering; use std::fmt::Debug; use std::hash::{Hash, Hasher}; /// This codebase is based on routines defined in the IETF draft for verifiable random functions diff --git a/stacks-signer/Cargo.toml b/stacks-signer/Cargo.toml index 8944c1034..0bf969e4a 100644 --- a/stacks-signer/Cargo.toml +++ b/stacks-signer/Cargo.toml @@ -42,9 +42,9 @@ tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } wsts = { workspace = true } rand = { workspace = true } +url = "2.1.0" [dev-dependencies] -serial_test = "3.0.0" clarity = { path = "../clarity", features = ["testing"] } [dependencies.serde_json] diff --git a/stacks-signer/src/cli.rs b/stacks-signer/src/cli.rs index 48cd9a87f..907a64320 100644 --- a/stacks-signer/src/cli.rs +++ b/stacks-signer/src/cli.rs @@ -67,7 +67,7 @@ pub enum Command { pub struct StackerDBArgs { /// The Stacks node to connect to #[arg(long)] - pub host: SocketAddr, + pub host: String, /// The stacker-db contract to use. Must be in the format of "STACKS_ADDRESS.CONTRACT_NAME" #[arg(short, long, value_parser = parse_contract)] pub contract: QualifiedContractIdentifier, @@ -126,6 +126,10 @@ pub struct SignArgs { /// Path to config file #[arg(long, value_name = "FILE")] pub config: PathBuf, + /// The reward cycle the signer is registered for and wants to sign for + /// Note: this must be the current reward cycle of the node + #[arg(long, short)] + pub reward_cycle: u64, /// The data to sign #[arg(required = false, value_parser = parse_data)] // Note this weirdness is due to https://github.com/clap-rs/clap/discussions/4695 @@ -139,6 +143,9 @@ pub struct RunDkgArgs { /// Path to config file #[arg(long, value_name = "FILE")] pub config: PathBuf, + /// The reward cycle the signer is registered for and wants to peform DKG for + #[arg(long, short)] + pub reward_cycle: u64, } #[derive(Parser, Debug, Clone)] @@ -147,9 +154,6 @@ pub struct GenerateFilesArgs { /// The Stacks node to connect to #[arg(long)] pub host: SocketAddr, - /// The signers stacker-db contract to use. Must be in the format of "STACKS_ADDRESS.CONTRACT_NAME" - #[arg(short, long, value_parser = parse_contract)] - pub signers_contract: QualifiedContractIdentifier, #[arg( long, required_unless_present = "private_keys", @@ -160,9 +164,6 @@ pub struct GenerateFilesArgs { #[clap(long, value_name = "FILE")] /// A path to a file containing a list of hexadecimal Stacks private keys of the signers pub private_keys: Option, - #[arg(long)] - /// The total number of key ids to distribute among the signers - pub num_keys: u32, #[arg(long, value_parser = parse_network)] /// The network to use. One of "mainnet", "testnet", or "mocknet". pub network: Network, @@ -172,6 +173,9 @@ pub struct GenerateFilesArgs { /// The number of milliseconds to wait when polling for events from the stacker-db instance. #[arg(long)] pub timeout: Option, + #[arg(long)] + /// The authorization password to use to connect to the validate block proposal node endpoint + pub password: String, } #[derive(Clone, Debug)] diff --git a/stacks-signer/src/client/mod.rs b/stacks-signer/src/client/mod.rs index 4e23be7af..8f458811f 100644 --- a/stacks-signer/src/client/mod.rs +++ b/stacks-signer/src/client/mod.rs @@ -23,7 +23,6 @@ use std::time::Duration; use clarity::vm::errors::Error as ClarityError; use clarity::vm::types::serialization::SerializationError; -use clarity::vm::Value as ClarityValue; use libsigner::RPCError; use libstackerdb::Error as StackerDBError; use slog::slog_debug; @@ -71,8 +70,8 @@ pub enum ClientError { #[error("Failed to serialize Clarity value: {0}")] ClaritySerializationError(#[from] SerializationError), /// Failed to parse a Clarity value - #[error("Recieved a malformed clarity value: {0}")] - MalformedClarityValue(ClarityValue), + #[error("Received a malformed clarity value: {0}")] + MalformedClarityValue(String), /// Invalid Clarity Name #[error("Invalid Clarity Name: {0}")] InvalidClarityName(String), @@ -87,24 +86,33 @@ pub enum ClientError { InvalidSigningKey, /// Clarity interpreter error #[error("Clarity interpreter error: {0}")] - ClarityError(ClarityError), -} - -impl From for ClientError { - fn from(e: ClarityError) -> ClientError { - ClientError::ClarityError(e) - } + ClarityError(#[from] ClarityError), + /// Our stacks address does not belong to a registered signer + #[error("Our stacks address does not belong to a registered signer")] + NotRegistered, + /// Reward set not yet calculated for the given reward cycle + #[error("Reward set not yet calculated for reward cycle: {0}")] + RewardSetNotYetCalculated(u64), + /// Malformed reward set + #[error("Malformed contract data: {0}")] + MalformedContractData(String), + /// No reward set exists for the given reward cycle + #[error("No reward set exists for reward cycle {0}")] + NoRewardSet(u64), + /// Stacks node does not support a feature we need + #[error("Stacks node does not support a required feature: {0}")] + UnsupportedStacksFeature(String), } /// Retry a function F with an exponential backoff and notification on transient failure pub fn retry_with_exponential_backoff(request_fn: F) -> Result where F: FnMut() -> Result>, + E: std::fmt::Debug, { - let notify = |_err, dur| { + let notify = |err, dur| { debug!( - "Failed to connect to stacks-node. Next attempt in {:?}", - dur + "Failed to connect to stacks node and/or deserialize its response: {err:?}. Next attempt in {dur:?}" ); }; @@ -121,53 +129,85 @@ pub(crate) mod tests { use std::io::{Read, Write}; use std::net::{SocketAddr, TcpListener}; + use blockstack_lib::chainstate::stacks::boot::POX_4_NAME; + use blockstack_lib::net::api::getaccount::AccountEntryResponse; + use blockstack_lib::net::api::getinfo::RPCPeerInfoData; + use blockstack_lib::net::api::getpoxinfo::{ + RPCPoxCurrentCycleInfo, RPCPoxEpoch, RPCPoxInfoData, RPCPoxNextCycleInfo, + }; + use blockstack_lib::util_lib::boot::boot_code_id; + use clarity::vm::costs::ExecutionCost; + use clarity::vm::types::TupleData; + use clarity::vm::Value as ClarityValue; + use hashbrown::{HashMap, HashSet}; + use rand::distributions::Standard; + use rand::{thread_rng, Rng}; + use rand_core::{OsRng, RngCore}; + use stacks_common::types::chainstate::{ + BlockHeaderHash, ConsensusHash, StacksAddress, StacksPrivateKey, StacksPublicKey, + }; + use stacks_common::types::{StacksEpochId, StacksPublicKeyBuffer}; + use stacks_common::util::hash::{Hash160, Sha256Sum}; + use wsts::curve::ecdsa; + use wsts::curve::point::{Compressed, Point}; + use wsts::curve::scalar::Scalar; + use wsts::state_machine::PublicKeys; + use super::*; - use crate::config::Config; + use crate::config::{GlobalConfig, ParsedSignerEntries, SignerConfig}; + use crate::signer::SignerSlotID; - pub(crate) struct TestConfig { - pub(crate) mock_server: TcpListener, - pub(crate) client: StacksClient, - pub(crate) stackerdb: StackerDB, - pub(crate) config: Config, + pub struct MockServerClient { + pub server: TcpListener, + pub client: StacksClient, + pub config: GlobalConfig, } - impl TestConfig { - pub(crate) fn new() -> Self { - let mut config = Config::load_from_file("./src/tests/conf/signer-0.toml").unwrap(); - - let mut mock_server_addr = SocketAddr::from(([127, 0, 0, 1], 0)); - // Ask the OS to assign a random port to listen on by passing 0 - let mock_server = TcpListener::bind(mock_server_addr).unwrap(); - - // Update the config to use this port - mock_server_addr.set_port(mock_server.local_addr().unwrap().port()); - config.node_host = mock_server_addr; + impl MockServerClient { + /// Construct a new MockServerClient on a random port + pub fn new() -> Self { + let mut config = + GlobalConfig::load_from_file("./src/tests/conf/signer-0.toml").unwrap(); + let (server, mock_server_addr) = mock_server_random(); + config.node_host = mock_server_addr.to_string(); let client = StacksClient::from(&config); - let stackerdb = StackerDB::from(&config); Self { - mock_server, + server, client, - stackerdb, config, } } - pub(crate) fn from_config(config: Config) -> Self { - let mock_server = TcpListener::bind(config.node_host).unwrap(); - + /// Construct a new MockServerClient on the port specified in the config + pub fn from_config(config: GlobalConfig) -> Self { + let server = mock_server_from_config(&config); let client = StacksClient::from(&config); - let stackerdb = StackerDB::from(&config); Self { - mock_server, + server, client, - stackerdb, config, } } } - pub(crate) fn write_response(mock_server: TcpListener, bytes: &[u8]) -> [u8; 1024] { + /// Create a mock server on a random port and return the socket addr + pub fn mock_server_random() -> (TcpListener, SocketAddr) { + let mut mock_server_addr = SocketAddr::from(([127, 0, 0, 1], 0)); + // Ask the OS to assign a random port to listen on by passing 0 + let server = TcpListener::bind(mock_server_addr).unwrap(); + + mock_server_addr.set_port(server.local_addr().unwrap().port()); + (server, mock_server_addr) + } + + /// Create a mock server on a same port as in the config + pub fn mock_server_from_config(config: &GlobalConfig) -> TcpListener { + TcpListener::bind(config.node_host.to_string()).unwrap() + } + + /// Write a response to the mock server and return the request bytes + pub fn write_response(mock_server: TcpListener, bytes: &[u8]) -> [u8; 1024] { debug!("Writing a response..."); let mut request_bytes = [0u8; 1024]; { @@ -177,4 +217,322 @@ pub(crate) mod tests { } request_bytes } + + pub fn generate_random_consensus_hash() -> ConsensusHash { + let rng = rand::thread_rng(); + let bytes: Vec = rng.sample_iter(Standard).take(20).collect(); + let mut hash = [0u8; 20]; + hash.copy_from_slice(&bytes); + ConsensusHash(hash) + } + + /// Build a response for the get_last_round request + pub fn build_get_last_round_response(round: u64) -> String { + let value = ClarityValue::some(ClarityValue::UInt(round as u128)) + .expect("Failed to create response"); + build_read_only_response(&value) + } + + /// Build a response for the get_account_nonce request + pub fn build_account_nonce_response(nonce: u64) -> String { + let account_nonce_entry = AccountEntryResponse { + nonce, + balance: "0x00000000000000000000000000000000".to_string(), + locked: "0x00000000000000000000000000000000".to_string(), + unlock_height: thread_rng().next_u64(), + balance_proof: None, + nonce_proof: None, + }; + let account_nonce_entry_json = serde_json::to_string(&account_nonce_entry) + .expect("Failed to serialize account nonce entry"); + format!("HTTP/1.1 200 OK\n\n{account_nonce_entry_json}") + } + + /// Build a response to get_pox_data where it returns a specific reward cycle id and block height + pub fn build_get_pox_data_response( + reward_cycle: Option, + prepare_phase_start_height: Option, + epoch_25_activation_height: Option, + epoch_30_activation_height: Option, + ) -> (String, RPCPoxInfoData) { + // Populate some random data! + let epoch_25_start = epoch_25_activation_height.unwrap_or(thread_rng().next_u64()); + let epoch_30_start = + epoch_30_activation_height.unwrap_or(epoch_25_start.saturating_add(1000)); + let current_id = reward_cycle.unwrap_or(thread_rng().next_u64()); + let next_id = current_id.saturating_add(1); + let pox_info = RPCPoxInfoData { + contract_id: boot_code_id(POX_4_NAME, false).to_string(), + pox_activation_threshold_ustx: thread_rng().next_u64(), + first_burnchain_block_height: thread_rng().next_u64(), + current_burnchain_block_height: thread_rng().next_u64(), + prepare_phase_block_length: thread_rng().next_u64(), + reward_phase_block_length: thread_rng().next_u64(), + reward_slots: thread_rng().next_u64(), + rejection_fraction: None, + total_liquid_supply_ustx: thread_rng().next_u64(), + current_cycle: RPCPoxCurrentCycleInfo { + id: current_id, + min_threshold_ustx: thread_rng().next_u64(), + stacked_ustx: thread_rng().next_u64(), + is_pox_active: true, + }, + next_cycle: RPCPoxNextCycleInfo { + id: next_id, + min_threshold_ustx: thread_rng().next_u64(), + min_increment_ustx: thread_rng().next_u64(), + stacked_ustx: thread_rng().next_u64(), + prepare_phase_start_block_height: prepare_phase_start_height + .unwrap_or(thread_rng().next_u64()), + blocks_until_prepare_phase: thread_rng().next_u32() as i64, + reward_phase_start_block_height: thread_rng().next_u64(), + blocks_until_reward_phase: thread_rng().next_u64(), + ustx_until_pox_rejection: None, + }, + min_amount_ustx: thread_rng().next_u64(), + prepare_cycle_length: thread_rng().next_u64(), + reward_cycle_id: current_id, + epochs: vec![ + RPCPoxEpoch { + start_height: epoch_25_start, + end_height: epoch_30_start, + block_limit: ExecutionCost { + write_length: thread_rng().next_u64(), + write_count: thread_rng().next_u64(), + read_length: thread_rng().next_u64(), + read_count: thread_rng().next_u64(), + runtime: thread_rng().next_u64(), + }, + epoch_id: StacksEpochId::Epoch25, + network_epoch: 0, + }, + RPCPoxEpoch { + start_height: epoch_30_start, + end_height: epoch_30_start.saturating_add(1000), + block_limit: ExecutionCost { + write_length: thread_rng().next_u64(), + write_count: thread_rng().next_u64(), + read_length: thread_rng().next_u64(), + read_count: thread_rng().next_u64(), + runtime: thread_rng().next_u64(), + }, + epoch_id: StacksEpochId::Epoch30, + network_epoch: 0, + }, + ], + reward_cycle_length: thread_rng().next_u64(), + rejection_votes_left_required: None, + next_reward_cycle_in: thread_rng().next_u64(), + contract_versions: vec![], + }; + let pox_info_json = serde_json::to_string(&pox_info).expect("Failed to serialize pox info"); + (format!("HTTP/1.1 200 Ok\n\n{pox_info_json}"), pox_info) + } + + /// Build a response for the get_approved_aggregate_key request + pub fn build_get_approved_aggregate_key_response(point: Option) -> String { + let clarity_value = if let Some(point) = point { + ClarityValue::some( + ClarityValue::buff_from(point.compress().as_bytes().to_vec()) + .expect("BUG: Failed to create clarity value from point"), + ) + .expect("BUG: Failed to create clarity value from point") + } else { + ClarityValue::none() + }; + build_read_only_response(&clarity_value) + } + + /// Build a response for the get_approved_aggregate_key request + pub fn build_get_vote_for_aggregate_key_response(point: Option) -> String { + let clarity_value = if let Some(point) = point { + ClarityValue::some(ClarityValue::Tuple( + TupleData::from_data(vec![ + ( + "aggregate-public-key".into(), + ClarityValue::buff_from(point.compress().as_bytes().to_vec()) + .expect("BUG: Failed to create clarity value from point"), + ), + ("signer-weight".into(), ClarityValue::UInt(1)), // fixed for testing purposes + ]) + .expect("BUG: Failed to create clarity value from tuple data"), + )) + .expect("BUG: Failed to create clarity value from tuple data") + } else { + ClarityValue::none() + }; + build_read_only_response(&clarity_value) + } + + /// Build a response for the get_peer_info request with a specific stacks tip height and consensus hash + pub fn build_get_peer_info_response( + burn_block_height: Option, + pox_consensus_hash: Option, + ) -> (String, RPCPeerInfoData) { + // Generate some random info + let private_key = StacksPrivateKey::new(); + let public_key = StacksPublicKey::from_private(&private_key); + let public_key_buf = StacksPublicKeyBuffer::from_public_key(&public_key); + let public_key_hash = Hash160::from_node_public_key(&public_key); + let stackerdb_contract_ids = + vec![boot_code_id("fake", false), boot_code_id("fake_2", false)]; + let peer_info = RPCPeerInfoData { + peer_version: thread_rng().next_u32(), + pox_consensus: pox_consensus_hash.unwrap_or(generate_random_consensus_hash()), + burn_block_height: burn_block_height.unwrap_or(thread_rng().next_u64()), + stable_pox_consensus: generate_random_consensus_hash(), + stable_burn_block_height: 2, + server_version: "fake version".to_string(), + network_id: thread_rng().next_u32(), + parent_network_id: thread_rng().next_u32(), + stacks_tip_height: thread_rng().next_u64(), + stacks_tip: BlockHeaderHash([0x06; 32]), + stacks_tip_consensus_hash: generate_random_consensus_hash(), + unanchored_tip: None, + unanchored_seq: Some(0), + exit_at_block_height: None, + genesis_chainstate_hash: Sha256Sum::zero(), + node_public_key: Some(public_key_buf), + node_public_key_hash: Some(public_key_hash), + affirmations: None, + last_pox_anchor: None, + stackerdbs: Some( + stackerdb_contract_ids + .into_iter() + .map(|cid| format!("{}", cid)) + .collect(), + ), + }; + let peer_info_json = + serde_json::to_string(&peer_info).expect("Failed to serialize peer info"); + (format!("HTTP/1.1 200 OK\n\n{peer_info_json}"), peer_info) + } + + /// Build a response to a read only clarity contract call + pub fn build_read_only_response(value: &ClarityValue) -> String { + let hex = value + .serialize_to_hex() + .expect("Failed to serialize hex value"); + format!("HTTP/1.1 200 OK\n\n{{\"okay\":true,\"result\":\"{hex}\"}}") + } + + /// Generate a signer config with the given number of signers and keys where the first signer is + /// obtained from the provided global config + pub fn generate_signer_config( + config: &GlobalConfig, + num_signers: u32, + num_keys: u32, + ) -> SignerConfig { + assert!( + num_signers > 0, + "Cannot generate 0 signers...Specify at least 1 signer." + ); + assert!( + num_keys > 0, + "Cannot generate 0 keys for the provided signers...Specify at least 1 key." + ); + let mut public_keys = PublicKeys { + signers: HashMap::new(), + key_ids: HashMap::new(), + }; + let reward_cycle = thread_rng().next_u64(); + let rng = &mut OsRng; + let num_keys = num_keys / num_signers; + let remaining_keys = num_keys % num_signers; + let mut coordinator_key_ids = HashMap::new(); + let mut signer_key_ids = HashMap::new(); + let mut signer_ids = HashMap::new(); + let mut start_key_id = 1u32; + let mut end_key_id = start_key_id; + let mut signer_public_keys = HashMap::new(); + let mut signer_slot_ids = vec![]; + let ecdsa_private_key = config.ecdsa_private_key; + let ecdsa_public_key = + ecdsa::PublicKey::new(&ecdsa_private_key).expect("Failed to create ecdsa public key"); + // Key ids start from 1 hence the wrapping adds everywhere + for signer_id in 0..num_signers { + end_key_id = if signer_id.wrapping_add(1) == num_signers { + end_key_id.wrapping_add(remaining_keys) + } else { + end_key_id.wrapping_add(num_keys) + }; + if signer_id == 0 { + public_keys.signers.insert(signer_id, ecdsa_public_key); + let signer_public_key = + Point::try_from(&Compressed::from(ecdsa_public_key.to_bytes())).unwrap(); + signer_public_keys.insert(signer_id, signer_public_key); + public_keys.signers.insert(signer_id, ecdsa_public_key); + for k in start_key_id..end_key_id { + public_keys.key_ids.insert(k, ecdsa_public_key); + coordinator_key_ids + .entry(signer_id) + .or_insert(HashSet::new()) + .insert(k); + signer_key_ids + .entry(signer_id) + .or_insert(Vec::new()) + .push(k); + } + start_key_id = end_key_id; + let address = StacksAddress::p2pkh( + false, + &StacksPublicKey::from_slice(ecdsa_public_key.to_bytes().as_slice()) + .expect("Failed to create stacks public key"), + ); + signer_slot_ids.push(SignerSlotID(signer_id)); + signer_ids.insert(address, signer_id); + + continue; + } + let private_key = Scalar::random(rng); + let public_key = ecdsa::PublicKey::new(&private_key).unwrap(); + let signer_public_key = + Point::try_from(&Compressed::from(public_key.to_bytes())).unwrap(); + signer_public_keys.insert(signer_id, signer_public_key); + public_keys.signers.insert(signer_id, public_key); + for k in start_key_id..end_key_id { + public_keys.key_ids.insert(k, public_key); + coordinator_key_ids + .entry(signer_id) + .or_insert(HashSet::new()) + .insert(k); + signer_key_ids + .entry(signer_id) + .or_insert(Vec::new()) + .push(k); + } + let address = StacksAddress::p2pkh( + false, + &StacksPublicKey::from_slice(public_key.to_bytes().as_slice()) + .expect("Failed to create stacks public key"), + ); + signer_slot_ids.push(SignerSlotID(signer_id)); + signer_ids.insert(address, signer_id); + start_key_id = end_key_id; + } + SignerConfig { + reward_cycle, + signer_id: 0, + signer_slot_id: SignerSlotID(rand::thread_rng().gen_range(0..num_signers)), // Give a random signer slot id between 0 and num_signers + key_ids: signer_key_ids.get(&0).cloned().unwrap_or_default(), + signer_entries: ParsedSignerEntries { + public_keys, + coordinator_key_ids, + signer_key_ids, + signer_ids, + signer_public_keys, + }, + signer_slot_ids, + ecdsa_private_key: config.ecdsa_private_key, + stacks_private_key: config.stacks_private_key, + node_host: config.node_host.to_string(), + mainnet: config.network.is_mainnet(), + dkg_end_timeout: config.dkg_end_timeout, + dkg_private_timeout: config.dkg_private_timeout, + dkg_public_timeout: config.dkg_public_timeout, + nonce_timeout: config.nonce_timeout, + sign_timeout: config.sign_timeout, + tx_fee_ustx: config.tx_fee_ustx, + } + } } diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs index 0889b9fa8..78275f56c 100644 --- a/stacks-signer/src/client/stackerdb.rs +++ b/stacks-signer/src/client/stackerdb.rs @@ -14,10 +14,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . // -use std::net::SocketAddr; - use blockstack_lib::chainstate::nakamoto::signer_set::NakamotoSigners; use blockstack_lib::chainstate::stacks::StacksTransaction; +use blockstack_lib::util_lib::boot::boot_code_addr; use clarity::vm::types::QualifiedContractIdentifier; use clarity::vm::ContractName; use hashbrown::HashMap; @@ -26,97 +25,88 @@ use libstackerdb::{StackerDBChunkAckData, StackerDBChunkData}; use slog::{slog_debug, slog_warn}; use stacks_common::codec::{read_next, StacksMessageCodec}; use stacks_common::consts::SIGNER_SLOTS_PER_USER; -use stacks_common::types::chainstate::{StacksAddress, StacksPrivateKey, StacksPublicKey}; +use stacks_common::types::chainstate::StacksPrivateKey; use stacks_common::{debug, warn}; use super::ClientError; use crate::client::retry_with_exponential_backoff; -use crate::config::Config; +use crate::config::SignerConfig; +use crate::signer::SignerSlotID; /// The StackerDB client for communicating with the .signers contract pub struct StackerDB { - /// The stacker-db session for the signer StackerDB. Used for querying signer addresses and - /// other system metadata. - signers_stackerdb_session: StackerDBSession, /// The stacker-db sessions for each signer set and message type. - /// Maps (signer-set, message ID) to the DB session. - signers_message_stackerdb_sessions: HashMap<(u32, u32), StackerDBSession>, + /// Maps message ID to the DB session. + signers_message_stackerdb_sessions: HashMap, /// The private key used in all stacks node communications stacks_private_key: StacksPrivateKey, - /// A map of a (signer-set, message ID) to last chunk version for each session - slot_versions: HashMap<(u32, u32), HashMap>, - /// The signer ID -- the index into the signer list for this signer daemon's signing key. - signer_slot_id: u32, - /// Which signer set to use (0 or 1). - /// Depends on whether or not we're signing in an even or odd reward cycle - signer_set: u32, + /// A map of a message ID to last chunk version for each session + slot_versions: HashMap>, + /// The signer slot ID -- the index into the signer list for this signer daemon's signing key. + signer_slot_id: SignerSlotID, + /// The reward cycle of the connecting signer + reward_cycle: u64, + /// The stacker-db transaction msg session for the NEXT reward cycle + next_transaction_session: StackerDBSession, } -impl From<&Config> for StackerDB { - fn from(config: &Config) -> Self { - let mut signers_message_stackerdb_sessions = HashMap::new(); - for signer_set in 0..2 { - for msg_id in 0..SIGNER_SLOTS_PER_USER { - signers_message_stackerdb_sessions.insert( - (signer_set as u32, msg_id), - StackerDBSession::new( - config.node_host.clone(), - QualifiedContractIdentifier::new( - config.stackerdb_contract_id.issuer.clone(), - ContractName::from( - NakamotoSigners::make_signers_db_name(signer_set, msg_id).as_str(), - ), - ), - ), - ); - } - } - Self { - signers_stackerdb_session: StackerDBSession::new( - config.node_host, - config.stackerdb_contract_id.clone(), - ), - signers_message_stackerdb_sessions, - stacks_private_key: config.stacks_private_key, - slot_versions: HashMap::new(), - signer_slot_id: config.signer_id, - signer_set: 0, - } +impl From<&SignerConfig> for StackerDB { + fn from(config: &SignerConfig) -> Self { + StackerDB::new( + &config.node_host, + config.stacks_private_key, + config.mainnet, + config.reward_cycle, + config.signer_slot_id, + ) } } - impl StackerDB { /// Create a new StackerDB client pub fn new( - host: SocketAddr, - stackerdb_contract_id: QualifiedContractIdentifier, + host: &str, stacks_private_key: StacksPrivateKey, - signer_id: u32, + is_mainnet: bool, + reward_cycle: u64, + signer_slot_id: SignerSlotID, ) -> Self { let mut signers_message_stackerdb_sessions = HashMap::new(); - for signer_set in 0..2 { - for msg_id in 0..SIGNER_SLOTS_PER_USER { - signers_message_stackerdb_sessions.insert( - (signer_set as u32, msg_id), - StackerDBSession::new( - host.clone(), - QualifiedContractIdentifier::new( - stackerdb_contract_id.issuer.clone(), - ContractName::from( - NakamotoSigners::make_signers_db_name(signer_set, msg_id).as_str(), - ), + let stackerdb_issuer = boot_code_addr(is_mainnet); + for msg_id in 0..SIGNER_SLOTS_PER_USER { + signers_message_stackerdb_sessions.insert( + msg_id, + StackerDBSession::new( + host, + QualifiedContractIdentifier::new( + stackerdb_issuer.into(), + ContractName::from( + NakamotoSigners::make_signers_db_name(reward_cycle, msg_id).as_str(), ), ), - ); - } + ), + ); } + let next_transaction_session = StackerDBSession::new( + host, + QualifiedContractIdentifier::new( + stackerdb_issuer.into(), + ContractName::from( + NakamotoSigners::make_signers_db_name( + reward_cycle.wrapping_add(1), + TRANSACTIONS_MSG_ID, + ) + .as_str(), + ), + ), + ); + Self { - signers_stackerdb_session: StackerDBSession::new(host, stackerdb_contract_id), signers_message_stackerdb_sessions, stacks_private_key, slot_versions: HashMap::new(), - signer_slot_id: signer_id, - signer_set: 0, + signer_slot_id, + reward_cycle, + next_transaction_session, } } @@ -127,44 +117,38 @@ impl StackerDB { ) -> Result { let message_bytes = message.serialize_to_vec(); let msg_id = message.msg_id(); - let signer_set = self.signer_set; let slot_id = self.signer_slot_id; loop { - let slot_version = - if let Some(versions) = self.slot_versions.get_mut(&(signer_set, msg_id)) { - if let Some(version) = versions.get(&slot_id) { - *version - } else { - versions.insert(slot_id, 0); - 1 - } + let slot_version = if let Some(versions) = self.slot_versions.get_mut(&msg_id) { + if let Some(version) = versions.get(&slot_id) { + *version } else { - let mut versions = HashMap::new(); versions.insert(slot_id, 0); - self.slot_versions.insert((signer_set, msg_id), versions); 1 - }; + } + } else { + let mut versions = HashMap::new(); + versions.insert(slot_id, 0); + self.slot_versions.insert(msg_id, versions); + 1 + }; - let mut chunk = StackerDBChunkData::new(slot_id, slot_version, message_bytes.clone()); + let mut chunk = StackerDBChunkData::new(slot_id.0, slot_version, message_bytes.clone()); chunk.sign(&self.stacks_private_key)?; - let Some(session) = self - .signers_message_stackerdb_sessions - .get_mut(&(signer_set, msg_id)) - else { + let Some(session) = self.signers_message_stackerdb_sessions.get_mut(&msg_id) else { panic!("FATAL: would loop forever trying to send a message with ID {}, for which we don't have a session", msg_id); }; debug!( - "Sending a chunk to stackerdb slot ID {slot_id} with version {slot_version} to contract {:?}!\n{:?}", - &session.stackerdb_contract_id, - &chunk + "Sending a chunk to stackerdb slot ID {slot_id} with version {slot_version} to contract {:?}!\n{chunk:?}", + &session.stackerdb_contract_id ); let send_request = || session.put_chunk(&chunk).map_err(backoff::Error::transient); let chunk_ack: StackerDBChunkAckData = retry_with_exponential_backoff(send_request)?; - if let Some(versions) = self.slot_versions.get_mut(&(signer_set, msg_id)) { + if let Some(versions) = self.slot_versions.get_mut(&msg_id) { // NOTE: per the above, this is always executed versions.insert(slot_id, slot_version.saturating_add(1)); } else { @@ -172,17 +156,17 @@ impl StackerDB { } if chunk_ack.accepted { - debug!("Chunk accepted by stackerdb: {:?}", chunk_ack); + debug!("Chunk accepted by stackerdb: {chunk_ack:?}"); return Ok(chunk_ack); } else { - warn!("Chunk rejected by stackerdb: {:?}", chunk_ack); + warn!("Chunk rejected by stackerdb: {chunk_ack:?}"); } if let Some(reason) = chunk_ack.reason { // TODO: fix this jankiness. Update stackerdb to use an error code mapping instead of just a string // See: https://github.com/stacks-network/stacks-blockchain/issues/3917 if reason.contains("Data for this slot and version already exist") { warn!("Failed to send message to stackerdb due to wrong version number {}. Incrementing and retrying...", slot_version); - if let Some(versions) = self.slot_versions.get_mut(&(signer_set, msg_id)) { + if let Some(versions) = self.slot_versions.get_mut(&msg_id) { // NOTE: per the above, this is always executed versions.insert(slot_id, slot_version.saturating_add(1)); } else { @@ -196,25 +180,14 @@ impl StackerDB { } } - /// Get the latest signer transactions from signer ids - pub fn get_signer_transactions_with_retry( - &mut self, - signer_ids: &[u32], + /// Get the transactions from stackerdb for the signers + fn get_transactions( + transactions_session: &mut StackerDBSession, + signer_ids: &[SignerSlotID], ) -> Result, ClientError> { - debug!( - "Getting latest chunks from stackerdb for the following signers: {:?}", - signer_ids - ); - let Some(transactions_session) = self - .signers_message_stackerdb_sessions - .get_mut(&(self.signer_set, TRANSACTIONS_MSG_ID)) - else { - return Err(ClientError::NotConnected); - }; - let send_request = || { transactions_session - .get_latest_chunks(signer_ids) + .get_latest_chunks(&signer_ids.iter().map(|id| id.0).collect::>()) .map_err(backoff::Error::transient) }; let chunk_ack = retry_with_exponential_backoff(send_request)?; @@ -227,13 +200,11 @@ impl StackerDB { continue; }; let Ok(message) = read_next::(&mut &data[..]) else { - if data.len() > 0 { + if !data.is_empty() { warn!("Failed to deserialize chunk data into a SignerMessage"); debug!( - "signer #{}: Failed chunk ({}): {:?}", - signer_id, + "signer #{signer_id}: Failed chunk ({}): {data:?}", &data.len(), - &data[..] ); } continue; @@ -253,53 +224,59 @@ impl StackerDB { Ok(transactions) } - /// Retrieve the signer contract id - pub fn signers_contract_id(&self) -> &QualifiedContractIdentifier { - &self.signers_stackerdb_session.stackerdb_contract_id + /// Get this signer's latest transactions from stackerdb + pub fn get_current_transactions_with_retry( + &mut self, + ) -> Result, ClientError> { + let Some(transactions_session) = self + .signers_message_stackerdb_sessions + .get_mut(&TRANSACTIONS_MSG_ID) + else { + return Err(ClientError::NotConnected); + }; + Self::get_transactions(transactions_session, &[self.signer_slot_id]) + } + + /// Get the latest signer transactions from signer ids for the next reward cycle + pub fn get_next_transactions_with_retry( + &mut self, + signer_ids: &[SignerSlotID], + ) -> Result, ClientError> { + debug!("Getting latest chunks from stackerdb for the following signers: {signer_ids:?}",); + Self::get_transactions(&mut self.next_transaction_session, signer_ids) } /// Retrieve the signer set this stackerdb client is attached to pub fn get_signer_set(&self) -> u32 { - self.signer_set + u32::try_from(self.reward_cycle % 2).expect("FATAL: reward cycle % 2 exceeds u32::MAX") } - /// Set the signer set from a reward cycle - pub fn set_signer_set(&mut self, set: u32) { - self.signer_set = set - } - - /// Set the signer slot ID - pub fn set_signer_slot_id(&mut self, slot_id: u32) { - self.signer_slot_id = slot_id; - } - - /// Get our signer address - pub fn get_signer_address(&self, mainnet: bool) -> StacksAddress { - StacksAddress::p2pkh( - mainnet, - &StacksPublicKey::from_private(&self.stacks_private_key), - ) + /// Retrieve the signer slot ID + pub fn get_signer_slot_id(&mut self) -> SignerSlotID { + self.signer_slot_id } } #[cfg(test)] mod tests { use std::thread::spawn; + use std::time::Duration; use blockstack_lib::chainstate::stacks::{ TransactionAnchorMode, TransactionAuth, TransactionPayload, TransactionPostConditionMode, TransactionSmartContract, TransactionVersion, }; use blockstack_lib::util_lib::strings::StacksString; - use serial_test::serial; use super::*; - use crate::client::tests::{write_response, TestConfig}; + use crate::client::tests::{generate_signer_config, mock_server_from_config, write_response}; + use crate::config::GlobalConfig; #[test] - #[serial] fn get_signer_transactions_with_retry_should_succeed() { - let mut config = TestConfig::new(); + let config = GlobalConfig::load_from_file("./src/tests/conf/signer-0.toml").unwrap(); + let signer_config = generate_signer_config(&config, 5, 20); + let mut stackerdb = StackerDB::from(&signer_config); let sk = StacksPrivateKey::new(); let tx = StacksTransaction { version: TransactionVersion::Testnet, @@ -320,31 +297,30 @@ mod tests { let signer_message = SignerMessage::Transactions(vec![tx.clone()]); let message = signer_message.serialize_to_vec(); - let signer_ids = vec![0, 1]; - let h = spawn(move || { - config - .stackerdb - .get_signer_transactions_with_retry(&signer_ids) - }); + let signer_slot_ids = vec![SignerSlotID(0), SignerSlotID(1)]; + let h = spawn(move || stackerdb.get_next_transactions_with_retry(&signer_slot_ids)); let mut response_bytes = b"HTTP/1.1 200 OK\n\n".to_vec(); response_bytes.extend(message); - write_response(config.mock_server, response_bytes.as_slice()); + let mock_server = mock_server_from_config(&config); + write_response(mock_server, response_bytes.as_slice()); let signer_message = SignerMessage::Transactions(vec![]); let message = signer_message.serialize_to_vec(); - let test_config = TestConfig::from_config(config.config); let mut response_bytes = b"HTTP/1.1 200 OK\n\n".to_vec(); response_bytes.extend(message); - write_response(test_config.mock_server, response_bytes.as_slice()); + let mock_server = mock_server_from_config(&config); + write_response(mock_server, response_bytes.as_slice()); let transactions = h.join().unwrap().unwrap(); assert_eq!(transactions, vec![tx]); } #[test] - #[serial] fn send_signer_message_with_retry_should_succeed() { - let mut config = TestConfig::new(); + let config = GlobalConfig::load_from_file("./src/tests/conf/signer-1.toml").unwrap(); + let signer_config = generate_signer_config(&config, 5, 20); + let mut stackerdb = StackerDB::from(&signer_config); + let sk = StacksPrivateKey::new(); let tx = StacksTransaction { version: TransactionVersion::Testnet, @@ -368,12 +344,13 @@ mod tests { reason: None, metadata: None, }; + let mock_server = mock_server_from_config(&config); + let h = spawn(move || stackerdb.send_message_with_retry(signer_message)); let mut response_bytes = b"HTTP/1.1 200 OK\n\n".to_vec(); let payload = serde_json::to_string(&ack).expect("Failed to serialize ack"); response_bytes.extend(payload.as_bytes()); - let h = spawn(move || config.stackerdb.send_message_with_retry(signer_message)); - std::thread::sleep(std::time::Duration::from_millis(100)); - write_response(config.mock_server, response_bytes.as_slice()); + std::thread::sleep(Duration::from_millis(500)); + write_response(mock_server, response_bytes.as_slice()); assert_eq!(ack, h.join().unwrap().unwrap()); } } diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs index d1d104934..80481d598 100644 --- a/stacks-signer/src/client/stacks_client.rs +++ b/stacks-signer/src/client/stacks_client.rs @@ -13,40 +13,42 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use std::net::SocketAddr; + use blockstack_lib::burnchains::Txid; use blockstack_lib::chainstate::nakamoto::NakamotoBlock; -use blockstack_lib::chainstate::stacks::boot::POX_4_NAME; +use blockstack_lib::chainstate::stacks::boot::{ + NakamotoSignerEntry, SIGNERS_VOTING_FUNCTION_NAME, SIGNERS_VOTING_NAME, +}; use blockstack_lib::chainstate::stacks::{ StacksTransaction, StacksTransactionSigner, TransactionAnchorMode, TransactionAuth, TransactionContractCall, TransactionPayload, TransactionPostConditionMode, TransactionSpendingCondition, TransactionVersion, }; -use blockstack_lib::core::{ - BITCOIN_MAINNET_STACKS_25_BURN_HEIGHT, BITCOIN_MAINNET_STACKS_30_BURN_HEIGHT, - BITCOIN_TESTNET_STACKS_25_BURN_HEIGHT, BITCOIN_TESTNET_STACKS_30_BURN_HEIGHT, -}; use blockstack_lib::net::api::callreadonly::CallReadOnlyResponse; use blockstack_lib::net::api::getaccount::AccountEntryResponse; use blockstack_lib::net::api::getinfo::RPCPeerInfoData; use blockstack_lib::net::api::getpoxinfo::RPCPoxInfoData; +use blockstack_lib::net::api::getstackers::GetStackersResponse; use blockstack_lib::net::api::postblock_proposal::NakamotoBlockProposal; -use blockstack_lib::util_lib::boot::boot_code_id; +use blockstack_lib::util_lib::boot::{boot_code_addr, boot_code_id}; use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier}; -use clarity::vm::{ClarityName, ContractName, Value as ClarityValue, Value}; +use clarity::vm::{ClarityName, ContractName, Value as ClarityValue}; +use reqwest::header::AUTHORIZATION; use serde_json::json; use slog::slog_debug; use stacks_common::codec::StacksMessageCodec; -use stacks_common::consts::CHAIN_ID_MAINNET; +use stacks_common::consts::{CHAIN_ID_MAINNET, CHAIN_ID_TESTNET}; use stacks_common::debug; -use stacks_common::types::chainstate::{ - ConsensusHash, StacksAddress, StacksPrivateKey, StacksPublicKey, -}; +use stacks_common::types::chainstate::{StacksAddress, StacksPrivateKey, StacksPublicKey}; +use stacks_common::types::StacksEpochId; use wsts::curve::point::{Compressed, Point}; use crate::client::{retry_with_exponential_backoff, ClientError}; -use crate::config::Config; +use crate::config::GlobalConfig; /// The Stacks signer client used to communicate with the stacks node +#[derive(Clone, Debug)] pub struct StacksClient { /// The stacks address of the signer stacks_address: StacksAddress, @@ -58,23 +60,16 @@ pub struct StacksClient { tx_version: TransactionVersion, /// The chain we are interacting with chain_id: u32, + /// Whether we are mainnet or not + mainnet: bool, /// The Client used to make HTTP connects stacks_node_client: reqwest::blocking::Client, + /// the auth password for the stacks node + auth_password: String, } -/// The supported epoch IDs -#[derive(Debug, PartialEq)] -pub enum EpochId { - /// The mainnet epoch ID - Epoch30, - /// The testnet epoch ID - Epoch25, - /// Unsuporrted epoch ID - UnsupportedEpoch, -} - -impl From<&Config> for StacksClient { - fn from(config: &Config) -> Self { +impl From<&GlobalConfig> for StacksClient { + fn from(config: &GlobalConfig) -> Self { Self { stacks_private_key: config.stacks_private_key, stacks_address: config.stacks_address, @@ -82,11 +77,49 @@ impl From<&Config> for StacksClient { tx_version: config.network.to_transaction_version(), chain_id: config.network.to_chain_id(), stacks_node_client: reqwest::blocking::Client::new(), + mainnet: config.network.is_mainnet(), + auth_password: config.auth_password.clone(), } } } impl StacksClient { + /// Create a new signer StacksClient with the provided private key, stacks node host endpoint, version, and auth password + pub fn new( + stacks_private_key: StacksPrivateKey, + node_host: SocketAddr, + auth_password: String, + mainnet: bool, + ) -> Self { + let pubkey = StacksPublicKey::from_private(&stacks_private_key); + let tx_version = if mainnet { + TransactionVersion::Mainnet + } else { + TransactionVersion::Testnet + }; + let chain_id = if mainnet { + CHAIN_ID_MAINNET + } else { + CHAIN_ID_TESTNET + }; + let stacks_address = StacksAddress::p2pkh(mainnet, &pubkey); + Self { + stacks_private_key, + stacks_address, + http_origin: format!("http://{}", node_host), + tx_version, + chain_id, + stacks_node_client: reqwest::blocking::Client::new(), + mainnet, + auth_password, + } + } + + /// Get our signer address + pub fn get_signer_address(&self) -> &StacksAddress { + &self.stacks_address + } + /// Retrieve the signer slots stored within the stackerdb contract pub fn get_stackerdb_signer_slots( &self, @@ -95,8 +128,8 @@ impl StacksClient { ) -> Result, ClientError> { let function_name_str = "stackerdb-get-signer-slots-page"; let function_name = ClarityName::from(function_name_str); - let function_args = &[Value::UInt(page.into())]; - let value = self.read_only_contract_call_with_retry( + let function_args = &[ClarityValue::UInt(page.into())]; + let value = self.read_only_contract_call( &stackerdb_contract.issuer.clone().into(), &stackerdb_contract.name, &function_name, @@ -104,35 +137,91 @@ impl StacksClient { )?; self.parse_signer_slots(value) } - /// Retrieve the stacks tip consensus hash from the stacks node - pub fn get_stacks_tip_consensus_hash(&self) -> Result { - let peer_info = self.get_peer_info()?; - Ok(peer_info.stacks_tip_consensus_hash) + + /// Helper function that attempts to deserialize a clarity hext string as a list of signer slots and their associated number of signer slots + fn parse_signer_slots( + &self, + value: ClarityValue, + ) -> Result, ClientError> { + debug!("Parsing signer slots..."); + let value = value.clone().expect_result_ok()?; + let values = value.expect_list()?; + let mut signer_slots = Vec::with_capacity(values.len()); + for value in values { + let tuple_data = value.expect_tuple()?; + let principal_data = tuple_data.get("signer")?.clone().expect_principal()?; + let signer = if let PrincipalData::Standard(signer) = principal_data { + signer.into() + } else { + panic!("BUG: Signers stackerdb contract is corrupted"); + }; + let num_slots = tuple_data.get("num-slots")?.clone().expect_u128()?; + signer_slots.push((signer, num_slots)); + } + Ok(signer_slots) + } + + /// Get the vote for a given round, reward cycle, and signer address + pub fn get_vote_for_aggregate_public_key( + &self, + round: u64, + reward_cycle: u64, + signer: StacksAddress, + ) -> Result, ClientError> { + debug!("Getting vote for aggregate public key..."); + let function_name = ClarityName::from("get-vote"); + let function_args = &[ + ClarityValue::UInt(reward_cycle as u128), + ClarityValue::UInt(round as u128), + ClarityValue::Principal(signer.into()), + ]; + let value = self.read_only_contract_call( + &boot_code_addr(self.mainnet), + &ContractName::from(SIGNERS_VOTING_NAME), + &function_name, + function_args, + )?; + // Return value is of type: + // ```clarity + // (option { aggregate-public-key: (buff 33), signer-weight: uint }) + // ``` + let inner_data = value.expect_optional()?; + if let Some(inner_data) = inner_data { + let tuple = inner_data.expect_tuple()?; + let key_value = tuple.get_owned("aggregate-public-key")?; + self.parse_aggregate_public_key(key_value) + } else { + Ok(None) + } } /// Determine the stacks node current epoch - pub fn get_node_epoch(&self) -> Result { - let is_mainnet = self.chain_id == CHAIN_ID_MAINNET; + pub fn get_node_epoch(&self) -> Result { + let pox_info = self.get_pox_data()?; let burn_block_height = self.get_burn_block_height()?; - let (epoch25_activation_height, epoch_30_activation_height) = if is_mainnet { - ( - BITCOIN_MAINNET_STACKS_25_BURN_HEIGHT, - BITCOIN_MAINNET_STACKS_30_BURN_HEIGHT, - ) - } else { - ( - BITCOIN_TESTNET_STACKS_25_BURN_HEIGHT, - BITCOIN_TESTNET_STACKS_30_BURN_HEIGHT, - ) - }; + let epoch_25 = pox_info + .epochs + .iter() + .find(|epoch| epoch.epoch_id == StacksEpochId::Epoch25) + .ok_or(ClientError::UnsupportedStacksFeature( + "/v2/pox must report epochs".into(), + ))?; - if burn_block_height < epoch25_activation_height { - Ok(EpochId::UnsupportedEpoch) - } else if burn_block_height < epoch_30_activation_height { - Ok(EpochId::Epoch25) + let epoch_30 = pox_info + .epochs + .iter() + .find(|epoch| epoch.epoch_id == StacksEpochId::Epoch30) + .ok_or(ClientError::UnsupportedStacksFeature( + "/v2/pox mut report epochs".into(), + ))?; + + if burn_block_height < epoch_25.start_height { + Ok(StacksEpochId::Epoch24) + } else if burn_block_height < epoch_30.start_height { + Ok(StacksEpochId::Epoch25) } else { - Ok(EpochId::Epoch30) + Ok(StacksEpochId::Epoch30) } } @@ -146,6 +235,7 @@ impl StacksClient { self.stacks_node_client .post(self.block_proposal_path()) .header("Content-Type", "application/json") + .header(AUTHORIZATION, self.auth_password.clone()) .json(&block_proposal) .send() .map_err(backoff::Error::transient) @@ -158,20 +248,26 @@ impl StacksClient { Ok(()) } - /// Retrieve the current DKG aggregate public key - pub fn get_aggregate_public_key(&self) -> Result, ClientError> { - let reward_cycle = self.get_current_reward_cycle()?; - let function_name_str = "get-aggregate-public-key"; - let function_name = ClarityName::from(function_name_str); - let pox_contract_id = boot_code_id(POX_4_NAME, self.chain_id == CHAIN_ID_MAINNET); + /// Retrieve the approved DKG aggregate public key for the given reward cycle + pub fn get_approved_aggregate_key( + &self, + reward_cycle: u64, + ) -> Result, ClientError> { + let function_name = ClarityName::from("get-approved-aggregate-key"); + let pox_contract_id = boot_code_id(SIGNERS_VOTING_NAME, self.mainnet); let function_args = &[ClarityValue::UInt(reward_cycle as u128)]; - let value = self.read_only_contract_call_with_retry( + let value = self.read_only_contract_call( &pox_contract_id.issuer.into(), &pox_contract_id.name, &function_name, function_args, )?; - self.parse_aggregate_public_key(value) + let inner_data = value.expect_optional()?; + if let Some(key_value) = inner_data { + self.parse_aggregate_public_key(key_value) + } else { + Ok(None) + } } /// Retrieve the current account nonce for the provided address @@ -180,8 +276,8 @@ impl StacksClient { Ok(account_entry.nonce) } - // Helper function to retrieve the peer info data from the stacks node - fn get_peer_info(&self) -> Result { + /// Get the current peer info data from the stacks node + pub fn get_peer_info(&self) -> Result { debug!("Getting stacks node info..."); let send_request = || { self.stacks_node_client @@ -197,8 +293,55 @@ impl StacksClient { Ok(peer_info_data) } - // Helper function to retrieve the pox data from the stacks node - fn get_pox_data(&self) -> Result { + /// Retrieve the last DKG vote round number for the current reward cycle + pub fn get_last_round(&self, reward_cycle: u64) -> Result, ClientError> { + debug!("Getting the last DKG vote round of reward cycle {reward_cycle}..."); + let contract_addr = boot_code_addr(self.mainnet); + let contract_name = ContractName::from(SIGNERS_VOTING_NAME); + let function_name = ClarityName::from("get-last-round"); + let function_args = &[ClarityValue::UInt(reward_cycle as u128)]; + let opt_value = self + .read_only_contract_call( + &contract_addr, + &contract_name, + &function_name, + function_args, + )? + .expect_optional()?; + let round = if let Some(value) = opt_value { + Some(u64::try_from(value.expect_u128()?).map_err(|e| { + ClientError::MalformedContractData(format!( + "Failed to convert vote round to u64: {e}" + )) + })?) + } else { + None + }; + Ok(round) + } + + /// Get the reward set signers from the stacks node for the given reward cycle + pub fn get_reward_set_signers( + &self, + reward_cycle: u64, + ) -> Result>, ClientError> { + debug!("Getting reward set for reward cycle {reward_cycle}..."); + let send_request = || { + self.stacks_node_client + .get(self.reward_set_path(reward_cycle)) + .send() + .map_err(backoff::Error::transient) + }; + let response = retry_with_exponential_backoff(send_request)?; + if !response.status().is_success() { + return Err(ClientError::RequestFailure(response.status())); + } + let stackers_response = response.json::()?; + Ok(stackers_response.stacker_set.signers) + } + + /// Retreive the current pox data from the stacks node + pub fn get_pox_data(&self) -> Result { debug!("Getting pox data..."); let send_request = || { self.stacks_node_client @@ -220,17 +363,16 @@ impl StacksClient { Ok(peer_info.burn_block_height) } - /// Helper function to retrieve the current reward cycle number from the stacks node + /// Get the current reward cycle from the stacks node pub fn get_current_reward_cycle(&self) -> Result { let pox_data = self.get_pox_data()?; - Ok(pox_data.reward_cycle_id) - } - - /// Helper function to retrieve the next possible nonce for the signer from the stacks node - #[allow(dead_code)] - fn get_next_possible_nonce(&self) -> Result { - //FIXME: use updated RPC call to get mempool nonces. Depends on https://github.com/stacks-network/stacks-blockchain/issues/4000 - todo!("Get the next possible nonce from the stacks node"); + let blocks_mined = pox_data + .current_burnchain_block_height + .saturating_sub(pox_data.first_burnchain_block_height); + let reward_cycle_length = pox_data + .reward_phase_block_length + .saturating_add(pox_data.prepare_phase_block_length); + Ok(blocks_mined / reward_cycle_length) } /// Helper function to retrieve the account info from the stacks node for a specific address @@ -259,77 +401,59 @@ impl StacksClient { value: ClarityValue, ) -> Result, ClientError> { debug!("Parsing aggregate public key..."); - // Due to pox 4 definition, the aggregate public key is always an optional clarity value hence the use of expect - // If this fails, we have bigger problems than the signer crashing... - let value_opt = value.expect_optional()?; - let Some(value) = value_opt else { - return Ok(None); - }; - // A point should have 33 bytes exactly due to the pox 4 definition hence the use of expect - // If this fails, we have bigger problems than the signer crashing... - let data = value.clone().expect_buff(33)?; + let data = value.expect_buff(33)?; // It is possible that the point was invalid though when voted upon and this cannot be prevented by pox 4 definitions... // Pass up this error if the conversions fail. - let compressed_data = Compressed::try_from(data.as_slice()) - .map_err(|_e| ClientError::MalformedClarityValue(value.clone()))?; - let point = Point::try_from(&compressed_data) - .map_err(|_e| ClientError::MalformedClarityValue(value))?; - Ok(Some(point)) + let compressed_data = Compressed::try_from(data.as_slice()).map_err(|e| { + ClientError::MalformedClarityValue(format!( + "Failed to convert aggregate public key to compressed data: {e}" + )) + })?; + let dkg_public_key = Point::try_from(&compressed_data).map_err(|e| { + ClientError::MalformedClarityValue(format!( + "Failed to convert aggregate public key to a point: {e}" + )) + })?; + Ok(Some(dkg_public_key)) } - /// Helper function that attempts to deserialize a clarity hext string as a list of signer slots and their associated number of signer slots - fn parse_signer_slots( + /// Helper function to create a stacks transaction for a modifying contract call + pub fn build_vote_for_aggregate_public_key( &self, - value: ClarityValue, - ) -> Result, ClientError> { - debug!("Parsing signer slots from {:?}", &value); - // Due to .signers definition, the signer slots is always an OK result of a list of tuples of signer addresses and the number of slots they have - // If this fails, we have bigger problems than the signer crashing... - let value = value.expect_result_ok()?; - let values = value.expect_list()?; - let mut signer_slots = Vec::with_capacity(values.len()); - for value in values { - let tuple_data = value.expect_tuple()?; - let principal_data = tuple_data.get("signer")?.clone().expect_principal()?; - let signer = if let PrincipalData::Standard(signer) = principal_data { - signer.into() - } else { - panic!("BUG: Signers stackerdb contract is corrupted"); - }; - let num_slots = tuple_data.get("num-slots")?.clone().expect_u128()?; - signer_slots.push((signer, num_slots)); - } - Ok(signer_slots) - } + signer_index: u32, + round: u64, + dkg_public_key: Point, + reward_cycle: u64, + tx_fee: Option, + nonce: u64, + ) -> Result { + debug!("Building {SIGNERS_VOTING_FUNCTION_NAME} transaction..."); + let contract_address = boot_code_addr(self.mainnet); + let contract_name = ContractName::from(SIGNERS_VOTING_NAME); + let function_name = ClarityName::from(SIGNERS_VOTING_FUNCTION_NAME); + let function_args = vec![ + ClarityValue::UInt(signer_index as u128), + ClarityValue::buff_from(dkg_public_key.compress().data.to_vec())?, + ClarityValue::UInt(round as u128), + ClarityValue::UInt(reward_cycle as u128), + ]; + let tx_fee = tx_fee.unwrap_or(0); - /// Sends a transaction to the stacks node for a modifying contract call - #[allow(dead_code)] - fn transaction_contract_call( - &self, - contract_addr: &StacksAddress, - contract_name: ContractName, - function_name: ClarityName, - function_args: &[ClarityValue], - ) -> Result { - debug!("Making a contract call to {contract_addr}.{contract_name}..."); - let nonce = self.get_account_nonce(&self.stacks_address)?; - // TODO: make tx_fee configurable - let signed_tx = Self::build_signed_contract_call_transaction( - contract_addr, + Self::build_signed_contract_call_transaction( + &contract_address, contract_name, function_name, - function_args, + &function_args, &self.stacks_private_key, self.tx_version, self.chain_id, nonce, - 10_000, - )?; - self.submit_tx(&signed_tx) + tx_fee, + ) } - /// Helper function to submit a transaction to the Stacks node - fn submit_tx(&self, tx: &StacksTransaction) -> Result { + /// Helper function to submit a transaction to the Stacks mempool + pub fn submit_transaction(&self, tx: &StacksTransaction) -> Result { let txid = tx.txid(); let tx = tx.serialize_to_vec(); let send_request = || { @@ -338,7 +462,10 @@ impl StacksClient { .header("Content-Type", "application/octet-stream") .body(tx.clone()) .send() - .map_err(backoff::Error::transient) + .map_err(|e| { + debug!("Failed to submit transaction to the Stacks node: {e:?}"); + backoff::Error::transient(e) + }) }; let response = retry_with_exponential_backoff(send_request)?; if !response.status().is_success() { @@ -348,17 +475,14 @@ impl StacksClient { } /// Makes a read only contract call to a stacks contract - pub fn read_only_contract_call_with_retry( + pub fn read_only_contract_call( &self, contract_addr: &StacksAddress, contract_name: &ContractName, function_name: &ClarityName, function_args: &[ClarityValue], ) -> Result { - debug!( - "Calling read-only function {function_name} with args {:?}...", - function_args - ); + debug!("Calling read-only function {function_name} with args {function_args:?}..."); let args = function_args .iter() .filter_map(|arg| arg.serialize_to_hex().ok()) @@ -372,15 +496,12 @@ impl StacksClient { let body = json!({"sender": self.stacks_address.to_string(), "arguments": args}).to_string(); let path = self.read_only_path(contract_addr, contract_name, function_name); - let send_request = || { - self.stacks_node_client - .post(path.clone()) - .header("Content-Type", "application/json") - .body(body.clone()) - .send() - .map_err(backoff::Error::transient) - }; - let response = retry_with_exponential_backoff(send_request)?; + let response = self + .stacks_node_client + .post(path.clone()) + .header("Content-Type", "application/json") + .body(body.clone()) + .send()?; if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); } @@ -430,7 +551,12 @@ impl StacksClient { format!("{}/v2/accounts/{stacks_address}?proof=0", self.http_origin) } + fn reward_set_path(&self, reward_cycle: u64) -> String { + format!("{}/v2/stacker_set/{reward_cycle}", self.http_origin) + } + /// Helper function to create a stacks transaction for a modifying contract call + #[allow(clippy::too_many_arguments)] pub fn build_signed_contract_call_transaction( contract_addr: &StacksAddress, contract_name: ContractName, @@ -460,9 +586,6 @@ impl StacksClient { let mut unsigned_tx = StacksTransaction::new(tx_version, tx_auth, tx_payload); - // FIXME: Because signers are given priority, we can put down a tx fee of 0 - // https://github.com/stacks-network/stacks-blockchain/issues/4006 - // Note: if set to 0 now, will cause a failure (MemPoolRejection::FeeTooLow) unsigned_tx.set_tx_fee(tx_fee); unsigned_tx.set_origin_nonce(nonce); @@ -488,67 +611,78 @@ mod tests { use std::io::{BufWriter, Write}; use std::thread::spawn; + use blockstack_lib::chainstate::nakamoto::NakamotoBlockHeader; + use blockstack_lib::chainstate::stacks::address::PoxAddress; + use blockstack_lib::chainstate::stacks::boot::{ + NakamotoSignerEntry, PoxStartCycleInfo, RewardSet, + }; + use blockstack_lib::chainstate::stacks::ThresholdSignature; + use rand::thread_rng; + use rand_core::RngCore; + use stacks_common::bitvec::BitVec; use stacks_common::consts::{CHAIN_ID_TESTNET, SIGNER_SLOTS_PER_USER}; + use stacks_common::types::chainstate::{ConsensusHash, StacksBlockId, TrieHash}; + use stacks_common::util::hash::Sha512Trunc256Sum; + use stacks_common::util::secp256k1::MessageSignature; use wsts::curve::scalar::Scalar; use super::*; - use crate::client::tests::{write_response, TestConfig}; + use crate::client::tests::{ + build_account_nonce_response, build_get_approved_aggregate_key_response, + build_get_last_round_response, build_get_peer_info_response, build_get_pox_data_response, + build_get_vote_for_aggregate_key_response, build_read_only_response, write_response, + MockServerClient, + }; #[test] fn read_only_contract_call_200_success() { - let config = TestConfig::new(); + let mock = MockServerClient::new(); let value = ClarityValue::UInt(10_u128); - let hex = value - .serialize_to_hex() - .expect("Failed to serialize hex value"); - let response_bytes = format!("HTTP/1.1 200 OK\n\n{{\"okay\":true,\"result\":\"{hex}\"}}",); + let response = build_read_only_response(&value); let h = spawn(move || { - config.client.read_only_contract_call_with_retry( - &config.client.stacks_address, + mock.client.read_only_contract_call( + &mock.client.stacks_address, &ContractName::from("contract-name"), &ClarityName::from("function-name"), &[], ) }); - write_response(config.mock_server, response_bytes.as_bytes()); + write_response(mock.server, response.as_bytes()); let result = h.join().unwrap().unwrap(); assert_eq!(result, value); } #[test] fn read_only_contract_call_with_function_args_200_success() { - let config = TestConfig::new(); + let mock = MockServerClient::new(); let value = ClarityValue::UInt(10_u128); - let hex = value - .serialize_to_hex() - .expect("Failed to serialize hex value"); - let response_bytes = format!("HTTP/1.1 200 OK\n\n{{\"okay\":true,\"result\":\"{hex}\"}}",); + let response = build_read_only_response(&value); let h = spawn(move || { - config.client.read_only_contract_call_with_retry( - &config.client.stacks_address, + mock.client.read_only_contract_call( + &mock.client.stacks_address, &ContractName::from("contract-name"), &ClarityName::from("function-name"), &[ClarityValue::UInt(10_u128)], ) }); - write_response(config.mock_server, response_bytes.as_bytes()); + write_response(mock.server, response.as_bytes()); let result = h.join().unwrap().unwrap(); assert_eq!(result, value); } #[test] fn read_only_contract_call_200_failure() { - let config = TestConfig::new(); + let mock = MockServerClient::new(); let h = spawn(move || { - config.client.read_only_contract_call_with_retry( - &config.client.stacks_address, + mock.client.read_only_contract_call( + &mock.client.stacks_address, &ContractName::from("contract-name"), &ClarityName::from("function-name"), &[], ) }); write_response( - config.mock_server, + mock.server, b"HTTP/1.1 200 OK\n\n{\"okay\":false,\"cause\":\"Some reason\"}", ); let result = h.join().unwrap(); @@ -557,17 +691,17 @@ mod tests { #[test] fn read_only_contract_call_400_failure() { - let config = TestConfig::new(); + let mock = MockServerClient::new(); // Simulate a 400 Bad Request response let h = spawn(move || { - config.client.read_only_contract_call_with_retry( - &config.client.stacks_address, + mock.client.read_only_contract_call( + &mock.client.stacks_address, &ContractName::from("contract-name"), &ClarityName::from("function-name"), &[], ) }); - write_response(config.mock_server, b"HTTP/1.1 400 Bad Request\n\n"); + write_response(mock.server, b"HTTP/1.1 400 Bad Request\n\n"); let result = h.join().unwrap(); assert!(matches!( result, @@ -579,17 +713,17 @@ mod tests { #[test] fn read_only_contract_call_404_failure() { - let config = TestConfig::new(); + let mock = MockServerClient::new(); // Simulate a 400 Bad Request response let h = spawn(move || { - config.client.read_only_contract_call_with_retry( - &config.client.stacks_address, + mock.client.read_only_contract_call( + &mock.client.stacks_address, &ContractName::from("contract-name"), &ClarityName::from("function-name"), &[], ) }); - write_response(config.mock_server, b"HTTP/1.1 404 Not Found\n\n"); + write_response(mock.server, b"HTTP/1.1 404 Not Found\n\n"); let result = h.join().unwrap(); assert!(matches!( result, @@ -599,117 +733,79 @@ mod tests { #[test] fn valid_reward_cycle_should_succeed() { - let config = TestConfig::new(); - let h = spawn(move || config.client.get_current_reward_cycle()); - write_response( - config.mock_server, - b"HTTP/1.1 200 Ok\n\n{\"contract_id\":\"ST000000000000000000002AMW42H.pox-3\",\"pox_activation_threshold_ustx\":829371801288885,\"first_burnchain_block_height\":2000000,\"current_burnchain_block_height\":2572192,\"prepare_phase_block_length\":50,\"reward_phase_block_length\":1000,\"reward_slots\":2000,\"rejection_fraction\":12,\"total_liquid_supply_ustx\":41468590064444294,\"current_cycle\":{\"id\":544,\"min_threshold_ustx\":5190000000000,\"stacked_ustx\":853258144644000,\"is_pox_active\":true},\"next_cycle\":{\"id\":545,\"min_threshold_ustx\":5190000000000,\"min_increment_ustx\":5183573758055,\"stacked_ustx\":847278759574000,\"prepare_phase_start_block_height\":2572200,\"blocks_until_prepare_phase\":8,\"reward_phase_start_block_height\":2572250,\"blocks_until_reward_phase\":58,\"ustx_until_pox_rejection\":4976230807733304},\"min_amount_ustx\":5190000000000,\"prepare_cycle_length\":50,\"reward_cycle_id\":544,\"reward_cycle_length\":1050,\"rejection_votes_left_required\":4976230807733304,\"next_reward_cycle_in\":58,\"contract_versions\":[{\"contract_id\":\"ST000000000000000000002AMW42H.pox\",\"activation_burnchain_block_height\":2000000,\"first_reward_cycle_id\":0},{\"contract_id\":\"ST000000000000000000002AMW42H.pox-2\",\"activation_burnchain_block_height\":2422102,\"first_reward_cycle_id\":403},{\"contract_id\":\"ST000000000000000000002AMW42H.pox-3\",\"activation_burnchain_block_height\":2432545,\"first_reward_cycle_id\":412}]}", - ); + let mock = MockServerClient::new(); + let (pox_data_response, pox_data) = build_get_pox_data_response(None, None, None, None); + let h = spawn(move || mock.client.get_current_reward_cycle()); + write_response(mock.server, pox_data_response.as_bytes()); let current_cycle_id = h.join().unwrap().unwrap(); - assert_eq!(544, current_cycle_id); + let blocks_mined = pox_data + .current_burnchain_block_height + .saturating_sub(pox_data.first_burnchain_block_height); + let reward_cycle_length = pox_data + .reward_phase_block_length + .saturating_add(pox_data.prepare_phase_block_length); + let id = blocks_mined / reward_cycle_length; + assert_eq!(current_cycle_id, id); } #[test] fn invalid_reward_cycle_should_fail() { - let config = TestConfig::new(); - let h = spawn(move || config.client.get_current_reward_cycle()); + let mock = MockServerClient::new(); + let h = spawn(move || mock.client.get_current_reward_cycle()); write_response( - config.mock_server, + mock.server, b"HTTP/1.1 200 Ok\n\n{\"current_cycle\":{\"id\":\"fake id\", \"is_pox_active\":false}}", ); let res = h.join().unwrap(); assert!(matches!(res, Err(ClientError::ReqwestError(_)))); } - #[test] - fn missing_reward_cycle_should_fail() { - let config = TestConfig::new(); - let h = spawn(move || config.client.get_current_reward_cycle()); - write_response( - config.mock_server, - b"HTTP/1.1 200 Ok\n\n{\"current_cycle\":{\"is_pox_active\":false}}", - ); - let res = h.join().unwrap(); - assert!(matches!(res, Err(ClientError::ReqwestError(_)))); - } - #[test] fn get_aggregate_public_key_should_succeed() { - let current_reward_cycle_response = b"HTTP/1.1 200 Ok\n\n{\"contract_id\":\"ST000000000000000000002AMW42H.pox-3\",\"pox_activation_threshold_ustx\":829371801288885,\"first_burnchain_block_height\":2000000,\"current_burnchain_block_height\":2572192,\"prepare_phase_block_length\":50,\"reward_phase_block_length\":1000,\"reward_slots\":2000,\"rejection_fraction\":12,\"total_liquid_supply_ustx\":41468590064444294,\"current_cycle\":{\"id\":544,\"min_threshold_ustx\":5190000000000,\"stacked_ustx\":853258144644000,\"is_pox_active\":true},\"next_cycle\":{\"id\":545,\"min_threshold_ustx\":5190000000000,\"min_increment_ustx\":5183573758055,\"stacked_ustx\":847278759574000,\"prepare_phase_start_block_height\":2572200,\"blocks_until_prepare_phase\":8,\"reward_phase_start_block_height\":2572250,\"blocks_until_reward_phase\":58,\"ustx_until_pox_rejection\":4976230807733304},\"min_amount_ustx\":5190000000000,\"prepare_cycle_length\":50,\"reward_cycle_id\":544,\"reward_cycle_length\":1050,\"rejection_votes_left_required\":4976230807733304,\"next_reward_cycle_in\":58,\"contract_versions\":[{\"contract_id\":\"ST000000000000000000002AMW42H.pox\",\"activation_burnchain_block_height\":2000000,\"first_reward_cycle_id\":0},{\"contract_id\":\"ST000000000000000000002AMW42H.pox-2\",\"activation_burnchain_block_height\":2422102,\"first_reward_cycle_id\":403},{\"contract_id\":\"ST000000000000000000002AMW42H.pox-3\",\"activation_burnchain_block_height\":2432545,\"first_reward_cycle_id\":412}]}"; let orig_point = Point::from(Scalar::random(&mut rand::thread_rng())); - let clarity_value = ClarityValue::some( - ClarityValue::buff_from(orig_point.compress().as_bytes().to_vec()) - .expect("BUG: Failed to create clarity value from point"), - ) - .expect("BUG: Failed to create clarity value from point"); - let hex = clarity_value - .serialize_to_hex() - .expect("Failed to serialize clarity value"); - let response = format!("HTTP/1.1 200 OK\n\n{{\"okay\":true,\"result\":\"{hex}\"}}"); - - let test_config = TestConfig::new(); - let config = test_config.config; - let h = spawn(move || test_config.client.get_aggregate_public_key()); - write_response(test_config.mock_server, current_reward_cycle_response); - - let test_config = TestConfig::from_config(config); - write_response(test_config.mock_server, response.as_bytes()); + let response = build_get_approved_aggregate_key_response(Some(orig_point)); + let mock = MockServerClient::new(); + let h = spawn(move || mock.client.get_approved_aggregate_key(0)); + write_response(mock.server, response.as_bytes()); let res = h.join().unwrap().unwrap(); assert_eq!(res, Some(orig_point)); - let clarity_value = ClarityValue::none(); - let hex = clarity_value - .serialize_to_hex() - .expect("Failed to serialize clarity value"); - let response = format!("HTTP/1.1 200 OK\n\n{{\"okay\":true,\"result\":\"{hex}\"}}"); - - let test_config = TestConfig::new(); - let config = test_config.config; - let h = spawn(move || test_config.client.get_aggregate_public_key()); - write_response(test_config.mock_server, current_reward_cycle_response); - - let test_config = TestConfig::from_config(config); - write_response(test_config.mock_server, response.as_bytes()); - + let response = build_get_approved_aggregate_key_response(None); + let mock = MockServerClient::new(); + let h = spawn(move || mock.client.get_approved_aggregate_key(0)); + write_response(mock.server, response.as_bytes()); let res = h.join().unwrap().unwrap(); assert!(res.is_none()); } #[test] fn parse_valid_aggregate_public_key_should_succeed() { - let config = TestConfig::new(); + let mock = MockServerClient::new(); let orig_point = Point::from(Scalar::random(&mut rand::thread_rng())); - let clarity_value = ClarityValue::some( - ClarityValue::buff_from(orig_point.compress().as_bytes().to_vec()) - .expect("BUG: Failed to create clarity value from point"), - ) - .expect("BUG: Failed to create clarity value from point"); - let result = config + let clarity_value = ClarityValue::buff_from(orig_point.compress().as_bytes().to_vec()) + .expect("BUG: Failed to create clarity value from point"); + let result = mock .client .parse_aggregate_public_key(clarity_value) .unwrap(); assert_eq!(result, Some(orig_point)); - - let value = ClarityValue::none(); - let result = config.client.parse_aggregate_public_key(value).unwrap(); - assert!(result.is_none()); } #[test] fn parse_invalid_aggregate_public_key_should_fail() { - let config = TestConfig::new(); + let mock = MockServerClient::new(); let value = ClarityValue::UInt(10_u128); - let result = config.client.parse_aggregate_public_key(value); + let result = mock.client.parse_aggregate_public_key(value); assert!(result.is_err()) } #[ignore] #[test] fn transaction_contract_call_should_send_bytes_to_node() { - let config = TestConfig::new(); + let mock = MockServerClient::new(); let private_key = StacksPrivateKey::new(); let tx = StacksClient::build_signed_contract_call_transaction( - &config.client.stacks_address, + &mock.client.stacks_address, ContractName::from("contract-name"), ClarityName::from("function-name"), &[], @@ -738,10 +834,10 @@ mod tests { + 1; let tx_clone = tx.clone(); - let h = spawn(move || config.client.submit_tx(&tx_clone)); + let h = spawn(move || mock.client.submit_transaction(&tx_clone)); let request_bytes = write_response( - config.mock_server, + mock.server, format!("HTTP/1.1 200 OK\n\n{}", tx.txid()).as_bytes(), ); let returned_txid = h.join().unwrap().unwrap(); @@ -757,67 +853,76 @@ mod tests { #[ignore] #[test] - fn transaction_contract_call_should_succeed() { - let config = TestConfig::new(); + fn build_vote_for_aggregate_public_key_should_succeed() { + let mock = MockServerClient::new(); + let point = Point::from(Scalar::random(&mut rand::thread_rng())); + let nonce = thread_rng().next_u64(); + let signer_index = thread_rng().next_u32(); + let round = thread_rng().next_u64(); + let reward_cycle = thread_rng().next_u64(); + let h = spawn(move || { - config.client.transaction_contract_call( - &config.client.stacks_address, - ContractName::from("contract-name"), - ClarityName::from("function-name"), - &[], + mock.client.build_vote_for_aggregate_public_key( + signer_index, + round, + point, + reward_cycle, + None, + nonce, ) }); + assert!(h.join().unwrap().is_ok()); + } + + #[ignore] + #[test] + fn broadcast_vote_for_aggregate_public_key_should_succeed() { + let mock = MockServerClient::new(); + let point = Point::from(Scalar::random(&mut rand::thread_rng())); + let nonce = thread_rng().next_u64(); + let signer_index = thread_rng().next_u32(); + let round = thread_rng().next_u64(); + let reward_cycle = thread_rng().next_u64(); + + let h = spawn(move || { + let tx = mock + .client + .clone() + .build_vote_for_aggregate_public_key( + signer_index, + round, + point, + reward_cycle, + None, + nonce, + ) + .unwrap(); + mock.client.submit_transaction(&tx) + }); + let mock = MockServerClient::from_config(mock.config); write_response( - config.mock_server, + mock.server, b"HTTP/1.1 200 OK\n\n4e99f99bc4a05437abb8c7d0c306618f45b203196498e2ebe287f10497124958", ); assert!(h.join().unwrap().is_ok()); } - #[test] - fn core_info_call_for_consensus_hash_should_succeed() { - let config = TestConfig::new(); - let h = spawn(move || config.client.get_stacks_tip_consensus_hash()); - write_response( - config.mock_server, - b"HTTP/1.1 200 OK\n\n{\"stacks_tip_consensus_hash\":\"64c8c3049ff6b939c65828e3168210e6bb32d880\",\"peer_version\":4207599113,\"pox_consensus\":\"64c8c3049ff6b939c65828e3168210e6bb32d880\",\"burn_block_height\":2575799,\"stable_pox_consensus\":\"72277bf9a3b115e13c0942825480d6cee0e9a0e8\",\"stable_burn_block_height\":2575792,\"server_version\":\"stacks-node d657bdd (feat/epoch-2.4:d657bdd, release build, linux [x86_64])\",\"network_id\":2147483648,\"parent_network_id\":118034699,\"stacks_tip_height\":145152,\"stacks_tip\":\"77219884fe434c0fa270d65592b4f082ab3e5d9922ac2bdaac34310aedc3d298\",\"genesis_chainstate_hash\":\"74237aa39aa50a83de11a4f53e9d3bb7d43461d1de9873f402e5453ae60bc59b\",\"unanchored_tip\":\"dde44222b6e6d81583b6b9c55db83e8716943ae9d0dc332fc39448ddd9b99dc2\",\"unanchored_seq\":0,\"exit_at_block_height\":null,\"node_public_key\":\"023c940136d5795d9dd82c0e87f4dd6a2a1db245444e7d70e34bb9605c3c3917b0\",\"node_public_key_hash\":\"e26cce8f6abe06b9fc81c3b11bcc821d2f1b8fd0\"}", - ); - let consensus_hash = h.join().unwrap().expect("Failed to deserialize response"); - assert_eq!( - consensus_hash.to_hex(), - "64c8c3049ff6b939c65828e3168210e6bb32d880" - ); - } - - #[test] - fn core_info_call_with_invalid_response_should_fail() { - let config = TestConfig::new(); - let h = spawn(move || config.client.get_stacks_tip_consensus_hash()); - write_response( - config.mock_server, - b"HTTP/1.1 200 OK\n\n4e99f99bc4a05437abb8c7d0c306618f45b203196498e2ebe287f10497124958", - ); - assert!(h.join().unwrap().is_err()); - } - #[test] fn core_info_call_for_burn_block_height_should_succeed() { - let config = TestConfig::new(); - let h = spawn(move || config.client.get_burn_block_height()); - write_response( - config.mock_server, - b"HTTP/1.1 200 OK\n\n{\"burn_block_height\":2575799,\"peer_version\":4207599113,\"pox_consensus\":\"64c8c3049ff6b939c65828e3168210e6bb32d880\",\"stable_pox_consensus\":\"72277bf9a3b115e13c0942825480d6cee0e9a0e8\",\"stable_burn_block_height\":2575792,\"server_version\":\"stacks-node d657bdd (feat/epoch-2.4:d657bdd, release build, linux [x86_64])\",\"network_id\":2147483648,\"parent_network_id\":118034699,\"stacks_tip_height\":145152,\"stacks_tip\":\"77219884fe434c0fa270d65592b4f082ab3e5d9922ac2bdaac34310aedc3d298\",\"stacks_tip_consensus_hash\":\"64c8c3049ff6b939c65828e3168210e6bb32d880\",\"genesis_chainstate_hash\":\"74237aa39aa50a83de11a4f53e9d3bb7d43461d1de9873f402e5453ae60bc59b\",\"unanchored_tip\":\"dde44222b6e6d81583b6b9c55db83e8716943ae9d0dc332fc39448ddd9b99dc2\",\"unanchored_seq\":0,\"exit_at_block_height\":null,\"node_public_key\":\"023c940136d5795d9dd82c0e87f4dd6a2a1db245444e7d70e34bb9605c3c3917b0\",\"node_public_key_hash\":\"e26cce8f6abe06b9fc81c3b11bcc821d2f1b8fd0\"}", - ); + let mock = MockServerClient::new(); + let h = spawn(move || mock.client.get_burn_block_height()); + let (response, peer_info) = build_get_peer_info_response(None, None); + write_response(mock.server, response.as_bytes()); let burn_block_height = h.join().unwrap().expect("Failed to deserialize response"); - assert_eq!(burn_block_height, 2575799); + assert_eq!(burn_block_height, peer_info.burn_block_height); } #[test] fn core_info_call_for_burn_block_height_should_fail() { - let config = TestConfig::new(); - let h = spawn(move || config.client.get_burn_block_height()); + let mock = MockServerClient::new(); + let h = spawn(move || mock.client.get_burn_block_height()); write_response( - config.mock_server, + mock.server, b"HTTP/1.1 200 OK\n\n4e99f99bc4a05437abb8c7d0c306618f45b203196498e2ebe287f10497124958", ); assert!(h.join().unwrap().is_err()); @@ -825,24 +930,22 @@ mod tests { #[test] fn get_account_nonce_should_succeed() { - let config = TestConfig::new(); - let address = config.client.stacks_address; - let h = spawn(move || config.client.get_account_nonce(&address)); - write_response( - config.mock_server, - b"HTTP/1.1 200 OK\n\n{\"nonce\":0,\"balance\":\"0x00000000000000000000000000000000\",\"locked\":\"0x00000000000000000000000000000000\",\"unlock_height\":0}" - ); - let nonce = h.join().unwrap().expect("Failed to deserialize response"); - assert_eq!(nonce, 0); + let mock = MockServerClient::new(); + let address = mock.client.stacks_address; + let h = spawn(move || mock.client.get_account_nonce(&address)); + let nonce = thread_rng().next_u64(); + write_response(mock.server, build_account_nonce_response(nonce).as_bytes()); + let returned_nonce = h.join().unwrap().expect("Failed to deserialize response"); + assert_eq!(returned_nonce, nonce); } #[test] fn get_account_nonce_should_fail() { - let config = TestConfig::new(); - let address = config.client.stacks_address; - let h = spawn(move || config.client.get_account_nonce(&address)); + let mock = MockServerClient::new(); + let address = mock.client.stacks_address; + let h = spawn(move || mock.client.get_account_nonce(&address)); write_response( - config.mock_server, + mock.server, b"HTTP/1.1 200 OK\n\n{\"nonce\":\"invalid nonce\",\"balance\":\"0x00000000000000000000000000000000\",\"locked\":\"0x00000000000000000000000000000000\",\"unlock_height\":0}" ); assert!(h.join().unwrap().is_err()); @@ -850,11 +953,11 @@ mod tests { #[test] fn parse_valid_signer_slots_should_succeed() { - let config = TestConfig::new(); + let mock = MockServerClient::new(); let clarity_value_hex = "0x070b000000050c00000002096e756d2d736c6f7473010000000000000000000000000000000c067369676e6572051a8195196a9a7cf9c37cb13e1ed69a7bc047a84e050c00000002096e756d2d736c6f7473010000000000000000000000000000000c067369676e6572051a6505471146dcf722f0580911183f28bef30a8a890c00000002096e756d2d736c6f7473010000000000000000000000000000000c067369676e6572051a1d7f8e3936e5da5f32982cc47f31d7df9fb1b38a0c00000002096e756d2d736c6f7473010000000000000000000000000000000c067369676e6572051a126d1a814313c952e34c7840acec9211e1727fb80c00000002096e756d2d736c6f7473010000000000000000000000000000000c067369676e6572051a7374ea6bb39f2e8d3d334d62b9f302a977de339a"; let value = ClarityValue::try_deserialize_hex_untyped(clarity_value_hex).unwrap(); - let signer_slots = config.client.parse_signer_slots(value).unwrap(); + let signer_slots = mock.client.parse_signer_slots(value).unwrap(); assert_eq!(signer_slots.len(), 5); signer_slots .into_iter() @@ -863,41 +966,217 @@ mod tests { #[test] fn get_node_epoch_should_succeed() { - let config = TestConfig::new(); - let h = spawn(move || config.client.get_node_epoch()); - write_response( - config.mock_server, - b"HTTP/1.1 200 OK\n\n{\"burn_block_height\":2575799,\"peer_version\":4207599113,\"pox_consensus\":\"64c8c3049ff6b939c65828e3168210e6bb32d880\",\"stable_pox_consensus\":\"72277bf9a3b115e13c0942825480d6cee0e9a0e8\",\"stable_burn_block_height\":2575792,\"server_version\":\"stacks-node d657bdd (feat/epoch-2.4:d657bdd, release build, linux [x86_64])\",\"network_id\":2147483648,\"parent_network_id\":118034699,\"stacks_tip_height\":145152,\"stacks_tip\":\"77219884fe434c0fa270d65592b4f082ab3e5d9922ac2bdaac34310aedc3d298\",\"stacks_tip_consensus_hash\":\"64c8c3049ff6b939c65828e3168210e6bb32d880\",\"genesis_chainstate_hash\":\"74237aa39aa50a83de11a4f53e9d3bb7d43461d1de9873f402e5453ae60bc59b\",\"unanchored_tip\":\"dde44222b6e6d81583b6b9c55db83e8716943ae9d0dc332fc39448ddd9b99dc2\",\"unanchored_seq\":0,\"exit_at_block_height\":null,\"node_public_key\":\"023c940136d5795d9dd82c0e87f4dd6a2a1db245444e7d70e34bb9605c3c3917b0\",\"node_public_key_hash\":\"e26cce8f6abe06b9fc81c3b11bcc821d2f1b8fd0\"}", - ); + let mock = MockServerClient::new(); + // The burn block height is one BEHIND the activation height of 2.5, therefore is 2.4 + let burn_block_height: u64 = 100; + let pox_response = build_get_pox_data_response( + None, + None, + Some(burn_block_height.saturating_add(1)), + None, + ) + .0; + let peer_response = build_get_peer_info_response(Some(burn_block_height), None).0; + let h = spawn(move || mock.client.get_node_epoch()); + write_response(mock.server, pox_response.as_bytes()); + let mock = MockServerClient::from_config(mock.config); + write_response(mock.server, peer_response.as_bytes()); let epoch = h.join().unwrap().expect("Failed to deserialize response"); - assert_eq!(epoch, EpochId::UnsupportedEpoch); + assert_eq!(epoch, StacksEpochId::Epoch24); - let config = TestConfig::new(); - let h = spawn(move || config.client.get_node_epoch()); - let height = BITCOIN_TESTNET_STACKS_25_BURN_HEIGHT; - let response_bytes = format!("HTTP/1.1 200 OK\n\n{{\"burn_block_height\":{height},\"peer_version\":4207599113,\"pox_consensus\":\"64c8c3049ff6b939c65828e3168210e6bb32d880\",\"stable_pox_consensus\":\"72277bf9a3b115e13c0942825480d6cee0e9a0e8\",\"stable_burn_block_height\":2575792,\"server_version\":\"stacks-node d657bdd (feat/epoch-2.4:d657bdd, release build, linux [x86_64])\",\"network_id\":2147483648,\"parent_network_id\":118034699,\"stacks_tip_height\":145152,\"stacks_tip\":\"77219884fe434c0fa270d65592b4f082ab3e5d9922ac2bdaac34310aedc3d298\",\"stacks_tip_consensus_hash\":\"64c8c3049ff6b939c65828e3168210e6bb32d880\",\"genesis_chainstate_hash\":\"74237aa39aa50a83de11a4f53e9d3bb7d43461d1de9873f402e5453ae60bc59b\",\"unanchored_tip\":\"dde44222b6e6d81583b6b9c55db83e8716943ae9d0dc332fc39448ddd9b99dc2\",\"unanchored_seq\":0,\"exit_at_block_height\":null,\"node_public_key\":\"023c940136d5795d9dd82c0e87f4dd6a2a1db245444e7d70e34bb9605c3c3917b0\",\"node_public_key_hash\":\"e26cce8f6abe06b9fc81c3b11bcc821d2f1b8fd0\"}}"); - - write_response(config.mock_server, response_bytes.as_bytes()); + // The burn block height is the same as the activation height of 2.5, therefore is 2.5 + let pox_response = build_get_pox_data_response(None, None, Some(burn_block_height), None).0; + let peer_response = build_get_peer_info_response(Some(burn_block_height), None).0; + let mock = MockServerClient::from_config(mock.config); + let h = spawn(move || mock.client.get_node_epoch()); + write_response(mock.server, pox_response.as_bytes()); + let mock = MockServerClient::from_config(mock.config); + write_response(mock.server, peer_response.as_bytes()); let epoch = h.join().unwrap().expect("Failed to deserialize response"); - assert_eq!(epoch, EpochId::Epoch25); + assert_eq!(epoch, StacksEpochId::Epoch25); - let config = TestConfig::new(); - let h = spawn(move || config.client.get_node_epoch()); - let height = BITCOIN_TESTNET_STACKS_30_BURN_HEIGHT; - let response_bytes = format!("HTTP/1.1 200 OK\n\n{{\"burn_block_height\":{height},\"peer_version\":4207599113,\"pox_consensus\":\"64c8c3049ff6b939c65828e3168210e6bb32d880\",\"stable_pox_consensus\":\"72277bf9a3b115e13c0942825480d6cee0e9a0e8\",\"stable_burn_block_height\":2575792,\"server_version\":\"stacks-node d657bdd (feat/epoch-2.4:d657bdd, release build, linux [x86_64])\",\"network_id\":2147483648,\"parent_network_id\":118034699,\"stacks_tip_height\":145152,\"stacks_tip\":\"77219884fe434c0fa270d65592b4f082ab3e5d9922ac2bdaac34310aedc3d298\",\"stacks_tip_consensus_hash\":\"64c8c3049ff6b939c65828e3168210e6bb32d880\",\"genesis_chainstate_hash\":\"74237aa39aa50a83de11a4f53e9d3bb7d43461d1de9873f402e5453ae60bc59b\",\"unanchored_tip\":\"dde44222b6e6d81583b6b9c55db83e8716943ae9d0dc332fc39448ddd9b99dc2\",\"unanchored_seq\":0,\"exit_at_block_height\":null,\"node_public_key\":\"023c940136d5795d9dd82c0e87f4dd6a2a1db245444e7d70e34bb9605c3c3917b0\",\"node_public_key_hash\":\"e26cce8f6abe06b9fc81c3b11bcc821d2f1b8fd0\"}}"); - write_response(config.mock_server, response_bytes.as_bytes()); + // The burn block height is the AFTER as the activation height of 2.5 but BEFORE the activation height of 3.0, therefore is 2.5 + let pox_response = build_get_pox_data_response( + None, + None, + Some(burn_block_height.saturating_sub(1)), + Some(burn_block_height.saturating_add(1)), + ) + .0; + let peer_response = build_get_peer_info_response(Some(burn_block_height), None).0; + let mock = MockServerClient::from_config(mock.config); + let h = spawn(move || mock.client.get_node_epoch()); + write_response(mock.server, pox_response.as_bytes()); + let mock = MockServerClient::from_config(mock.config); + write_response(mock.server, peer_response.as_bytes()); let epoch = h.join().unwrap().expect("Failed to deserialize response"); - assert_eq!(epoch, EpochId::Epoch30); + assert_eq!(epoch, StacksEpochId::Epoch25); + + // The burn block height is the AFTER as the activation height of 2.5 and the SAME as the activation height of 3.0, therefore is 3.0 + let pox_response = build_get_pox_data_response( + None, + None, + Some(burn_block_height.saturating_sub(1)), + Some(burn_block_height), + ) + .0; + let peer_response = build_get_peer_info_response(Some(burn_block_height), None).0; + let mock = MockServerClient::from_config(mock.config); + let h = spawn(move || mock.client.get_node_epoch()); + write_response(mock.server, pox_response.as_bytes()); + let mock = MockServerClient::from_config(mock.config); + write_response(mock.server, peer_response.as_bytes()); + let epoch = h.join().unwrap().expect("Failed to deserialize response"); + assert_eq!(epoch, StacksEpochId::Epoch30); + + // The burn block height is the AFTER as the activation height of 2.5 and AFTER the activation height of 3.0, therefore is 3.0 + let pox_response = build_get_pox_data_response( + None, + None, + Some(burn_block_height.saturating_sub(1)), + Some(burn_block_height), + ) + .0; + let peer_response = + build_get_peer_info_response(Some(burn_block_height.saturating_add(1)), None).0; + let mock = MockServerClient::from_config(mock.config); + let h = spawn(move || mock.client.get_node_epoch()); + write_response(mock.server, pox_response.as_bytes()); + let mock = MockServerClient::from_config(mock.config); + write_response(mock.server, peer_response.as_bytes()); + let epoch = h.join().unwrap().expect("Failed to deserialize response"); + assert_eq!(epoch, StacksEpochId::Epoch30); } #[test] fn get_node_epoch_should_fail() { - let config = TestConfig::new(); - let h = spawn(move || config.client.get_node_epoch()); + let mock = MockServerClient::new(); + let h = spawn(move || mock.client.get_node_epoch()); write_response( - config.mock_server, + mock.server, b"HTTP/1.1 200 OK\n\n4e99f99bc4a05437abb8c7d0c306618f45b203196498e2ebe287f10497124958", ); assert!(h.join().unwrap().is_err()); } + + #[test] + fn submit_block_for_validation_should_succeed() { + let mock = MockServerClient::new(); + let header = NakamotoBlockHeader { + version: 1, + chain_length: 2, + burn_spent: 3, + consensus_hash: ConsensusHash([0x04; 20]), + parent_block_id: StacksBlockId([0x05; 32]), + tx_merkle_root: Sha512Trunc256Sum([0x06; 32]), + state_index_root: TrieHash([0x07; 32]), + miner_signature: MessageSignature::empty(), + signer_signature: ThresholdSignature::empty(), + signer_bitvec: BitVec::zeros(1).unwrap(), + }; + let block = NakamotoBlock { + header, + txs: vec![], + }; + let h = spawn(move || mock.client.submit_block_for_validation(block)); + write_response(mock.server, b"HTTP/1.1 200 OK\n\n"); + assert!(h.join().unwrap().is_ok()); + } + + #[test] + fn submit_block_for_validation_should_fail() { + let mock = MockServerClient::new(); + let header = NakamotoBlockHeader { + version: 1, + chain_length: 2, + burn_spent: 3, + consensus_hash: ConsensusHash([0x04; 20]), + parent_block_id: StacksBlockId([0x05; 32]), + tx_merkle_root: Sha512Trunc256Sum([0x06; 32]), + state_index_root: TrieHash([0x07; 32]), + miner_signature: MessageSignature::empty(), + signer_signature: ThresholdSignature::empty(), + signer_bitvec: BitVec::zeros(1).unwrap(), + }; + let block = NakamotoBlock { + header, + txs: vec![], + }; + let h = spawn(move || mock.client.submit_block_for_validation(block)); + write_response(mock.server, b"HTTP/1.1 404 Not Found\n\n"); + assert!(h.join().unwrap().is_err()); + } + + #[test] + fn get_peer_info_should_succeed() { + let mock = MockServerClient::new(); + let (response, peer_info) = build_get_peer_info_response(None, None); + let h = spawn(move || mock.client.get_peer_info()); + write_response(mock.server, response.as_bytes()); + assert_eq!(h.join().unwrap().unwrap(), peer_info); + } + + #[test] + fn get_last_round_should_succeed() { + let mock = MockServerClient::new(); + let round = rand::thread_rng().next_u64(); + let response = build_get_last_round_response(round); + let h = spawn(move || mock.client.get_last_round(0)); + + write_response(mock.server, response.as_bytes()); + assert_eq!(h.join().unwrap().unwrap().unwrap(), round); + } + + #[test] + fn get_reward_set_should_succeed() { + let mock = MockServerClient::new(); + let point = Point::from(Scalar::random(&mut rand::thread_rng())).compress(); + let mut bytes = [0u8; 33]; + bytes.copy_from_slice(point.as_bytes()); + let stacker_set = RewardSet { + rewarded_addresses: vec![PoxAddress::standard_burn_address(false)], + start_cycle_state: PoxStartCycleInfo { + missed_reward_slots: vec![], + }, + signers: Some(vec![NakamotoSignerEntry { + signing_key: bytes, + stacked_amt: rand::thread_rng().next_u64() as u128, + weight: 1, + }]), + }; + let stackers_response = GetStackersResponse { + stacker_set: stacker_set.clone(), + }; + + let stackers_response_json = serde_json::to_string(&stackers_response) + .expect("Failed to serialize get stacker response"); + let response = format!("HTTP/1.1 200 OK\n\n{stackers_response_json}"); + let h = spawn(move || mock.client.get_reward_set_signers(0)); + write_response(mock.server, response.as_bytes()); + assert_eq!(h.join().unwrap().unwrap(), stacker_set.signers); + } + + #[test] + fn get_vote_for_aggregate_public_key_should_succeed() { + let mock = MockServerClient::new(); + let point = Point::from(Scalar::random(&mut rand::thread_rng())); + let stacks_address = mock.client.stacks_address; + let key_response = build_get_vote_for_aggregate_key_response(Some(point)); + let h = spawn(move || { + mock.client + .get_vote_for_aggregate_public_key(0, 0, stacks_address) + }); + write_response(mock.server, key_response.as_bytes()); + assert_eq!(h.join().unwrap().unwrap(), Some(point)); + + let mock = MockServerClient::new(); + let stacks_address = mock.client.stacks_address; + let key_response = build_get_vote_for_aggregate_key_response(None); + let h = spawn(move || { + mock.client + .get_vote_for_aggregate_public_key(0, 0, stacks_address) + }); + write_response(mock.server, key_response.as_bytes()); + assert_eq!(h.join().unwrap().unwrap(), None); + } } diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs index 2be974b4f..2897bfdf0 100644 --- a/stacks-signer/src/config.rs +++ b/stacks-signer/src/config.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// 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 @@ -14,16 +14,13 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryFrom; use std::fs; use std::net::{SocketAddr, ToSocketAddrs}; use std::path::PathBuf; use std::time::Duration; use blockstack_lib::chainstate::stacks::TransactionVersion; -use blockstack_lib::util_lib::boot::boot_code_id; -use clarity::vm::types::QualifiedContractIdentifier; -use hashbrown::HashMap; +use hashbrown::{HashMap, HashSet}; use serde::Deserialize; use stacks_common::address::{ AddressHashMode, C32_ADDRESS_VERSION_MAINNET_SINGLESIG, C32_ADDRESS_VERSION_TESTNET_SINGLESIG, @@ -31,14 +28,16 @@ use stacks_common::address::{ use stacks_common::consts::{CHAIN_ID_MAINNET, CHAIN_ID_TESTNET}; use stacks_common::types::chainstate::{StacksAddress, StacksPrivateKey, StacksPublicKey}; use stacks_common::types::PrivateKey; -use wsts::curve::ecdsa; +use wsts::curve::point::Point; use wsts::curve::scalar::Scalar; use wsts::state_machine::PublicKeys; -/// List of key_ids for each signer_id -pub type SignerKeyIds = HashMap>; +use crate::signer::SignerSlotID; const EVENT_TIMEOUT_MS: u64 = 5000; +// Default transaction fee in microstacks (if unspecificed in the config file) +// TODO: Use the fee estimation endpoint to get the default fee. +const TX_FEE_USTX: u64 = 10_000; #[derive(thiserror::Error, Debug)] /// An error occurred parsing the provided configuration @@ -71,12 +70,11 @@ pub enum Network { impl std::fmt::Display for Network { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let network = match self { - Self::Mainnet => "mainnet", - Self::Testnet => "testnet", - Self::Mocknet => "mocknet", - }; - write!(f, "{}", network) + match self { + Self::Mainnet => write!(f, "mainnet"), + Self::Testnet => write!(f, "testnet"), + Self::Mocknet => write!(f, "mocknet"), + } } } @@ -114,15 +112,66 @@ impl Network { } } +/// Parsed Reward Set +#[derive(Debug, Clone)] +pub struct ParsedSignerEntries { + /// The signer addresses mapped to signer id + pub signer_ids: HashMap, + /// The signer ids mapped to public key and key ids mapped to public keys + pub public_keys: PublicKeys, + /// The signer ids mapped to key ids + pub signer_key_ids: HashMap>, + /// The signer ids mapped to wsts public keys + pub signer_public_keys: HashMap, + /// The signer ids mapped to a hash set of key ids + /// The wsts coordinator uses a hash set for each signer since it needs to do lots of lookups + pub coordinator_key_ids: HashMap>, +} + +/// The Configuration info needed for an individual signer per reward cycle +#[derive(Debug, Clone)] +pub struct SignerConfig { + /// The reward cycle of the configuration + pub reward_cycle: u64, + /// The signer ID assigned to this signer to be used in DKG and Sign rounds + pub signer_id: u32, + /// The signer stackerdb slot id (may be different from signer_id) + pub signer_slot_id: SignerSlotID, + /// This signer's key ids + pub key_ids: Vec, + /// The registered signers for this reward cycle + pub signer_entries: ParsedSignerEntries, + /// The signer slot ids of all signers registered for this reward cycle + pub signer_slot_ids: Vec, + /// The Scalar representation of the private key for signer communication + pub ecdsa_private_key: Scalar, + /// The private key for this signer + pub stacks_private_key: StacksPrivateKey, + /// The node host for this signer + pub node_host: String, + /// Whether this signer is running on mainnet or not + pub mainnet: bool, + /// timeout to gather DkgPublicShares messages + pub dkg_public_timeout: Option, + /// timeout to gather DkgPrivateShares messages + pub dkg_private_timeout: Option, + /// timeout to gather DkgEnd messages + pub dkg_end_timeout: Option, + /// timeout to gather nonces + pub nonce_timeout: Option, + /// timeout to gather signature shares + pub sign_timeout: Option, + /// the STX tx fee to use in uSTX + pub tx_fee_ustx: u64, +} + /// The parsed configuration for the signer #[derive(Clone, Debug)] -pub struct Config { +pub struct GlobalConfig { /// endpoint to the stacks node - pub node_host: SocketAddr, + pub node_host: String, /// endpoint to the event receiver pub endpoint: SocketAddr, - /// smart contract that controls the target signers' stackerdb - pub stackerdb_contract_id: QualifiedContractIdentifier, /// The Scalar representation of the private key for signer communication pub ecdsa_private_key: Scalar, /// The signer's Stacks private key @@ -131,14 +180,6 @@ pub struct Config { pub stacks_address: StacksAddress, /// The network to use. One of "mainnet" or "testnet". pub network: Network, - /// The signer ID and key ids mapped to a public key - pub signer_ids_public_keys: PublicKeys, - /// The signer IDs mapped to their Key IDs - pub signer_key_ids: SignerKeyIds, - /// This signer's ID - pub signer_id: u32, - /// All signer IDs participating in the current reward cycle - pub signer_ids: Vec, /// The time to wait for a response from the stacker-db instance pub event_timeout: Duration, /// timeout to gather DkgPublicShares messages @@ -151,13 +192,10 @@ pub struct Config { pub nonce_timeout: Option, /// timeout to gather signature shares pub sign_timeout: Option, -} - -/// Internal struct for loading up the config file signer data -#[derive(Clone, Deserialize, Default, Debug)] -struct RawSigners { - pub public_key: String, - pub key_ids: Vec, + /// the STX tx fee to use in uSTX + pub tx_fee_ustx: u64, + /// the authorization password for the block proposal endpoint + pub auth_password: String, } /// Internal struct for loading up the config file @@ -167,19 +205,11 @@ struct RawConfigFile { pub node_host: String, /// endpoint to event receiver pub endpoint: String, - /// Signers' Stacker db contract identifier - pub stackerdb_contract_id: Option, /// The hex representation of the signer's Stacks private key used for communicating /// with the Stacks Node, including writing to the Stacker DB instance. pub stacks_private_key: String, /// The network to use. One of "mainnet" or "testnet". pub network: Network, - // TODO: Optionally retrieve the signers from the pox contract - // See: https://github.com/stacks-network/stacks-blockchain/issues/3912 - /// The signers, IDs, and their private keys - pub signers: Vec, - /// The signer ID - pub signer_id: u32, /// The time to wait (in millisecs) for a response from the stacker-db instance pub event_timeout_ms: Option, /// timeout in (millisecs) to gather DkgPublicShares messages @@ -192,13 +222,17 @@ struct RawConfigFile { pub nonce_timeout_ms: Option, /// timeout in (millisecs) to gather signature shares pub sign_timeout_ms: Option, + /// the STX tx fee to use in uSTX + pub tx_fee_ustx: Option, + /// The authorization password for the block proposal endpoint + pub auth_password: String, } impl RawConfigFile { /// load the config from a string pub fn load_from_str(data: &str) -> Result { let config: RawConfigFile = - toml::from_str(data).map_err(|e| ConfigError::ParseError(format!("{:?}", &e)))?; + toml::from_str(data).map_err(|e| ConfigError::ParseError(format!("{e:?}")))?; Ok(config) } /// load the config from a file and parse it @@ -213,28 +247,20 @@ impl TryFrom<&PathBuf> for RawConfigFile { fn try_from(path: &PathBuf) -> Result { RawConfigFile::load_from_str(&fs::read_to_string(path).map_err(|e| { - ConfigError::InvalidConfig(format!("failed to read config file: {:?}", &e)) + ConfigError::InvalidConfig(format!("failed to read config file: {e:?}")) })?) } } -impl TryFrom for Config { +impl TryFrom for GlobalConfig { type Error = ConfigError; /// Attempt to decode the raw config file's primitive types into our types. /// NOTE: network access is required for this to work fn try_from(raw_data: RawConfigFile) -> Result { - let node_host = raw_data - .node_host - .to_socket_addrs() - .map_err(|_| { - ConfigError::BadField("node_host".to_string(), raw_data.node_host.clone()) - })? - .next() - .ok_or(ConfigError::BadField( - "node_host".to_string(), - raw_data.node_host.clone(), - ))?; + url::Url::parse(&format!("http://{}", raw_data.node_host)).map_err(|_| { + ConfigError::BadField("node_host".to_string(), raw_data.node_host.clone()) + })?; let endpoint = raw_data .endpoint @@ -246,13 +272,6 @@ impl TryFrom for Config { raw_data.endpoint.clone(), ))?; - let stackerdb_contract_id = match raw_data.stackerdb_contract_id { - Some(id) => QualifiedContractIdentifier::parse(&id).map_err(|_| { - ConfigError::BadField("stackerdb_contract_id".to_string(), id.clone()) - })?, - None => boot_code_id("signers", raw_data.network == Network::Mainnet), - }; - let stacks_private_key = StacksPrivateKey::from_hex(&raw_data.stacks_private_key).map_err(|_| { ConfigError::BadField( @@ -276,29 +295,6 @@ impl TryFrom for Config { &vec![stacks_public_key], ) .ok_or(ConfigError::UnsupportedAddressVersion)?; - let mut signer_ids = vec![]; - let mut public_keys = PublicKeys::default(); - let mut signer_key_ids = SignerKeyIds::default(); - for (i, s) in raw_data.signers.iter().enumerate() { - let signer_public_key = - ecdsa::PublicKey::try_from(s.public_key.as_str()).map_err(|_| { - ConfigError::BadField("signers.public_key".to_string(), s.public_key.clone()) - })?; - for key_id in &s.key_ids { - //We do not allow a key id of 0. - if *key_id == 0 { - return Err(ConfigError::BadField( - "signers.key_ids".to_string(), - key_id.to_string(), - )); - } - public_keys.key_ids.insert(*key_id, signer_public_key); - } - let signer_id = u32::try_from(i).unwrap(); - public_keys.signers.insert(signer_id, signer_public_key); - signer_key_ids.insert(signer_id, s.key_ids.clone()); - signer_ids.push(signer_id); - } let event_timeout = Duration::from_millis(raw_data.event_timeout_ms.unwrap_or(EVENT_TIMEOUT_MS)); let dkg_end_timeout = raw_data.dkg_end_timeout_ms.map(Duration::from_millis); @@ -307,28 +303,25 @@ impl TryFrom for Config { let nonce_timeout = raw_data.nonce_timeout_ms.map(Duration::from_millis); let sign_timeout = raw_data.sign_timeout_ms.map(Duration::from_millis); Ok(Self { - node_host, + node_host: raw_data.node_host, endpoint, - stackerdb_contract_id, - ecdsa_private_key, stacks_private_key, + ecdsa_private_key, stacks_address, network: raw_data.network, - signer_ids_public_keys: public_keys, - signer_id: raw_data.signer_id, - signer_ids, - signer_key_ids, event_timeout, dkg_end_timeout, dkg_public_timeout, dkg_private_timeout, nonce_timeout, sign_timeout, + tx_fee_ustx: raw_data.tx_fee_ustx.unwrap_or(TX_FEE_USTX), + auth_password: raw_data.auth_password, }) } } -impl TryFrom<&PathBuf> for Config { +impl TryFrom<&PathBuf> for GlobalConfig { type Error = ConfigError; fn try_from(path: &PathBuf) -> Result { let config_file = RawConfigFile::try_from(path)?; @@ -336,57 +329,55 @@ impl TryFrom<&PathBuf> for Config { } } -impl Config { +impl GlobalConfig { /// load the config from a string and parse it - #[allow(dead_code)] pub fn load_from_str(data: &str) -> Result { RawConfigFile::load_from_str(data)?.try_into() } /// load the config from a file and parse it - #[allow(dead_code)] pub fn load_from_file(path: &str) -> Result { Self::try_from(&PathBuf::from(path)) } } -#[cfg(test)] -mod tests { - use blockstack_lib::util_lib::boot::boot_code_id; +/// Helper function for building a signer config for each provided signer private key +pub fn build_signer_config_tomls( + stacks_private_keys: &[StacksPrivateKey], + node_host: &str, + timeout: Option, + network: &Network, + password: &str, +) -> Vec { + let mut signer_config_tomls = vec![]; - use super::{Config, Network, RawConfigFile}; + let mut port = 30000; + for stacks_private_key in stacks_private_keys { + let endpoint = format!("localhost:{}", port); + port += 1; + let stacks_private_key = stacks_private_key.to_hex(); + let mut signer_config_toml = format!( + r#" +stacks_private_key = "{stacks_private_key}" +node_host = "{node_host}" +endpoint = "{endpoint}" +network = "{network}" +auth_password = "{password}" +"# + ); - fn create_raw_config(overrides: impl FnOnce(&mut RawConfigFile)) -> RawConfigFile { - let mut config = RawConfigFile { - node_host: "127.0.0.1:20443".to_string(), - endpoint: "127.0.0.1:30000".to_string(), - stackerdb_contract_id: None, - stacks_private_key: - "69be0e68947fa7128702761151dc8d9b39ee1401e547781bb2ec3e5b4eb1b36f01".to_string(), - network: Network::Testnet, - signers: vec![], - signer_id: 0, - event_timeout_ms: None, - dkg_end_timeout_ms: None, - dkg_public_timeout_ms: None, - dkg_private_timeout_ms: None, - nonce_timeout_ms: None, - sign_timeout_ms: None, - }; - overrides(&mut config); - config + if let Some(timeout) = timeout { + let event_timeout_ms = timeout.as_millis(); + signer_config_toml = format!( + r#" +{signer_config_toml} +event_timeout = {event_timeout_ms} +"# + ) + } + + signer_config_tomls.push(signer_config_toml); } - #[test] - fn test_config_default_signerdb() { - let testnet_config = create_raw_config(|_| {}); - - let config = Config::try_from(testnet_config).expect("Failed to parse config"); - assert_eq!(config.stackerdb_contract_id, boot_code_id("signers", false)); - - let mainnet_config = create_raw_config(|c| c.network = Network::Mainnet); - - let config = Config::try_from(mainnet_config).expect("Failed to parse config"); - assert_eq!(config.stackerdb_contract_id, boot_code_id("signers", true)); - } + signer_config_tomls } diff --git a/stacks-signer/src/coordinator.rs b/stacks-signer/src/coordinator.rs new file mode 100644 index 000000000..234d1ade8 --- /dev/null +++ b/stacks-signer/src/coordinator.rs @@ -0,0 +1,230 @@ +// 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 . + +use std::time::Instant; + +use blockstack_lib::chainstate::burn::ConsensusHashExtensions; +use slog::slog_debug; +use stacks_common::debug; +use stacks_common::types::chainstate::ConsensusHash; +use stacks_common::util::hash::Sha256Sum; +use wsts::curve::ecdsa; +use wsts::state_machine::PublicKeys; + +/// TODO: test this value and adjust as necessary. Maybe make configurable? +pub const COORDINATOR_OPERATION_TIMEOUT_SECS: u64 = 300; + +/// TODO: test this value and adjust as necessary. Maybe make configurable? +pub const COORDINATOR_TENURE_TIMEOUT_SECS: u64 = 600; + +/// The coordinator selector +#[derive(Clone, Debug)] +pub struct CoordinatorSelector { + /// The ordered list of potential coordinators for a specific consensus hash + coordinator_ids: Vec, + /// The current coordinator id + coordinator_id: u32, + /// The current coordinator index into the coordinator ids list + coordinator_index: usize, + /// The last message received time for the current coordinator + pub last_message_time: Option, + /// The time the coordinator started its tenure + tenure_start: Instant, + /// The public keys of the coordinators + public_keys: PublicKeys, +} + +impl From for CoordinatorSelector { + /// Create a new Coordinator selector from the given list of public keys + fn from(public_keys: PublicKeys) -> Self { + let coordinator_ids = + CoordinatorSelector::calculate_coordinator_ids(&public_keys, &ConsensusHash::empty()); + let coordinator_id = *coordinator_ids + .first() + .expect("FATAL: No registered signers"); + let coordinator_index = 0; + let last_message_time = None; + let tenure_start = Instant::now(); + Self { + coordinator_ids, + coordinator_id, + coordinator_index, + last_message_time, + tenure_start, + public_keys, + } + } +} + +impl CoordinatorSelector { + /// Update the coordinator id + fn update_coordinator(&mut self, new_coordinator_ids: Vec) { + self.last_message_time = None; + self.coordinator_index = if new_coordinator_ids != self.coordinator_ids { + // We have advanced our block height and should select from the new list + let mut new_index: usize = 0; + self.coordinator_ids = new_coordinator_ids; + let new_coordinator_id = *self + .coordinator_ids + .first() + .expect("FATAL: No registered signers"); + if new_coordinator_id == self.coordinator_id { + // If the newly selected coordinator is the same as the current and we have more than one available, advance immediately to the next + if self.coordinator_ids.len() > 1 { + new_index = new_index.saturating_add(1); + } + } + new_index + } else { + let mut new_index = self.coordinator_index.saturating_add(1); + if new_index == self.coordinator_ids.len() { + // We have exhausted all potential coordinators. Go back to the start + new_index = 0; + } + new_index + }; + self.coordinator_id = *self + .coordinator_ids + .get(self.coordinator_index) + .expect("FATAL: Invalid number of registered signers"); + self.tenure_start = Instant::now(); + self.last_message_time = None; + } + + /// Check the coordinator timeouts and update the selected coordinator accordingly + /// Returns the resulting coordinator ID. (Note: it may be unchanged) + pub fn refresh_coordinator(&mut self, pox_consensus_hash: &ConsensusHash) -> u32 { + let new_coordinator_ids = + Self::calculate_coordinator_ids(&self.public_keys, pox_consensus_hash); + if let Some(time) = self.last_message_time { + if time.elapsed().as_secs() > COORDINATOR_OPERATION_TIMEOUT_SECS { + // We have not received a message in a while from this coordinator. + // We should consider the operation finished and use a new coordinator id. + self.update_coordinator(new_coordinator_ids); + } + } else if self.tenure_start.elapsed().as_secs() > COORDINATOR_TENURE_TIMEOUT_SECS + || new_coordinator_ids != self.coordinator_ids + { + // Our tenure has been exceeded or we have advanced our block height and should select from the new list + self.update_coordinator(new_coordinator_ids); + } + self.coordinator_id + } + + /// Get the current coordinator id and public key + pub fn get_coordinator(&self) -> (u32, ecdsa::PublicKey) { + ( + self.coordinator_id, + *self + .public_keys + .signers + .get(&self.coordinator_id) + .expect("FATAL: missing public key for selected coordinator id"), + ) + } + + /// Calculate the ordered list of coordinator ids by comparing the provided public keys against the pox consensus hash + pub fn calculate_coordinator_ids( + public_keys: &PublicKeys, + pox_consensus_hash: &ConsensusHash, + ) -> Vec { + debug!("Using pox_consensus_hash {pox_consensus_hash:?} for selecting coordinator"); + // Create combined hash of each signer's public key with pox_consensus_hash + let mut selection_ids = public_keys + .signers + .iter() + .map(|(&id, pk)| { + let pk_bytes = pk.to_bytes(); + let mut buffer = + Vec::with_capacity(pk_bytes.len() + pox_consensus_hash.as_bytes().len()); + buffer.extend_from_slice(&pk_bytes[..]); + buffer.extend_from_slice(pox_consensus_hash.as_bytes()); + let digest = Sha256Sum::from_data(&buffer).as_bytes().to_vec(); + (id, digest) + }) + .collect::>(); + + // Sort the selection IDs based on the hash + selection_ids.sort_by_key(|(_, hash)| hash.clone()); + // Return only the ids + selection_ids.iter().map(|(id, _)| *id).collect() + } +} +#[cfg(test)] +mod tests { + use super::*; + use crate::client::tests::{generate_random_consensus_hash, generate_signer_config}; + use crate::config::GlobalConfig; + + #[test] + fn calculate_coordinator_different_consensus_hashes_produces_unique_results() { + let number_of_tests = 5; + let config = GlobalConfig::load_from_file("./src/tests/conf/signer-0.toml").unwrap(); + let public_keys = generate_signer_config(&config, 10, 4000) + .signer_entries + .public_keys; + let mut results = Vec::new(); + + for _ in 0..number_of_tests { + let result = CoordinatorSelector::calculate_coordinator_ids( + &public_keys, + &generate_random_consensus_hash(), + ); + results.push(result); + } + + // Check that not all coordinator IDs are the same + let all_ids_same = results.iter().all(|ids| ids == &results[0]); + assert!(!all_ids_same, "Not all coordinator IDs should be the same"); + } + + fn generate_calculate_coordinator_test_results( + random_consensus: bool, + count: usize, + ) -> Vec> { + let config = GlobalConfig::load_from_file("./src/tests/conf/signer-0.toml").unwrap(); + let public_keys = generate_signer_config(&config, 10, 4000) + .signer_entries + .public_keys; + let mut results = Vec::new(); + let same_hash = generate_random_consensus_hash(); + for _ in 0..count { + let hash = if random_consensus { + generate_random_consensus_hash() + } else { + same_hash + }; + let result = CoordinatorSelector::calculate_coordinator_ids(&public_keys, &hash); + results.push(result); + } + results + } + + #[test] + fn calculate_coordinator_results_should_vary_or_match_based_on_hash() { + let results_with_random_hash = generate_calculate_coordinator_test_results(true, 5); + let all_ids_same = results_with_random_hash + .iter() + .all(|ids| ids == &results_with_random_hash[0]); + assert!(!all_ids_same, "Not all coordinator IDs should be the same"); + + let results_with_static_hash = generate_calculate_coordinator_test_results(false, 5); + let all_ids_same = results_with_static_hash + .iter() + .all(|ids| ids == &results_with_static_hash[0]); + assert!(all_ids_same, "All coordinator IDs should be the same"); + } +} diff --git a/stacks-signer/src/lib.rs b/stacks-signer/src/lib.rs index cadb72c8a..f3438e8bb 100644 --- a/stacks-signer/src/lib.rs +++ b/stacks-signer/src/lib.rs @@ -26,7 +26,9 @@ pub mod cli; pub mod client; /// The configuration module for the signer pub mod config; +/// The coordinator selector for the signer +pub mod coordinator; /// The primary runloop for the signer pub mod runloop; -/// Util functions -pub mod utils; +/// The signer module for processing events +pub mod signer; diff --git a/stacks-signer/src/main.rs b/stacks-signer/src/main.rs index 7ba74b55a..38cfea578 100644 --- a/stacks-signer/src/main.rs +++ b/stacks-signer/src/main.rs @@ -4,7 +4,7 @@ //! //! // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// 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 @@ -28,7 +28,6 @@ extern crate toml; use std::fs::File; use std::io::{self, BufRead, Write}; -use std::net::SocketAddr; use std::path::{Path, PathBuf}; use std::sync::mpsc::{channel, Receiver, Sender}; use std::time::Duration; @@ -41,8 +40,7 @@ use libsigner::{RunningSigner, Signer, SignerEventReceiver, SignerSession, Stack use libstackerdb::StackerDBChunkData; use slog::{slog_debug, slog_error}; use stacks_common::codec::read_next; -use stacks_common::consts::SIGNER_SLOTS_PER_USER; -use stacks_common::types::chainstate::{StacksAddress, StacksPrivateKey}; +use stacks_common::types::chainstate::StacksPrivateKey; use stacks_common::util::hash::to_hex; use stacks_common::util::secp256k1::{MessageSignature, Secp256k1PublicKey}; use stacks_common::{debug, error}; @@ -50,14 +48,12 @@ use stacks_signer::cli::{ Cli, Command, GenerateFilesArgs, GenerateStackingSignatureArgs, GetChunkArgs, GetLatestChunkArgs, PutChunkArgs, RunDkgArgs, SignArgs, StackerDBArgs, }; -use stacks_signer::config::Config; +use stacks_signer::config::{build_signer_config_tomls, GlobalConfig}; use stacks_signer::runloop::{RunLoop, RunLoopCommand}; -use stacks_signer::utils::{build_signer_config_tomls, build_stackerdb_contract, to_addr}; +use stacks_signer::signer::Command as SignerCommand; use tracing_subscriber::prelude::*; use tracing_subscriber::{fmt, EnvFilter}; -use wsts::state_machine::coordinator::fire::Coordinator as FireCoordinator; use wsts::state_machine::OperationResult; -use wsts::v2; struct SpawnedSigner { running_signer: RunningSigner>, @@ -66,9 +62,9 @@ struct SpawnedSigner { } /// Create a new stacker db session -fn stackerdb_session(host: SocketAddr, contract: QualifiedContractIdentifier) -> StackerDBSession { +fn stackerdb_session(host: &str, contract: QualifiedContractIdentifier) -> StackerDBSession { let mut session = StackerDBSession::new(host, contract.clone()); - session.connect(host, contract).unwrap(); + session.connect(host.to_string(), contract).unwrap(); session } @@ -87,21 +83,15 @@ fn write_chunk_to_stdout(chunk_opt: Option>) { // Spawn a running signer and return its handle, command sender, and result receiver fn spawn_running_signer(path: &PathBuf) -> SpawnedSigner { - let config = Config::try_from(path).unwrap(); + let config = GlobalConfig::try_from(path).unwrap(); + let endpoint = config.endpoint; let (cmd_send, cmd_recv) = channel(); let (res_send, res_recv) = channel(); - let ev = SignerEventReceiver::new( - vec![config.stackerdb_contract_id.clone()], - config.network.is_mainnet(), - ); - let runloop: RunLoop> = RunLoop::from(&config); - let mut signer: Signer< - RunLoopCommand, - Vec, - RunLoop>, - SignerEventReceiver, - > = Signer::new(runloop, ev, cmd_recv, res_send); - let running_signer = signer.spawn(config.endpoint).unwrap(); + let ev = SignerEventReceiver::new(config.network.is_mainnet()); + let runloop = RunLoop::from(config); + let mut signer: Signer, RunLoop, SignerEventReceiver> = + Signer::new(runloop, ev, cmd_recv, res_send); + let running_signer = signer.spawn(endpoint).unwrap(); SpawnedSigner { running_signer, cmd_send, @@ -169,28 +159,28 @@ fn process_sign_result(sign_res: &[OperationResult]) { fn handle_get_chunk(args: GetChunkArgs) { debug!("Getting chunk..."); - let mut session = stackerdb_session(args.db_args.host, args.db_args.contract); + let mut session = stackerdb_session(&args.db_args.host, args.db_args.contract); let chunk_opt = session.get_chunk(args.slot_id, args.slot_version).unwrap(); write_chunk_to_stdout(chunk_opt); } fn handle_get_latest_chunk(args: GetLatestChunkArgs) { debug!("Getting latest chunk..."); - let mut session = stackerdb_session(args.db_args.host, args.db_args.contract); + let mut session = stackerdb_session(&args.db_args.host, args.db_args.contract); let chunk_opt = session.get_latest_chunk(args.slot_id).unwrap(); write_chunk_to_stdout(chunk_opt); } fn handle_list_chunks(args: StackerDBArgs) { debug!("Listing chunks..."); - let mut session = stackerdb_session(args.host, args.contract); + let mut session = stackerdb_session(&args.host, args.contract); let chunk_list = session.list_chunks().unwrap(); println!("{}", serde_json::to_string(&chunk_list).unwrap()); } fn handle_put_chunk(args: PutChunkArgs) { debug!("Putting chunk..."); - let mut session = stackerdb_session(args.db_args.host, args.db_args.contract); + let mut session = stackerdb_session(&args.db_args.host, args.db_args.contract); let mut chunk = StackerDBChunkData::new(args.slot_id, args.slot_version, args.data); chunk.sign(&args.private_key).unwrap(); let chunk_ack = session.put_chunk(&chunk).unwrap(); @@ -200,7 +190,11 @@ fn handle_put_chunk(args: PutChunkArgs) { fn handle_dkg(args: RunDkgArgs) { debug!("Running DKG..."); let spawned_signer = spawn_running_signer(&args.config); - spawned_signer.cmd_send.send(RunLoopCommand::Dkg).unwrap(); + let dkg_command = RunLoopCommand { + reward_cycle: args.reward_cycle, + command: SignerCommand::Dkg, + }; + spawned_signer.cmd_send.send(dkg_command).unwrap(); let dkg_res = spawned_signer.res_recv.recv().unwrap(); process_dkg_result(&dkg_res); spawned_signer.running_signer.stop(); @@ -214,14 +208,15 @@ fn handle_sign(args: SignArgs) { spawned_signer.running_signer.stop(); return; }; - spawned_signer - .cmd_send - .send(RunLoopCommand::Sign { + let sign_command = RunLoopCommand { + reward_cycle: args.reward_cycle, + command: SignerCommand::Sign { block, is_taproot: false, merkle_root: None, - }) - .unwrap(); + }, + }; + spawned_signer.cmd_send.send(sign_command).unwrap(); let sign_res = spawned_signer.res_recv.recv().unwrap(); process_sign_result(&sign_res); spawned_signer.running_signer.stop(); @@ -235,16 +230,21 @@ fn handle_dkg_sign(args: SignArgs) { spawned_signer.running_signer.stop(); return; }; - // First execute DKG, then sign - spawned_signer.cmd_send.send(RunLoopCommand::Dkg).unwrap(); - spawned_signer - .cmd_send - .send(RunLoopCommand::Sign { + let dkg_command = RunLoopCommand { + reward_cycle: args.reward_cycle, + command: SignerCommand::Dkg, + }; + let sign_command = RunLoopCommand { + reward_cycle: args.reward_cycle, + command: SignerCommand::Sign { block, is_taproot: false, merkle_root: None, - }) - .unwrap(); + }, + }; + // First execute DKG, then sign + spawned_signer.cmd_send.send(dkg_command).unwrap(); + spawned_signer.cmd_send.send(sign_command).unwrap(); let dkg_res = spawned_signer.res_recv.recv().unwrap(); process_dkg_result(&dkg_res); let sign_res = spawned_signer.res_recv.recv().unwrap(); @@ -285,22 +285,13 @@ fn handle_generate_files(args: GenerateFilesArgs) { .map(|_| StacksPrivateKey::new()) .collect::>() }; - let signer_stacks_addresses = signer_stacks_private_keys - .iter() - .map(|key| to_addr(key, &args.network)) - .collect::>(); - // Build the signer and miner stackerdb contract - let signer_stackerdb_contract = - build_stackerdb_contract(&signer_stacks_addresses, SIGNER_SLOTS_PER_USER); - write_file(&args.dir, "signers.clar", &signer_stackerdb_contract); let signer_config_tomls = build_signer_config_tomls( &signer_stacks_private_keys, - args.num_keys, &args.host.to_string(), - &args.signers_contract.to_string(), args.timeout.map(Duration::from_millis), &args.network, + &args.password, ); debug!("Built {:?} signer config tomls.", signer_config_tomls.len()); for (i, file_contents) in signer_config_tomls.iter().enumerate() { @@ -312,7 +303,7 @@ fn handle_generate_stacking_signature( args: GenerateStackingSignatureArgs, do_print: bool, ) -> MessageSignature { - let config = Config::try_from(&args.config).unwrap(); + let config = GlobalConfig::try_from(&args.config).unwrap(); let private_key = config.stacks_private_key; let public_key = Secp256k1PublicKey::from_private(&private_key); @@ -321,7 +312,7 @@ fn handle_generate_stacking_signature( &args.pox_address, &private_key, // args.reward_cycle.into(), - &args.method.topic(), + args.method.topic(), config.network.to_chain_id(), args.period.into(), ) @@ -403,7 +394,7 @@ pub mod tests { use stacks_signer::cli::parse_pox_addr; use super::{handle_generate_stacking_signature, *}; - use crate::{Config, GenerateStackingSignatureArgs}; + use crate::{GenerateStackingSignatureArgs, GlobalConfig}; fn call_verify_signer_sig( pox_addr: &PoxAddress, @@ -426,19 +417,18 @@ pub mod tests { to_hex(signature.as_slice()), to_hex(public_key.to_bytes_compressed().as_slice()), ); - let result = execute_v2(&program) + execute_v2(&program) .expect("FATAL: could not execute program") .expect("Expected result") .expect_result_ok() .expect("Expected ok result") .expect_bool() - .expect("Expected buff"); - result + .expect("Expected buff") } #[test] fn test_stacking_signature_with_pox_code() { - let config = Config::load_from_file("./src/tests/conf/signer-0.toml").unwrap(); + let config = GlobalConfig::load_from_file("./src/tests/conf/signer-0.toml").unwrap(); let btc_address = "bc1p8vg588hldsnv4a558apet4e9ff3pr4awhqj2hy8gy6x2yxzjpmqsvvpta4"; let mut args = GenerateStackingSignatureArgs { config: "./src/tests/conf/signer-0.toml".into(), @@ -482,7 +472,7 @@ pub mod tests { #[test] fn test_generate_stacking_signature() { - let config = Config::load_from_file("./src/tests/conf/signer-0.toml").unwrap(); + let config = GlobalConfig::load_from_file("./src/tests/conf/signer-0.toml").unwrap(); let btc_address = "bc1p8vg588hldsnv4a558apet4e9ff3pr4awhqj2hy8gy6x2yxzjpmqsvvpta4"; let args = GenerateStackingSignatureArgs { config: "./src/tests/conf/signer-0.toml".into(), diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index 1804466cb..ef5ffacdc 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -1,3 +1,4 @@ +use std::collections::VecDeque; // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation // Copyright (C) 2020-2024 Stacks Open Internet Foundation // @@ -13,1063 +14,311 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::collections::VecDeque; use std::sync::mpsc::Sender; use std::time::Duration; -use blockstack_lib::chainstate::nakamoto::NakamotoBlock; -use blockstack_lib::chainstate::stacks::StacksTransaction; -use blockstack_lib::net::api::postblock_proposal::BlockValidateResponse; +use blockstack_lib::chainstate::burn::ConsensusHashExtensions; +use blockstack_lib::chainstate::stacks::boot::{NakamotoSignerEntry, SIGNERS_NAME}; +use blockstack_lib::util_lib::boot::boot_code_id; use hashbrown::{HashMap, HashSet}; -use libsigner::{ - BlockRejection, BlockResponse, RejectCode, SignerEvent, SignerMessage, SignerRunLoop, -}; +use libsigner::{SignerEvent, SignerRunLoop}; use slog::{slog_debug, slog_error, slog_info, slog_warn}; -use stacks_common::codec::{read_next, StacksMessageCodec}; -use stacks_common::types::chainstate::StacksAddress; -use stacks_common::util::hash::{Sha256Sum, Sha512Trunc256Sum}; +use stacks_common::types::chainstate::{ConsensusHash, StacksAddress, StacksPublicKey}; use stacks_common::{debug, error, info, warn}; -use wsts::common::{MerkleRoot, Signature}; use wsts::curve::ecdsa; -use wsts::curve::keys::PublicKey; use wsts::curve::point::{Compressed, Point}; -use wsts::net::{Message, NonceRequest, Packet, SignatureShareRequest}; -use wsts::state_machine::coordinator::fire::Coordinator as FireCoordinator; -use wsts::state_machine::coordinator::{Config as CoordinatorConfig, Coordinator}; -use wsts::state_machine::signer::Signer; -use wsts::state_machine::{OperationResult, PublicKeys, SignError}; -use wsts::v2; +use wsts::state_machine::coordinator::State as CoordinatorState; +use wsts::state_machine::{OperationResult, PublicKeys}; -use crate::client::{ - retry_with_exponential_backoff, ClientError, EpochId, StackerDB, StacksClient, -}; -use crate::config::{Config, Network}; +use crate::client::{retry_with_exponential_backoff, ClientError, StacksClient}; +use crate::config::{GlobalConfig, ParsedSignerEntries, SignerConfig}; +use crate::signer::{Command as SignerCommand, Signer, SignerSlotID, State as SignerState}; /// Which operation to perform -#[derive(PartialEq, Clone)] -pub enum RunLoopCommand { - /// Generate a DKG aggregate public key - Dkg, - /// Sign a message - Sign { - /// The block to sign over - block: NakamotoBlock, - /// Whether to make a taproot signature - is_taproot: bool, - /// Taproot merkle root - merkle_root: Option, - }, +#[derive(PartialEq, Clone, Debug)] +pub struct RunLoopCommand { + /// Which signer operation to perform + pub command: SignerCommand, + /// The reward cycle we are performing the operation for + pub reward_cycle: u64, } -/// The RunLoop state +/// The runloop state #[derive(PartialEq, Debug)] pub enum State { - // TODO: Uninitialized should indicate we need to replay events/configure the signer - /// The runloop signer is uninitialized + /// The runloop is uninitialized Uninitialized, - /// The runloop is idle - Idle, - /// The runloop is executing a DKG round - Dkg, - /// The runloop is executing a signing round - Sign, -} - -/// Additional Info about a proposed block -pub struct BlockInfo { - /// The block we are considering - block: NakamotoBlock, - /// Our vote on the block if we have one yet - vote: Option>, - /// Whether the block contents are valid - valid: Option, - /// The associated packet nonce request if we have one - nonce_request: Option, - /// Whether this block is already being signed over - signed_over: bool, -} - -impl BlockInfo { - /// Create a new BlockInfo - pub fn new(block: NakamotoBlock) -> Self { - Self { - block, - vote: None, - valid: None, - nonce_request: None, - signed_over: false, - } - } - - /// Create a new BlockInfo with an associated nonce request packet - pub fn new_with_request(block: NakamotoBlock, nonce_request: NonceRequest) -> Self { - Self { - block, - vote: None, - valid: None, - nonce_request: Some(nonce_request), - signed_over: true, - } - } + /// The runloop is initialized + Initialized, } /// The runloop for the stacks signer -pub struct RunLoop { - /// The timeout for events - pub event_timeout: Duration, - /// The coordinator for inbound messages - pub coordinator: C, - /// The signing round used to sign messages - pub signing_round: Signer, +pub struct RunLoop { + /// Configuration info + pub config: GlobalConfig, /// The stacks node client pub stacks_client: StacksClient, - /// The stacker db client - pub stackerdb: StackerDB, - /// Received Commands that need to be processed - pub commands: VecDeque, - /// The current state + /// The internal signer for an odd or even reward cycle + /// Keyed by reward cycle % 2 + pub stacks_signers: HashMap, + /// The state of the runloop pub state: State, - /// Wether mainnet or not - pub mainnet: bool, - /// Observed blocks that we have seen so far - // TODO: cleanup storage and garbage collect this stuff - pub blocks: HashMap, - /// Transactions that we expect to see in the next block - // TODO: fill this in and do proper garbage collection - pub transactions: Vec, - /// This signer's ID - pub signer_id: u32, - /// The signer set for this runloop - pub signer_set: Option, - /// The index into the signers list of this signer's key (may be different from signer_id) - pub signer_slot_id: Option, - /// The IDs of all signers partipating in the current reward cycle - pub signer_ids: Vec, - /// The stacks addresses of the signers participating in the current reward cycle - pub signer_addresses: Vec, + /// The commands received thus far + pub commands: VecDeque, } -impl RunLoop { - /// Get and store the signer set assignment for this runloop. - /// This assigns the runloop to the _next_ reward cycle, not the current one. - /// Returns (signer-set, signer-slot-id) - fn get_or_set_signer_info(&mut self) -> Result<(u32, u32), ClientError> { - match (self.signer_set.as_ref(), self.signer_slot_id.as_ref()) { - (Some(signer_set), Some(signer_slot_id)) => { - return Ok((*signer_set, *signer_slot_id)); - } - (_, _) => {} - } - - let signer_set = if let Some(signer_set) = self.signer_set.as_ref() { - *signer_set - } else { - let rc = self - .stacks_client - .get_current_reward_cycle()? - .saturating_add(1); - debug!("Next reward cycle is {}", rc); - let signer_set = u32::try_from(rc % 2).expect("FATAL: infallible"); - self.signer_set = Some(signer_set); - self.stackerdb.set_signer_set(signer_set); - signer_set - }; - - // Get the signer writers from the stacker-db to verify transactions against - self.signer_addresses = self - .stacks_client - .get_stackerdb_signer_slots( - self.stackerdb.signers_contract_id(), - self.stackerdb.get_signer_set(), - )? - .into_iter() - .map(|(address, _)| address) - .collect(); - - let signer_slot_id = if let Some(signer_slot_id) = self.signer_slot_id.as_ref() { - *signer_slot_id - } else { - let addr = self.stackerdb.get_signer_address(self.mainnet); - self.signer_slot_id = self - .signer_addresses - .iter() - .position(|signer_addr| signer_addr == &addr) - .map(|pos| u32::try_from(pos).expect("FATAL: position exceeds u32::MAX")); - - let Some(signer_slot_id) = self.signer_slot_id.as_ref() else { - return Err(ClientError::InvalidSigningKey); - }; - self.stackerdb.set_signer_slot_id(*signer_slot_id); - *signer_slot_id - }; - - Ok((signer_set, signer_slot_id)) - } - - /// Initialize the signer, reading the stacker-db state and setting the aggregate public key - fn initialize(&mut self) -> Result<(), ClientError> { - // determine what signer set we're using, so we use the right stackerdb replicas - let (signer_set, signer_slot_id) = self.get_or_set_signer_info()?; - debug!( - "Signer #{}: Self-assigning to signer set {} slot {} address {}", - self.signer_id, - signer_set, - signer_slot_id, - self.stackerdb.get_signer_address(self.mainnet) - ); - - // Check if the aggregate key is set in the pox contract - if let Some(key) = self.stacks_client.get_aggregate_public_key()? { - debug!( - "Signer #{}: Aggregate public key is set: {:?}", - self.signer_id, key - ); - self.coordinator.set_aggregate_public_key(Some(key)); - } else { - debug!( - "Signer #{}: Aggregate public key is not set. Coordinator must trigger DKG...", - self.signer_id - ); - // Update the state to IDLE so we don't needlessy requeue the DKG command. - let (coordinator_id, _) = - calculate_coordinator(&self.signing_round.public_keys, &self.stacks_client); - if coordinator_id == self.signer_id - && self.commands.front() != Some(&RunLoopCommand::Dkg) - { - self.commands.push_front(RunLoopCommand::Dkg); - } - } - - self.state = State::Idle; - Ok(()) - } - - /// Execute the given command and update state accordingly - /// Returns true when it is successfully executed, else false - fn execute_command(&mut self, command: &RunLoopCommand) -> bool { - match command { - RunLoopCommand::Dkg => { - info!("Signer #{}: Starting DKG", self.signer_id); - match self.coordinator.start_dkg_round() { - Ok(msg) => { - let ack = self.stackerdb.send_message_with_retry(msg.into()); - debug!("Signer #{}: ACK: {:?}", self.signer_id, ack); - self.state = State::Dkg; - true - } - Err(e) => { - error!("Failed to start DKG: {:?}", e); - warn!("Resetting coordinator's internal state."); - self.coordinator.reset(); - false - } - } - } - RunLoopCommand::Sign { - block, - is_taproot, - merkle_root, - } => { - let signer_signature_hash = block.header.signer_signature_hash(); - let block_info = self - .blocks - .entry(signer_signature_hash) - .or_insert_with(|| BlockInfo::new(block.clone())); - if block_info.signed_over { - debug!("Signer #{}: Received a sign command for a block we are already signing over. Ignore it.", self.signer_id); - return false; - } - info!("Signer #{}: Signing block: {:?}", self.signer_id, block); - match self.coordinator.start_signing_round( - &block.serialize_to_vec(), - *is_taproot, - *merkle_root, - ) { - Ok(msg) => { - let ack = self.stackerdb.send_message_with_retry(msg.into()); - debug!("Signer #{}: ACK: {:?}", self.signer_id, ack); - self.state = State::Sign; - block_info.signed_over = true; - true - } - Err(e) => { - error!( - "Signer #{}: Failed to start signing message: {:?}", - self.signer_id, e - ); - warn!( - "Signer #{}: Resetting coordinator's internal state.", - self.signer_id - ); - self.coordinator.reset(); - false - } - } - } - } - } - - /// Attempt to process the next command in the queue, and update state accordingly - fn process_next_command(&mut self) { - match self.state { - State::Uninitialized => { - debug!( - "Signer #{}: uninitialized. Waiting for aggregate public key from stacks node...", self.signer_id - ); - } - State::Idle => { - if let Some(command) = self.commands.pop_front() { - while !self.execute_command(&command) { - warn!( - "Signer #{}: Failed to execute command. Retrying...", - self.signer_id - ); - } - } else { - debug!( - "Signer #{}: Nothing to process. Waiting for command...", - self.signer_id - ); - } - } - State::Dkg | State::Sign => { - // We cannot execute the next command until the current one is finished... - // Do nothing... - debug!( - "Signer #{}: Waiting for {:?} operation to finish", - self.signer_id, self.state - ); - } - } - } - - /// Handle the block validate response returned from our prior calls to submit a block for validation - fn handle_block_validate_response( - &mut self, - block_validate_response: BlockValidateResponse, - res: Sender>, - ) { - let block_info = match block_validate_response { - BlockValidateResponse::Ok(block_validate_ok) => { - let signer_signature_hash = block_validate_ok.signer_signature_hash; - // For mutability reasons, we need to take the block_info out of the map and add it back after processing - let Some(mut block_info) = self.blocks.remove(&signer_signature_hash) else { - // We have not seen this block before. Why are we getting a response for it? - debug!("Received a block validate response for a block we have not seen before. Ignoring..."); - return; - }; - let is_valid = self.verify_transactions(&block_info.block); - block_info.valid = Some(is_valid); - info!( - "Signer #{}: Treating block validation for block {} as valid: {:?}", - self.signer_id, - &block_info.block.block_id(), - block_info.valid - ); - // Add the block info back to the map - self.blocks - .entry(signer_signature_hash) - .or_insert(block_info) - } - BlockValidateResponse::Reject(block_validate_reject) => { - let signer_signature_hash = block_validate_reject.signer_signature_hash; - let Some(block_info) = self.blocks.get_mut(&signer_signature_hash) else { - // We have not seen this block before. Why are we getting a response for it? - debug!("Signer #{}: Received a block validate response for a block we have not seen before. Ignoring...", self.signer_id); - return; - }; - block_info.valid = Some(false); - // Submit a rejection response to the .signers contract for miners - // to observe so they know to send another block and to prove signers are doing work); - warn!("Signer #{}: Broadcasting a block rejection due to stacks node validation failure...", self.signer_id); - if let Err(e) = self - .stackerdb - .send_message_with_retry(block_validate_reject.into()) - { - warn!( - "Signer #{}: Failed to send block rejection to stacker-db: {:?}", - self.signer_id, e - ); - } - block_info - } - }; - - if let Some(mut nonce_request) = block_info.nonce_request.take() { - debug!("Signer #{}: Received a block validate response from the stacks node for a block we already received a nonce request for. Responding to the nonce request...", self.signer_id); - // We have received validation from the stacks node. Determine our vote and update the request message - Self::determine_vote(self.signer_id, block_info, &mut nonce_request); - // Send the nonce request through with our vote - let packet = Packet { - msg: Message::NonceRequest(nonce_request), - sig: vec![], - }; - self.handle_packets(res, &[packet]); - } else { - let (coordinator_id, _) = - calculate_coordinator(&self.signing_round.public_keys, &self.stacks_client); - if block_info.valid.unwrap_or(false) - && !block_info.signed_over - && coordinator_id == self.signer_id - { - // We are the coordinator. Trigger a signing round for this block - debug!( - "Signer triggering a signing round over the block."; - "block_hash" => block_info.block.header.block_hash(), - "signer_id" => self.signer_id, - ); - self.commands.push_back(RunLoopCommand::Sign { - block: block_info.block.clone(), - is_taproot: false, - merkle_root: None, - }); - } else { - debug!( - "Signer ignoring block."; - "block_hash" => block_info.block.header.block_hash(), - "valid" => block_info.valid, - "signed_over" => block_info.signed_over, - "coordinator_id" => coordinator_id, - "signer_id" => self.signer_id, - ); - } - } - } - - /// Handle signer messages submitted to signers stackerdb - fn handle_signer_messages( - &mut self, - res: Sender>, - messages: Vec, - ) { - let (coordinator_id, coordinator_public_key) = - calculate_coordinator(&self.signing_round.public_keys, &self.stacks_client); - debug!( - "Signer #{}: coordinator is signer #{} public key {}", - self.signer_id, coordinator_id, &coordinator_public_key - ); - let packets: Vec = messages - .into_iter() - .filter_map(|msg| match msg { - SignerMessage::BlockResponse(_) | SignerMessage::Transactions(_) => None, - SignerMessage::Packet(packet) => { - self.verify_packet(packet, &coordinator_public_key) - } - }) - .collect(); - self.handle_packets(res, &packets); - } - - /// Handle proposed blocks submitted by the miners to stackerdb - fn handle_proposed_blocks(&mut self, blocks: Vec) { - for block in blocks { - // Store the block in our cache - self.blocks.insert( - block.header.signer_signature_hash(), - BlockInfo::new(block.clone()), - ); - // Submit the block for validation - self.stacks_client - .submit_block_for_validation(block) - .unwrap_or_else(|e| { - warn!("Failed to submit block for validation: {:?}", e); - }); - } - } - - /// Process inbound packets as both a signer and a coordinator - /// Will send outbound packets and operation results as appropriate - fn handle_packets(&mut self, res: Sender>, packets: &[Packet]) { - let signer_outbound_messages = self - .signing_round - .process_inbound_messages(packets) - .unwrap_or_else(|e| { - error!("Failed to process inbound messages as a signer: {e}"); - vec![] - }); - - // Next process the message as the coordinator - let (coordinator_outbound_messages, operation_results) = self - .coordinator - .process_inbound_messages(packets) - .unwrap_or_else(|e| { - error!("Failed to process inbound messages as a coordinator: {e}"); - (vec![], vec![]) - }); - - if !operation_results.is_empty() { - // We have finished a signing or DKG round, either successfully or due to error. - // Regardless of the why, update our state to Idle as we should not expect the operation to continue. - self.state = State::Idle; - self.process_operation_results(&operation_results); - self.send_operation_results(res, operation_results); - } - self.send_outbound_messages(signer_outbound_messages); - self.send_outbound_messages(coordinator_outbound_messages); - } - - /// Validate a signature share request, updating its message where appropriate. - /// If the request is for a block it has already agreed to sign, it will overwrite the message with the agreed upon value - /// Returns whether the request is valid or not. - fn validate_signature_share_request(&self, request: &mut SignatureShareRequest) -> bool { - let message_len = request.message.len(); - // Note that the message must always be either 32 bytes (the block hash) or 33 bytes (block hash + b'n') - let hash_bytes = if message_len == 33 && request.message[32] == b'n' { - // Pop off the 'n' byte from the block hash - &request.message[..32] - } else if message_len == 32 { - // This is the block hash - &request.message - } else { - // We will only sign across block hashes or block hashes + b'n' byte - debug!("Signer #{}: Received a signature share request for an unknown message stream. Reject it.", self.signer_id); - return false; - }; - - let Some(hash) = Sha512Trunc256Sum::from_bytes(hash_bytes) else { - // We will only sign across valid block hashes - debug!("Signer #{}: Received a signature share request for an invalid block hash. Reject it.", self.signer_id); - return false; - }; - match self.blocks.get(&hash).map(|block_info| &block_info.vote) { - Some(Some(vote)) => { - // Overwrite with our agreed upon value in case another message won majority or the coordinator is trying to cheat... - debug!( - "Signer #{}: set vote for {} to {:?}", - self.signer_id, &hash, &vote - ); - request.message = vote.clone(); - true - } - Some(None) => { - // We never agreed to sign this block. Reject it. - // This can happen if the coordinator received enough votes to sign yes - // or no on a block before we received validation from the stacks node. - debug!("Signer #{}: Received a signature share request for a block we never agreed to sign. Ignore it.", self.signer_id); - false - } - None => { - // We will only sign across block hashes or block hashes + b'n' byte for - // blocks we have seen a Nonce Request for (and subsequent validation) - // We are missing the context here necessary to make a decision. Reject the block - debug!("Signer #{}: Received a signature share request from an unknown block. Reject it.", self.signer_id); - false - } - } - } - - /// Validate a nonce request, updating its message appropriately. - /// If the request is for a block, we will update the request message - /// as either a hash indicating a vote no or the signature hash indicating a vote yes - /// Returns whether the request is valid or not - fn validate_nonce_request(&mut self, nonce_request: &mut NonceRequest) -> bool { - let Some(block) = read_next::(&mut &nonce_request.message[..]).ok() - else { - // We currently reject anything that is not a block - debug!( - "Signer #{}: Received a nonce request for an unknown message stream. Reject it.", - self.signer_id - ); - return false; - }; - let signer_signature_hash = block.header.signer_signature_hash(); - let Some(block_info) = self.blocks.get_mut(&signer_signature_hash) else { - // We have not seen this block before. Cache it. Send a RPC to the stacks node to validate it. - debug!("Signer #{}: We have received a block sign request for a block we have not seen before. Cache the nonce request and submit the block for validation...", self.signer_id); - // Store the block in our cache - self.blocks.insert( - signer_signature_hash, - BlockInfo::new_with_request(block.clone(), nonce_request.clone()), - ); - self.stacks_client - .submit_block_for_validation(block) - .unwrap_or_else(|e| { - warn!( - "Signer #{}: Failed to submit block for validation: {:?}", - self.signer_id, e - ); - }); - return false; - }; - - if block_info.valid.is_none() { - // We have not yet received validation from the stacks node. Cache the request and wait for validation - debug!("Signer #{}: We have yet to receive validation from the stacks node for a nonce request. Cache the nonce request and wait for block validation...", self.signer_id); - block_info.nonce_request = Some(nonce_request.clone()); - return false; - } - - Self::determine_vote(self.signer_id, block_info, nonce_request); - true - } - - /// Verify the transactions in a block are as expected - fn verify_transactions(&mut self, block: &NakamotoBlock) -> bool { - if let Ok(expected_transactions) = self.get_expected_transactions() { - //It might be worth building a hashset of the blocks' txids and checking that against the expected transaction's txid. - let block_tx_hashset = block.txs.iter().map(|tx| tx.txid()).collect::>(); - // Ensure the block contains the transactions we expect - let missing_transactions = expected_transactions - .into_iter() - .filter_map(|tx| { - if !block_tx_hashset.contains(&tx.txid()) { - debug!( - "Signer #{}: expected txid {} is in the block", - self.signer_id, - &tx.txid() - ); - Some(tx) - } else { - debug!( - "Signer #{}: missing expected txid {}", - self.signer_id, - &tx.txid() - ); - None - } - }) - .collect::>(); - let is_valid = missing_transactions.is_empty(); - if !is_valid { - debug!("Signer #{}: Broadcasting a block rejection due to missing expected transactions...", self.signer_id); - let block_rejection = BlockRejection::new( - block.header.signer_signature_hash(), - RejectCode::MissingTransactions(missing_transactions), - ); - // Submit signature result to miners to observe - if let Err(e) = self - .stackerdb - .send_message_with_retry(block_rejection.into()) - { - warn!("Failed to send block submission to stacker-db: {:?}", e); - } - } - is_valid - } else { - // Failed to connect to the stacks node to get transactions. Cannot validate the block. Reject it. - debug!( - "Signer #{}: Broadcasting a block rejection due to signer connectivity issues...", - self.signer_id - ); - let block_rejection = BlockRejection::new( - block.header.signer_signature_hash(), - RejectCode::ConnectivityIssues, - ); - // Submit signature result to miners to observe - if let Err(e) = self - .stackerdb - .send_message_with_retry(block_rejection.into()) - { - warn!( - "Signer #{}: Failed to send block submission to stacker-db: {:?}", - self.signer_id, e - ); - } - false - } - } - - /// Get the transactions we expect to see in the next block - fn get_expected_transactions(&mut self) -> Result, ClientError> { - let signer_ids = self - .signing_round - .public_keys - .signers - .keys() - .cloned() - .collect::>(); - let transactions = self - .stackerdb - .get_signer_transactions_with_retry(&signer_ids)?.into_iter().filter_map(|transaction| { - // TODO: Filter out transactions that are not special cased transactions (cast votes, etc.) - // Filter out transactions that have already been confirmed (can happen if a signer did not update stacker db since the last block was processed) - let origin_address = transaction.origin_address(); - let origin_nonce = transaction.get_origin_nonce(); - let Ok(account_nonce) = self.stacks_client.get_account_nonce(&origin_address) else { - warn!("Signer #{}: Unable to get account for address: {origin_address}. Ignoring it for this block...", self.signer_id); - return None; - }; - if !self.signer_addresses.contains(&origin_address) || origin_nonce < account_nonce { - debug!("Signer #{}: Received a transaction for signer id ({}) that is either not valid or has already been confirmed (origin={}, account={}). Ignoring it.", self.signer_id, &origin_address, origin_nonce, account_nonce); - return None; - } - debug!("Signer #{}: Expect transaction {} ({:?})", self.signer_id, transaction.txid(), &transaction); - Some(transaction) - }).collect(); - Ok(transactions) - } - - /// Determine the vote for a block and update the block info and nonce request accordingly - fn determine_vote( - signer_id: u32, - block_info: &mut BlockInfo, - nonce_request: &mut NonceRequest, - ) { - let mut vote_bytes = block_info.block.header.signer_signature_hash().0.to_vec(); - // Validate the block contents - if !block_info.valid.unwrap_or(false) { - // We don't like this block. Update the request to be across its hash with a byte indicating a vote no. - debug!( - "Signer #{}: Updating the request with a block hash with a vote no.", - signer_id - ); - vote_bytes.push(b'n'); - } else { - debug!("Signer #{}: The block passed validation. Update the request with the signature hash.", signer_id); - } - - // Cache our vote - block_info.vote = Some(vote_bytes.clone()); - nonce_request.message = vote_bytes; - } - - /// Verify a chunk is a valid wsts packet. Returns the packet if it is valid, else None. - /// NOTE: The packet will be updated if the signer wishes to respond to NonceRequest - /// and SignatureShareRequests with a different message than what the coordinator originally sent. - /// This is done to prevent a malicious coordinator from sending a different message than what was - /// agreed upon and to support the case where the signer wishes to reject a block by voting no - fn verify_packet( - &mut self, - mut packet: Packet, - coordinator_public_key: &PublicKey, - ) -> Option { - // We only care about verified wsts packets. Ignore anything else. - if packet.verify(&self.signing_round.public_keys, coordinator_public_key) { - match &mut packet.msg { - Message::SignatureShareRequest(request) => { - if !self.validate_signature_share_request(request) { - return None; - } - } - Message::NonceRequest(request) => { - if !self.validate_nonce_request(request) { - return None; - } - } - _ => { - // Nothing to do for other message types - } - } - Some(packet) - } else { - debug!( - "Signer #{}: Failed to verify wsts packet with {}: {:?}", - self.signer_id, coordinator_public_key, &packet - ); - None - } - } - - /// Processes the operation results, broadcasting block acceptance or rejection messages - /// and DKG vote results accordingly - fn process_operation_results(&mut self, operation_results: &[OperationResult]) { - for operation_result in operation_results { - // Signers only every trigger non-taproot signing rounds over blocks. Ignore SignTaproot results - match operation_result { - OperationResult::Sign(signature) => { - debug!("Signer #{}: Received signature result", self.signer_id); - self.process_signature(signature); - } - OperationResult::SignTaproot(_) => { - debug!("Signer #{}: Received a signature result for a taproot signature. Nothing to broadcast as we currently sign blocks with a FROST signature.", self.signer_id); - } - OperationResult::Dkg(_point) => { - // TODO: cast the aggregate public key for the latest round here - // Broadcast via traditional methods to the stacks node if we are pre nakamoto or we cannot determine our Epoch - let epoch = self - .stacks_client - .get_node_epoch() - .unwrap_or(EpochId::UnsupportedEpoch); - match epoch { - EpochId::UnsupportedEpoch => { - debug!("Signer #{}: Received a DKG result, but are in an unsupported epoch. Do not broadcast the result.", self.signer_id); - } - EpochId::Epoch25 => { - debug!("Signer #{}: Received a DKG result, but are in epoch 2.5. Broadcast the transaction to the mempool.", self.signer_id); - //TODO: Cast the aggregate public key vote here - } - EpochId::Epoch30 => { - debug!("Signer #{}: Received a DKG result, but are in epoch 3. Broadcast the transaction to stackerDB.", self.signer_id); - let signer_message = - SignerMessage::Transactions(self.transactions.clone()); - if let Err(e) = self.stackerdb.send_message_with_retry(signer_message) { - warn!( - "Signer #{}: Failed to update transactions in stacker-db: {:?}", - self.signer_id, e - ); - } - } - } - } - OperationResult::SignError(e) => { - self.process_sign_error(e); - } - OperationResult::DkgError(e) => { - warn!("Signer #{}: Received a DKG error: {:?}", self.signer_id, e); - } - } - } - } - - /// Process a signature from a signing round by deserializing the signature and - /// broadcasting an appropriate Reject or Approval message to stackerdb - fn process_signature(&mut self, signature: &Signature) { - // Deserialize the signature result and broadcast an appropriate Reject or Approval message to stackerdb - let Some(aggregate_public_key) = &self.coordinator.get_aggregate_public_key() else { - debug!( - "Signer #{}: No aggregate public key set. Cannot validate signature...", - self.signer_id - ); - return; - }; - let message = self.coordinator.get_message(); - // This jankiness is because a coordinator could have signed a rejection we need to find the underlying block hash - let signer_signature_hash_bytes = if message.len() > 32 { - &message[..32] - } else { - &message - }; - let Some(signer_signature_hash) = - Sha512Trunc256Sum::from_bytes(signer_signature_hash_bytes) - else { - debug!("Signer #{}: Received a signature result for a signature over a non-block. Nothing to broadcast.", self.signer_id); - return; - }; - - // TODO: proper garbage collection...This is currently our only cleanup of blocks - self.blocks.remove(&signer_signature_hash); - - // This signature is no longer valid. Do not broadcast it. - if !signature.verify(aggregate_public_key, &message) { - warn!("Signer #{}: Received an invalid signature result across the block. Do not broadcast it.", self.signer_id); - // TODO: should we reinsert it and trigger a sign round across the block again? - return; - } - - let block_submission = if message == signer_signature_hash.0.to_vec() { - // we agreed to sign the block hash. Return an approval message - BlockResponse::accepted(signer_signature_hash, signature.clone()).into() - } else { - // We signed a rejection message. Return a rejection message - BlockResponse::rejected(signer_signature_hash, signature.clone()).into() - }; - - // Submit signature result to miners to observe - debug!( - "Signer #{}: submit block response {:?}", - self.signer_id, &block_submission - ); - if let Err(e) = self.stackerdb.send_message_with_retry(block_submission) { - warn!( - "Signer #{}: Failed to send block submission to stacker-db: {:?}", - self.signer_id, e - ); - } - } - - /// Process a sign error from a signing round, broadcasting a rejection message to stackerdb accordingly - fn process_sign_error(&mut self, e: &SignError) { - warn!("Received a signature error: {:?}", e); - match e { - SignError::NonceTimeout(_valid_signers, _malicious_signers) => { - //TODO: report these malicious signers - debug!("Signer #{}: Received a nonce timeout.", self.signer_id); - } - SignError::InsufficientSigners(malicious_signers) => { - debug!("Signer #{}: Insufficient signers.", self.signer_id); - let message = self.coordinator.get_message(); - let block = read_next::(&mut &message[..]).ok().unwrap_or({ - // This is not a block so maybe its across its hash - // This jankiness is because a coordinator could have signed a rejection we need to find the underlying block hash - let signer_signature_hash_bytes = if message.len() > 32 { - &message[..32] - } else { - &message - }; - let Some(signer_signature_hash) = Sha512Trunc256Sum::from_bytes(signer_signature_hash_bytes) else { - debug!("Signer #{}: Received a signature result for a signature over a non-block. Nothing to broadcast.", self.signer_id); - return; - }; - let Some(block_info) = self.blocks.remove(&signer_signature_hash) else { - debug!("Signer #{}: Received a signature result for a block we have not seen before. Ignoring...", self.signer_id); - return; - }; - block_info.block - }); - // We don't have enough signers to sign the block. Broadcast a rejection - let block_rejection = BlockRejection::new( - block.header.signer_signature_hash(), - RejectCode::InsufficientSigners(malicious_signers.clone()), - ); - debug!( - "Signer #{}: Insufficient signers for block; send rejection {:?}", - self.signer_id, &block_rejection - ); - - // Submit signature result to miners to observe - if let Err(e) = self - .stackerdb - .send_message_with_retry(block_rejection.into()) - { - warn!( - "Signer #{}: Failed to send block submission to stacker-db: {:?}", - self.signer_id, e - ); - } - } - SignError::Aggregator(e) => { - warn!( - "Signer #{}: Received an aggregator error: {:?}", - self.signer_id, e - ); - } - } - // TODO: should reattempt to sign the block here or should we just broadcast a rejection or do nothing and wait for the signers to propose a new block? - } - - /// Send any operation results across the provided channel - fn send_operation_results( - &mut self, - res: Sender>, - operation_results: Vec, - ) { - let nmb_results = operation_results.len(); - match res.send(operation_results) { - Ok(_) => { - debug!( - "Signer #{}: Successfully sent {} operation result(s)", - self.signer_id, nmb_results - ) - } - Err(e) => { - warn!( - "Signer #{}: Failed to send {} operation results: {:?}", - self.signer_id, nmb_results, e - ); - } - } - } - - /// Sending all provided packets through stackerdb with a retry - fn send_outbound_messages(&mut self, outbound_messages: Vec) { - debug!( - "Signer #{}: Sending {} messages to other stacker-db instances.", - self.signer_id, - outbound_messages.len() - ); - for msg in outbound_messages { - let ack = self.stackerdb.send_message_with_retry(msg.into()); - if let Ok(ack) = ack { - debug!("Signer #{}: send outbound ACK: {:?}", self.signer_id, ack); - } else { - warn!( - "Signer #{}: Failed to send message to stacker-db instance: {:?}", - self.signer_id, ack - ); - } - } - } -} - -impl From<&Config> for RunLoop> { +impl From for RunLoop { /// Creates new runloop from a config - fn from(config: &Config) -> Self { - // TODO: this should be a config option - // See: https://github.com/stacks-network/stacks-blockchain/issues/3914 - let threshold = ((config.signer_ids_public_keys.key_ids.len() * 7) / 10) - .try_into() - .unwrap(); - let dkg_threshold = ((config.signer_ids_public_keys.key_ids.len() * 9) / 10) - .try_into() - .unwrap(); - let total_signers = config - .signer_ids_public_keys - .signers - .len() - .try_into() - .unwrap(); - let total_keys = config - .signer_ids_public_keys - .key_ids - .len() - .try_into() - .unwrap(); - let key_ids = config - .signer_key_ids - .get(&config.signer_id) - .unwrap() - .clone(); - // signer uses a Vec for its key_ids, but coordinator uses a HashSet for each signer since it needs to do lots of lookups - let signer_key_ids = config - .signer_key_ids - .iter() - .map(|(i, ids)| (*i, ids.iter().copied().collect::>())) - .collect::>>(); - let signer_public_keys = config - .signer_ids_public_keys - .signers - .iter() - .map(|(i, ecdsa_key)| { - ( - *i, - Point::try_from(&Compressed::from(ecdsa_key.to_bytes())).unwrap(), - ) - }) - .collect::>(); + fn from(config: GlobalConfig) -> Self { + let stacks_client = StacksClient::from(&config); + RunLoop { + config, + stacks_client, + stacks_signers: HashMap::with_capacity(2), + state: State::Uninitialized, + commands: VecDeque::new(), + } + } +} - let coordinator_config = CoordinatorConfig { - threshold, - dkg_threshold, - num_signers: total_signers, - num_keys: total_keys, - message_private_key: config.ecdsa_private_key, - dkg_public_timeout: config.dkg_public_timeout, - dkg_private_timeout: config.dkg_private_timeout, - dkg_end_timeout: config.dkg_end_timeout, - nonce_timeout: config.nonce_timeout, - sign_timeout: config.sign_timeout, +impl RunLoop { + /// Parse Nakamoto signer entries into relevant signer information + pub fn parse_nakamoto_signer_entries( + signers: &[NakamotoSignerEntry], + is_mainnet: bool, + ) -> ParsedSignerEntries { + let mut weight_end = 1; + let mut coordinator_key_ids = HashMap::with_capacity(4000); + let mut signer_key_ids = HashMap::with_capacity(signers.len()); + let mut signer_ids = HashMap::with_capacity(signers.len()); + let mut public_keys = PublicKeys { + signers: HashMap::with_capacity(signers.len()), + key_ids: HashMap::with_capacity(4000), + }; + let mut signer_public_keys = HashMap::with_capacity(signers.len()); + for (i, entry) in signers.iter().enumerate() { + // TODO: track these signer ids as non participating if any of the conversions fail + let signer_id = u32::try_from(i).expect("FATAL: number of signers exceeds u32::MAX"); + let ecdsa_public_key = ecdsa::PublicKey::try_from(entry.signing_key.as_slice()) + .expect("FATAL: corrupted signing key"); + let signer_public_key = Point::try_from(&Compressed::from(ecdsa_public_key.to_bytes())) + .expect("FATAL: corrupted signing key"); + let stacks_public_key = StacksPublicKey::from_slice(entry.signing_key.as_slice()) + .expect("FATAL: Corrupted signing key"); + + let stacks_address = StacksAddress::p2pkh(is_mainnet, &stacks_public_key); + signer_ids.insert(stacks_address, signer_id); + signer_public_keys.insert(signer_id, signer_public_key); + let weight_start = weight_end; + weight_end = weight_start + entry.weight; + for key_id in weight_start..weight_end { + public_keys.key_ids.insert(key_id, ecdsa_public_key); + public_keys.signers.insert(signer_id, ecdsa_public_key); + coordinator_key_ids + .entry(signer_id) + .or_insert(HashSet::with_capacity(entry.weight as usize)) + .insert(key_id); + signer_key_ids + .entry(signer_id) + .or_insert(Vec::with_capacity(entry.weight as usize)) + .push(key_id); + } + } + ParsedSignerEntries { + signer_ids, + public_keys, signer_key_ids, signer_public_keys, - }; - let coordinator = FireCoordinator::new(coordinator_config); - let signing_round = Signer::new( - threshold, - total_signers, - total_keys, - config.signer_id, - key_ids, - config.ecdsa_private_key, - config.signer_ids_public_keys.clone(), - ); - let stacks_client = StacksClient::from(config); - let stackerdb = StackerDB::from(config); - RunLoop { - event_timeout: config.event_timeout, - coordinator, - signing_round, - stacks_client, - stackerdb, - commands: VecDeque::new(), - state: State::Uninitialized, - mainnet: config.network == Network::Mainnet, - blocks: HashMap::new(), - transactions: Vec::new(), - signer_ids: config.signer_ids.clone(), - signer_id: config.signer_id, - signer_slot_id: None, // will be updated on .initialize() - signer_set: None, // will be updated on .initialize() - signer_addresses: vec![], + coordinator_key_ids, } } + + /// Get the registered signers for a specific reward cycle + /// Returns None if no signers are registered or its not Nakamoto cycle + pub fn get_parsed_reward_set( + &self, + reward_cycle: u64, + ) -> Result, ClientError> { + debug!("Getting registered signers for reward cycle {reward_cycle}..."); + let Some(signers) = self.stacks_client.get_reward_set_signers(reward_cycle)? else { + warn!("No reward set signers found for reward cycle {reward_cycle}."); + return Ok(None); + }; + if signers.is_empty() { + warn!("No registered signers found for reward cycle {reward_cycle}."); + return Ok(None); + } + Ok(Some(Self::parse_nakamoto_signer_entries( + &signers, + self.config.network.is_mainnet(), + ))) + } + + /// Get the stackerdb signer slots for a specific reward cycle + pub fn get_parsed_signer_slots( + &self, + stacks_client: &StacksClient, + reward_cycle: u64, + ) -> Result, ClientError> { + let signer_set = + u32::try_from(reward_cycle % 2).expect("FATAL: reward_cycle % 2 exceeds u32::MAX"); + let signer_stackerdb_contract_id = + boot_code_id(SIGNERS_NAME, self.config.network.is_mainnet()); + // Get the signer writers from the stacker-db to find the signer slot id + let stackerdb_signer_slots = + stacks_client.get_stackerdb_signer_slots(&signer_stackerdb_contract_id, signer_set)?; + let mut signer_slot_ids = HashMap::with_capacity(stackerdb_signer_slots.len()); + for (index, (address, _)) in stackerdb_signer_slots.into_iter().enumerate() { + signer_slot_ids.insert( + address, + SignerSlotID( + u32::try_from(index).expect("FATAL: number of signers exceeds u32::MAX"), + ), + ); + } + Ok(signer_slot_ids) + } + /// Get a signer configuration for a specific reward cycle from the stacks node + fn get_signer_config(&mut self, reward_cycle: u64) -> Option { + // We can only register for a reward cycle if a reward set exists. + let signer_entries = self.get_parsed_reward_set(reward_cycle).ok()??; + let signer_slot_ids = self + .get_parsed_signer_slots(&self.stacks_client, reward_cycle) + .ok()?; + let current_addr = self.stacks_client.get_signer_address(); + + let Some(signer_slot_id) = signer_slot_ids.get(current_addr) else { + warn!( + "Signer {current_addr} was not found in stacker db. Must not be registered for this reward cycle {reward_cycle}." + ); + return None; + }; + let Some(signer_id) = signer_entries.signer_ids.get(current_addr) else { + warn!( + "Signer {current_addr} was found in stacker db but not the reward set for reward cycle {reward_cycle}." + ); + return None; + }; + info!( + "Signer #{signer_id} ({current_addr}) is registered for reward cycle {reward_cycle}." + ); + let key_ids = signer_entries + .signer_key_ids + .get(signer_id) + .cloned() + .unwrap_or_default(); + Some(SignerConfig { + reward_cycle, + signer_id: *signer_id, + signer_slot_id: *signer_slot_id, + key_ids, + signer_entries, + signer_slot_ids: signer_slot_ids.into_values().collect(), + ecdsa_private_key: self.config.ecdsa_private_key, + stacks_private_key: self.config.stacks_private_key, + node_host: self.config.node_host.to_string(), + mainnet: self.config.network.is_mainnet(), + dkg_end_timeout: self.config.dkg_end_timeout, + dkg_private_timeout: self.config.dkg_private_timeout, + dkg_public_timeout: self.config.dkg_public_timeout, + nonce_timeout: self.config.nonce_timeout, + sign_timeout: self.config.sign_timeout, + tx_fee_ustx: self.config.tx_fee_ustx, + }) + } + + /// Refresh signer configuration for a specific reward cycle + fn refresh_signer_config(&mut self, reward_cycle: u64) { + let reward_index = reward_cycle % 2; + let mut needs_refresh = false; + if let Some(signer) = self.stacks_signers.get_mut(&reward_index) { + let old_reward_cycle = signer.reward_cycle; + if old_reward_cycle == reward_cycle { + //If the signer is already registered for the reward cycle, we don't need to do anything further here + debug!("Signer is configured for reward cycle {reward_cycle}.") + } else { + needs_refresh = true; + } + } else { + needs_refresh = true; + }; + if needs_refresh { + if let Some(new_signer_config) = self.get_signer_config(reward_cycle) { + let signer_id = new_signer_config.signer_id; + debug!("Signer is registered for reward cycle {reward_cycle} as signer #{signer_id}. Initializing signer state."); + let prior_reward_cycle = reward_cycle.saturating_sub(1); + let prior_reward_set = prior_reward_cycle % 2; + if let Some(signer) = self.stacks_signers.get_mut(&prior_reward_set) { + if signer.reward_cycle == prior_reward_cycle { + // The signers have been calculated for the next reward cycle. Update the current one + debug!("Signer #{}: Next reward cycle ({reward_cycle}) signer set calculated. Updating current reward cycle ({prior_reward_cycle}) signer.", signer.signer_id); + signer.next_signer_addresses = new_signer_config + .signer_entries + .signer_ids + .keys() + .copied() + .collect(); + signer.next_signer_slot_ids = new_signer_config.signer_slot_ids.clone(); + } + } + self.stacks_signers + .insert(reward_index, Signer::from(new_signer_config)); + debug!("Signer #{signer_id} for reward cycle {reward_cycle} initialized. Initialized {} signers", self.stacks_signers.len()); + } else { + warn!("Signer is not registered for reward cycle {reward_cycle}. Waiting for confirmed registration..."); + } + } + } + + /// Refresh the signer configuration by retrieving the necessary information from the stacks node + /// Note: this will trigger DKG if required + fn refresh_signers(&mut self, current_reward_cycle: u64) -> Result<(), ClientError> { + let next_reward_cycle = current_reward_cycle.saturating_add(1); + self.refresh_signer_config(current_reward_cycle); + self.refresh_signer_config(next_reward_cycle); + // TODO: do not use an empty consensus hash + let pox_consensus_hash = ConsensusHash::empty(); + for signer in self.stacks_signers.values_mut() { + let old_coordinator_id = signer.coordinator_selector.get_coordinator().0; + let updated_coordinator_id = signer + .coordinator_selector + .refresh_coordinator(&pox_consensus_hash); + if old_coordinator_id != updated_coordinator_id { + debug!( + "Signer #{}: Coordinator updated. Resetting state to Idle.", signer.signer_id; + "old_coordinator_id" => {old_coordinator_id}, + "updated_coordinator_id" => {updated_coordinator_id}, + "pox_consensus_hash" => %pox_consensus_hash + ); + signer.coordinator.state = CoordinatorState::Idle; + signer.state = SignerState::Idle; + } + if signer.approved_aggregate_public_key.is_none() { + retry_with_exponential_backoff(|| { + signer + .update_dkg(&self.stacks_client) + .map_err(backoff::Error::transient) + })?; + } + } + if self.stacks_signers.is_empty() { + info!("Signer is not registered for the current {current_reward_cycle} or next {next_reward_cycle} reward cycles. Waiting for confirmed registration..."); + self.state = State::Uninitialized; + return Err(ClientError::NotRegistered); + } + self.state = State::Initialized; + info!("Runloop successfully initialized!"); + Ok(()) + } } -impl SignerRunLoop, RunLoopCommand> for RunLoop { +impl SignerRunLoop, RunLoopCommand> for RunLoop { fn set_event_timeout(&mut self, timeout: Duration) { - self.event_timeout = timeout; + self.config.event_timeout = timeout; } fn get_event_timeout(&self) -> Duration { - self.event_timeout + self.config.event_timeout } fn run_one_pass( @@ -1078,543 +327,99 @@ impl SignerRunLoop, RunLoopCommand> for Run cmd: Option, res: Sender>, ) -> Option> { - info!( - "Running one pass for signer ID# {}. Current state: {:?}", - self.signer_id, self.state + debug!( + "Running one pass for the signer. state={:?}, cmd={cmd:?}, event={event:?}", + self.state ); - if let Some(command) = cmd { - self.commands.push_back(command); + if let Some(cmd) = cmd { + self.commands.push_back(cmd); } - // TODO: This should be called every time as DKG can change at any time...but until we have the node - // set up to receive cast votes...just do on initialization. - if self.state == State::Uninitialized { - let request_fn = || { - self.initialize().map_err(|e| { - warn!("Failed to initialize: {:?}", &e); - backoff::Error::transient(e) - }) - }; - retry_with_exponential_backoff(request_fn) - .expect("Failed to connect to initialize due to timeout. Stacks node may be down."); + // TODO: queue events and process them potentially after initialization success (similar to commands)? + let Ok(current_reward_cycle) = retry_with_exponential_backoff(|| { + self.stacks_client + .get_current_reward_cycle() + .map_err(backoff::Error::transient) + }) else { + error!("Failed to retrieve current reward cycle"); + warn!("Ignoring event: {event:?}"); + return None; + }; + if let Err(e) = self.refresh_signers(current_reward_cycle) { + if self.state == State::Uninitialized { + // If we were never actually initialized, we cannot process anything. Just return. + warn!("Failed to initialize signers. Are you sure this signer is correctly registered for the current or next reward cycle?"); + warn!("Ignoring event: {event:?}"); + return None; + } + error!("Failed to refresh signers: {e}. Signer may have an outdated view of the network. Attempting to process event anyway."); } - // Process any arrived events - debug!("Signer #{}: Processing event: {:?}", self.signer_id, event); - match event { - Some(SignerEvent::BlockValidationResponse(block_validate_response)) => { - debug!( - "Signer #{}: Received a block proposal result from the stacks node...", - self.signer_id + for signer in self.stacks_signers.values_mut() { + if let Err(e) = signer.process_event( + &self.stacks_client, + event.as_ref(), + res.clone(), + current_reward_cycle, + ) { + error!( + "Signer #{} for reward cycle {} errored processing event: {e}", + signer.signer_id, signer.reward_cycle ); - self.handle_block_validate_response(block_validate_response, res) } - Some(SignerEvent::SignerMessages(messages)) => { - debug!( - "Signer #{}: Received {} messages from the other signers...", - self.signer_id, - messages.len() - ); - self.handle_signer_messages(res, messages); - } - Some(SignerEvent::ProposedBlocks(blocks)) => { - debug!( - "Signer #{}: Received {} block proposals from the miners...", - self.signer_id, - blocks.len() - ); - self.handle_proposed_blocks(blocks); - } - Some(SignerEvent::StatusCheck) => { - debug!("Signer #{}: Received a status check event.", self.signer_id) - } - None => { - // No event. Do nothing. - debug!("Signer #{}: No event received", self.signer_id) + if let Some(command) = self.commands.pop_front() { + let reward_cycle = command.reward_cycle; + if signer.reward_cycle != reward_cycle { + warn!( + "Signer #{}: not registered for reward cycle {reward_cycle}. Ignoring command: {command:?}", signer.signer_id + ); + } else { + info!( + "Signer #{}: Queuing an external runloop command ({:?}): {command:?}", + signer.signer_id, + signer + .signing_round + .public_keys + .signers + .get(&signer.signer_id) + ); + signer.commands.push_back(command.command); + } } + // After processing event, run the next command for each signer + signer.process_next_command(&self.stacks_client); } - - // The process the next command - // Must be called AFTER processing the event as the state may update to IDLE due to said event. - self.process_next_command(); None } } - -/// Helper function for determining the coordinator public key given the the public keys -pub fn calculate_coordinator( - public_keys: &PublicKeys, - stacks_client: &StacksClient, -) -> (u32, ecdsa::PublicKey) { - let stacks_tip_consensus_hash = match stacks_client.get_stacks_tip_consensus_hash() { - Ok(hash) => hash, - Err(e) => { - error!("Error in fetching consensus hash: {:?}", e); - return (0, public_keys.signers.get(&0).cloned().unwrap()); - } - }; - debug!( - "Using stacks_tip_consensus_hash {:?} for selecting coordinator", - &stacks_tip_consensus_hash - ); - - // Create combined hash of each signer's public key with stacks_tip_consensus_hash - let mut selection_ids = public_keys - .signers - .iter() - .map(|(&id, pk)| { - let pk_bytes = pk.to_bytes(); - let mut buffer = - Vec::with_capacity(pk_bytes.len() + stacks_tip_consensus_hash.as_bytes().len()); - buffer.extend_from_slice(&pk_bytes[..]); - buffer.extend_from_slice(stacks_tip_consensus_hash.as_bytes()); - let digest = Sha256Sum::from_data(&buffer).as_bytes().to_vec(); - (digest, id) - }) - .collect::>(); - - // Sort the selection IDs based on the hash - selection_ids.sort_by_key(|(hash, _)| hash.clone()); - - // Get the first ID from the sorted list and retrieve its public key, - // or default to the first signer if none are found - selection_ids - .first() - .and_then(|(_, id)| public_keys.signers.get(id).map(|pk| (*id, *pk))) - .unwrap_or((0, public_keys.signers.get(&0).cloned().unwrap())) -} - #[cfg(test)] mod tests { - use std::fmt::Write; - use std::net::TcpListener; - use std::thread::{sleep, spawn}; + use blockstack_lib::chainstate::stacks::boot::NakamotoSignerEntry; + use stacks_common::types::chainstate::{StacksPrivateKey, StacksPublicKey}; - use blockstack_lib::chainstate::nakamoto::NakamotoBlockHeader; - use blockstack_lib::chainstate::stacks::boot::SIGNERS_VOTING_NAME; - use blockstack_lib::chainstate::stacks::{ThresholdSignature, TransactionVersion}; - use blockstack_lib::util_lib::boot::boot_code_addr; - use clarity::vm::types::{ResponseData, TupleData}; - use clarity::vm::{ClarityName, Value as ClarityValue}; - use rand::distributions::Standard; - use rand::Rng; - use serial_test::serial; - use stacks_common::bitvec::BitVec; - use stacks_common::consts::SIGNER_SLOTS_PER_USER; - use stacks_common::types::chainstate::{ - ConsensusHash, StacksBlockId, StacksPrivateKey, TrieHash, - }; - use stacks_common::util::hash::{Hash160, MerkleTree}; - use stacks_common::util::secp256k1::MessageSignature; - use wsts::curve::point::Point; - use wsts::curve::scalar::Scalar; - - use super::*; - use crate::client::tests::{write_response, TestConfig}; - - fn generate_random_consensus_hash() -> String { - let rng = rand::thread_rng(); - let bytes: Vec = rng.sample_iter(Standard).take(20).collect(); - let hex_string = bytes.iter().fold(String::new(), |mut acc, &b| { - write!(&mut acc, "{:02x}", b).expect("Error writing to string"); - acc - }); - hex_string - } - - fn mock_stacks_client_response(mock_server: TcpListener, random_consensus: bool) { - let consensus_hash = match random_consensus { - true => generate_random_consensus_hash(), - false => "64c8c3049ff6b939c65828e3168210e6bb32d880".to_string(), - }; - - println!("{}", consensus_hash); - let response = format!( - "HTTP/1.1 200 OK\n\n{{\"stacks_tip_consensus_hash\":\"{}\",\"peer_version\":4207599113,\"pox_consensus\":\"64c8c3049ff6b939c65828e3168210e6bb32d880\",\"burn_block_height\":2575799,\"stable_pox_consensus\":\"72277bf9a3b115e13c0942825480d6cee0e9a0e8\",\"stable_burn_block_height\":2575792,\"server_version\":\"stacks-node d657bdd (feat/epoch-2.4:d657bdd, release build, linux [x86_64])\",\"network_id\":2147483648,\"parent_network_id\":118034699,\"stacks_tip_height\":145152,\"stacks_tip\":\"77219884fe434c0fa270d65592b4f082ab3e5d9922ac2bdaac34310aedc3d298\",\"genesis_chainstate_hash\":\"74237aa39aa50a83de11a4f53e9d3bb7d43461d1de9873f402e5453ae60bc59b\",\"unanchored_tip\":\"dde44222b6e6d81583b6b9c55db83e8716943ae9d0dc332fc39448ddd9b99dc2\",\"unanchored_seq\":0,\"exit_at_block_height\":null,\"node_public_key\":\"023c940136d5795d9dd82c0e87f4dd6a2a1db245444e7d70e34bb9605c3c3917b0\",\"node_public_key_hash\":\"e26cce8f6abe06b9fc81c3b11bcc821d2f1b8fd0\"}}", - consensus_hash - ); - - spawn(move || { - write_response(mock_server, response.as_bytes()); - }); - sleep(Duration::from_millis(100)); - } + use super::RunLoop; #[test] - fn calculate_coordinator_should_produce_unique_results() { - let config = Config::load_from_file("./src/tests/conf/signer-0.toml").unwrap(); - let number_of_tests = 5; - - let mut results = Vec::new(); - - for _ in 0..number_of_tests { - let test_config = TestConfig::new(); - mock_stacks_client_response(test_config.mock_server, true); - - let (coordinator_id, coordinator_public_key) = - calculate_coordinator(&config.signer_ids_public_keys, &test_config.client); - - results.push((coordinator_id, coordinator_public_key)); + fn parse_nakamoto_signer_entries_test() { + let nmb_signers = 10; + let weight = 10; + let mut signer_entries = Vec::with_capacity(nmb_signers); + for _ in 0..nmb_signers { + let key = StacksPublicKey::from_private(&StacksPrivateKey::new()).to_bytes_compressed(); + let mut signing_key = [0u8; 33]; + signing_key.copy_from_slice(&key); + signer_entries.push(NakamotoSignerEntry { + signing_key, + stacked_amt: 0, + weight, + }); } - // Check that not all coordinator IDs are the same - let all_ids_same = results.iter().all(|&(id, _)| id == results[0].0); - assert!(!all_ids_same, "Not all coordinator IDs should be the same"); - - // Check that not all coordinator public keys are the same - let all_keys_same = results - .iter() - .all(|&(_, key)| key.key.data == results[0].1.key.data); - assert!( - !all_keys_same, - "Not all coordinator public keys should be the same" + let parsed_entries = RunLoop::parse_nakamoto_signer_entries(&signer_entries, false); + assert_eq!(parsed_entries.signer_ids.len(), nmb_signers); + let mut signer_ids = parsed_entries.signer_ids.into_values().collect::>(); + signer_ids.sort(); + assert_eq!( + signer_ids, + (0..nmb_signers).map(|id| id as u32).collect::>() ); } - fn generate_test_results(random_consensus: bool, count: usize) -> Vec<(u32, ecdsa::PublicKey)> { - let mut results = Vec::new(); - let config = Config::load_from_file("./src/tests/conf/signer-0.toml").unwrap(); - - for _ in 0..count { - let test_config = TestConfig::new(); - mock_stacks_client_response(test_config.mock_server, random_consensus); - let result = calculate_coordinator(&config.signer_ids_public_keys, &test_config.client); - results.push(result); - } - results - } - - #[test] - fn calculate_coordinator_results_should_vary_or_match_based_on_hash() { - let results_with_random_hash = generate_test_results(true, 5); - let all_ids_same = results_with_random_hash - .iter() - .all(|&(id, _)| id == results_with_random_hash[0].0); - let all_keys_same = results_with_random_hash - .iter() - .all(|&(_, key)| key.key.data == results_with_random_hash[0].1.key.data); - assert!(!all_ids_same, "Not all coordinator IDs should be the same"); - assert!( - !all_keys_same, - "Not all coordinator public keys should be the same" - ); - - let results_with_static_hash = generate_test_results(false, 5); - let all_ids_same = results_with_static_hash - .iter() - .all(|&(id, _)| id == results_with_static_hash[0].0); - let all_keys_same = results_with_static_hash - .iter() - .all(|&(_, key)| key.key.data == results_with_static_hash[0].1.key.data); - assert!(all_ids_same, "All coordinator IDs should be the same"); - assert!( - all_keys_same, - "All coordinator public keys should be the same" - ); - } - - fn build_get_signer_slots_response(config: &Config) -> String { - let mut signers_public_keys = config - .signer_ids_public_keys - .signers - .iter() - .map(|(signer_id, signer_public_key)| { - let bytes = signer_public_key.to_bytes(); - let signer_hash = Hash160::from_data(&bytes); - let signing_address = StacksAddress::p2pkh_from_hash(false, signer_hash); - (signer_id, signing_address) - }) - .collect::>(); - signers_public_keys.sort_by(|(a, _), (b, _)| a.cmp(b)); - - let mut list_data = vec![]; - for (_, signers_public_key) in signers_public_keys { - let tuple_data = vec![ - ( - ClarityName::from("signer"), - ClarityValue::Principal(signers_public_key.into()), - ), - ( - ClarityName::from("num-slots"), - ClarityValue::UInt(SIGNER_SLOTS_PER_USER as u128), - ), - ]; - let tuple = ClarityValue::Tuple( - TupleData::from_data(tuple_data).expect("Failed to create tuple data"), - ); - list_data.push(tuple); - } - - let result_data = - ClarityValue::cons_list_unsanitized(list_data).expect("Failed to construct list data"); - let response_clarity = ClarityValue::Response(ResponseData { - committed: true, - data: Box::new(result_data), - }); - let hex = response_clarity - .serialize_to_hex() - .expect("Failed to serialize clarity value"); - format!("HTTP/1.1 200 OK\n\n{{\"okay\":true,\"result\":\"{hex}\"}}") - } - - fn build_get_aggregate_public_key_response_some() -> (String, String) { - let current_reward_cycle_response = "HTTP/1.1 200 Ok\n\n{\"contract_id\":\"ST000000000000000000002AMW42H.pox-3\",\"pox_activation_threshold_ustx\":829371801288885,\"first_burnchain_block_height\":2000000,\"current_burnchain_block_height\":2572192,\"prepare_phase_block_length\":50,\"reward_phase_block_length\":1000,\"reward_slots\":2000,\"rejection_fraction\":12,\"total_liquid_supply_ustx\":41468590064444294,\"current_cycle\":{\"id\":544,\"min_threshold_ustx\":5190000000000,\"stacked_ustx\":853258144644000,\"is_pox_active\":true},\"next_cycle\":{\"id\":545,\"min_threshold_ustx\":5190000000000,\"min_increment_ustx\":5183573758055,\"stacked_ustx\":847278759574000,\"prepare_phase_start_block_height\":2572200,\"blocks_until_prepare_phase\":8,\"reward_phase_start_block_height\":2572250,\"blocks_until_reward_phase\":58,\"ustx_until_pox_rejection\":4976230807733304},\"min_amount_ustx\":5190000000000,\"prepare_cycle_length\":50,\"reward_cycle_id\":544,\"reward_cycle_length\":1050,\"rejection_votes_left_required\":4976230807733304,\"next_reward_cycle_in\":58,\"contract_versions\":[{\"contract_id\":\"ST000000000000000000002AMW42H.pox\",\"activation_burnchain_block_height\":2000000,\"first_reward_cycle_id\":0},{\"contract_id\":\"ST000000000000000000002AMW42H.pox-2\",\"activation_burnchain_block_height\":2422102,\"first_reward_cycle_id\":403},{\"contract_id\":\"ST000000000000000000002AMW42H.pox-3\",\"activation_burnchain_block_height\":2432545,\"first_reward_cycle_id\":412}]}".to_string(); - let orig_point = Point::from(Scalar::random(&mut rand::thread_rng())); - let clarity_value = ClarityValue::some( - ClarityValue::buff_from(orig_point.compress().as_bytes().to_vec()) - .expect("BUG: Failed to create clarity value from point"), - ) - .expect("BUG: Failed to create clarity value from point"); - let hex = clarity_value - .serialize_to_hex() - .expect("Failed to serialize clarity value"); - let point_response = format!("HTTP/1.1 200 OK\n\n{{\"okay\":true,\"result\":\"{hex}\"}}"); - - (current_reward_cycle_response, point_response) - } - - fn simulate_initialize_response(config: Config) { - let (current_reward_cycle_response, aggregate_key_response) = - build_get_aggregate_public_key_response_some(); - let signer_slots_response = build_get_signer_slots_response(&config); - let test_config = TestConfig::from_config(config.clone()); - write_response( - test_config.mock_server, - current_reward_cycle_response.as_bytes(), - ); - let test_config = TestConfig::from_config(config.clone()); - write_response(test_config.mock_server, signer_slots_response.as_bytes()); - - let test_config = TestConfig::from_config(config.clone()); - write_response( - test_config.mock_server, - current_reward_cycle_response.as_bytes(), - ); - - let test_config = TestConfig::from_config(config.clone()); - write_response(test_config.mock_server, aggregate_key_response.as_bytes()); - } - - fn simulate_nonce_response(config: &Config, num_transactions: usize) { - for _ in 0..num_transactions { - let nonce_response = b"HTTP/1.1 200 OK\n\n{\"nonce\":1,\"balance\":\"0x00000000000000000000000000000000\",\"locked\":\"0x00000000000000000000000000000000\",\"unlock_height\":0}"; - let test_config = TestConfig::from_config(config.clone()); - write_response(test_config.mock_server, nonce_response); - } - } - - #[test] - #[serial] - // TODO(CI): This test function stalls in CI. Ignoring for now, but this test needs to be fixed. - #[ignore] - fn get_expected_transactions_should_filter_invalid_transactions() { - // Create a runloop of a valid signer - let config = Config::load_from_file("./src/tests/conf/signer-0.toml").unwrap(); - let mut valid_signer_runloop: RunLoop> = - RunLoop::from(&config); - - let signer_private_key = config.stacks_private_key; - let non_signer_private_key = StacksPrivateKey::new(); - let signers_contract_addr = boot_code_addr(false); - // Create a valid transaction signed by the signer private key coresponding to the slot into which it is being inserted (signer id 0) - // TODO use cast_aggregate_vote_tx fn to create a valid transaction when it is implmented and update this test - let valid_tx = StacksClient::build_signed_contract_call_transaction( - &signers_contract_addr, - SIGNERS_VOTING_NAME.into(), - "fake-function".into(), - &[], - &signer_private_key, - TransactionVersion::Testnet, - config.network.to_chain_id(), - 1, - 10, - ) - .unwrap(); - let invalid_tx_bad_signer = StacksClient::build_signed_contract_call_transaction( - &signers_contract_addr, - SIGNERS_VOTING_NAME.into(), - "fake-function".into(), - &[], - &non_signer_private_key, - TransactionVersion::Testnet, - config.network.to_chain_id(), - 0, - 10, - ) - .unwrap(); - let invalid_tx_outdated_nonce = StacksClient::build_signed_contract_call_transaction( - &signers_contract_addr, - SIGNERS_VOTING_NAME.into(), - "fake-function".into(), - &[], - &signer_private_key, - TransactionVersion::Testnet, - config.network.to_chain_id(), - 0, - 5, - ) - .unwrap(); - - let transactions = vec![ - valid_tx.clone(), - invalid_tx_outdated_nonce, - invalid_tx_bad_signer, - ]; - let num_transactions = transactions.len(); - - let h = spawn(move || { - valid_signer_runloop.initialize().unwrap(); - valid_signer_runloop.get_expected_transactions().unwrap() - }); - - // Must initialize the signers before attempting to retrieve their transactions - simulate_initialize_response(config.clone()); - - // Simulate the response to the request for transactions - let signer_message = SignerMessage::Transactions(transactions); - let message = signer_message.serialize_to_vec(); - let mut response_bytes = b"HTTP/1.1 200 OK\n\n".to_vec(); - response_bytes.extend(message); - let test_config = TestConfig::from_config(config.clone()); - write_response(test_config.mock_server, response_bytes.as_slice()); - - let signer_message = SignerMessage::Transactions(vec![]); - let message = signer_message.serialize_to_vec(); - let mut response_bytes = b"HTTP/1.1 200 OK\n\n".to_vec(); - response_bytes.extend(message); - let test_config = TestConfig::from_config(config.clone()); - write_response(test_config.mock_server, response_bytes.as_slice()); - - let signer_message = SignerMessage::Transactions(vec![]); - let message = signer_message.serialize_to_vec(); - let mut response_bytes = b"HTTP/1.1 200 OK\n\n".to_vec(); - response_bytes.extend(message); - let test_config = TestConfig::from_config(config.clone()); - write_response(test_config.mock_server, response_bytes.as_slice()); - - let signer_message = SignerMessage::Transactions(vec![]); - let message = signer_message.serialize_to_vec(); - let mut response_bytes = b"HTTP/1.1 200 OK\n\n".to_vec(); - response_bytes.extend(message); - let test_config = TestConfig::from_config(config.clone()); - write_response(test_config.mock_server, response_bytes.as_slice()); - - let signer_message = SignerMessage::Transactions(vec![]); - let message = signer_message.serialize_to_vec(); - let mut response_bytes = b"HTTP/1.1 200 OK\n\n".to_vec(); - response_bytes.extend(message); - let test_config = TestConfig::from_config(config.clone()); - write_response(test_config.mock_server, response_bytes.as_slice()); - - simulate_nonce_response(&config, num_transactions); - - let filtered_txs = h.join().unwrap(); - assert_eq!(filtered_txs, vec![valid_tx]); - } - - #[test] - #[serial] - #[ignore] - fn verify_transactions_valid() { - let config = Config::load_from_file("./src/tests/conf/signer-0.toml").unwrap(); - let mut runloop: RunLoop> = RunLoop::from(&config); - - let signer_private_key = config.stacks_private_key; - let signers_contract_addr = boot_code_addr(false); - // Create a valid transaction signed by the signer private key coresponding to the slot into which it is being inserted (signer id 0) - // TODO use cast_aggregate_vote_tx fn to create a valid transaction when it is implmented and update this test - let valid_tx = StacksClient::build_signed_contract_call_transaction( - &signers_contract_addr, - SIGNERS_VOTING_NAME.into(), - "fake-function".into(), - &[], - &signer_private_key, - TransactionVersion::Testnet, - config.network.to_chain_id(), - 1, - 10, - ) - .unwrap(); - - // Create a block - let header = NakamotoBlockHeader { - version: 1, - chain_length: 2, - burn_spent: 3, - consensus_hash: ConsensusHash([0x04; 20]), - parent_block_id: StacksBlockId([0x05; 32]), - tx_merkle_root: Sha512Trunc256Sum([0x06; 32]), - state_index_root: TrieHash([0x07; 32]), - miner_signature: MessageSignature::empty(), - signer_signature: ThresholdSignature::empty(), - signer_bitvec: BitVec::zeros(1).unwrap(), - }; - let mut block = NakamotoBlock { - header, - txs: vec![valid_tx.clone()], - }; - let tx_merkle_root = { - let txid_vecs = block - .txs - .iter() - .map(|tx| tx.txid().as_bytes().to_vec()) - .collect(); - - MerkleTree::::new(&txid_vecs).root() - }; - block.header.tx_merkle_root = tx_merkle_root; - - // Ensure this is a block the signer has seen already - runloop.blocks.insert( - block.header.signer_signature_hash(), - BlockInfo::new(block.clone()), - ); - - let h = spawn(move || { - runloop.initialize().unwrap(); - runloop.verify_transactions(&block) - }); - - // Must initialize the signers before attempting to retrieve their transactions - simulate_initialize_response(config.clone()); - - // Simulate the response to the request for transactions with the expected transaction - let signer_message = SignerMessage::Transactions(vec![valid_tx]); - let message = signer_message.serialize_to_vec(); - let mut response_bytes = b"HTTP/1.1 200 OK\n\n".to_vec(); - response_bytes.extend(message); - let test_config = TestConfig::from_config(config.clone()); - write_response(test_config.mock_server, response_bytes.as_slice()); - - let signer_message = SignerMessage::Transactions(vec![]); - let message = signer_message.serialize_to_vec(); - let mut response_bytes = b"HTTP/1.1 200 OK\n\n".to_vec(); - response_bytes.extend(message); - let test_config = TestConfig::from_config(config.clone()); - write_response(test_config.mock_server, response_bytes.as_slice()); - - let signer_message = SignerMessage::Transactions(vec![]); - let message = signer_message.serialize_to_vec(); - let mut response_bytes = b"HTTP/1.1 200 OK\n\n".to_vec(); - response_bytes.extend(message); - let test_config = TestConfig::from_config(config.clone()); - write_response(test_config.mock_server, response_bytes.as_slice()); - - let signer_message = SignerMessage::Transactions(vec![]); - let message = signer_message.serialize_to_vec(); - let mut response_bytes = b"HTTP/1.1 200 OK\n\n".to_vec(); - response_bytes.extend(message); - let test_config = TestConfig::from_config(config.clone()); - write_response(test_config.mock_server, response_bytes.as_slice()); - - let signer_message = SignerMessage::Transactions(vec![]); - let message = signer_message.serialize_to_vec(); - let mut response_bytes = b"HTTP/1.1 200 OK\n\n".to_vec(); - response_bytes.extend(message); - let test_config = TestConfig::from_config(config.clone()); - write_response(test_config.mock_server, response_bytes.as_slice()); - - simulate_nonce_response(&config, 1); - //simulate_send_message_with_retry_response(config.clone()); - - let valid = h.join().unwrap(); - assert!(valid); - } } diff --git a/stacks-signer/src/signer.rs b/stacks-signer/src/signer.rs new file mode 100644 index 000000000..59962e5ae --- /dev/null +++ b/stacks-signer/src/signer.rs @@ -0,0 +1,1225 @@ +// 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 . +use std::collections::VecDeque; +use std::sync::mpsc::Sender; +use std::time::Instant; + +use blockstack_lib::chainstate::nakamoto::signer_set::NakamotoSigners; +use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockVote}; +use blockstack_lib::chainstate::stacks::boot::SIGNERS_VOTING_FUNCTION_NAME; +use blockstack_lib::chainstate::stacks::StacksTransaction; +use blockstack_lib::net::api::postblock_proposal::BlockValidateResponse; +use hashbrown::{HashMap, HashSet}; +use libsigner::{BlockRejection, BlockResponse, RejectCode, SignerEvent, SignerMessage}; +use slog::{slog_debug, slog_error, slog_info, slog_warn}; +use stacks_common::codec::{read_next, StacksMessageCodec}; +use stacks_common::types::chainstate::StacksAddress; +use stacks_common::types::StacksEpochId; +use stacks_common::util::hash::Sha512Trunc256Sum; +use stacks_common::{debug, error, info, warn}; +use wsts::common::{MerkleRoot, Signature}; +use wsts::curve::keys::PublicKey; +use wsts::curve::point::Point; +use wsts::net::{Message, NonceRequest, Packet, SignatureShareRequest}; +use wsts::state_machine::coordinator::fire::Coordinator as FireCoordinator; +use wsts::state_machine::coordinator::{ + Config as CoordinatorConfig, Coordinator, State as CoordinatorState, +}; +use wsts::state_machine::signer::Signer as WSTSSigner; +use wsts::state_machine::{OperationResult, SignError}; +use wsts::v2; + +use crate::client::{retry_with_exponential_backoff, ClientError, StackerDB, StacksClient}; +use crate::config::SignerConfig; +use crate::coordinator::CoordinatorSelector; + +/// The signer StackerDB slot ID, purposefully wrapped to prevent conflation with SignerID +#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy, PartialOrd, Ord)] +pub struct SignerSlotID(pub u32); + +impl std::fmt::Display for SignerSlotID { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Additional Info about a proposed block +pub struct BlockInfo { + /// The block we are considering + block: NakamotoBlock, + /// Our vote on the block if we have one yet + vote: Option, + /// Whether the block contents are valid + valid: Option, + /// The associated packet nonce request if we have one + nonce_request: Option, + /// Whether this block is already being signed over + signed_over: bool, +} + +impl BlockInfo { + /// Create a new BlockInfo + pub fn new(block: NakamotoBlock) -> Self { + Self { + block, + vote: None, + valid: None, + nonce_request: None, + signed_over: false, + } + } + + /// Create a new BlockInfo with an associated nonce request packet + pub fn new_with_request(block: NakamotoBlock, nonce_request: NonceRequest) -> Self { + Self { + block, + vote: None, + valid: None, + nonce_request: Some(nonce_request), + signed_over: true, + } + } +} + +/// Which signer operation to perform +#[derive(PartialEq, Clone, Debug)] +pub enum Command { + /// Generate a DKG aggregate public key + Dkg, + /// Sign a message + Sign { + /// The block to sign over + block: NakamotoBlock, + /// Whether to make a taproot signature + is_taproot: bool, + /// Taproot merkle root + merkle_root: Option, + }, +} + +/// The Signer state +#[derive(PartialEq, Debug, Clone)] +pub enum State { + /// The signer is idle, waiting for messages and commands + Idle, + /// The signer is executing a DKG or Sign round + OperationInProgress, +} + +/// The stacks signer registered for the reward cycle +pub struct Signer { + /// The coordinator for inbound messages for a specific reward cycle + pub coordinator: FireCoordinator, + /// The signing round used to sign messages for a specific reward cycle + pub signing_round: WSTSSigner, + /// the state of the signer + pub state: State, + /// Observed blocks that we have seen so far + // TODO: cleanup storage and garbage collect this stuff + pub blocks: HashMap, + /// Received Commands that need to be processed + pub commands: VecDeque, + /// The stackerdb client + pub stackerdb: StackerDB, + /// Whether the signer is a mainnet signer or not + pub mainnet: bool, + /// The signer id + pub signer_id: u32, + /// The signer slot ids for the signers in the reward cycle + pub signer_slot_ids: Vec, + /// The addresses of other signers + pub signer_addresses: Vec, + /// The signer slot ids for the signers in the NEXT reward cycle + pub next_signer_slot_ids: Vec, + /// The addresses of the signers for the NEXT reward cycle + pub next_signer_addresses: Vec, + /// The reward cycle this signer belongs to + pub reward_cycle: u64, + /// The tx fee in uSTX to use if the epoch is pre Nakamoto (Epoch 3.0) + pub tx_fee_ustx: u64, + /// The coordinator info for the signer + pub coordinator_selector: CoordinatorSelector, + /// The approved key registered to the contract + pub approved_aggregate_public_key: Option, +} + +impl From for Signer { + fn from(signer_config: SignerConfig) -> Self { + let stackerdb = StackerDB::from(&signer_config); + + let num_signers = u32::try_from(signer_config.signer_entries.public_keys.signers.len()) + .expect("FATAL: Too many registered signers to fit in a u32"); + let num_keys = u32::try_from(signer_config.signer_entries.public_keys.key_ids.len()) + .expect("FATAL: Too many key ids to fit in a u32"); + let threshold = (num_keys as f64 * 7_f64 / 10_f64).ceil() as u32; + let dkg_threshold = (num_keys as f64 * 9_f64 / 10_f64).ceil() as u32; + + let coordinator_config = CoordinatorConfig { + threshold, + dkg_threshold, + num_signers, + num_keys, + message_private_key: signer_config.ecdsa_private_key, + dkg_public_timeout: signer_config.dkg_public_timeout, + dkg_private_timeout: signer_config.dkg_private_timeout, + dkg_end_timeout: signer_config.dkg_end_timeout, + nonce_timeout: signer_config.nonce_timeout, + sign_timeout: signer_config.sign_timeout, + signer_key_ids: signer_config.signer_entries.coordinator_key_ids, + signer_public_keys: signer_config.signer_entries.signer_public_keys, + }; + + let coordinator = FireCoordinator::new(coordinator_config); + let signing_round = WSTSSigner::new( + threshold, + num_signers, + num_keys, + signer_config.signer_id, + signer_config.key_ids, + signer_config.ecdsa_private_key, + signer_config.signer_entries.public_keys.clone(), + ); + let coordinator_selector = + CoordinatorSelector::from(signer_config.signer_entries.public_keys); + + debug!( + "Signer #{}: initial coordinator is signer {}", + signer_config.signer_id, + coordinator_selector.get_coordinator().0 + ); + + Self { + coordinator, + signing_round, + state: State::Idle, + blocks: HashMap::new(), + commands: VecDeque::new(), + stackerdb, + mainnet: signer_config.mainnet, + signer_id: signer_config.signer_id, + signer_addresses: signer_config + .signer_entries + .signer_ids + .into_keys() + .collect(), + signer_slot_ids: signer_config.signer_slot_ids.clone(), + next_signer_slot_ids: vec![], + next_signer_addresses: vec![], + reward_cycle: signer_config.reward_cycle, + tx_fee_ustx: signer_config.tx_fee_ustx, + coordinator_selector, + approved_aggregate_public_key: None, + } + } +} + +impl Signer { + /// Finish an operation and update the coordinator selector accordingly + fn finish_operation(&mut self) { + self.state = State::Idle; + self.coordinator_selector.last_message_time = None; + } + + /// Update operation + fn update_operation(&mut self) { + self.state = State::OperationInProgress; + self.coordinator_selector.last_message_time = Some(Instant::now()); + } + + /// Execute the given command and update state accordingly + fn execute_command(&mut self, stacks_client: &StacksClient, command: &Command) { + match command { + Command::Dkg => { + if self.approved_aggregate_public_key.is_some() { + debug!("Signer #{}: Already have an aggregate key for reward cycle {}. Ignoring DKG command.", self.signer_id, self.reward_cycle); + return; + } + let vote_round = match retry_with_exponential_backoff(|| { + stacks_client + .get_last_round(self.reward_cycle) + .map_err(backoff::Error::transient) + }) { + Ok(last_round) => last_round, + Err(e) => { + error!("Signer #{}: Unable to perform DKG. Failed to get last round from stacks node: {e:?}", self.signer_id); + return; + } + }; + // The dkg id will increment internally following "start_dkg_round" so do not increment it here + self.coordinator.current_dkg_id = vote_round.unwrap_or(0); + info!( + "Signer #{}: Starting DKG vote", + self.signer_id; + "round" => self.coordinator.current_dkg_id.wrapping_add(1), + "cycle" => self.reward_cycle, + ); + match self.coordinator.start_dkg_round() { + Ok(msg) => { + let ack = self.stackerdb.send_message_with_retry(msg.into()); + debug!("Signer #{}: ACK: {ack:?}", self.signer_id); + } + Err(e) => { + error!("Signer #{}: Failed to start DKG: {e:?}", self.signer_id); + return; + } + } + } + Command::Sign { + block, + is_taproot, + merkle_root, + } => { + if self.approved_aggregate_public_key.is_none() { + debug!("Signer #{}: Cannot sign a block without an approved aggregate public key. Ignore it.", self.signer_id); + return; + } + let signer_signature_hash = block.header.signer_signature_hash(); + let block_info = self + .blocks + .entry(signer_signature_hash) + .or_insert_with(|| BlockInfo::new(block.clone())); + if block_info.signed_over { + debug!("Signer #{}: Received a sign command for a block we are already signing over. Ignore it.", self.signer_id); + return; + } + info!("Signer #{}: Signing block", self.signer_id; + "block_consensus_hash" => %block.header.consensus_hash, + "block_height" => block.header.chain_length, + "pre_sign_block_id" => %block.block_id(), + ); + match self.coordinator.start_signing_round( + &block.serialize_to_vec(), + *is_taproot, + *merkle_root, + ) { + Ok(msg) => { + let ack = self.stackerdb.send_message_with_retry(msg.into()); + debug!("Signer #{}: ACK: {ack:?}", self.signer_id); + block_info.signed_over = true; + } + Err(e) => { + error!( + "Signer #{}: Failed to start signing block: {e:?}", + self.signer_id + ); + return; + } + } + } + } + self.update_operation(); + } + + /// Attempt to process the next command in the queue, and update state accordingly + pub fn process_next_command(&mut self, stacks_client: &StacksClient) { + let coordinator_id = self.coordinator_selector.get_coordinator().0; + match &self.state { + State::Idle => { + if coordinator_id != self.signer_id { + debug!( + "Signer #{}: Coordinator is {coordinator_id:?}. Will not process any commands...", + self.signer_id + ); + return; + } + if let Some(command) = self.commands.pop_front() { + self.execute_command(stacks_client, &command); + } else { + debug!( + "Signer #{}: Nothing to process. Waiting for command...", + self.signer_id + ); + } + } + State::OperationInProgress => { + // We cannot execute the next command until the current one is finished... + debug!( + "Signer #{}: Waiting for coordinator {coordinator_id:?} operation to finish...", + self.signer_id, + ); + } + } + } + + /// Handle the block validate response returned from our prior calls to submit a block for validation + fn handle_block_validate_response( + &mut self, + stacks_client: &StacksClient, + block_validate_response: &BlockValidateResponse, + res: Sender>, + ) { + let block_info = match block_validate_response { + BlockValidateResponse::Ok(block_validate_ok) => { + let signer_signature_hash = block_validate_ok.signer_signature_hash; + // For mutability reasons, we need to take the block_info out of the map and add it back after processing + let Some(mut block_info) = self.blocks.remove(&signer_signature_hash) else { + // We have not seen this block before. Why are we getting a response for it? + debug!("Signer #{}: Received a block validate response for a block we have not seen before. Ignoring...", self.signer_id); + return; + }; + let is_valid = self.verify_block_transactions(stacks_client, &block_info.block); + block_info.valid = Some(is_valid); + info!( + "Signer #{}: Treating block validation for block {} as valid: {:?}", + self.signer_id, + &block_info.block.block_id(), + block_info.valid + ); + // Add the block info back to the map + self.blocks + .entry(signer_signature_hash) + .or_insert(block_info) + } + BlockValidateResponse::Reject(block_validate_reject) => { + let signer_signature_hash = block_validate_reject.signer_signature_hash; + let Some(block_info) = self.blocks.get_mut(&signer_signature_hash) else { + // We have not seen this block before. Why are we getting a response for it? + debug!("Signer #{}: Received a block validate response for a block we have not seen before. Ignoring...", self.signer_id); + return; + }; + block_info.valid = Some(false); + // Submit a rejection response to the .signers contract for miners + // to observe so they know to send another block and to prove signers are doing work); + warn!("Signer #{}: Broadcasting a block rejection due to stacks node validation failure...", self.signer_id); + if let Err(e) = self + .stackerdb + .send_message_with_retry(block_validate_reject.clone().into()) + { + warn!( + "Signer #{}: Failed to send block rejection to stacker-db: {e:?}", + self.signer_id + ); + } + block_info + } + }; + if let Some(mut nonce_request) = block_info.nonce_request.take() { + debug!("Signer #{}: Received a block validate response from the stacks node for a block we already received a nonce request for. Responding to the nonce request...", self.signer_id); + // We have received validation from the stacks node. Determine our vote and update the request message + Self::determine_vote(self.signer_id, block_info, &mut nonce_request); + // Send the nonce request through with our vote + let packet = Packet { + msg: Message::NonceRequest(nonce_request), + sig: vec![], + }; + self.handle_packets(stacks_client, res, &[packet]); + } else { + let coordinator_id = self.coordinator_selector.get_coordinator().0; + if block_info.valid.unwrap_or(false) + && !block_info.signed_over + && coordinator_id == self.signer_id + { + // We are the coordinator. Trigger a signing round for this block + debug!( + "Signer #{}: triggering a signing round over the block {}", + self.signer_id, + block_info.block.header.block_hash() + ); + self.commands.push_back(Command::Sign { + block: block_info.block.clone(), + is_taproot: false, + merkle_root: None, + }); + } else { + debug!( + "Signer #{} ignoring block.", self.signer_id; + "block_hash" => block_info.block.header.block_hash(), + "valid" => block_info.valid, + "signed_over" => block_info.signed_over, + "coordinator_id" => coordinator_id, + ); + } + } + } + + /// Handle signer messages submitted to signers stackerdb + fn handle_signer_messages( + &mut self, + stacks_client: &StacksClient, + res: Sender>, + messages: &[SignerMessage], + ) { + let coordinator_pubkey = self.coordinator_selector.get_coordinator().1; + let packets: Vec = messages + .iter() + .filter_map(|msg| match msg { + SignerMessage::BlockResponse(_) | SignerMessage::Transactions(_) => None, + // TODO: if a signer tries to trigger DKG and we already have one set in the contract, ignore the request. + SignerMessage::Packet(packet) => { + self.verify_packet(stacks_client, packet.clone(), &coordinator_pubkey) + } + }) + .collect(); + self.handle_packets(stacks_client, res, &packets); + } + + /// Handle proposed blocks submitted by the miners to stackerdb + fn handle_proposed_blocks(&mut self, stacks_client: &StacksClient, blocks: &[NakamotoBlock]) { + for block in blocks { + // Store the block in our cache + self.blocks.insert( + block.header.signer_signature_hash(), + BlockInfo::new(block.clone()), + ); + // Submit the block for validation + stacks_client + .submit_block_for_validation(block.clone()) + .unwrap_or_else(|e| { + warn!( + "Signer #{}: Failed to submit block for validation: {e:?}", + self.signer_id + ); + }); + } + } + + /// Process inbound packets as both a signer and a coordinator + /// Will send outbound packets and operation results as appropriate + fn handle_packets( + &mut self, + stacks_client: &StacksClient, + res: Sender>, + packets: &[Packet], + ) { + let signer_outbound_messages = self + .signing_round + .process_inbound_messages(packets) + .unwrap_or_else(|e| { + error!( + "Signer #{}: Failed to process inbound messages as a signer: {e:?}", + self.signer_id + ); + vec![] + }); + + // Next process the message as the coordinator + let (coordinator_outbound_messages, operation_results) = self + .coordinator + .process_inbound_messages(packets) + .unwrap_or_else(|e| { + error!( + "Signer #{}: Failed to process inbound messages as a coordinator: {e:?}", + self.signer_id + ); + (vec![], vec![]) + }); + + if !operation_results.is_empty() { + // We have finished a signing or DKG round, either successfully or due to error. + // Regardless of the why, update our state to Idle as we should not expect the operation to continue. + self.process_operation_results(stacks_client, &operation_results); + self.send_operation_results(res, operation_results); + self.finish_operation(); + } else if !packets.is_empty() && self.coordinator.state != CoordinatorState::Idle { + // We have received a message and are in the middle of an operation. Update our state accordingly + self.update_operation(); + } + self.send_outbound_messages(signer_outbound_messages); + self.send_outbound_messages(coordinator_outbound_messages); + } + + /// Validate a signature share request, updating its message where appropriate. + /// If the request is for a block it has already agreed to sign, it will overwrite the message with the agreed upon value + /// Returns whether the request is valid or not. + fn validate_signature_share_request(&self, request: &mut SignatureShareRequest) -> bool { + let Some(block_vote): Option = read_next(&mut &request.message[..]).ok() + else { + // We currently reject anything that is not a block vote + debug!( + "Signer #{}: Received a signature share request for an unknown message stream. Reject it.", + self.signer_id + ); + return false; + }; + match self + .blocks + .get(&block_vote.signer_signature_hash) + .map(|block_info| &block_info.vote) + { + Some(Some(vote)) => { + // Overwrite with our agreed upon value in case another message won majority or the coordinator is trying to cheat... + debug!( + "Signer #{}: set vote for {} to {vote:?}", + self.signer_id, block_vote.rejected + ); + request.message = vote.serialize_to_vec(); + true + } + Some(None) => { + // We never agreed to sign this block. Reject it. + // This can happen if the coordinator received enough votes to sign yes + // or no on a block before we received validation from the stacks node. + debug!("Signer #{}: Received a signature share request for a block we never agreed to sign. Ignore it.", self.signer_id); + false + } + None => { + // We will only sign across block hashes or block hashes + b'n' byte for + // blocks we have seen a Nonce Request for (and subsequent validation) + // We are missing the context here necessary to make a decision. Reject the block + debug!("Signer #{}: Received a signature share request from an unknown block. Reject it.", self.signer_id); + false + } + } + } + + /// Validate a nonce request, updating its message appropriately. + /// If the request is for a block, we will update the request message + /// as either a hash indicating a vote no or the signature hash indicating a vote yes + /// Returns whether the request is valid or not + fn validate_nonce_request( + &mut self, + stacks_client: &StacksClient, + nonce_request: &mut NonceRequest, + ) -> bool { + let Some(block): Option = read_next(&mut &nonce_request.message[..]).ok() + else { + // We currently reject anything that is not a block + debug!( + "Signer #{}: Received a nonce request for an unknown message stream. Reject it.", + self.signer_id + ); + return false; + }; + let signer_signature_hash = block.header.signer_signature_hash(); + let Some(block_info) = self.blocks.get_mut(&signer_signature_hash) else { + // We have not seen this block before. Cache it. Send a RPC to the stacks node to validate it. + debug!("Signer #{}: We have received a block sign request for a block we have not seen before. Cache the nonce request and submit the block for validation...", self.signer_id); + // We need to update our state to OperationInProgress so we can respond to the nonce request from this signer once we get our validation back + self.update_operation(); + // Store the block in our cache + self.blocks.insert( + signer_signature_hash, + BlockInfo::new_with_request(block.clone(), nonce_request.clone()), + ); + stacks_client + .submit_block_for_validation(block) + .unwrap_or_else(|e| { + warn!( + "Signer #{}: Failed to submit block for validation: {e:?}", + self.signer_id + ); + }); + return false; + }; + + if block_info.valid.is_none() { + // We have not yet received validation from the stacks node. Cache the request and wait for validation + debug!("Signer #{}: We have yet to receive validation from the stacks node for a nonce request. Cache the nonce request and wait for block validation...", self.signer_id); + block_info.nonce_request = Some(nonce_request.clone()); + return false; + } + + Self::determine_vote(self.signer_id, block_info, nonce_request); + true + } + + /// Verify the transactions in a block are as expected + fn verify_block_transactions( + &mut self, + stacks_client: &StacksClient, + block: &NakamotoBlock, + ) -> bool { + if self.approved_aggregate_public_key.is_some() { + // We do not enforce a block contain any transactions except the aggregate votes when it is NOT already set + // TODO: should be only allow special cased transactions during prepare phase before a key is set? + debug!("Signer #{}: Already have an aggregate key for reward cycle {}. Skipping transaction verification...", self.signer_id, self.reward_cycle); + return true; + } + if let Ok(expected_transactions) = self.get_expected_transactions(stacks_client) { + //It might be worth building a hashset of the blocks' txids and checking that against the expected transaction's txid. + let block_tx_hashset = block.txs.iter().map(|tx| tx.txid()).collect::>(); + // Ensure the block contains the transactions we expect + let missing_transactions = expected_transactions + .into_iter() + .filter_map(|tx| { + if !block_tx_hashset.contains(&tx.txid()) { + debug!( + "Signer #{}: expected txid {} is in the block", + self.signer_id, + &tx.txid() + ); + Some(tx) + } else { + debug!( + "Signer #{}: missing expected txid {}", + self.signer_id, + &tx.txid() + ); + None + } + }) + .collect::>(); + let is_valid = missing_transactions.is_empty(); + if !is_valid { + debug!("Signer #{}: Broadcasting a block rejection due to missing expected transactions...", self.signer_id); + let block_rejection = BlockRejection::new( + block.header.signer_signature_hash(), + RejectCode::MissingTransactions(missing_transactions), + ); + // Submit signature result to miners to observe + if let Err(e) = self + .stackerdb + .send_message_with_retry(block_rejection.into()) + { + warn!( + "Signer #{}: Failed to send block rejection to stacker-db: {e:?}", + self.signer_id + ); + } + } + is_valid + } else { + // Failed to connect to the stacks node to get transactions. Cannot validate the block. Reject it. + debug!( + "Signer #{}: Broadcasting a block rejection due to signer connectivity issues...", + self.signer_id + ); + let block_rejection = BlockRejection::new( + block.header.signer_signature_hash(), + RejectCode::ConnectivityIssues, + ); + // Submit signature result to miners to observe + if let Err(e) = self + .stackerdb + .send_message_with_retry(block_rejection.into()) + { + warn!( + "Signer #{}: Failed to send block submission to stacker-db: {e:?}", + self.signer_id + ); + } + false + } + } + + /// Get transactions from stackerdb for the given addresses and account nonces, filtering out any malformed transactions + fn get_signer_transactions( + &mut self, + nonces: &std::collections::HashMap, + ) -> Result, ClientError> { + let transactions: Vec<_> = self + .stackerdb + .get_current_transactions_with_retry()? + .into_iter() + .filter_map(|tx| { + if !NakamotoSigners::valid_vote_transaction(nonces, &tx, self.mainnet) { + return None; + } + Some(tx) + }) + .collect(); + Ok(transactions) + } + + /// Get the transactions that should be included in the block, filtering out any invalid transactions + fn get_expected_transactions( + &mut self, + stacks_client: &StacksClient, + ) -> Result, ClientError> { + if self.next_signer_slot_ids.is_empty() { + debug!( + "Signer #{}: No next signers. Skipping transaction retrieval.", + self.signer_id + ); + return Ok(vec![]); + } + // Get all the account nonces for the next signers + let account_nonces = self.get_account_nonces(stacks_client, &self.next_signer_addresses); + let transactions: Vec<_> = self + .stackerdb + .get_next_transactions_with_retry(&self.next_signer_slot_ids)?; + let mut filtered_transactions = std::collections::HashMap::new(); + NakamotoSigners::update_filtered_transactions( + &mut filtered_transactions, + &account_nonces, + self.mainnet, + transactions, + ); + // We only allow enforcement of one special cased transaction per signer address per block + Ok(filtered_transactions.into_values().collect()) + } + + /// Determine the vote for a block and update the block info and nonce request accordingly + fn determine_vote( + signer_id: u32, + block_info: &mut BlockInfo, + nonce_request: &mut NonceRequest, + ) { + let rejected = !block_info.valid.unwrap_or(false); + if rejected { + debug!( + "Signer #{}: Rejecting block {}", + signer_id, + block_info.block.block_id() + ); + } else { + debug!( + "Signer #{}: Accepting block {}", + signer_id, + block_info.block.block_id() + ); + } + let block_vote = NakamotoBlockVote { + signer_signature_hash: block_info.block.header.signer_signature_hash(), + rejected: !block_info.valid.unwrap_or(false), + }; + let block_vote_bytes = block_vote.serialize_to_vec(); + // Cache our vote + block_info.vote = Some(block_vote); + nonce_request.message = block_vote_bytes; + } + + /// Verify a chunk is a valid wsts packet. Returns the packet if it is valid, else None. + /// NOTE: The packet will be updated if the signer wishes to respond to NonceRequest + /// and SignatureShareRequests with a different message than what the coordinator originally sent. + /// This is done to prevent a malicious coordinator from sending a different message than what was + /// agreed upon and to support the case where the signer wishes to reject a block by voting no + fn verify_packet( + &mut self, + stacks_client: &StacksClient, + mut packet: Packet, + coordinator_public_key: &PublicKey, + ) -> Option { + // We only care about verified wsts packets. Ignore anything else. + if packet.verify(&self.signing_round.public_keys, coordinator_public_key) { + match &mut packet.msg { + Message::SignatureShareRequest(request) => { + if !self.validate_signature_share_request(request) { + return None; + } + } + Message::NonceRequest(request) => { + if !self.validate_nonce_request(stacks_client, request) { + return None; + } + } + _ => { + // Nothing to do for other message types + } + } + Some(packet) + } else { + debug!( + "Signer #{}: Failed to verify wsts packet with {}: {packet:?}", + self.signer_id, coordinator_public_key + ); + None + } + } + + /// Processes the operation results, broadcasting block acceptance or rejection messages + /// and DKG vote results accordingly + fn process_operation_results( + &mut self, + stacks_client: &StacksClient, + operation_results: &[OperationResult], + ) { + for operation_result in operation_results { + // Signers only every trigger non-taproot signing rounds over blocks. Ignore SignTaproot results + match operation_result { + OperationResult::Sign(signature) => { + debug!("Signer #{}: Received signature result", self.signer_id); + self.process_signature(signature); + } + OperationResult::SignTaproot(_) => { + debug!("Signer #{}: Received a signature result for a taproot signature. Nothing to broadcast as we currently sign blocks with a FROST signature.", self.signer_id); + } + OperationResult::Dkg(dkg_public_key) => { + self.process_dkg(stacks_client, dkg_public_key); + } + OperationResult::SignError(e) => { + warn!("Signer #{}: Received a Sign error: {e:?}", self.signer_id); + self.process_sign_error(e); + } + OperationResult::DkgError(e) => { + warn!("Signer #{}: Received a DKG error: {e:?}", self.signer_id); + // TODO: process these errors and track malicious signers to report + } + } + } + } + + /// Process a dkg result by broadcasting a vote to the stacks node + fn process_dkg(&mut self, stacks_client: &StacksClient, dkg_public_key: &Point) { + let epoch = retry_with_exponential_backoff(|| { + stacks_client + .get_node_epoch() + .map_err(backoff::Error::transient) + }) + .unwrap_or(StacksEpochId::Epoch24); + let tx_fee = if epoch < StacksEpochId::Epoch30 { + debug!( + "Signer #{}: in pre Epoch 3.0 cycles, must set a transaction fee for the DKG vote.", + self.signer_id + ); + Some(self.tx_fee_ustx) + } else { + None + }; + // Get our current nonce from the stacks node and compare it against what we have sitting in the stackerdb instance + let signer_address = stacks_client.get_signer_address(); + // Retreieve ALL account nonces as we may have transactions from other signers in our stackerdb slot that we care about + let account_nonces = self.get_account_nonces(stacks_client, &self.signer_addresses); + let account_nonce = account_nonces.get(signer_address).unwrap_or(&0); + let signer_transactions = retry_with_exponential_backoff(|| { + self.get_signer_transactions(&account_nonces) + .map_err(backoff::Error::transient) + }) + .map_err(|e| { + warn!( + "Signer #{}: Unable to get signer transactions: {e:?}", + self.signer_id + ); + }) + .unwrap_or_default(); + // If we have a transaction in the stackerdb slot, we need to increment the nonce hence the +1, else should use the account nonce + let next_nonce = signer_transactions + .first() + .map(|tx| tx.get_origin_nonce().wrapping_add(1)) + .unwrap_or(*account_nonce); + match stacks_client.build_vote_for_aggregate_public_key( + self.stackerdb.get_signer_slot_id().0, + self.coordinator.current_dkg_id, + *dkg_public_key, + self.reward_cycle, + tx_fee, + next_nonce, + ) { + Ok(new_transaction) => { + if let Err(e) = self.broadcast_dkg_vote( + stacks_client, + epoch, + signer_transactions, + new_transaction, + ) { + warn!( + "Signer #{}: Failed to broadcast DKG public key vote ({dkg_public_key:?}): {e:?}", + self.signer_id + ); + } + } + Err(e) => { + warn!( + "Signer #{}: Failed to build DKG public key vote ({dkg_public_key:?}) transaction: {e:?}.", + self.signer_id + ); + } + } + } + + // Get the account nonces for the provided list of signer addresses + fn get_account_nonces( + &self, + stacks_client: &StacksClient, + signer_addresses: &[StacksAddress], + ) -> std::collections::HashMap { + let mut account_nonces = std::collections::HashMap::with_capacity(signer_addresses.len()); + for address in signer_addresses { + let Ok(account_nonce) = retry_with_exponential_backoff(|| { + stacks_client + .get_account_nonce(address) + .map_err(backoff::Error::transient) + }) else { + warn!( + "Signer #{}: Unable to get account nonce for address: {address}.", + self.signer_id + ); + continue; + }; + account_nonces.insert(*address, account_nonce); + } + account_nonces + } + + /// broadcast the dkg vote transaction according to the current epoch + fn broadcast_dkg_vote( + &mut self, + stacks_client: &StacksClient, + epoch: StacksEpochId, + mut signer_transactions: Vec, + new_transaction: StacksTransaction, + ) -> Result<(), ClientError> { + let txid = new_transaction.txid(); + if self.approved_aggregate_public_key.is_some() { + // We do not enforce a block contain any transactions except the aggregate votes when it is NOT already set + info!( + "Signer #{}: Already has an aggregate key for reward cycle {}. Do not broadcast the transaction ({txid:?}).", + self.signer_id, self.reward_cycle + ); + return Ok(()); + } + if epoch >= StacksEpochId::Epoch30 { + debug!("Signer #{}: Received a DKG result while in epoch 3.0. Broadcast the transaction only to stackerDB.", self.signer_id); + } else if epoch == StacksEpochId::Epoch25 { + debug!("Signer #{}: Received a DKG result while in epoch 2.5. Broadcast the transaction to the mempool.", self.signer_id); + stacks_client.submit_transaction(&new_transaction)?; + info!( + "Signer #{}: Submitted DKG vote transaction ({txid:?}) to the mempool", + self.signer_id + ); + } else { + debug!("Signer #{}: Received a DKG result, but are in an unsupported epoch. Do not broadcast the transaction ({}).", self.signer_id, new_transaction.txid()); + return Ok(()); + } + // For all Pox-4 epochs onwards, broadcast the results also to stackerDB for other signers/miners to observe + signer_transactions.push(new_transaction); + let signer_message = SignerMessage::Transactions(signer_transactions); + self.stackerdb.send_message_with_retry(signer_message)?; + info!( + "Signer #{}: Broadcasted DKG vote transaction ({txid}) to stacker DB", + self.signer_id, + ); + Ok(()) + } + + /// Process a signature from a signing round by deserializing the signature and + /// broadcasting an appropriate Reject or Approval message to stackerdb + fn process_signature(&mut self, signature: &Signature) { + // Deserialize the signature result and broadcast an appropriate Reject or Approval message to stackerdb + let message = self.coordinator.get_message(); + let Some(block_vote): Option = read_next(&mut &message[..]).ok() else { + debug!( + "Signer #{}: Received a signature result for a non-block. Nothing to broadcast.", + self.signer_id + ); + return; + }; + + // TODO: proper garbage collection...This is currently our only cleanup of blocks + self.blocks.remove(&block_vote.signer_signature_hash); + + let block_submission = if block_vote.rejected { + // We signed a rejection message. Return a rejection message + BlockResponse::rejected(block_vote.signer_signature_hash, signature.clone()).into() + } else { + // we agreed to sign the block hash. Return an approval message + BlockResponse::accepted(block_vote.signer_signature_hash, signature.clone()).into() + }; + + // Submit signature result to miners to observe + debug!( + "Signer #{}: submit block response {block_submission:?}", + self.signer_id + ); + if let Err(e) = self.stackerdb.send_message_with_retry(block_submission) { + warn!( + "Signer #{}: Failed to send block submission to stacker-db: {e:?}", + self.signer_id + ); + } + } + + /// Process a sign error from a signing round, broadcasting a rejection message to stackerdb accordingly + fn process_sign_error(&mut self, e: &SignError) { + let message = self.coordinator.get_message(); + // We do not sign across blocks, but across their hashes. however, the first sign request is always across the block + // so we must handle this case first + + let block: NakamotoBlock = read_next(&mut &message[..]).ok().unwrap_or({ + // This is not a block so maybe its across its hash + let Some(block_vote): Option = read_next(&mut &message[..]).ok() else { + // This is not a block vote either. We cannot process this error + debug!("Signer #{}: Received a signature error for a non-block. Nothing to broadcast.", self.signer_id); + return; + }; + let Some(block_info) = self.blocks.remove(&block_vote.signer_signature_hash) else { + debug!("Signer #{}: Received a signature result for a block we have not seen before. Ignoring...", self.signer_id); + return; + }; + block_info.block + }); + let block_rejection = + BlockRejection::new(block.header.signer_signature_hash(), RejectCode::from(e)); + debug!( + "Signer #{}: Broadcasting block rejection: {block_rejection:?}", + self.signer_id + ); + // Submit signature result to miners to observe + if let Err(e) = self + .stackerdb + .send_message_with_retry(block_rejection.into()) + { + warn!( + "Signer #{}: Failed to send block rejection submission to stacker-db: {e:?}", + self.signer_id + ); + } + } + + /// Send any operation results across the provided channel + fn send_operation_results( + &mut self, + res: Sender>, + operation_results: Vec, + ) { + let nmb_results = operation_results.len(); + match res.send(operation_results) { + Ok(_) => { + debug!( + "Signer #{}: Successfully sent {} operation result(s)", + self.signer_id, nmb_results + ) + } + Err(e) => { + warn!( + "Signer #{}: Failed to send {nmb_results} operation results: {e:?}", + self.signer_id + ); + } + } + } + + /// Sending all provided packets through stackerdb with a retry + fn send_outbound_messages(&mut self, outbound_messages: Vec) { + debug!( + "Signer #{}: Sending {} messages to other stacker-db instances.", + self.signer_id, + outbound_messages.len() + ); + for msg in outbound_messages { + let ack = self.stackerdb.send_message_with_retry(msg.into()); + if let Ok(ack) = ack { + debug!("Signer #{}: send outbound ACK: {ack:?}", self.signer_id); + } else { + warn!( + "Signer #{}: Failed to send message to stacker-db instance: {ack:?}", + self.signer_id + ); + } + } + } + + /// Update the DKG for the provided signer info, triggering it if required + pub fn update_dkg(&mut self, stacks_client: &StacksClient) -> Result<(), ClientError> { + let reward_cycle = self.reward_cycle; + self.approved_aggregate_public_key = + stacks_client.get_approved_aggregate_key(reward_cycle)?; + if self.approved_aggregate_public_key.is_some() { + // TODO: this will never work as is. We need to have stored our party shares on the side etc for this particular aggregate key. + // Need to update state to store the necessary info, check against it to see if we have participated in the winning round and + // then overwrite our value accordingly. Otherwise, we will be locked out of the round and should not participate. + self.coordinator + .set_aggregate_public_key(self.approved_aggregate_public_key); + // We have an approved aggregate public key. Do nothing further + debug!( + "Signer #{}: Have updated DKG value to {:?}.", + self.signer_id, self.approved_aggregate_public_key + ); + return Ok(()); + }; + let coordinator_id = self.coordinator_selector.get_coordinator().0; + if self.signer_id == coordinator_id && self.state == State::Idle { + debug!( + "Signer #{}: Checking if old vote transaction exists in StackerDB...", + self.signer_id + ); + // Have I already voted and have a pending transaction? Check stackerdb for the same round number and reward cycle vote transaction + // Only get the account nonce of THIS signer as we only care about our own votes, not other signer votes + let signer_address = stacks_client.get_signer_address(); + let account_nonces = self.get_account_nonces(stacks_client, &[*signer_address]); + let old_transactions = self.get_signer_transactions(&account_nonces).map_err(|e| { + warn!("Signer #{}: Failed to get old signer transactions: {e:?}. May trigger DKG unnecessarily", self.signer_id); + }).unwrap_or_default(); + // Check if we have an existing vote transaction for the same round and reward cycle + for transaction in old_transactions.iter() { + let params = + NakamotoSigners::parse_vote_for_aggregate_public_key(transaction).unwrap_or_else(|| panic!("BUG: Signer #{}: Received an invalid {SIGNERS_VOTING_FUNCTION_NAME} transaction in an already filtered list: {transaction:?}", self.signer_id)); + if Some(params.aggregate_key) == self.coordinator.aggregate_public_key + && params.voting_round == self.coordinator.current_dkg_id + && reward_cycle == self.reward_cycle + { + debug!("Signer #{}: Not triggering a DKG round. Already have a pending vote transaction.", self.signer_id; + "txid" => %transaction.txid(), + "aggregate_key" => %params.aggregate_key, + "voting_round" => params.voting_round + ); + return Ok(()); + } + } + if stacks_client + .get_vote_for_aggregate_public_key( + self.coordinator.current_dkg_id, + self.reward_cycle, + *stacks_client.get_signer_address(), + )? + .is_some() + { + // TODO Check if the vote failed and we need to retrigger the DKG round not just if we have already voted... + // TODO need logic to trigger another DKG round if a certain amount of time passes and we still have no confirmed DKG vote + debug!("Signer #{}: Not triggering a DKG round. Already voted and we may need to wait for more votes to arrive.", self.signer_id); + return Ok(()); + } + if self.commands.front() != Some(&Command::Dkg) { + info!("Signer #{} is the current coordinator for {reward_cycle} and must trigger DKG. Queuing DKG command...", self.signer_id); + self.commands.push_front(Command::Dkg); + } + } + Ok(()) + } + + /// Process the event + pub fn process_event( + &mut self, + stacks_client: &StacksClient, + event: Option<&SignerEvent>, + res: Sender>, + current_reward_cycle: u64, + ) -> Result<(), ClientError> { + debug!("Signer #{}: Processing event: {event:?}", self.signer_id); + match event { + Some(SignerEvent::BlockValidationResponse(block_validate_response)) => { + debug!( + "Signer #{}: Received a block proposal result from the stacks node...", + self.signer_id + ); + self.handle_block_validate_response(stacks_client, block_validate_response, res) + } + Some(SignerEvent::SignerMessages(signer_set, messages)) => { + if *signer_set != self.stackerdb.get_signer_set() { + debug!("Signer #{}: Received a signer message for a reward cycle that does not belong to this signer. Ignoring...", self.signer_id); + return Ok(()); + } + debug!( + "Signer #{}: Received {} messages from the other signers...", + self.signer_id, + messages.len() + ); + self.handle_signer_messages(stacks_client, res, messages); + } + Some(SignerEvent::ProposedBlocks(blocks)) => { + if current_reward_cycle != self.reward_cycle { + // There is not point in processing blocks if we are not the current reward cycle (we can never actually contribute to signing these blocks) + debug!("Signer #{}: Received a proposed block, but this signer's reward cycle ({}) is not the current one ({}). Ignoring...", self.signer_id, self.reward_cycle, current_reward_cycle); + return Ok(()); + } + debug!( + "Signer #{}: Received {} block proposals from the miners...", + self.signer_id, + blocks.len() + ); + self.handle_proposed_blocks(stacks_client, blocks); + } + Some(SignerEvent::StatusCheck) => { + debug!("Signer #{}: Received a status check event.", self.signer_id) + } + None => { + // No event. Do nothing. + debug!("Signer #{}: No event received", self.signer_id) + } + } + Ok(()) + } +} diff --git a/stacks-signer/src/tests/conf/signer-0.toml b/stacks-signer/src/tests/conf/signer-0.toml index 86d79c949..449392c2e 100644 --- a/stacks-signer/src/tests/conf/signer-0.toml +++ b/stacks-signer/src/tests/conf/signer-0.toml @@ -1,14 +1,5 @@ - stacks_private_key = "6a1fc1a3183018c6d79a4e11e154d2bdad2d89ac8bc1b0a021de8b4d28774fbb01" node_host = "127.0.0.1:20443" endpoint = "localhost:30000" network = "testnet" -stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" -signer_id = 0 -signers = [ - {public_key = "27MvzC7LYTFfjBQdZropBqzWSKQYgFVHWh3YXchYrh5Ug", key_ids = [1, 2, 3, 4]}, - {public_key = "f1Y6JdedrZyaZLnScbXbc1A7DhdLMjCipKCxkKUA93YQ", key_ids = [5, 6, 7, 8]}, - {public_key = "nKPew4JetMvV97EghsdikNMhgyYF37ZeNvmJNSJueyjQ", key_ids = [9, 10, 11, 12]}, - {public_key = "x3LcNnYgKKFBUaf9fZTEGHghFCQQyd6F9XNWj7nRXLt7", key_ids = [13, 14, 15, 16]}, - {public_key = "nUVH972kFxpKbD62muCb9L48nTKqNw11yp3vFM9VDzqw", key_ids = [17, 18, 19, 20]} -] +auth_password = "12345" diff --git a/stacks-signer/src/tests/conf/signer-1.toml b/stacks-signer/src/tests/conf/signer-1.toml index 114c25ed2..3d293af64 100644 --- a/stacks-signer/src/tests/conf/signer-1.toml +++ b/stacks-signer/src/tests/conf/signer-1.toml @@ -1,14 +1,5 @@ - stacks_private_key = "126e916e77359ccf521e168feea1fcb9626c59dc375cae00c7464303381c7dff01" -node_host = "127.0.0.1:20443" +node_host = "127.0.0.1:20444" endpoint = "localhost:30001" network = "testnet" -stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" -signer_id = 1 -signers = [ - {public_key = "27MvzC7LYTFfjBQdZropBqzWSKQYgFVHWh3YXchYrh5Ug", key_ids = [1, 2, 3, 4]}, - {public_key = "f1Y6JdedrZyaZLnScbXbc1A7DhdLMjCipKCxkKUA93YQ", key_ids = [5, 6, 7, 8]}, - {public_key = "nKPew4JetMvV97EghsdikNMhgyYF37ZeNvmJNSJueyjQ", key_ids = [9, 10, 11, 12]}, - {public_key = "x3LcNnYgKKFBUaf9fZTEGHghFCQQyd6F9XNWj7nRXLt7", key_ids = [13, 14, 15, 16]}, - {public_key = "nUVH972kFxpKbD62muCb9L48nTKqNw11yp3vFM9VDzqw", key_ids = [17, 18, 19, 20]} -] +auth_password = "12345" diff --git a/stacks-signer/src/tests/conf/signer-2.toml b/stacks-signer/src/tests/conf/signer-2.toml deleted file mode 100644 index d37072f4e..000000000 --- a/stacks-signer/src/tests/conf/signer-2.toml +++ /dev/null @@ -1,14 +0,0 @@ - -stacks_private_key = "b169d0d1408f66d16beb321857f525f9014dfc289f1aeedbcf96e78afeb8eb4001" -node_host = "127.0.0.1:20443" -endpoint = "localhost:30002" -network = "testnet" -stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" -signer_id = 2 -signers = [ - {public_key = "27MvzC7LYTFfjBQdZropBqzWSKQYgFVHWh3YXchYrh5Ug", key_ids = [1, 2, 3, 4]}, - {public_key = "f1Y6JdedrZyaZLnScbXbc1A7DhdLMjCipKCxkKUA93YQ", key_ids = [5, 6, 7, 8]}, - {public_key = "nKPew4JetMvV97EghsdikNMhgyYF37ZeNvmJNSJueyjQ", key_ids = [9, 10, 11, 12]}, - {public_key = "x3LcNnYgKKFBUaf9fZTEGHghFCQQyd6F9XNWj7nRXLt7", key_ids = [13, 14, 15, 16]}, - {public_key = "nUVH972kFxpKbD62muCb9L48nTKqNw11yp3vFM9VDzqw", key_ids = [17, 18, 19, 20]} -] diff --git a/stacks-signer/src/tests/conf/signer-3.toml b/stacks-signer/src/tests/conf/signer-3.toml deleted file mode 100644 index 4f6fb7ff2..000000000 --- a/stacks-signer/src/tests/conf/signer-3.toml +++ /dev/null @@ -1,14 +0,0 @@ - -stacks_private_key = "63cef3cd8880969b7f2450ca13b9ca57fd3cd3f7ee57ec6ed7654a84d39181e401" -node_host = "127.0.0.1:20443" -endpoint = "localhost:30003" -network = "testnet" -stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" -signer_id = 3 -signers = [ - {public_key = "27MvzC7LYTFfjBQdZropBqzWSKQYgFVHWh3YXchYrh5Ug", key_ids = [1, 2, 3, 4]}, - {public_key = "f1Y6JdedrZyaZLnScbXbc1A7DhdLMjCipKCxkKUA93YQ", key_ids = [5, 6, 7, 8]}, - {public_key = "nKPew4JetMvV97EghsdikNMhgyYF37ZeNvmJNSJueyjQ", key_ids = [9, 10, 11, 12]}, - {public_key = "x3LcNnYgKKFBUaf9fZTEGHghFCQQyd6F9XNWj7nRXLt7", key_ids = [13, 14, 15, 16]}, - {public_key = "nUVH972kFxpKbD62muCb9L48nTKqNw11yp3vFM9VDzqw", key_ids = [17, 18, 19, 20]} -] diff --git a/stacks-signer/src/tests/conf/signer-4.toml b/stacks-signer/src/tests/conf/signer-4.toml index fa15e83cf..0e80a1aa6 100644 --- a/stacks-signer/src/tests/conf/signer-4.toml +++ b/stacks-signer/src/tests/conf/signer-4.toml @@ -3,12 +3,4 @@ stacks_private_key = "e427196ae29197b1db6d5495ff26bf0675f48a4f07b200c0814b95734e node_host = "127.0.0.1:20443" endpoint = "localhost:30004" network = "testnet" -stackerdb_contract_id = "ST11Z60137Y96MF89K1KKRTA3CR6B25WY1Y931668.signers-stackerdb" -signer_id = 4 -signers = [ - {public_key = "27MvzC7LYTFfjBQdZropBqzWSKQYgFVHWh3YXchYrh5Ug", key_ids = [1, 2, 3, 4]}, - {public_key = "f1Y6JdedrZyaZLnScbXbc1A7DhdLMjCipKCxkKUA93YQ", key_ids = [5, 6, 7, 8]}, - {public_key = "nKPew4JetMvV97EghsdikNMhgyYF37ZeNvmJNSJueyjQ", key_ids = [9, 10, 11, 12]}, - {public_key = "x3LcNnYgKKFBUaf9fZTEGHghFCQQyd6F9XNWj7nRXLt7", key_ids = [13, 14, 15, 16]}, - {public_key = "nUVH972kFxpKbD62muCb9L48nTKqNw11yp3vFM9VDzqw", key_ids = [17, 18, 19, 20]} -] +auth_password = "12345" diff --git a/stacks-signer/src/tests/config.rs b/stacks-signer/src/tests/config.rs deleted file mode 100644 index 8b1378917..000000000 --- a/stacks-signer/src/tests/config.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/stacks-signer/src/tests/contracts/signers-stackerdb.clar b/stacks-signer/src/tests/contracts/signers-stackerdb.clar deleted file mode 100644 index 9f113eaf8..000000000 --- a/stacks-signer/src/tests/contracts/signers-stackerdb.clar +++ /dev/null @@ -1,59 +0,0 @@ - ;; stacker DB - (define-read-only (stackerdb-get-signer-slots-page (page-id uint)) - (ok (list - { - signer: 'ST24GDPTR7D9G3GFRR233JMWSD9HA296EXXG5XVGA, - num-slots: u10 - } - { - signer: 'ST1MR26HR7MMDE847BE2QC1CTNQY4WKN9XDKNPEP3, - num-slots: u10 - } - { - signer: 'ST110M4DRDXX2RF3W8EY1HCRQ25CS24PGY22DZ004, - num-slots: u10 - } - { - signer: 'ST69990VH3BVCV39QWT6CJAVVA9QPB1715HTSN75, - num-slots: u10 - } - { - signer: 'STCZSBZJK6C3MMAAW9N9RHSDKRKB9AKGJ2JMVDKN, - num-slots: u10 - } - ))) - - ;; stacker DB - (define-read-only (stackerdb-get-signer-slots) - (ok (list - { - signer: 'ST24GDPTR7D9G3GFRR233JMWSD9HA296EXXG5XVGA, - num-slots: u10 - } - { - signer: 'ST1MR26HR7MMDE847BE2QC1CTNQY4WKN9XDKNPEP3, - num-slots: u10 - } - { - signer: 'ST110M4DRDXX2RF3W8EY1HCRQ25CS24PGY22DZ004, - num-slots: u10 - } - { - signer: 'ST69990VH3BVCV39QWT6CJAVVA9QPB1715HTSN75, - num-slots: u10 - } - { - signer: 'STCZSBZJK6C3MMAAW9N9RHSDKRKB9AKGJ2JMVDKN, - num-slots: u10 - } - ))) - - (define-read-only (stackerdb-get-config) - (ok { - chunk-size: u4096, - write-freq: u0, - max-writes: u4096, - max-neighbors: u32, - hint-replicas: (list ) - })) - diff --git a/stacks-signer/src/tests/mod.rs b/stacks-signer/src/tests/mod.rs deleted file mode 100644 index 9db1e142e..000000000 --- a/stacks-signer/src/tests/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 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 . - -pub mod config; diff --git a/stacks-signer/src/utils.rs b/stacks-signer/src/utils.rs deleted file mode 100644 index 1c934cd46..000000000 --- a/stacks-signer/src/utils.rs +++ /dev/null @@ -1,149 +0,0 @@ -// 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 . -use std::time::Duration; - -use slog::slog_debug; -use stacks_common::debug; -use stacks_common::types::chainstate::{StacksAddress, StacksPrivateKey, StacksPublicKey}; -use stacks_common::types::PrivateKey; -use wsts::curve::ecdsa; -use wsts::curve::scalar::Scalar; - -use crate::config::Network; - -/// Helper function for building a signer config for each provided signer private key -pub fn build_signer_config_tomls( - stacks_private_keys: &[StacksPrivateKey], - num_keys: u32, - node_host: &str, - stackerdb_contract_id: &str, - timeout: Option, - network: &Network, -) -> Vec { - let num_signers = stacks_private_keys.len() as u32; - let keys_per_signer = num_keys / num_signers; - let mut key_id: u32 = 1; - let mut key_ids = Vec::new(); - for i in 0..num_signers { - let mut ids = Vec::new(); - for _ in 0..keys_per_signer { - ids.push(format!("{key_id}")); - key_id += 1; - } - if i + 1 == num_signers { - for _ in 0..num_keys % num_signers { - // We have requested a number of keys that cannot fit evenly into the number of signers - // Append the remaining keys to the last signer - ids.push(format!("{key_id}")); - key_id += 1; - debug!("Appending extra key to last signer..."); - } - } - key_ids.push(ids.join(", ")); - } - - let mut signer_config_tomls = vec![]; - let mut signers_array = String::new(); - - signers_array += "signers = ["; - for (i, stacks_private_key) in stacks_private_keys.iter().enumerate() { - let scalar = Scalar::try_from(&stacks_private_key.to_bytes()[..32]) - .expect("BUG: failed to convert the StacksPrivateKey to a Scalar"); - let ecdsa_public_key = ecdsa::PublicKey::new(&scalar) - .expect("BUG: failed to get a ecdsa::PublicKey from the provided Scalar") - .to_string(); - let ids = key_ids[i].clone(); - signers_array += &format!( - r#" - {{public_key = "{ecdsa_public_key}", key_ids = [{ids}]}}"# - ); - if i != stacks_private_keys.len() - 1 { - signers_array += ","; - } - } - signers_array += "\n]"; - - let mut port = 30000; - for (i, stacks_private_key) in stacks_private_keys.iter().enumerate() { - let endpoint = format!("localhost:{}", port); - port += 1; - let id = i; - let stacks_private_key = stacks_private_key.to_hex(); - let mut signer_config_toml = format!( - r#" -stacks_private_key = "{stacks_private_key}" -node_host = "{node_host}" -endpoint = "{endpoint}" -network = "{network}" -stackerdb_contract_id = "{stackerdb_contract_id}" -signer_id = {id} -{signers_array} -"# - ); - - if let Some(timeout) = timeout { - let event_timeout_ms = timeout.as_millis(); - signer_config_toml = format!( - r#" -{signer_config_toml} -event_timeout = {event_timeout_ms} -"# - ) - } - - signer_config_tomls.push(signer_config_toml); - } - - signer_config_tomls -} - -/// Helper function for building a stackerdb contract from the provided signer stacks addresses -pub fn build_stackerdb_contract( - signer_stacks_addresses: &[StacksAddress], - slots_per_user: u32, -) -> String { - let stackers_list: Vec = signer_stacks_addresses - .iter() - .map(|signer_addr| format!("{{ signer: '{signer_addr}, num-slots: u{slots_per_user}}}")) - .collect(); - let stackers_joined = stackers_list.join(" "); - - let stackerdb_contract = format!( - " - ;; stacker DB - (define-read-only (stackerdb-get-signer-slots (page uint)) - (ok (list {stackers_joined}))) - (define-read-only (stackerdb-get-page-count) (ok u1)) - (define-read-only (stackerdb-get-config) - (ok {{ - chunk-size: u4096, - write-freq: u0, - max-writes: u4096, - max-neighbors: u32, - hint-replicas: (list ) - }} )) - " - ); - stackerdb_contract -} - -/// Helper function to convert a private key to a Stacks address -pub fn to_addr(stacks_private_key: &StacksPrivateKey, network: &Network) -> StacksAddress { - StacksAddress::p2pkh( - network.is_mainnet(), - &StacksPublicKey::from_private(stacks_private_key), - ) -} diff --git a/stackslib/Cargo.toml b/stackslib/Cargo.toml index ba67b5c1f..8e2c48301 100644 --- a/stackslib/Cargo.toml +++ b/stackslib/Cargo.toml @@ -103,6 +103,7 @@ clarity = { features = ["default", "testing"], path = "../clarity" } stacks-common = { features = ["default", "testing"], path = "../stacks-common" } rstest = "0.17.0" rstest_reuse = "0.5.0" +mutants = "0.0.3" [features] default = [] diff --git a/stackslib/src/blockstack_cli.rs b/stackslib/src/blockstack_cli.rs index d708383f1..6fb9f45ed 100644 --- a/stackslib/src/blockstack_cli.rs +++ b/stackslib/src/blockstack_cli.rs @@ -23,7 +23,6 @@ extern crate blockstack_lib; extern crate clarity; extern crate stacks_common; -use std::convert::TryFrom; use std::io::prelude::*; use std::io::Read; use std::{env, fs, io}; diff --git a/stackslib/src/burnchains/affirmation.rs b/stackslib/src/burnchains/affirmation.rs index 245807bb6..1eb4874e4 100644 --- a/stackslib/src/burnchains/affirmation.rs +++ b/stackslib/src/burnchains/affirmation.rs @@ -232,7 +232,6 @@ /// use std::cmp; use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; -use std::convert::{TryFrom, TryInto}; use std::fmt; use std::fmt::Write; use std::sync::mpsc::SyncSender; @@ -370,6 +369,7 @@ impl AffirmationMap { self.affirmations.push(entry) } + #[cfg_attr(test, mutants::skip)] pub fn pop(&mut self) -> Option { self.affirmations.pop() } diff --git a/stackslib/src/burnchains/bitcoin/indexer.rs b/stackslib/src/burnchains/bitcoin/indexer.rs index 2d3e981e2..51de78a53 100644 --- a/stackslib/src/burnchains/bitcoin/indexer.rs +++ b/stackslib/src/burnchains/bitcoin/indexer.rs @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryFrom; use std::net::Shutdown; use std::ops::{Deref, DerefMut}; use std::path::PathBuf; diff --git a/stackslib/src/burnchains/burnchain.rs b/stackslib/src/burnchains/burnchain.rs index 532a6842c..e3947bd5a 100644 --- a/stackslib/src/burnchains/burnchain.rs +++ b/stackslib/src/burnchains/burnchain.rs @@ -15,8 +15,6 @@ // along with this program. If not, see . use std::collections::{HashMap, HashSet}; -use std::convert::TryFrom; -use std::marker::Send; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::mpsc::sync_channel; @@ -55,7 +53,7 @@ use crate::chainstate::burn::distribution::BurnSamplePoint; use crate::chainstate::burn::operations::leader_block_commit::MissedBlockCommit; use crate::chainstate::burn::operations::{ BlockstackOperationType, DelegateStxOp, LeaderBlockCommitOp, LeaderKeyRegisterOp, PreStxOp, - StackStxOp, TransferStxOp, UserBurnSupportOp, + StackStxOp, TransferStxOp, }; use crate::chainstate::burn::{BlockSnapshot, Opcodes}; use crate::chainstate::coordinator::comm::CoordinatorChannels; @@ -101,14 +99,13 @@ impl BurnchainStateTransition { block_ops: &Vec, missed_commits: &[MissedBlockCommit], ) -> Result { - // block commits and support burns discovered in this block. + // block commits discovered in this block. let mut block_commits: Vec = vec![]; let mut accepted_ops = Vec::with_capacity(block_ops.len()); assert!(Burnchain::ops_are_sorted(block_ops)); - // identify which user burns and block commits are consumed and which are not - let mut all_user_burns: HashMap = HashMap::new(); + // identify which block commits are consumed and which are not let mut all_block_commits: HashMap = HashMap::new(); // accept all leader keys we found. @@ -136,11 +133,6 @@ impl BurnchainStateTransition { all_block_commits.insert(op.txid.clone(), op.clone()); block_commits.push(op.clone()); } - BlockstackOperationType::UserBurnSupport(ref op) => { - // we don't know yet which user burns are going to be accepted until we have - // the burn distribution, so just account for them for now. - all_user_burns.insert(op.txid.clone(), op.clone()); - } }; } @@ -254,7 +246,7 @@ impl BurnchainStateTransition { ); BurnSamplePoint::prometheus_update_miner_commitments(&burn_dist); - // find out which user burns and block commits we're going to take + // find out which block commits we're going to take for i in 0..burn_dist.len() { let burn_point = &burn_dist[i]; @@ -263,18 +255,10 @@ impl BurnchainStateTransition { burn_point.candidate.clone(), )); all_block_commits.remove(&burn_point.candidate.txid); - - // taking each user burn in this sample point - for j in 0..burn_point.user_burns.len() { - accepted_ops.push(BlockstackOperationType::UserBurnSupport( - burn_point.user_burns[j].clone(), - )); - all_user_burns.remove(&burn_point.user_burns[j].txid); - } } - // accepted_ops contains all accepted commits and user burns now. - // only rejected ones remain in all_user_burns and all_block_commits + // accepted_ops contains all accepted commits now. + // only rejected ones remain in all_block_commits for op in all_block_commits.values() { warn!( "REJECTED({}) block commit {} at {},{}: Committed to an already-consumed VRF key", @@ -282,10 +266,6 @@ impl BurnchainStateTransition { ); } - for op in all_user_burns.values() { - warn!("REJECTED({}) user burn support {} at {},{}: No matching block commit in this block", op.block_height, &op.txid, op.block_height, op.vtxindex); - } - accepted_ops.sort_by(|ref a, ref b| a.vtxindex().partial_cmp(&b.vtxindex()).unwrap()); Ok(BurnchainStateTransition { @@ -759,20 +739,6 @@ impl Burnchain { } } } - x if x == Opcodes::UserBurnSupport as u8 => { - match UserBurnSupportOp::from_tx(block_header, burn_tx) { - Ok(op) => Some(BlockstackOperationType::UserBurnSupport(op)), - Err(e) => { - warn!( - "Failed to parse user burn support tx"; - "txid" => %burn_tx.txid(), - "data" => %to_hex(&burn_tx.data()), - "error" => ?e, - ); - None - } - } - } x if x == Opcodes::PreStx as u8 => { match PreStxOp::from_tx( block_header, diff --git a/stackslib/src/burnchains/mod.rs b/stackslib/src/burnchains/mod.rs index 8734d605d..aa3c83323 100644 --- a/stackslib/src/burnchains/mod.rs +++ b/stackslib/src/burnchains/mod.rs @@ -15,8 +15,6 @@ // along with this program. If not, see . use std::collections::HashMap; -use std::convert::TryFrom; -use std::default::Default; use std::marker::PhantomData; use std::{error, fmt, io}; @@ -684,6 +682,8 @@ pub enum Error { UnknownBlock(BurnchainHeaderHash), NonCanonicalPoxId(PoxId, PoxId), CoordinatorClosed, + /// Graceful shutdown error + ShutdownInitiated, } impl fmt::Display for Error { @@ -708,6 +708,7 @@ impl fmt::Display for Error { parent, child ), Error::CoordinatorClosed => write!(f, "ChainsCoordinator channel hung up"), + Error::ShutdownInitiated => write!(f, "Graceful shutdown was initiated"), } } } @@ -730,6 +731,7 @@ impl error::Error for Error { Error::UnknownBlock(_) => None, Error::NonCanonicalPoxId(_, _) => None, Error::CoordinatorClosed => None, + Error::ShutdownInitiated => None, } } } diff --git a/stackslib/src/burnchains/tests/burnchain.rs b/stackslib/src/burnchains/tests/burnchain.rs index 18b24413e..97c9366fe 100644 --- a/stackslib/src/burnchains/tests/burnchain.rs +++ b/stackslib/src/burnchains/tests/burnchain.rs @@ -38,7 +38,7 @@ use crate::chainstate::burn::db::sortdb::{SortitionDB, SortitionHandleTx}; use crate::chainstate::burn::distribution::BurnSamplePoint; use crate::chainstate::burn::operations::leader_block_commit::BURN_BLOCK_MINED_AT_MODULUS; use crate::chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, + BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, }; use crate::chainstate::burn::{ BlockSnapshot, ConsensusHash, ConsensusHashExtensions, OpsHash, SortitionHash, @@ -151,170 +151,6 @@ fn test_process_block_ops() { burn_header_hash: block_121_hash.clone(), }; - let user_burn_1 = UserBurnSupportOp { - address: StacksAddress::new(1, Hash160([1u8; 20])), - consensus_hash: ConsensusHash::from_bytes( - &hex_bytes("0000000000000000000000000000000000000000").unwrap(), - ) - .unwrap(), - public_key: VRFPublicKey::from_bytes( - &hex_bytes("a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a").unwrap(), - ) - .unwrap(), - block_header_hash_160: Hash160::from_bytes( - &hex_bytes("7150f635054b87df566a970b21e07030d6444bf2").unwrap(), - ) - .unwrap(), // 22222....2222 - key_block_ptr: 123, - key_vtxindex: 456, - burn_fee: 10000, - - txid: Txid::from_bytes( - &hex_bytes("1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c716b").unwrap(), - ) - .unwrap(), - vtxindex: 13, - block_height: 124, - burn_header_hash: block_124_hash_initial.clone(), - }; - - let user_burn_1_2 = UserBurnSupportOp { - address: StacksAddress::new(2, Hash160([2u8; 20])), - consensus_hash: ConsensusHash::from_bytes( - &hex_bytes("0000000000000000000000000000000000000000").unwrap(), - ) - .unwrap(), - public_key: VRFPublicKey::from_bytes( - &hex_bytes("a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a").unwrap(), - ) - .unwrap(), - block_header_hash_160: Hash160::from_bytes( - &hex_bytes("7150f635054b87df566a970b21e07030d6444bf2").unwrap(), - ) - .unwrap(), // 22222....2222 - key_block_ptr: 123, - key_vtxindex: 456, - burn_fee: 30000, - - txid: Txid::from_bytes( - &hex_bytes("1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c716c").unwrap(), - ) - .unwrap(), - vtxindex: 14, - block_height: 124, - burn_header_hash: block_124_hash_initial.clone(), - }; - - let user_burn_2 = UserBurnSupportOp { - address: StacksAddress::new(3, Hash160([3u8; 20])), - consensus_hash: ConsensusHash::from_bytes( - &hex_bytes("0000000000000000000000000000000000000000").unwrap(), - ) - .unwrap(), - public_key: VRFPublicKey::from_bytes( - &hex_bytes("bb519494643f79f1dea0350e6fb9a1da88dfdb6137117fc2523824a8aa44fe1c").unwrap(), - ) - .unwrap(), - block_header_hash_160: Hash160::from_bytes( - &hex_bytes("037a1e860899a4fa823c18b66f6264d20236ec58").unwrap(), - ) - .unwrap(), // 22222....2223 - key_block_ptr: 122, - key_vtxindex: 457, - burn_fee: 20000, - - txid: Txid::from_bytes( - &hex_bytes("1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c716d").unwrap(), - ) - .unwrap(), - vtxindex: 15, - block_height: 124, - burn_header_hash: block_124_hash_initial.clone(), - }; - - let user_burn_2_2 = UserBurnSupportOp { - address: StacksAddress::new(4, Hash160([4u8; 20])), - consensus_hash: ConsensusHash::from_bytes( - &hex_bytes("0000000000000000000000000000000000000000").unwrap(), - ) - .unwrap(), - public_key: VRFPublicKey::from_bytes( - &hex_bytes("bb519494643f79f1dea0350e6fb9a1da88dfdb6137117fc2523824a8aa44fe1c").unwrap(), - ) - .unwrap(), - block_header_hash_160: Hash160::from_bytes( - &hex_bytes("037a1e860899a4fa823c18b66f6264d20236ec58").unwrap(), - ) - .unwrap(), // 22222....2223 - key_block_ptr: 122, - key_vtxindex: 457, - burn_fee: 40000, - - txid: Txid::from_bytes( - &hex_bytes("1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c716e").unwrap(), - ) - .unwrap(), - vtxindex: 16, - block_height: 124, - burn_header_hash: block_124_hash_initial.clone(), - }; - - // should be rejected - let user_burn_noblock = UserBurnSupportOp { - address: StacksAddress::new(5, Hash160([5u8; 20])), - consensus_hash: ConsensusHash::from_bytes( - &hex_bytes("0000000000000000000000000000000000000000").unwrap(), - ) - .unwrap(), - public_key: VRFPublicKey::from_bytes( - &hex_bytes("a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a").unwrap(), - ) - .unwrap(), - block_header_hash_160: Hash160::from_bytes( - &hex_bytes("3333333333333333333333333333333333333333").unwrap(), - ) - .unwrap(), - key_block_ptr: 122, - key_vtxindex: 772, - burn_fee: 12345, - - txid: Txid::from_bytes( - &hex_bytes("1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c716f").unwrap(), - ) - .unwrap(), - vtxindex: 12, - block_height: 123, - burn_header_hash: block_123_hash.clone(), - }; - - // should be rejected - let user_burn_nokey = UserBurnSupportOp { - address: StacksAddress::new(6, Hash160([6u8; 20])), - consensus_hash: ConsensusHash::from_bytes( - &hex_bytes("0000000000000000000000000000000000000000").unwrap(), - ) - .unwrap(), - public_key: VRFPublicKey::from_bytes( - &hex_bytes("3f3338db51f2b1f6ac0cf6177179a24ee130c04ef2f9849a64a216969ab60e70").unwrap(), - ) - .unwrap(), - block_header_hash_160: Hash160::from_bytes( - &hex_bytes("037a1e860899a4fa823c18b66f6264d20236ec58").unwrap(), - ) - .unwrap(), - key_block_ptr: 122, - key_vtxindex: 457, - burn_fee: 12345, - - txid: Txid::from_bytes( - &hex_bytes("1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c7170").unwrap(), - ) - .unwrap(), - vtxindex: 15, - block_height: 123, - burn_header_hash: block_123_hash.clone(), - }; - let block_commit_1 = LeaderBlockCommitOp { sunset_burn: 0, commit_outs: vec![], @@ -524,11 +360,9 @@ fn test_process_block_ops() { miner_pk_hash: None, }; - let block_ops_123 = vec![ - BlockstackOperationType::UserBurnSupport(user_burn_noblock.clone()), - BlockstackOperationType::UserBurnSupport(user_burn_nokey.clone()), - BlockstackOperationType::LeaderKeyRegister(leader_key_1.clone()), - ]; + let block_ops_123 = vec![BlockstackOperationType::LeaderKeyRegister( + leader_key_1.clone(), + )]; let block_opshash_123 = OpsHash::from_txids(&vec![ // notably, the user burns here _wont_ be included in the consensus hash leader_key_1.txid.clone(), @@ -728,7 +562,6 @@ fn test_process_block_ops() { let burn_total = block_ops_124.iter().fold(0u64, |mut acc, op| { let bf = match op { BlockstackOperationType::LeaderBlockCommit(ref op) => op.burn_fee, - BlockstackOperationType::UserBurnSupport(ref op) => 0, _ => 0, }; acc += bf; diff --git a/stackslib/src/burnchains/tests/db.rs b/stackslib/src/burnchains/tests/db.rs index 13db19bcf..7b2a87be4 100644 --- a/stackslib/src/burnchains/tests/db.rs +++ b/stackslib/src/burnchains/tests/db.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::cmp; -use std::convert::TryInto; use stacks_common::address::AddressHashMode; use stacks_common::deps_common::bitcoin::blockdata::transaction::Transaction as BtcTx; diff --git a/stackslib/src/burnchains/tests/mod.rs b/stackslib/src/burnchains/tests/mod.rs index b65501d12..fc7f6993a 100644 --- a/stackslib/src/burnchains/tests/mod.rs +++ b/stackslib/src/burnchains/tests/mod.rs @@ -523,11 +523,6 @@ impl TestBurnchainBlock { assert_eq!(data.block_height, self.block_height); data.consensus_hash = parent_snapshot.consensus_hash.clone(); } - - BlockstackOperationType::UserBurnSupport(ref mut data) => { - assert_eq!(data.block_height, self.block_height); - data.consensus_hash = parent_snapshot.consensus_hash.clone(); - } _ => {} } } @@ -789,7 +784,6 @@ fn process_next_sortition( BlockSnapshot, Vec, Vec, - Vec, ) { assert_eq!(miners.len(), block_hashes.len()); @@ -834,8 +828,7 @@ fn process_next_sortition( fork.append_block(block); let tip_snapshot = node.mine_fork(fork); - // TODO: user burn support - (tip_snapshot, next_prev_keys, next_commits, vec![]) + (tip_snapshot, next_prev_keys, next_commits) } fn verify_keys_accepted(node: &mut TestBurnchainNode, prev_keys: &Vec) -> () { @@ -909,14 +902,13 @@ fn mine_10_stacks_blocks_1_fork() { next_block_hashes.push(hash); } - let (next_snapshot, mut next_prev_keys, next_block_commits, next_user_burns) = - process_next_sortition( - &mut node, - &mut fork, - &mut miners, - &prev_keys, - &next_block_hashes, - ); + let (next_snapshot, mut next_prev_keys, next_block_commits) = process_next_sortition( + &mut node, + &mut fork, + &mut miners, + &prev_keys, + &next_block_hashes, + ); verify_keys_accepted(&mut node, &prev_keys); verify_commits_accepted(&mut node, &next_block_commits); @@ -958,14 +950,13 @@ fn mine_10_stacks_blocks_2_forks_disjoint() { next_block_hashes.push(hash); } - let (next_snapshot, mut next_prev_keys, next_block_commits, next_user_burns) = - process_next_sortition( - &mut node, - &mut fork_1, - &mut miners, - &prev_keys_1, - &next_block_hashes, - ); + let (next_snapshot, mut next_prev_keys, next_block_commits) = process_next_sortition( + &mut node, + &mut fork_1, + &mut miners, + &prev_keys_1, + &next_block_hashes, + ); verify_keys_accepted(&mut node, &prev_keys_1); verify_commits_accepted(&mut node, &next_block_commits); @@ -1011,22 +1002,20 @@ fn mine_10_stacks_blocks_2_forks_disjoint() { next_block_hashes_2.push(hash); } - let (next_snapshot_1, mut next_prev_keys_1, next_block_commits_1, next_user_burns_1) = - process_next_sortition( - &mut node, - &mut fork_1, - &mut miners_1, - &prev_keys_1, - &next_block_hashes_1, - ); - let (next_snapshot_2, mut next_prev_keys_2, next_block_commits_2, next_user_burns_2) = - process_next_sortition( - &mut node, - &mut fork_2, - &mut miners_2, - &prev_keys_2, - &next_block_hashes_2, - ); + let (next_snapshot_1, mut next_prev_keys_1, next_block_commits_1) = process_next_sortition( + &mut node, + &mut fork_1, + &mut miners_1, + &prev_keys_1, + &next_block_hashes_1, + ); + let (next_snapshot_2, mut next_prev_keys_2, next_block_commits_2) = process_next_sortition( + &mut node, + &mut fork_2, + &mut miners_2, + &prev_keys_2, + &next_block_hashes_2, + ); assert!(next_snapshot_1.burn_header_hash != next_snapshot_2.burn_header_hash); @@ -1076,14 +1065,13 @@ fn mine_10_stacks_blocks_2_forks_disjoint_same_blocks() { next_block_hashes.push(hash); } - let (snapshot, mut next_prev_keys, next_block_commits, next_user_burns) = - process_next_sortition( - &mut node, - &mut fork_1, - &mut miners, - &prev_keys_1, - &next_block_hashes, - ); + let (snapshot, mut next_prev_keys, next_block_commits) = process_next_sortition( + &mut node, + &mut fork_1, + &mut miners, + &prev_keys_1, + &next_block_hashes, + ); verify_keys_accepted(&mut node, &prev_keys_1); verify_commits_accepted(&mut node, &next_block_commits); @@ -1131,22 +1119,20 @@ fn mine_10_stacks_blocks_2_forks_disjoint_same_blocks() { next_block_hashes_2.push(hash); } - let (snapshot_1, mut next_prev_keys_1, next_block_commits_1, next_user_burns_1) = - process_next_sortition( - &mut node, - &mut fork_1, - &mut miners_1, - &prev_keys_1, - &next_block_hashes_1, - ); - let (snapshot_2, mut next_prev_keys_2, next_block_commits_2, next_user_burns_2) = - process_next_sortition( - &mut node, - &mut fork_2, - &mut miners_2, - &prev_keys_2, - &next_block_hashes_2, - ); + let (snapshot_1, mut next_prev_keys_1, next_block_commits_1) = process_next_sortition( + &mut node, + &mut fork_1, + &mut miners_1, + &prev_keys_1, + &next_block_hashes_1, + ); + let (snapshot_2, mut next_prev_keys_2, next_block_commits_2) = process_next_sortition( + &mut node, + &mut fork_2, + &mut miners_2, + &prev_keys_2, + &next_block_hashes_2, + ); assert!(snapshot_1.burn_header_hash != snapshot_2.burn_header_hash); assert!(snapshot_1.consensus_hash != snapshot_2.consensus_hash); diff --git a/stackslib/src/chainstate/burn/db/processing.rs b/stackslib/src/chainstate/burn/db/processing.rs index 0c899770d..dadfcdba7 100644 --- a/stackslib/src/chainstate/burn/db/processing.rs +++ b/stackslib/src/chainstate/burn/db/processing.rs @@ -68,15 +68,6 @@ impl<'a> SortitionHandleTx<'a> { BurnchainError::OpError(e) }) } - BlockstackOperationType::UserBurnSupport(ref op) => { - op.check(burnchain, self).map_err(|e| { - warn!( - "REJECTED({}) user burn support {} at {},{}: {:?}", - op.block_height, &op.txid, op.block_height, op.vtxindex, &e - ); - BurnchainError::OpError(e) - }) - } BlockstackOperationType::StackStx(ref op) => op.check().map_err(|e| { warn!( "REJECTED({}) stack stx op {} at {},{}: {:?}", @@ -138,7 +129,6 @@ impl<'a> SortitionHandleTx<'a> { .try_fold(0u64, |acc, op| { let bf = match op { BlockstackOperationType::LeaderBlockCommit(ref op) => op.burn_fee, - BlockstackOperationType::UserBurnSupport(ref op) => op.burn_fee, _ => 0, }; acc.checked_add(bf) diff --git a/stackslib/src/chainstate/burn/db/sortdb.rs b/stackslib/src/chainstate/burn/db/sortdb.rs index f03a7b9ad..d027f6ffd 100644 --- a/stackslib/src/chainstate/burn/db/sortdb.rs +++ b/stackslib/src/chainstate/burn/db/sortdb.rs @@ -14,9 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::cmp::{Ord, Ordering}; +use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; -use std::convert::{From, TryFrom, TryInto}; use std::io::{ErrorKind, Write}; use std::ops::{Deref, DerefMut}; use std::str::FromStr; @@ -59,7 +58,7 @@ use crate::chainstate::burn::operations::leader_block_commit::{ }; use crate::chainstate::burn::operations::{ BlockstackOperationType, DelegateStxOp, LeaderBlockCommitOp, LeaderKeyRegisterOp, PreStxOp, - StackStxOp, TransferStxOp, UserBurnSupportOp, + StackStxOp, TransferStxOp, }; use crate::chainstate::burn::{ BlockSnapshot, ConsensusHash, ConsensusHashExtensions, Opcodes, OpsHash, SortitionHash, @@ -306,44 +305,6 @@ impl FromRow for LeaderBlockCommitOp { } } -impl FromRow for UserBurnSupportOp { - fn from_row<'a>(row: &'a Row) -> Result { - let txid = Txid::from_column(row, "txid")?; - let vtxindex: u32 = row.get_unwrap("vtxindex"); - let block_height = u64::from_column(row, "block_height")?; - let burn_header_hash = BurnchainHeaderHash::from_column(row, "burn_header_hash")?; - - let address = StacksAddress::from_column(row, "address")?; - let consensus_hash = ConsensusHash::from_column(row, "consensus_hash")?; - let public_key = VRFPublicKey::from_column(row, "public_key")?; - let key_block_ptr: u32 = row.get_unwrap("key_block_ptr"); - let key_vtxindex: u16 = row.get_unwrap("key_vtxindex"); - let block_header_hash_160 = Hash160::from_column(row, "block_header_hash_160")?; - - let burn_fee_str: String = row.get_unwrap("burn_fee"); - - let burn_fee = burn_fee_str - .parse::() - .map_err(|_e| db_error::ParseError)?; - - let user_burn = UserBurnSupportOp { - address: address, - consensus_hash: consensus_hash, - public_key: public_key, - key_block_ptr: key_block_ptr, - key_vtxindex: key_vtxindex, - block_header_hash_160: block_header_hash_160, - burn_fee: burn_fee, - - txid: txid, - vtxindex: vtxindex, - block_height: block_height, - burn_header_hash: burn_header_hash, - }; - Ok(user_burn) - } -} - impl FromRow for StackStxOp { fn from_row<'a>(row: &'a Row) -> Result { let txid = Txid::from_column(row, "txid")?; @@ -590,26 +551,6 @@ const SORTITION_DB_INITIAL_SCHEMA: &'static [&'static str] = &[ FOREIGN KEY(sortition_id) REFERENCES snapshots(sortition_id) );"#, r#" - CREATE TABLE user_burn_support( - txid TEXT NOT NULL, - vtxindex INTEGER NOT NULL, - block_height INTEGER NOT NULL, - burn_header_hash TEXT NOT NULL, - sortition_id TEXT NOT NULL, - - address TEXT NOT NULL, - consensus_hash TEXT NOT NULL, - public_key TEXT NOT NULL, - key_block_ptr INTEGER NOT NULL, - key_vtxindex INTEGER NOT NULL, - block_header_hash_160 TEXT NOT NULL, - - burn_fee TEXT NOT NULL, - - PRIMARY KEY(txid,sortition_id), - FOREIGN KEY(sortition_id) REFERENCES snapshots(sortition_id) - );"#, - r#" CREATE TABLE stack_stx ( txid TEXT NOT NULL, vtxindex INTEGER NOT NULL, @@ -740,9 +681,6 @@ const SORTITION_DB_INDEXES: &'static [&'static str] = &[ "CREATE INDEX IF NOT EXISTS index_leader_keys_sortition_id_block_height_vtxindex ON leader_keys(sortition_id,block_height,vtxindex);", "CREATE INDEX IF NOT EXISTS index_block_commits_sortition_id_vtxindex ON block_commits(sortition_id,vtxindex);", "CREATE INDEX IF NOT EXISTS index_block_commits_sortition_id_block_height_vtxindex ON block_commits(sortition_id,block_height,vtxindex);", - "CREATE INDEX IF NOT EXISTS index_user_burn_support_txid ON user_burn_support(txid);", - "CREATE INDEX IF NOT EXISTS index_user_burn_support_sortition_id_vtxindex ON user_burn_support(sortition_id,vtxindex);", - "CREATE INDEX IF NOT EXISTS index_user_burn_support_sortition_id_hash_160_key_vtxindex_key_block_ptr_vtxindex ON user_burn_support(sortition_id,block_header_hash_160,key_vtxindex,key_block_ptr,vtxindex ASC);", "CREATE INDEX IF NOT EXISTS index_stack_stx_burn_header_hash ON stack_stx(burn_header_hash);", "CREATE INDEX IF NOT EXISTS index_transfer_stx_burn_header_hash ON transfer_stx(burn_header_hash);", "CREATE INDEX IF NOT EXISTS index_missed_commits_intended_sortition_id ON missed_commits(intended_sortition_id);", @@ -2081,64 +2019,6 @@ impl<'a> SortitionHandleConn<'a> { SortitionDB::get_ancestor_snapshot(self, block_height, &self.context.chain_tip) } - /// Get all user burns that burned for the winning block in the chain_tip sortition - /// Returns list of user burns in order by vtxindex. - pub fn get_winning_user_burns_by_block(&self) -> Result, db_error> { - let snapshot = match self.get_tip_snapshot()? { - Some(sn) => sn, - None => { - // no such snapshot, so no such users - return Ok(vec![]); - } - }; - - if !snapshot.sortition { - // no winner - return Ok(vec![]); - } - let qry = "SELECT * FROM block_commits WHERE sortition_id = ?1 AND txid = ?2"; - let args: [&dyn ToSql; 2] = [&snapshot.sortition_id, &snapshot.winning_block_txid]; - let winning_commit: LeaderBlockCommitOp = query_row(self, qry, &args)? - .expect("BUG: sortition exists, but winner cannot be found"); - - let winning_block_hash160 = - Hash160::from_sha256(snapshot.winning_stacks_block_hash.as_bytes()); - - let qry = "SELECT * FROM user_burn_support \ - WHERE sortition_id = ?1 AND block_header_hash_160 = ?2 AND key_vtxindex = ?3 AND key_block_ptr = ?4 \ - ORDER BY vtxindex ASC"; - let args: [&dyn ToSql; 4] = [ - &snapshot.sortition_id, - &winning_block_hash160, - &winning_commit.key_vtxindex, - &winning_commit.key_block_ptr, - ]; - - let mut winning_user_burns: Vec = query_rows(self, qry, &args)?; - - // were there multiple miners with the same VRF key and block header hash? (i.e., are these user burns shared?) - let qry = "SELECT COUNT(*) FROM block_commits \ - WHERE sortition_id = ?1 AND block_header_hash = ?2 AND key_vtxindex = ?3 AND key_block_ptr = ?4"; - let args: [&dyn ToSql; 4] = [ - &snapshot.sortition_id, - &snapshot.winning_stacks_block_hash, - &winning_commit.key_vtxindex, - &winning_commit.key_block_ptr, - ]; - let shared_miners = query_count(self, qry, &args)? as u64; - - assert!( - shared_miners >= 1, - "BUG: Should be at least 1 matching miner for the winning block commit" - ); - - for winning_user_burn in winning_user_burns.iter_mut() { - winning_user_burn.burn_fee /= shared_miners; - } - - Ok(winning_user_burns) - } - /// Get the block snapshot of the parent stacks block of the given stacks block. /// The returned block-commit is for the given (consensus_hash, block_hash). /// The returned BlockSnapshot is for the parent of the block identified by (consensus_hash, @@ -3119,6 +2999,7 @@ impl SortitionDB { } /// Get the height of a consensus hash, even if it's not on the canonical PoX fork. + #[cfg_attr(test, mutants::skip)] pub fn get_consensus_hash_height(&self, ch: &ConsensusHash) -> Result, db_error> { let qry = "SELECT block_height FROM snapshots WHERE consensus_hash = ?1"; let mut heights: Vec = query_rows(self.conn(), qry, &[ch])?; @@ -3302,6 +3183,7 @@ impl SortitionDB { Ok(()) } + #[cfg_attr(test, mutants::skip)] fn apply_schema_5(tx: &DBTx, epochs: &[StacksEpoch]) -> Result<(), db_error> { // the schema 5 changes simply **replace** the contents of the epochs table // by dropping all the current rows and then revalidating and inserting @@ -3320,6 +3202,7 @@ impl SortitionDB { Ok(()) } + #[cfg_attr(test, mutants::skip)] fn apply_schema_6(tx: &DBTx, epochs: &[StacksEpoch]) -> Result<(), db_error> { for sql_exec in SORTITION_DB_SCHEMA_6 { tx.execute_batch(sql_exec)?; @@ -3335,6 +3218,7 @@ impl SortitionDB { Ok(()) } + #[cfg_attr(test, mutants::skip)] fn apply_schema_7(tx: &DBTx, epochs: &[StacksEpoch]) -> Result<(), db_error> { for sql_exec in SORTITION_DB_SCHEMA_7 { tx.execute_batch(sql_exec)?; @@ -3720,6 +3604,7 @@ impl<'a> SortitionDBConn<'a> { Ok(Some(parent_block_snapshot)) } + #[cfg_attr(test, mutants::skip)] pub fn get_reward_set_size_at(&mut self, sortition_id: &SortitionId) -> Result { self.get_indexed(sortition_id, &db_keys::pox_reward_set_size()) .map(|x| { @@ -3830,6 +3715,7 @@ impl SortitionDB { /// Mark a Stacks block snapshot as valid again, but update its memoized canonical Stacks tip /// height and block-accepted flag. + #[cfg_attr(test, mutants::skip)] pub fn revalidate_snapshot_with_block( tx: &DBTx, sortition_id: &SortitionId, @@ -4200,6 +4086,7 @@ impl SortitionDB { Ok(result.is_some()) } + #[cfg_attr(test, mutants::skip)] pub fn latest_stacks_blocks_processed( &self, sortition_id: &SortitionId, @@ -4727,16 +4614,10 @@ impl SortitionDB { conn: &Connection, block_snapshot: &BlockSnapshot, ) -> Result { - let user_burns = SortitionDB::get_user_burns_by_block(conn, &block_snapshot.sortition_id)?; let block_commits = SortitionDB::get_block_commits_by_block(conn, &block_snapshot.sortition_id)?; let mut burn_total: u64 = 0; - for i in 0..user_burns.len() { - burn_total = burn_total - .checked_add(user_burns[i].burn_fee) - .expect("Way too many tokens burned"); - } for i in 0..block_commits.len() { burn_total = burn_total .checked_add(block_commits[i].burn_fee) @@ -4745,18 +4626,6 @@ impl SortitionDB { Ok(burn_total) } - /// Get all user burns registered in a block on is fork. - /// Returns list of user burns in order by vtxindex. - pub fn get_user_burns_by_block( - conn: &Connection, - sortition: &SortitionId, - ) -> Result, db_error> { - let qry = "SELECT * FROM user_burn_support WHERE sortition_id = ?1 ORDER BY vtxindex ASC"; - let args: &[&dyn ToSql] = &[sortition]; - - query_rows(conn, qry, args) - } - /// Get all block commitments registered in a block on the burn chain's history in this fork. /// Returns the list of block commits in order by vtxindex. pub fn get_block_commits_by_block( @@ -5041,16 +4910,6 @@ impl SortitionDB { ))); } - // user burn? - let user_burn_sql = "SELECT * FROM user_burn_support WHERE txid = ?1 LIMIT 1".to_string(); - - let user_burn_res = query_row_panic(conn, &user_burn_sql, &args, || { - "Multiple user burns with same txid".to_string() - })?; - if let Some(user_burn) = user_burn_res { - return Ok(Some(BlockstackOperationType::UserBurnSupport(user_burn))); - } - Ok(None) } @@ -5370,13 +5229,6 @@ impl<'a> SortitionHandleTx<'a> { ); self.insert_block_commit(op, sort_id) } - BlockstackOperationType::UserBurnSupport(ref op) => { - info!( - "ACCEPTED({}) user burn support {} at {},{}", - op.block_height, &op.txid, op.block_height, op.vtxindex - ); - self.insert_user_burn(op, sort_id) - } BlockstackOperationType::StackStx(ref op) => { info!( "ACCEPTED({}) stack stx opt {} at {},{}", @@ -5570,42 +5422,6 @@ impl<'a> SortitionHandleTx<'a> { Ok(()) } - /// Insert a user support burn. - /// No validity checking will be done, beyond what is encoded in the user_burn_support table - /// constraints. That is, type mismatches and serialization errors will be caught, but nothing - /// else. - /// The corresponding snapshot must already be inserted - fn insert_user_burn( - &mut self, - user_burn: &UserBurnSupportOp, - sort_id: &SortitionId, - ) -> Result<(), db_error> { - assert!(user_burn.block_height < BLOCK_HEIGHT_MAX); - - // represent burn fee as TEXT - let burn_fee_str = format!("{}", user_burn.burn_fee); - - let args: &[&dyn ToSql] = &[ - &user_burn.txid, - &user_burn.vtxindex, - &u64_to_sql(user_burn.block_height)?, - &user_burn.burn_header_hash, - &user_burn.address.to_string(), - &user_burn.consensus_hash, - &user_burn.public_key.to_hex(), - &user_burn.key_block_ptr, - &user_burn.key_vtxindex, - &user_burn.block_header_hash_160, - &burn_fee_str, - sort_id, - ]; - - self.execute("INSERT INTO user_burn_support (txid, vtxindex, block_height, burn_header_hash, address, consensus_hash, public_key, key_block_ptr, key_vtxindex, block_header_hash_160, burn_fee, sortition_id) \ - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", args)?; - - Ok(()) - } - /// Insert a missed block commit fn insert_missed_block_commit(&mut self, op: &MissedBlockCommit) -> Result<(), db_error> { // serialize tx input to JSON @@ -6379,7 +6195,7 @@ pub mod tests { use crate::burnchains::*; use crate::chainstate::burn::operations::leader_block_commit::BURN_BLOCK_MINED_AT_MODULUS; use crate::chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, + BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, }; use crate::chainstate::burn::ConsensusHash; use crate::chainstate::stacks::index::TrieHashExtension; @@ -6807,95 +6623,6 @@ pub mod tests { } } - #[test] - fn test_insert_user_burn() { - let block_height = 123; - let vtxindex = 456; - let first_burn_hash = BurnchainHeaderHash::from_hex( - "0000000000000000000000000000000000000000000000000000000000000000", - ) - .unwrap(); - - let leader_key = LeaderKeyRegisterOp { - consensus_hash: ConsensusHash::from_bytes( - &hex_bytes("2222222222222222222222222222222222222222").unwrap(), - ) - .unwrap(), - public_key: VRFPublicKey::from_bytes( - &hex_bytes("a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a") - .unwrap(), - ) - .unwrap(), - memo: vec![01, 02, 03, 04, 05], - - txid: Txid::from_bytes_be( - &hex_bytes("1bfa831b5fc56c858198acb8e77e5863c1e9d8ac26d49ddb914e24d8d4083562") - .unwrap(), - ) - .unwrap(), - vtxindex: vtxindex, - block_height: block_height + 1, - burn_header_hash: BurnchainHeaderHash([0x01; 32]), - }; - - let user_burn = UserBurnSupportOp { - address: StacksAddress::new(1, Hash160([1u8; 20])), - consensus_hash: ConsensusHash::from_bytes( - &hex_bytes("2222222222222222222222222222222222222222").unwrap(), - ) - .unwrap(), - public_key: VRFPublicKey::from_bytes( - &hex_bytes("a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a") - .unwrap(), - ) - .unwrap(), - block_header_hash_160: Hash160::from_bytes( - &hex_bytes("3333333333333333333333333333333333333333").unwrap(), - ) - .unwrap(), - key_block_ptr: (block_height + 1) as u32, - key_vtxindex: vtxindex as u16, - burn_fee: 12345, - - txid: Txid::from_bytes_be( - &hex_bytes("1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c716c") - .unwrap(), - ) - .unwrap(), - vtxindex: vtxindex, - block_height: block_height + 2, - burn_header_hash: BurnchainHeaderHash([0x03; 32]), - }; - - let mut db = SortitionDB::connect_test(block_height, &first_burn_hash).unwrap(); - - let snapshot = test_append_snapshot( - &mut db, - BurnchainHeaderHash([0x01; 32]), - &vec![BlockstackOperationType::LeaderKeyRegister( - leader_key.clone(), - )], - ); - - let user_burn_snapshot = test_append_snapshot( - &mut db, - BurnchainHeaderHash([0x03; 32]), - &vec![BlockstackOperationType::UserBurnSupport(user_burn.clone())], - ); - - { - let res_user_burns = - SortitionDB::get_user_burns_by_block(db.conn(), &user_burn_snapshot.sortition_id) - .unwrap(); - assert_eq!(res_user_burns.len(), 1); - assert_eq!(res_user_burns[0], user_burn); - - let no_user_burns = - SortitionDB::get_user_burns_by_block(db.conn(), &snapshot.sortition_id).unwrap(); - assert_eq!(no_user_burns.len(), 0); - } - } - #[test] fn has_VRF_public_key() { let public_key = VRFPublicKey::from_bytes( @@ -7463,35 +7190,6 @@ pub mod tests { burn_header_hash: BurnchainHeaderHash([0x03; 32]), }; - let user_burn = UserBurnSupportOp { - address: StacksAddress::new(2, Hash160([2u8; 20])), - consensus_hash: ConsensusHash::from_bytes( - &hex_bytes("2222222222222222222222222222222222222222").unwrap(), - ) - .unwrap(), - public_key: VRFPublicKey::from_bytes( - &hex_bytes("a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a") - .unwrap(), - ) - .unwrap(), - block_header_hash_160: Hash160::from_bytes( - &hex_bytes("3333333333333333333333333333333333333333").unwrap(), - ) - .unwrap(), - key_block_ptr: (block_height + 1) as u32, - key_vtxindex: vtxindex as u16, - burn_fee: 12345, - - txid: Txid::from_bytes_be( - &hex_bytes("1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c716c") - .unwrap(), - ) - .unwrap(), - vtxindex: vtxindex + 1, - block_height: block_height + 2, - burn_header_hash: BurnchainHeaderHash([0x03; 32]), - }; - let mut db = SortitionDB::connect_test(block_height, &first_burn_hash).unwrap(); let key_snapshot = test_append_snapshot( @@ -7505,15 +7203,14 @@ pub mod tests { let commit_snapshot = test_append_snapshot( &mut db, BurnchainHeaderHash([0x03; 32]), - &vec![ - BlockstackOperationType::LeaderBlockCommit(block_commit.clone()), - BlockstackOperationType::UserBurnSupport(user_burn.clone()), - ], + &vec![BlockstackOperationType::LeaderBlockCommit( + block_commit.clone(), + )], ); { let burn_amt = SortitionDB::get_block_burn_amount(db.conn(), &commit_snapshot).unwrap(); - assert_eq!(burn_amt, block_commit.burn_fee + user_burn.burn_fee); + assert_eq!(burn_amt, block_commit.burn_fee); let no_burn_amt = SortitionDB::get_block_burn_amount(db.conn(), &key_snapshot).unwrap(); assert_eq!(no_burn_amt, 0); diff --git a/stackslib/src/chainstate/burn/distribution.rs b/stackslib/src/chainstate/burn/distribution.rs index 213b2f00f..2a1689710 100644 --- a/stackslib/src/chainstate/burn/distribution.rs +++ b/stackslib/src/chainstate/burn/distribution.rs @@ -16,7 +16,6 @@ use std::cmp; use std::collections::{BTreeMap, HashMap}; -use std::convert::TryInto; use stacks_common::address::AddressHashMode; use stacks_common::util::hash::Hash160; @@ -29,7 +28,7 @@ use crate::burnchains::{ }; use crate::chainstate::burn::operations::leader_block_commit::MissedBlockCommit; use crate::chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, + BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, }; use crate::chainstate::stacks::StacksPublicKey; use crate::core::MINING_COMMITMENT_WINDOW; @@ -42,7 +41,6 @@ pub struct BurnSamplePoint { pub range_start: Uint256, pub range_end: Uint256, pub candidate: LeaderBlockCommitOp, - pub user_burns: Vec, } #[derive(Debug, Clone)] @@ -291,7 +289,6 @@ impl BurnSamplePoint { range_start: Uint256::zero(), // To be filled in range_end: Uint256::zero(), // To be filled in candidate, - user_burns: vec![], } }) .collect(); @@ -331,7 +328,6 @@ impl BurnSamplePoint { pub fn make_distribution( all_block_candidates: Vec, _consumed_leader_keys: Vec, - user_burns: Vec, ) -> Vec { Self::make_min_median_distribution(vec![all_block_candidates], vec![], vec![true]) } @@ -419,7 +415,7 @@ mod tests { MissedBlockCommit, BURN_BLOCK_MINED_AT_MODULUS, }; use crate::chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, + BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, }; use crate::chainstate::burn::ConsensusHash; use crate::chainstate::stacks::address::StacksAddressExtensions; @@ -430,41 +426,9 @@ mod tests { struct BurnDistFixture { consumed_leader_keys: Vec, block_commits: Vec, - user_burns: Vec, res: Vec, } - fn make_user_burn( - burn_fee: u64, - vrf_ident: u32, - block_id: u64, - txid_id: u64, - block_height: u64, - ) -> UserBurnSupportOp { - let mut block_header_hash = [0; 32]; - block_header_hash[0..8].copy_from_slice(&block_id.to_be_bytes()); - let mut txid = [3; 32]; - txid[0..8].copy_from_slice(&txid_id.to_be_bytes()); - let txid = Txid(txid); - - UserBurnSupportOp { - address: StacksAddress { - version: 0, - bytes: Hash160([0; 20]), - }, - consensus_hash: ConsensusHash([0; 20]), - public_key: VRFPublicKey::from_private(&VRFPrivateKey::new()), - key_block_ptr: vrf_ident, - key_vtxindex: 0, - block_header_hash_160: Hash160::from_sha256(&block_header_hash), - burn_fee, - txid, - vtxindex: 0, // index in the block where this tx occurs - block_height, // block height at which this tx occurs - burn_header_hash: BurnchainHeaderHash([0; 32]), // hash of burnchain block with this tx - } - } - fn make_missed_commit(txid_id: u64, input_tx: u64) -> MissedBlockCommit { let mut txid = [0; 32]; txid[0..8].copy_from_slice(&txid_id.to_be_bytes()); @@ -565,14 +529,6 @@ mod tests { make_block_commit(3, 12, 12, 12, Some(10), 6), ], ]; - let user_burns = vec![ - vec![make_user_burn(1, 1, 1, 1, 1), make_user_burn(1, 2, 2, 2, 1)], - vec![make_user_burn(1, 4, 4, 4, 2)], - vec![make_user_burn(1, 6, 6, 6, 3)], - vec![make_user_burn(1, 8, 8, 8, 4)], - vec![make_user_burn(1, 10, 10, 10, 5)], - vec![make_user_burn(1, 12, 12, 12, 6)], - ]; let mut result = BurnSamplePoint::make_min_median_distribution( commits.clone(), @@ -592,9 +548,6 @@ mod tests { assert_eq!(result[0].candidate.txid, commits[5][0].txid); assert_eq!(result[1].candidate.txid, commits[5][1].txid); - assert_eq!(result[0].user_burns.len(), 0); - assert_eq!(result[1].user_burns.len(), 0); - // now correct the back pointers so that they point // at the correct UTXO position *post-sunset* for (ix, window_slice) in commits.iter_mut().enumerate() { @@ -626,9 +579,6 @@ mod tests { // make sure that we're associating with the last commit in the window. assert_eq!(result[0].candidate.txid, commits[5][0].txid); assert_eq!(result[1].candidate.txid, commits[5][1].txid); - - assert_eq!(result[0].user_burns.len(), 0); - assert_eq!(result[1].user_burns.len(), 0); } #[test] @@ -672,14 +622,6 @@ mod tests { make_block_commit(3, 12, 12, 12, Some(10), 6), ], ]; - let user_burns = vec![ - vec![make_user_burn(1, 1, 1, 1, 1), make_user_burn(1, 2, 2, 2, 1)], - vec![make_user_burn(1, 4, 4, 4, 2)], - vec![make_user_burn(1, 6, 6, 6, 3)], - vec![make_user_burn(1, 8, 8, 8, 4)], - vec![make_user_burn(1, 10, 10, 10, 5)], - vec![make_user_burn(1, 12, 12, 12, 6)], - ]; let mut result = BurnSamplePoint::make_min_median_distribution( commits.clone(), @@ -698,9 +640,6 @@ mod tests { assert_eq!(result[0].candidate.txid, commits[5][0].txid); assert_eq!(result[1].candidate.txid, commits[5][1].txid); - assert_eq!(result[0].user_burns.len(), 0); - assert_eq!(result[1].user_burns.len(), 0); - // test case 2: // miner 1: 4 4 5 4 5 3 // miner 2: 4 4 4 4 4 1 @@ -736,14 +675,6 @@ mod tests { make_block_commit(1, 11, 11, 12, Some(10), 6), ], ]; - let user_burns = vec![ - vec![], - vec![], - vec![], - vec![], - vec![], - vec![make_user_burn(2, 11, 11, 1, 6)], - ]; let mut result = BurnSamplePoint::make_min_median_distribution( commits.clone(), @@ -761,9 +692,6 @@ mod tests { // make sure that we're associating with the last commit in the window. assert_eq!(result[0].candidate.txid, commits[5][0].txid); assert_eq!(result[1].candidate.txid, commits[5][1].txid); - - assert_eq!(result[0].user_burns.len(), 0); - assert_eq!(result[1].user_burns.len(), 0); } #[test] @@ -904,198 +832,6 @@ mod tests { .unwrap(), }; - let user_burn_noblock = UserBurnSupportOp { - address: StacksAddress::new(1, Hash160([1u8; 20])), - consensus_hash: ConsensusHash::from_bytes( - &hex_bytes("4444444444444444444444444444444444444444").unwrap(), - ) - .unwrap(), - public_key: VRFPublicKey::from_bytes( - &hex_bytes("a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a") - .unwrap(), - ) - .unwrap(), - block_header_hash_160: Hash160::from_bytes( - &hex_bytes("3333333333333333333333333333333333333333").unwrap(), - ) - .unwrap(), - key_block_ptr: 1, - key_vtxindex: 772, - burn_fee: 12345, - - txid: Txid::from_bytes_be( - &hex_bytes("1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c716c") - .unwrap(), - ) - .unwrap(), - vtxindex: 12, - block_height: 124, - burn_header_hash: BurnchainHeaderHash::from_hex( - "0000000000000000000000000000000000000000000000000000000000000004", - ) - .unwrap(), - }; - - let user_burn_1 = UserBurnSupportOp { - address: StacksAddress::new(2, Hash160([2u8; 20])), - consensus_hash: ConsensusHash::from_bytes( - &hex_bytes("4444444444444444444444444444444444444444").unwrap(), - ) - .unwrap(), - public_key: VRFPublicKey::from_bytes( - &hex_bytes("a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a") - .unwrap(), - ) - .unwrap(), - block_header_hash_160: Hash160::from_bytes( - &hex_bytes("7150f635054b87df566a970b21e07030d6444bf2").unwrap(), - ) - .unwrap(), // 22222....2222 - key_block_ptr: 123, - key_vtxindex: 456, - burn_fee: 10000, - - txid: Txid::from_bytes_be( - &hex_bytes("1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c716c") - .unwrap(), - ) - .unwrap(), - vtxindex: 13, - block_height: 124, - burn_header_hash: BurnchainHeaderHash::from_hex( - "0000000000000000000000000000000000000000000000000000000000000004", - ) - .unwrap(), - }; - - let user_burn_1_2 = UserBurnSupportOp { - address: StacksAddress::new(3, Hash160([3u8; 20])), - consensus_hash: ConsensusHash::from_bytes( - &hex_bytes("4444444444444444444444444444444444444444").unwrap(), - ) - .unwrap(), - public_key: VRFPublicKey::from_bytes( - &hex_bytes("a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a") - .unwrap(), - ) - .unwrap(), - block_header_hash_160: Hash160::from_bytes( - &hex_bytes("7150f635054b87df566a970b21e07030d6444bf2").unwrap(), - ) - .unwrap(), // 22222....2222 - key_block_ptr: 123, - key_vtxindex: 456, - burn_fee: 30000, - - txid: Txid::from_bytes_be( - &hex_bytes("1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c716c") - .unwrap(), - ) - .unwrap(), - vtxindex: 14, - block_height: 124, - burn_header_hash: BurnchainHeaderHash::from_hex( - "0000000000000000000000000000000000000000000000000000000000000004", - ) - .unwrap(), - }; - - let user_burn_2 = UserBurnSupportOp { - address: StacksAddress::new(4, Hash160([4u8; 20])), - consensus_hash: ConsensusHash::from_bytes( - &hex_bytes("4444444444444444444444444444444444444444").unwrap(), - ) - .unwrap(), - public_key: VRFPublicKey::from_bytes( - &hex_bytes("bb519494643f79f1dea0350e6fb9a1da88dfdb6137117fc2523824a8aa44fe1c") - .unwrap(), - ) - .unwrap(), - block_header_hash_160: Hash160::from_bytes( - &hex_bytes("037a1e860899a4fa823c18b66f6264d20236ec58").unwrap(), - ) - .unwrap(), // 22222....2223 - key_block_ptr: 122, - key_vtxindex: 457, - burn_fee: 20000, - - txid: Txid::from_bytes_be( - &hex_bytes("1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c716d") - .unwrap(), - ) - .unwrap(), - vtxindex: 15, - block_height: 124, - burn_header_hash: BurnchainHeaderHash::from_hex( - "0000000000000000000000000000000000000000000000000000000000000004", - ) - .unwrap(), - }; - - let user_burn_2_2 = UserBurnSupportOp { - address: StacksAddress::new(5, Hash160([5u8; 20])), - consensus_hash: ConsensusHash::from_bytes( - &hex_bytes("4444444444444444444444444444444444444444").unwrap(), - ) - .unwrap(), - public_key: VRFPublicKey::from_bytes( - &hex_bytes("bb519494643f79f1dea0350e6fb9a1da88dfdb6137117fc2523824a8aa44fe1c") - .unwrap(), - ) - .unwrap(), - block_header_hash_160: Hash160::from_bytes( - &hex_bytes("037a1e860899a4fa823c18b66f6264d20236ec58").unwrap(), - ) - .unwrap(), // 22222....2223 - key_block_ptr: 122, - key_vtxindex: 457, - burn_fee: 40000, - - txid: Txid::from_bytes_be( - &hex_bytes("1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c716c") - .unwrap(), - ) - .unwrap(), - vtxindex: 16, - block_height: 124, - burn_header_hash: BurnchainHeaderHash::from_hex( - "0000000000000000000000000000000000000000000000000000000000000004", - ) - .unwrap(), - }; - - let user_burn_nokey = UserBurnSupportOp { - address: StacksAddress::new(6, Hash160([6u8; 20])), - consensus_hash: ConsensusHash::from_bytes( - &hex_bytes("4444444444444444444444444444444444444444").unwrap(), - ) - .unwrap(), - public_key: VRFPublicKey::from_bytes( - &hex_bytes("3f3338db51f2b1f6ac0cf6177179a24ee130c04ef2f9849a64a216969ab60e70") - .unwrap(), - ) - .unwrap(), - block_header_hash_160: Hash160::from_bytes( - &hex_bytes("037a1e860899a4fa823c18b66f6264d20236ec58").unwrap(), - ) - .unwrap(), - key_block_ptr: 121, - key_vtxindex: 772, - burn_fee: 12345, - - txid: Txid::from_bytes_be( - &hex_bytes("1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c716e") - .unwrap(), - ) - .unwrap(), - vtxindex: 17, - block_height: 124, - burn_header_hash: BurnchainHeaderHash::from_hex( - "0000000000000000000000000000000000000000000000000000000000000004", - ) - .unwrap(), - }; - let block_commit_1 = LeaderBlockCommitOp { sunset_burn: 0, block_header_hash: BlockHeaderHash::from_bytes( @@ -1252,26 +988,22 @@ mod tests { BurnDistFixture { consumed_leader_keys: vec![], block_commits: vec![], - user_burns: vec![], res: vec![], }, BurnDistFixture { consumed_leader_keys: vec![leader_key_1.clone()], block_commits: vec![block_commit_1.clone()], - user_burns: vec![], res: vec![BurnSamplePoint { burns: block_commit_1.burn_fee.into(), median_burn: block_commit_1.burn_fee.into(), range_start: Uint256::zero(), range_end: Uint256::max(), candidate: block_commit_1.clone(), - user_burns: vec![], }], }, BurnDistFixture { consumed_leader_keys: vec![leader_key_1.clone(), leader_key_2.clone()], block_commits: vec![block_commit_1.clone(), block_commit_2.clone()], - user_burns: vec![], res: vec![ BurnSamplePoint { burns: block_commit_1.burn_fee.into(), @@ -1285,7 +1017,6 @@ mod tests { 0x7fffffffffffffff, ]), candidate: block_commit_1.clone(), - user_burns: vec![], }, BurnSamplePoint { burns: block_commit_2.burn_fee.into(), @@ -1299,14 +1030,12 @@ mod tests { ]), range_end: Uint256::max(), candidate: block_commit_2.clone(), - user_burns: vec![], }, ], }, BurnDistFixture { consumed_leader_keys: vec![leader_key_1.clone(), leader_key_2.clone()], block_commits: vec![block_commit_1.clone(), block_commit_2.clone()], - user_burns: vec![user_burn_noblock.clone()], res: vec![ BurnSamplePoint { burns: block_commit_1.burn_fee.into(), @@ -1320,7 +1049,6 @@ mod tests { 0x7fffffffffffffff, ]), candidate: block_commit_1.clone(), - user_burns: vec![], }, BurnSamplePoint { burns: block_commit_2.burn_fee.into(), @@ -1334,14 +1062,12 @@ mod tests { ]), range_end: Uint256::max(), candidate: block_commit_2.clone(), - user_burns: vec![], }, ], }, BurnDistFixture { consumed_leader_keys: vec![leader_key_1.clone(), leader_key_2.clone()], block_commits: vec![block_commit_1.clone(), block_commit_2.clone()], - user_burns: vec![user_burn_nokey.clone()], res: vec![ BurnSamplePoint { burns: block_commit_1.burn_fee.into(), @@ -1355,7 +1081,6 @@ mod tests { 0x7fffffffffffffff, ]), candidate: block_commit_1.clone(), - user_burns: vec![], }, BurnSamplePoint { burns: block_commit_2.burn_fee.into(), @@ -1369,18 +1094,12 @@ mod tests { ]), range_end: Uint256::max(), candidate: block_commit_2.clone(), - user_burns: vec![], }, ], }, BurnDistFixture { consumed_leader_keys: vec![leader_key_1.clone(), leader_key_2.clone()], block_commits: vec![block_commit_1.clone(), block_commit_2.clone()], - user_burns: vec![ - user_burn_noblock.clone(), - user_burn_1.clone(), - user_burn_nokey.clone(), - ], res: vec![ BurnSamplePoint { burns: block_commit_1.burn_fee.into(), @@ -1394,7 +1113,6 @@ mod tests { 0x7fffffffffffffff, ]), candidate: block_commit_1.clone(), - user_burns: vec![], }, BurnSamplePoint { burns: block_commit_2.burn_fee.into(), @@ -1408,19 +1126,12 @@ mod tests { ]), range_end: Uint256::max(), candidate: block_commit_2.clone(), - user_burns: vec![], }, ], }, BurnDistFixture { consumed_leader_keys: vec![leader_key_1.clone(), leader_key_2.clone()], block_commits: vec![block_commit_1.clone(), block_commit_2.clone()], - user_burns: vec![ - user_burn_noblock.clone(), - user_burn_1.clone(), - user_burn_2.clone(), - user_burn_nokey.clone(), - ], res: vec![ BurnSamplePoint { burns: block_commit_1.burn_fee.into(), @@ -1434,7 +1145,6 @@ mod tests { 0x7fffffffffffffff, ]), candidate: block_commit_1.clone(), - user_burns: vec![], }, BurnSamplePoint { burns: block_commit_2.burn_fee.into(), @@ -1448,21 +1158,12 @@ mod tests { ]), range_end: Uint256::max(), candidate: block_commit_2.clone(), - user_burns: vec![], }, ], }, BurnDistFixture { consumed_leader_keys: vec![leader_key_1.clone(), leader_key_2.clone()], block_commits: vec![block_commit_1.clone(), block_commit_2.clone()], - user_burns: vec![ - user_burn_noblock.clone(), - user_burn_1.clone(), - user_burn_1_2.clone(), - user_burn_2.clone(), - user_burn_2_2.clone(), - user_burn_nokey.clone(), - ], res: vec![ BurnSamplePoint { burns: block_commit_1.burn_fee.into(), @@ -1476,7 +1177,6 @@ mod tests { 0x7fffffffffffffff, ]), candidate: block_commit_1.clone(), - user_burns: vec![], }, BurnSamplePoint { burns: block_commit_2.burn_fee.into(), @@ -1490,7 +1190,6 @@ mod tests { ]), range_end: Uint256::max(), candidate: block_commit_2.clone(), - user_burns: vec![], }, ], }, @@ -1505,14 +1204,6 @@ mod tests { block_commit_2.clone(), block_commit_3.clone(), ], - user_burns: vec![ - user_burn_noblock.clone(), - user_burn_1.clone(), - user_burn_1_2.clone(), - user_burn_2.clone(), - user_burn_2_2.clone(), - user_burn_nokey.clone(), - ], res: vec![ BurnSamplePoint { burns: block_commit_1.burn_fee.into(), @@ -1525,7 +1216,6 @@ mod tests { 0x41a3ed94d3cb0a84, ]), candidate: block_commit_1.clone(), - user_burns: vec![], }, BurnSamplePoint { burns: block_commit_2.burn_fee.into(), @@ -1543,7 +1233,6 @@ mod tests { 0x8347db29a7961508, ]), candidate: block_commit_2.clone(), - user_burns: vec![], }, BurnSamplePoint { burns: (block_commit_3.burn_fee).into(), @@ -1556,7 +1245,6 @@ mod tests { ]), range_end: Uint256::max(), candidate: block_commit_3.clone(), - user_burns: vec![], }, ], }, @@ -1568,7 +1256,6 @@ mod tests { let dist = BurnSamplePoint::make_distribution( f.block_commits.iter().cloned().collect(), f.consumed_leader_keys.iter().cloned().collect(), - f.user_burns.iter().cloned().collect(), ); assert_eq!(dist, f.res); } diff --git a/stackslib/src/chainstate/burn/mod.rs b/stackslib/src/chainstate/burn/mod.rs index 4010ba2fc..13f290d93 100644 --- a/stackslib/src/chainstate/burn/mod.rs +++ b/stackslib/src/chainstate/burn/mod.rs @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryInto; use std::fmt; use std::io::Write; @@ -64,7 +63,6 @@ impl_byte_array_newtype!(SortitionHash, u8, 32); pub enum Opcodes { LeaderBlockCommit = '[' as u8, LeaderKeyRegister = '^' as u8, - UserBurnSupport = '_' as u8, StackStx = 'x' as u8, PreStx = 'p' as u8, TransferStx = '$' as u8, @@ -198,7 +196,6 @@ impl Opcodes { match self { Opcodes::LeaderBlockCommit => Self::HTTP_BLOCK_COMMIT, Opcodes::LeaderKeyRegister => Self::HTTP_KEY_REGISTER, - Opcodes::UserBurnSupport => Self::HTTP_BURN_SUPPORT, Opcodes::StackStx => Self::HTTP_STACK_STX, Opcodes::PreStx => Self::HTTP_PRE_STX, Opcodes::TransferStx => Self::HTTP_TRANSFER_STX, @@ -210,7 +207,6 @@ impl Opcodes { let opcode = match input { Self::HTTP_BLOCK_COMMIT => Opcodes::LeaderBlockCommit, Self::HTTP_KEY_REGISTER => Opcodes::LeaderKeyRegister, - Self::HTTP_BURN_SUPPORT => Opcodes::UserBurnSupport, Self::HTTP_STACK_STX => Opcodes::StackStx, Self::HTTP_PRE_STX => Opcodes::PreStx, Self::HTTP_TRANSFER_STX => Opcodes::TransferStx, diff --git a/stackslib/src/chainstate/burn/operations/leader_block_commit.rs b/stackslib/src/chainstate/burn/operations/leader_block_commit.rs index 6d54bc7ad..43d11691c 100644 --- a/stackslib/src/chainstate/burn/operations/leader_block_commit.rs +++ b/stackslib/src/chainstate/burn/operations/leader_block_commit.rs @@ -33,7 +33,7 @@ use crate::burnchains::{ use crate::chainstate::burn::db::sortdb::{SortitionDB, SortitionHandle, SortitionHandleTx}; use crate::chainstate::burn::operations::{ parse_u16_from_be, parse_u32_from_be, BlockstackOperationType, Error as op_error, - LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, + LeaderBlockCommitOp, LeaderKeyRegisterOp, }; use crate::chainstate::burn::{ConsensusHash, Opcodes, SortitionId}; use crate::chainstate::stacks::address::PoxAddress; @@ -238,6 +238,7 @@ impl LeaderBlockCommitOp { ) } + #[cfg_attr(test, mutants::skip)] pub fn is_parent_genesis(&self) -> bool { self.parent_block_ptr == 0 && self.parent_vtxindex == 0 } diff --git a/stackslib/src/chainstate/burn/operations/leader_key_register.rs b/stackslib/src/chainstate/burn/operations/leader_key_register.rs index 22c88df6d..b892f7efd 100644 --- a/stackslib/src/chainstate/burn/operations/leader_key_register.rs +++ b/stackslib/src/chainstate/burn/operations/leader_key_register.rs @@ -31,7 +31,6 @@ use crate::burnchains::{ use crate::chainstate::burn::db::sortdb::SortitionHandleTx; use crate::chainstate::burn::operations::{ BlockstackOperationType, Error as op_error, LeaderBlockCommitOp, LeaderKeyRegisterOp, - UserBurnSupportOp, }; use crate::chainstate::burn::{ConsensusHash, Opcodes}; use crate::chainstate::stacks::{StacksPrivateKey, StacksPublicKey}; @@ -260,7 +259,7 @@ pub mod tests { use crate::burnchains::*; use crate::chainstate::burn::db::sortdb::*; use crate::chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, + BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, }; use crate::chainstate::burn::{BlockSnapshot, ConsensusHash, OpsHash, SortitionHash}; use crate::chainstate::stacks::address::StacksAddressExtensions; diff --git a/stackslib/src/chainstate/burn/operations/mod.rs b/stackslib/src/chainstate/burn/operations/mod.rs index 189acab16..e51a20f63 100644 --- a/stackslib/src/chainstate/burn/operations/mod.rs +++ b/stackslib/src/chainstate/burn/operations/mod.rs @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::{From, TryInto}; use std::{error, fmt, fs, io}; use clarity::vm::types::PrincipalData; @@ -45,7 +44,6 @@ pub mod leader_block_commit; pub mod leader_key_register; pub mod stack_stx; pub mod transfer_stx; -pub mod user_burn_support; #[cfg(test)] mod test; @@ -75,11 +73,6 @@ pub enum Error { // leader key register related errors LeaderKeyAlreadyRegistered, - // user burn supports related errors - UserBurnSupportBadConsensusHash, - UserBurnSupportNoLeaderKey, - UserBurnSupportNotSupported, - // transfer stx related errors TransferStxMustBePositive, TransferStxSelfSend, @@ -137,16 +130,6 @@ impl fmt::Display for Error { Error::LeaderKeyAlreadyRegistered => { write!(f, "Leader key has already been registered") } - Error::UserBurnSupportBadConsensusHash => { - write!(f, "User burn support has an invalid consensus hash") - } - Error::UserBurnSupportNoLeaderKey => write!( - f, - "User burn support does not match a registered leader key" - ), - Error::UserBurnSupportNotSupported => { - write!(f, "User burn operations are not supported") - } Error::TransferStxMustBePositive => write!(f, "Transfer STX must be positive amount"), Error::TransferStxSelfSend => write!(f, "Transfer STX must not send to self"), Error::StackStxMustBePositive => write!(f, "Stack STX must be positive amount"), @@ -268,24 +251,6 @@ pub struct LeaderKeyRegisterOp { pub burn_header_hash: BurnchainHeaderHash, // hash of burn chain block } -/// NOTE: this struct is currently not used -#[derive(Debug, PartialEq, Clone, Eq, Serialize, Deserialize)] -pub struct UserBurnSupportOp { - pub address: StacksAddress, - pub consensus_hash: ConsensusHash, - pub public_key: VRFPublicKey, - pub key_block_ptr: u32, - pub key_vtxindex: u16, - pub block_header_hash_160: Hash160, - pub burn_fee: u64, - - // common to all transactions - pub txid: Txid, // transaction ID - pub vtxindex: u32, // index in the block where this tx occurs - pub block_height: u64, // block height at which this tx occurs - pub burn_header_hash: BurnchainHeaderHash, // hash of burnchain block with this tx -} - #[derive(Debug, PartialEq, Clone, Eq, Serialize, Deserialize)] pub struct DelegateStxOp { pub sender: StacksAddress, @@ -343,7 +308,6 @@ fn principal_deserialize<'de, D: serde::Deserializer<'de>>( pub enum BlockstackOperationType { LeaderKeyRegister(LeaderKeyRegisterOp), LeaderBlockCommit(LeaderBlockCommitOp), - UserBurnSupport(UserBurnSupportOp), PreStx(PreStxOp), StackStx(StackStxOp), TransferStx(TransferStxOp), @@ -370,7 +334,6 @@ impl BlockstackOperationType { match *self { BlockstackOperationType::LeaderKeyRegister(_) => Opcodes::LeaderKeyRegister, BlockstackOperationType::LeaderBlockCommit(_) => Opcodes::LeaderBlockCommit, - BlockstackOperationType::UserBurnSupport(_) => Opcodes::UserBurnSupport, BlockstackOperationType::StackStx(_) => Opcodes::StackStx, BlockstackOperationType::PreStx(_) => Opcodes::PreStx, BlockstackOperationType::TransferStx(_) => Opcodes::TransferStx, @@ -386,7 +349,6 @@ impl BlockstackOperationType { match *self { BlockstackOperationType::LeaderKeyRegister(ref data) => &data.txid, BlockstackOperationType::LeaderBlockCommit(ref data) => &data.txid, - BlockstackOperationType::UserBurnSupport(ref data) => &data.txid, BlockstackOperationType::StackStx(ref data) => &data.txid, BlockstackOperationType::PreStx(ref data) => &data.txid, BlockstackOperationType::TransferStx(ref data) => &data.txid, @@ -398,7 +360,6 @@ impl BlockstackOperationType { match *self { BlockstackOperationType::LeaderKeyRegister(ref data) => data.vtxindex, BlockstackOperationType::LeaderBlockCommit(ref data) => data.vtxindex, - BlockstackOperationType::UserBurnSupport(ref data) => data.vtxindex, BlockstackOperationType::StackStx(ref data) => data.vtxindex, BlockstackOperationType::PreStx(ref data) => data.vtxindex, BlockstackOperationType::TransferStx(ref data) => data.vtxindex, @@ -410,7 +371,6 @@ impl BlockstackOperationType { match *self { BlockstackOperationType::LeaderKeyRegister(ref data) => data.block_height, BlockstackOperationType::LeaderBlockCommit(ref data) => data.block_height, - BlockstackOperationType::UserBurnSupport(ref data) => data.block_height, BlockstackOperationType::StackStx(ref data) => data.block_height, BlockstackOperationType::PreStx(ref data) => data.block_height, BlockstackOperationType::TransferStx(ref data) => data.block_height, @@ -422,7 +382,6 @@ impl BlockstackOperationType { match *self { BlockstackOperationType::LeaderKeyRegister(ref data) => data.burn_header_hash.clone(), BlockstackOperationType::LeaderBlockCommit(ref data) => data.burn_header_hash.clone(), - BlockstackOperationType::UserBurnSupport(ref data) => data.burn_header_hash.clone(), BlockstackOperationType::StackStx(ref data) => data.burn_header_hash.clone(), BlockstackOperationType::PreStx(ref data) => data.burn_header_hash.clone(), BlockstackOperationType::TransferStx(ref data) => data.burn_header_hash.clone(), @@ -437,7 +396,6 @@ impl BlockstackOperationType { BlockstackOperationType::LeaderBlockCommit(ref mut data) => { data.set_burn_height(height) } - BlockstackOperationType::UserBurnSupport(ref mut data) => data.block_height = height, BlockstackOperationType::StackStx(ref mut data) => data.block_height = height, BlockstackOperationType::PreStx(ref mut data) => data.block_height = height, BlockstackOperationType::TransferStx(ref mut data) => data.block_height = height, @@ -454,7 +412,6 @@ impl BlockstackOperationType { BlockstackOperationType::LeaderBlockCommit(ref mut data) => { data.burn_header_hash = hash } - BlockstackOperationType::UserBurnSupport(ref mut data) => data.burn_header_hash = hash, BlockstackOperationType::StackStx(ref mut data) => data.burn_header_hash = hash, BlockstackOperationType::PreStx(ref mut data) => data.burn_header_hash = hash, BlockstackOperationType::TransferStx(ref mut data) => data.burn_header_hash = hash, @@ -547,7 +504,6 @@ impl fmt::Display for BlockstackOperationType { BlockstackOperationType::PreStx(ref op) => write!(f, "{:?}", op), BlockstackOperationType::StackStx(ref op) => write!(f, "{:?}", op), BlockstackOperationType::LeaderBlockCommit(ref op) => write!(f, "{:?}", op), - BlockstackOperationType::UserBurnSupport(ref op) => write!(f, "{:?}", op), BlockstackOperationType::TransferStx(ref op) => write!(f, "{:?}", op), BlockstackOperationType::DelegateStx(ref op) => write!(f, "{:?}", op), } diff --git a/stackslib/src/chainstate/burn/operations/user_burn_support.rs b/stackslib/src/chainstate/burn/operations/user_burn_support.rs deleted file mode 100644 index cd0ff1a4f..000000000 --- a/stackslib/src/chainstate/burn/operations/user_burn_support.rs +++ /dev/null @@ -1,789 +0,0 @@ -// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020 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 . - -use std::io::{Read, Write}; -use std::marker::PhantomData; - -use stacks_common::codec::{write_next, Error as codec_error, StacksMessageCodec}; -use stacks_common::types::chainstate::{BlockHeaderHash, BurnchainHeaderHash, TrieHash}; -use stacks_common::util::hash::Hash160; -use stacks_common::util::log; -use stacks_common::util::vrf::{VRFPublicKey, VRF}; - -use crate::burnchains::{ - Address, Burnchain, BurnchainBlockHeader, BurnchainTransaction, PublicKey, Txid, -}; -use crate::chainstate::burn::db::sortdb::SortitionHandleTx; -use crate::chainstate::burn::operations::{ - parse_u16_from_be, parse_u32_from_be, BlockstackOperationType, Error as op_error, - LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, -}; -use crate::chainstate::burn::{ConsensusHash, Opcodes}; -use crate::net::Error as net_error; -use crate::util_lib::db::{DBConn, DBTx}; - -// return type for parse_data (below) -struct ParsedData { - pub consensus_hash: ConsensusHash, - pub public_key: VRFPublicKey, - pub key_block_ptr: u32, - pub key_vtxindex: u16, - pub block_header_hash_160: Hash160, -} - -impl UserBurnSupportOp { - fn parse_data(data: &Vec) -> Option { - /* - Wire format: - - 0 2 3 22 54 74 78 80 - |------|--|---------------|-----------------------|------------------|--------|---------| - magic op consensus hash proving public key block hash 160 key blk key - (truncated by 1) vtxindex - - - Note that `data` is missing the first 3 bytes -- the magic and op have been stripped - */ - if data.len() < 77 { - warn!( - "USER_BURN_SUPPORT payload is malformed ({} bytes)", - data.len() - ); - return None; - } - - let mut consensus_hash_trunc = data[0..19].to_vec(); - consensus_hash_trunc.push(0); - - let consensus_hash = ConsensusHash::from_vec(&consensus_hash_trunc) - .expect("FATAL: invalid data slice for consensus hash"); - let pubkey = match VRFPublicKey::from_bytes(&data[19..51]) { - Some(pubk) => pubk, - None => { - warn!("Invalid VRF public key"); - return None; - } - }; - - let block_header_hash_160 = Hash160::from_vec(&data[51..71].to_vec()) - .expect("FATAL: invalid data slice for block hash160"); - let key_block_ptr = parse_u32_from_be(&data[71..75]).unwrap(); - let key_vtxindex = parse_u16_from_be(&data[75..77]).unwrap(); - - Some(ParsedData { - consensus_hash, - public_key: pubkey, - block_header_hash_160, - key_block_ptr, - key_vtxindex, - }) - } - - fn parse_from_tx( - block_height: u64, - block_hash: &BurnchainHeaderHash, - tx: &BurnchainTransaction, - ) -> Result { - // can't be too careful... - let num_inputs = tx.num_signers(); - let outputs = tx.get_recipients(); - - if num_inputs == 0 || outputs.len() == 0 { - test_debug!( - "Invalid tx: inputs: {}, outputs: {}", - num_inputs, - outputs.len() - ); - return Err(op_error::InvalidInput); - } - - if outputs.len() < 2 { - test_debug!( - "Invalid tx: inputs: {}, outputs: {}", - num_inputs, - outputs.len() - ); - return Err(op_error::InvalidInput); - } - - if tx.opcode() != Opcodes::UserBurnSupport as u8 { - test_debug!("Invalid tx: invalid opcode {}", tx.opcode()); - return Err(op_error::InvalidInput); - } - - let output_0 = outputs[0].clone().ok_or_else(|| { - warn!("Invalid tx: unrecognized output 0"); - op_error::InvalidInput - })?; - - // outputs[0] should be the burn output - if !output_0.address.is_burn() { - // wrong burn output - test_debug!("Invalid tx: burn output missing (got {:?})", outputs[0]); - return Err(op_error::ParseError); - } - - let burn_fee = output_0.amount; - - let data = match UserBurnSupportOp::parse_data(&tx.data()) { - None => { - test_debug!("Invalid tx data"); - return Err(op_error::ParseError); - } - Some(d) => d, - }; - - // basic sanity checks - if data.key_block_ptr == 0 { - warn!("Invalid tx: key block pointer must be positive"); - return Err(op_error::ParseError); - } - - if data.key_block_ptr as u64 > block_height { - warn!( - "Invalid tx: key block back-pointer {} exceeds block height {}", - data.key_block_ptr, block_height - ); - return Err(op_error::ParseError); - } - - let output = outputs[1] - .as_ref() - .ok_or_else(|| { - warn!("Invalid tx: unrecognized output 1"); - op_error::InvalidInput - })? - .address - .clone() - .try_into_stacks_address() - .ok_or_else(|| { - warn!("Invalid tx: output must be representable as a StacksAddress"); - op_error::InvalidInput - })?; - - Ok(UserBurnSupportOp { - address: output, - consensus_hash: data.consensus_hash, - public_key: data.public_key, - block_header_hash_160: data.block_header_hash_160, - key_block_ptr: data.key_block_ptr, - key_vtxindex: data.key_vtxindex, - burn_fee: burn_fee, - - txid: tx.txid(), - vtxindex: tx.vtxindex(), - block_height: block_height, - burn_header_hash: block_hash.clone(), - }) - } -} - -impl StacksMessageCodec for UserBurnSupportOp { - /* - Wire format: - - 0 2 3 22 54 74 78 80 - |------|--|---------------|-----------------------|------------------|--------|---------| - magic op consensus hash proving public key block hash 160 key blk key - (truncated by 1) vtxindex - */ - fn consensus_serialize(&self, fd: &mut W) -> Result<(), codec_error> { - write_next(fd, &(Opcodes::UserBurnSupport as u8))?; - let truncated_consensus = self.consensus_hash.to_bytes(); - fd.write_all(&truncated_consensus[0..19]) - .map_err(codec_error::WriteError)?; - fd.write_all(&self.public_key.as_bytes()[..]) - .map_err(codec_error::WriteError)?; - write_next(fd, &self.block_header_hash_160)?; - write_next(fd, &self.key_block_ptr)?; - write_next(fd, &self.key_vtxindex)?; - Ok(()) - } - - fn consensus_deserialize(_fd: &mut R) -> Result { - // Op deserialized through burchain indexer - unimplemented!(); - } -} - -impl UserBurnSupportOp { - pub fn from_tx( - _block_header: &BurnchainBlockHeader, - _tx: &BurnchainTransaction, - ) -> Result { - Err(op_error::UserBurnSupportNotSupported) - } - - pub fn check(&self, burnchain: &Burnchain, tx: &mut SortitionHandleTx) -> Result<(), op_error> { - let leader_key_block_height = self.key_block_ptr as u64; - - ///////////////////////////////////////////////////////////////// - // Consensus hash must be recent and valid - ///////////////////////////////////////////////////////////////// - - // NOTE: we only care about the first 19 bytes - let is_fresh = tx.is_fresh_consensus_hash_check_19b( - burnchain.consensus_hash_lifetime.into(), - &self.consensus_hash, - )?; - - if !is_fresh { - warn!( - "Invalid user burn: invalid consensus hash {}", - &self.consensus_hash - ); - return Err(op_error::UserBurnSupportBadConsensusHash); - } - - ///////////////////////////////////////////////////////////////////////////////////// - // There must exist a previously-accepted LeaderKeyRegisterOp that matches this - // user support burn's VRF public key. - ///////////////////////////////////////////////////////////////////////////////////// - if self.key_block_ptr == 0 { - warn!("Invalid tx: key block back-pointer must be positive"); - return Err(op_error::ParseError); - } - - if self.key_block_ptr as u64 > self.block_height { - warn!( - "Invalid tx: key block back-pointer {} exceeds block height {}", - self.key_block_ptr, self.block_height - ); - return Err(op_error::ParseError); - } - - let chain_tip = tx.context.chain_tip.clone(); - let register_key_opt = tx.get_leader_key_at( - leader_key_block_height, - self.key_vtxindex.into(), - &chain_tip, - )?; - - if register_key_opt.is_none() { - warn!( - "Invalid user burn: no such leader VRF key {}", - &self.public_key.to_hex() - ); - return Err(op_error::UserBurnSupportNoLeaderKey); - } - - ///////////////////////////////////////////////////////////////////////////////////// - // The block hash can't be checked here -- the corresponding LeaderBlockCommitOp may - // not have been checked yet, so we don't know yet if it exists. The sortition - // algorithm will carry out this check, and only consider user burns if they match - // a block commit and the commit's corresponding leader key. - ///////////////////////////////////////////////////////////////////////////////////// - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use stacks_common::deps_common::bitcoin::blockdata::transaction::Transaction; - use stacks_common::deps_common::bitcoin::network::serialize::deserialize; - use stacks_common::types::chainstate::{SortitionId, StacksAddress}; - use stacks_common::util::hash::{hex_bytes, to_hex, Hash160}; - use stacks_common::util::{get_epoch_time_secs, log}; - - use super::*; - use crate::burnchains::bitcoin::address::BitcoinAddress; - use crate::burnchains::bitcoin::blocks::BitcoinBlockParser; - use crate::burnchains::bitcoin::keys::BitcoinPublicKey; - use crate::burnchains::bitcoin::BitcoinNetworkType; - use crate::burnchains::*; - use crate::chainstate::burn::db::sortdb::*; - use crate::chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, - }; - use crate::chainstate::burn::*; - use crate::chainstate::stacks::address::StacksAddressExtensions; - use crate::chainstate::stacks::index::TrieHashExtension; - use crate::core::StacksEpochId; - - struct OpFixture { - txstr: String, - opstr: String, - result: Option, - } - - struct CheckFixture { - op: UserBurnSupportOp, - res: Result<(), op_error>, - } - - fn make_tx(hex_str: &str) -> Result { - let tx_bin = hex_bytes(hex_str).map_err(|_e| "failed to decode hex string")?; - let tx = deserialize(&tx_bin.to_vec()).map_err(|_e| "failed to deserialize")?; - Ok(tx) - } - - #[test] - fn test_parse() { - let vtxindex = 1; - let _block_height = 694; - let burn_header_hash = BurnchainHeaderHash::from_hex( - "0000000000000000000000000000000000000000000000000000000000000000", - ) - .unwrap(); - - let tx_fixtures = vec![ - OpFixture { - txstr: "01000000011111111111111111111111111111111111111111111111111111111111111111000000006a47304402204c51707ac34b6dcbfc518ba40c5fc4ef737bf69cc21a9f8a8e6f621f511f78e002200caca0f102d5df509c045c4fe229d957aa7ef833dc8103dc2fe4db15a22bab9e012102d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d000000000030000000000000000536a4c5069645f2222222222222222222222222222222222222222a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a3333333333333333333333333333333333333333010203040539300000000000001976a914000000000000000000000000000000000000000088aca05b0000000000001976a9140be3e286a15ea85882761618e366586b5574100d88ac00000000".to_string(), - opstr: "69645f2222222222222222222222222222222222222222a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a33333333333333333333333333333333333333330102030405".to_string(), - result: Some(UserBurnSupportOp { - address: StacksAddress::from_legacy_bitcoin_address(&BitcoinAddress::from_string(&"mgbpit8FvkVJ9kuXY8QSM5P7eibnhcEMBk".to_string()).unwrap().expect_legacy()), - consensus_hash: ConsensusHash::from_bytes(&hex_bytes("2222222222222222222222222222222222222200").unwrap()).unwrap(), - public_key: VRFPublicKey::from_bytes(&hex_bytes("22a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c").unwrap()).unwrap(), - block_header_hash_160: Hash160::from_bytes(&hex_bytes("7a33333333333333333333333333333333333333").unwrap()).unwrap(), - key_block_ptr: 0x33010203, - key_vtxindex: 0x0405, - burn_fee: 12345, - - txid: Txid::from_bytes_be(&hex_bytes("1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c716c").unwrap()).unwrap(), - vtxindex: vtxindex, - block_height: 0x33010203 + 1, - burn_header_hash: burn_header_hash, - }) - }, - OpFixture { - // invalid -- no burn output - txstr: "01000000011111111111111111111111111111111111111111111111111111111111111111000000006a473044022073490a3958b9e6128d3b7a4a8c77203c56862b2da382e96551f7efae7029b0e1022046672d1e61bdfd3dca9cc199bffd0bfb9323e432f8431bb6749da3c5bd06e9ca012102d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d000000000020000000000000000536a4c5069645f2222222222222222222222222222222222222222a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a33333333333333333333333333333333333333330102030405a05b0000000000001976a9140be3e286a15ea85882761618e366586b5574100d88ac00000000".to_string(), - opstr: "69645f2222222222222222222222222222222222222222a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a33333333333333333333333333333333333333330102030405".to_string(), - result: None, - }, - OpFixture { - // invalid -- bad public key - txstr: "01000000011111111111111111111111111111111111111111111111111111111111111111000000006a47304402202bf944fa4d1dbbdd4f53e915c85f07c8a5afbf917f7cc9169e9c7d3bbadff05a022064b33a1020dd9cdd0ac6de213ee1bd8f364c9c876e716ad289f324c2a4bbe48a012102d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d000000000030000000000000000536a4c5069645f2222222222222222222222222222222222222222a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7b3333333333333333333333333333333333333333010203040539300000000000001976a914000000000000000000000000000000000000000088aca05b0000000000001976a9140be3e286a15ea85882761618e366586b5574100d88ac00000000".to_string(), - opstr: "69645f2222222222222222222222222222222222222222a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a33333333333333333333333333333333333333330102030405".to_string(), - result: None, - }, - OpFixture { - // invalid -- too short - txstr: "01000000011111111111111111111111111111111111111111111111111111111111111111000000006a473044022038534377d738ba91df50a4bc885bcd6328520438d42cc29636cc299a24dcb4c202202953e87b6c176697d01d66a742a27fd48b8d2167fb9db184d59a3be23a59992e012102d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d0000000000300000000000000004c6a4a69645f2222222222222222222222222222222222222222a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a3333333333333333333333333333333333333339300000000000001976a914000000000000000000000000000000000000000088aca05b0000000000001976a9140be3e286a15ea85882761618e366586b5574100d88ac00000000".to_string(), - opstr: "69645f2222222222222222222222222222222222222222a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a33333333333333333333333333333333333333330102030405".to_string(), - result: None, - }, - OpFixture { - // invalid -- wrong opcode - txstr: "01000000011111111111111111111111111111111111111111111111111111111111111111000000006a47304402200e6dbb4ccefc44582135091678a49228716431583dab3d789b1211d5737d02e402205b523ad156cad4ae6bb29f046b144c8c82b7c85698616ee8f5d59ea40d594dd4012102d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d000000000030000000000000000536a4c5069645e2222222222222222222222222222222222222222a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a3333333333333333333333333333333333333333010203040539300000000000001976a914000000000000000000000000000000000000000088aca05b0000000000001976a9140be3e286a15ea85882761618e366586b5574100d88ac00000000".to_string(), - opstr: "69645f2222222222222222222222222222222222222222a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a33333333333333333333333333333333333333330102030405".to_string(), - result: None, - } - ]; - - let parser = BitcoinBlockParser::new(BitcoinNetworkType::Testnet, BLOCKSTACK_MAGIC_MAINNET); - - for tx_fixture in tx_fixtures { - let tx = make_tx(&tx_fixture.txstr).unwrap(); - let burnchain_tx = BurnchainTransaction::Bitcoin( - parser - .parse_tx(&tx, vtxindex as usize, StacksEpochId::Epoch2_05) - .unwrap(), - ); - - let header = match tx_fixture.result { - Some(ref op) => BurnchainBlockHeader { - block_height: op.block_height, - block_hash: op.burn_header_hash.clone(), - parent_block_hash: op.burn_header_hash.clone(), - num_txs: 1, - timestamp: get_epoch_time_secs(), - }, - None => BurnchainBlockHeader { - block_height: 0, - block_hash: BurnchainHeaderHash::zero(), - parent_block_hash: BurnchainHeaderHash::zero(), - num_txs: 0, - timestamp: get_epoch_time_secs(), - }, - }; - - let op = UserBurnSupportOp::parse_from_tx( - header.block_height, - &header.block_hash, - &burnchain_tx, - ); - - match (op, tx_fixture.result) { - (Ok(parsed_tx), Some(result)) => { - let opstr = { - let mut buffer = vec![]; - let mut magic_bytes = BLOCKSTACK_MAGIC_MAINNET.as_bytes().to_vec(); - buffer.append(&mut magic_bytes); - parsed_tx - .consensus_serialize(&mut buffer) - .expect("FATAL: invalid operation"); - to_hex(&buffer) - }; - - assert_eq!(tx_fixture.opstr, opstr); - assert_eq!(parsed_tx, result); - } - (Err(_e), None) => {} - (Ok(_parsed_tx), None) => { - test_debug!("Parsed a tx when we should not have"); - assert!(false); - } - (Err(_e), Some(_result)) => { - test_debug!("Did not parse a tx when we should have: {:?}", _result); - assert!(false); - } - }; - } - } - - #[test] - fn test_check() { - let first_block_height = 121; - let first_burn_hash = BurnchainHeaderHash::from_hex( - "0000000000000000000000000000000000000000000000000000000000000123", - ) - .unwrap(); - - let block_122_hash = BurnchainHeaderHash::from_hex( - "0000000000000000000000000000000000000000000000000000000000000002", - ) - .unwrap(); - let block_123_hash = BurnchainHeaderHash::from_hex( - "0000000000000000000000000000000000000000000000000000000000000003", - ) - .unwrap(); - let block_124_hash = BurnchainHeaderHash::from_hex( - "0000000000000000000000000000000000000000000000000000000000000004", - ) - .unwrap(); - let block_125_hash = BurnchainHeaderHash::from_hex( - "0000000000000000000000000000000000000000000000000000000000000005", - ) - .unwrap(); - let block_126_hash = BurnchainHeaderHash::from_hex( - "0000000000000000000000000000000000000000000000000000000000000006", - ) - .unwrap(); - let block_127_hash = BurnchainHeaderHash::from_hex( - "0000000000000000000000000000000000000000000000000000000000000007", - ) - .unwrap(); - let block_128_hash = BurnchainHeaderHash::from_hex( - "0000000000000000000000000000000000000000000000000000000000000008", - ) - .unwrap(); - let block_129_hash = BurnchainHeaderHash::from_hex( - "0000000000000000000000000000000000000000000000000000000000000009", - ) - .unwrap(); - let block_130_hash = BurnchainHeaderHash::from_hex( - "000000000000000000000000000000000000000000000000000000000000000a", - ) - .unwrap(); - let block_131_hash = BurnchainHeaderHash::from_hex( - "000000000000000000000000000000000000000000000000000000000000000b", - ) - .unwrap(); - - let block_header_hashes = [ - block_122_hash.clone(), - block_123_hash.clone(), - block_124_hash.clone(), - block_125_hash.clone(), - block_126_hash.clone(), - block_127_hash.clone(), - block_128_hash.clone(), - block_129_hash.clone(), - block_130_hash.clone(), - block_131_hash.clone(), - ]; - let burnchain = Burnchain { - pox_constants: PoxConstants::test_default(), - peer_version: 0x012345678, - network_id: 0x9abcdef0, - chain_name: "bitcoin".to_string(), - network_name: "testnet".to_string(), - working_dir: "/nope".to_string(), - consensus_hash_lifetime: 24, - stable_confirmations: 7, - first_block_height, - initial_reward_start_block: first_block_height, - first_block_timestamp: 0, - first_block_hash: first_burn_hash.clone(), - }; - - let mut db = SortitionDB::connect_test(first_block_height, &first_burn_hash).unwrap(); - - let leader_key_1 = LeaderKeyRegisterOp { - consensus_hash: ConsensusHash::from_bytes( - &hex_bytes("0000000000000000000000000000000000000000").unwrap(), - ) - .unwrap(), - public_key: VRFPublicKey::from_bytes( - &hex_bytes("a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a") - .unwrap(), - ) - .unwrap(), - memo: vec![01, 02, 03, 04, 05], - - txid: Txid::from_bytes_be( - &hex_bytes("1bfa831b5fc56c858198acb8e77e5863c1e9d8ac26d49ddb914e24d8d4083562") - .unwrap(), - ) - .unwrap(), - vtxindex: 456, - block_height: 123, - burn_header_hash: block_123_hash.clone(), - }; - - let block_ops = vec![ - // 122 - vec![], - // 123 - vec![BlockstackOperationType::LeaderKeyRegister( - leader_key_1.clone(), - )], - // 124 - vec![], - // 125 - vec![], - // 126 - vec![], - // 127 - vec![], - // 128 - vec![], - // 129 - vec![], - // 130 - vec![], - // 131 - vec![], - ]; - - // populate consensus hashes - let tip_index_root = { - let mut prev_snapshot = SortitionDB::get_first_block_snapshot(db.conn()).unwrap(); - for i in 0..10 { - let mut snapshot_row = BlockSnapshot { - accumulated_coinbase_ustx: 0, - pox_valid: true, - block_height: i + 1 + first_block_height, - burn_header_timestamp: get_epoch_time_secs(), - burn_header_hash: block_header_hashes[i as usize].clone(), - sortition_id: SortitionId(block_header_hashes[i as usize].0.clone()), - parent_sortition_id: prev_snapshot.sortition_id.clone(), - parent_burn_header_hash: prev_snapshot.burn_header_hash.clone(), - consensus_hash: ConsensusHash::from_bytes(&[ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - (i + 1) as u8, - ]) - .unwrap(), - ops_hash: OpsHash::from_bytes(&[ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, i as u8, - ]) - .unwrap(), - total_burn: i, - sortition: true, - sortition_hash: SortitionHash::initial(), - winning_block_txid: Txid::from_hex( - "0000000000000000000000000000000000000000000000000000000000000000", - ) - .unwrap(), - winning_stacks_block_hash: BlockHeaderHash::from_hex( - "0000000000000000000000000000000000000000000000000000000000000000", - ) - .unwrap(), - index_root: TrieHash::from_empty_data(), - num_sortitions: i + 1, - stacks_block_accepted: false, - stacks_block_height: 0, - arrival_index: 0, - canonical_stacks_tip_height: 0, - canonical_stacks_tip_hash: BlockHeaderHash([0u8; 32]), - canonical_stacks_tip_consensus_hash: ConsensusHash([0u8; 20]), - ..BlockSnapshot::initial(0, &first_burn_hash, 0) - }; - let mut tx = - SortitionHandleTx::begin(&mut db, &prev_snapshot.sortition_id).unwrap(); - - let tip_index_root = tx - .append_chain_tip_snapshot( - &prev_snapshot, - &snapshot_row, - &block_ops[i as usize], - &vec![], - None, - None, - None, - ) - .unwrap(); - snapshot_row.index_root = tip_index_root; - - tx.commit().unwrap(); - prev_snapshot = snapshot_row; - } - - prev_snapshot.index_root.clone() - }; - - let check_fixtures = vec![ - CheckFixture { - // reject -- bad consensus hash - op: UserBurnSupportOp { - address: StacksAddress::new(1, Hash160([1u8; 20])), - consensus_hash: ConsensusHash::from_bytes( - &hex_bytes("1000000000000000000000000000000000000000").unwrap(), - ) - .unwrap(), - public_key: VRFPublicKey::from_bytes( - &hex_bytes( - "a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a", - ) - .unwrap(), - ) - .unwrap(), - block_header_hash_160: Hash160::from_bytes( - &hex_bytes("7150f635054b87df566a970b21e07030d6444bf2").unwrap(), - ) - .unwrap(), // 22222....2222 - key_block_ptr: 123, - key_vtxindex: 456, - burn_fee: 10000, - - txid: Txid::from_bytes_be( - &hex_bytes( - "1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c716b", - ) - .unwrap(), - ) - .unwrap(), - vtxindex: 13, - block_height: 124, - burn_header_hash: block_124_hash.clone(), - }, - res: Err(op_error::UserBurnSupportBadConsensusHash), - }, - CheckFixture { - // reject -- no leader key - op: UserBurnSupportOp { - address: StacksAddress::new(1, Hash160([1u8; 20])), - consensus_hash: ConsensusHash::from_bytes( - &hex_bytes("0000000000000000000000000000000000000000").unwrap(), - ) - .unwrap(), - public_key: VRFPublicKey::from_bytes( - &hex_bytes( - "bb519494643f79f1dea0350e6fb9a1da88dfdb6137117fc2523824a8aa44fe1c", - ) - .unwrap(), - ) - .unwrap(), - block_header_hash_160: Hash160::from_bytes( - &hex_bytes("7150f635054b87df566a970b21e07030d6444bf2").unwrap(), - ) - .unwrap(), // 22222....2222 - key_block_ptr: 123, - key_vtxindex: 457, - burn_fee: 10000, - - txid: Txid::from_bytes_be( - &hex_bytes( - "1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c716b", - ) - .unwrap(), - ) - .unwrap(), - vtxindex: 13, - block_height: 124, - burn_header_hash: block_124_hash.clone(), - }, - res: Err(op_error::UserBurnSupportNoLeaderKey), - }, - CheckFixture { - // accept - op: UserBurnSupportOp { - address: StacksAddress::new(1, Hash160([1u8; 20])), - consensus_hash: ConsensusHash::from_bytes( - &hex_bytes("0000000000000000000000000000000000000000").unwrap(), - ) - .unwrap(), - public_key: VRFPublicKey::from_bytes( - &hex_bytes( - "a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a", - ) - .unwrap(), - ) - .unwrap(), - block_header_hash_160: Hash160::from_bytes( - &hex_bytes("7150f635054b87df566a970b21e07030d6444bf2").unwrap(), - ) - .unwrap(), // 22222....2222 - key_block_ptr: 123, - key_vtxindex: 456, - burn_fee: 10000, - - txid: Txid::from_bytes_be( - &hex_bytes( - "1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c716b", - ) - .unwrap(), - ) - .unwrap(), - vtxindex: 13, - block_height: 124, - burn_header_hash: block_124_hash.clone(), - }, - res: Ok(()), - }, - ]; - - for fixture in check_fixtures { - let header = BurnchainBlockHeader { - block_height: fixture.op.block_height, - block_hash: fixture.op.burn_header_hash.clone(), - parent_block_hash: fixture.op.burn_header_hash.clone(), - num_txs: 1, - timestamp: get_epoch_time_secs(), - }; - let mut ic = SortitionHandleTx::begin( - &mut db, - &SortitionId::stubbed(&fixture.op.burn_header_hash), - ) - .unwrap(); - assert_eq!( - format!("{:?}", &fixture.res), - format!("{:?}", &fixture.op.check(&burnchain, &mut ic)) - ); - } - } -} diff --git a/stackslib/src/chainstate/burn/sortition.rs b/stackslib/src/chainstate/burn/sortition.rs index 95990e165..865b4f432 100644 --- a/stackslib/src/chainstate/burn/sortition.rs +++ b/stackslib/src/chainstate/burn/sortition.rs @@ -31,7 +31,7 @@ use crate::burnchains::{ use crate::chainstate::burn::db::sortdb::SortitionHandleTx; use crate::chainstate::burn::distribution::BurnSamplePoint; use crate::chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, + BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, }; use crate::chainstate::burn::{ BlockSnapshot, BurnchainHeaderHash, ConsensusHash, ConsensusHashExtensions, OpsHash, @@ -578,7 +578,6 @@ mod test { .unwrap(), ), ), - user_burns: vec![], }; let snapshot_no_burns = { diff --git a/stackslib/src/chainstate/coordinator/mod.rs b/stackslib/src/chainstate/coordinator/mod.rs index cc6376ca5..a05cbc94b 100644 --- a/stackslib/src/chainstate/coordinator/mod.rs +++ b/stackslib/src/chainstate/coordinator/mod.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; -use std::convert::{TryFrom, TryInto}; use std::path::PathBuf; use std::sync::atomic::AtomicBool; use std::sync::mpsc::SyncSender; @@ -3016,6 +3015,7 @@ impl< /// Try and replay a newly-discovered (or re-affirmed) sortition's associated Stacks block, if /// we have it. + #[cfg_attr(test, mutants::skip)] fn try_replay_stacks_block( &mut self, canonical_snapshot: &BlockSnapshot, @@ -3118,6 +3118,7 @@ impl< /// block." /// /// Returning None means "we can keep processing Stacks blocks" + #[cfg_attr(test, mutants::skip)] fn consider_pox_anchor( &self, pox_anchor: &BlockHeaderHash, @@ -3449,6 +3450,7 @@ pub fn check_chainstate_db_versions( /// Migrate all databases to their latest schemas. /// Verifies that this is possible as well +#[cfg_attr(test, mutants::skip)] pub fn migrate_chainstate_dbs( epochs: &[StacksEpoch], sortdb_path: &str, diff --git a/stackslib/src/chainstate/nakamoto/coordinator/tests.rs b/stackslib/src/chainstate/nakamoto/coordinator/tests.rs index fdb271347..721149789 100644 --- a/stackslib/src/chainstate/nakamoto/coordinator/tests.rs +++ b/stackslib/src/chainstate/nakamoto/coordinator/tests.rs @@ -22,7 +22,9 @@ use clarity::vm::Value; use rand::prelude::SliceRandom; use rand::{thread_rng, Rng, RngCore}; use stacks_common::address::{AddressHashMode, C32_ADDRESS_VERSION_TESTNET_SINGLESIG}; -use stacks_common::consts::{FIRST_BURNCHAIN_CONSENSUS_HASH, FIRST_STACKS_BLOCK_HASH}; +use stacks_common::consts::{ + FIRST_BURNCHAIN_CONSENSUS_HASH, FIRST_STACKS_BLOCK_HASH, SIGNER_SLOTS_PER_USER, +}; use stacks_common::types::chainstate::{ StacksAddress, StacksBlockId, StacksPrivateKey, StacksPublicKey, }; @@ -34,6 +36,7 @@ use wsts::curve::point::Point; use crate::chainstate::burn::db::sortdb::{SortitionDB, SortitionHandle}; use crate::chainstate::burn::operations::BlockstackOperationType; use crate::chainstate::coordinator::tests::{p2pkh_from, pox_addr_from}; +use crate::chainstate::nakamoto::signer_set::NakamotoSigners; use crate::chainstate::nakamoto::test_signers::TestSigners; use crate::chainstate::nakamoto::tests::get_account; use crate::chainstate::nakamoto::tests::node::TestStacker; diff --git a/stackslib/src/chainstate/nakamoto/miner.rs b/stackslib/src/chainstate/nakamoto/miner.rs index ebcd0cbb9..5edeac4c6 100644 --- a/stackslib/src/chainstate/nakamoto/miner.rs +++ b/stackslib/src/chainstate/nakamoto/miner.rs @@ -405,6 +405,7 @@ impl NakamotoBlockBuilder { tenure_info: NakamotoTenureInfo, settings: BlockBuilderSettings, event_observer: Option<&dyn MemPoolEventDispatcher>, + signer_transactions: Vec, ) -> Result<(NakamotoBlock, ExecutionCost, u64), Error> { let (tip_consensus_hash, tip_block_hash, tip_height) = ( parent_stacks_header.consensus_hash.clone(), @@ -437,13 +438,16 @@ impl NakamotoBlockBuilder { .block_limit() .expect("Failed to obtain block limit from miner's block connection"); - let initial_txs: Vec<_> = [ + let mut initial_txs: Vec<_> = [ tenure_info.tenure_change_tx.clone(), tenure_info.coinbase_tx.clone(), ] .into_iter() .filter_map(|x| x) .collect(); + initial_txs.extend(signer_transactions); + + // TODO: update this mempool check to prioritize signer vote transactions over other transactions let (blocked, tx_events) = match StacksBlockBuilder::select_and_apply_transactions( &mut tenure_tx, &mut builder, diff --git a/stackslib/src/chainstate/nakamoto/mod.rs b/stackslib/src/chainstate/nakamoto/mod.rs index 3236d4d42..8ab9f2269 100644 --- a/stackslib/src/chainstate/nakamoto/mod.rs +++ b/stackslib/src/chainstate/nakamoto/mod.rs @@ -60,7 +60,6 @@ use super::stacks::boot::{ BOOT_TEST_POX_4_AGG_KEY_FNAME, SIGNERS_MAX_LIST_SIZE, SIGNERS_NAME, SIGNERS_PK_LEN, }; use super::stacks::db::accounts::MinerReward; -use super::stacks::db::blocks::StagingUserBurnSupport; use super::stacks::db::{ ChainstateTx, ClarityTx, MinerPaymentSchedule, MinerPaymentTxFees, MinerRewardInfo, StacksBlockHeaderTypes, StacksDBTx, StacksEpochReceipt, StacksHeaderInfo, @@ -342,6 +341,33 @@ impl FromRow for NakamotoBlockHeader { } } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +/// A vote across the signer set for a block +pub struct NakamotoBlockVote { + pub signer_signature_hash: Sha512Trunc256Sum, + pub rejected: bool, +} + +impl StacksMessageCodec for NakamotoBlockVote { + fn consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { + write_next(fd, &self.signer_signature_hash)?; + if self.rejected { + write_next(fd, &1u8)?; + } + Ok(()) + } + + fn consensus_deserialize(fd: &mut R) -> Result { + let signer_signature_hash = read_next(fd)?; + let rejected_byte: Option = read_next(fd).ok(); + let rejected = rejected_byte.is_some(); + Ok(Self { + signer_signature_hash, + rejected, + }) + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct NakamotoBlock { pub header: NakamotoBlockHeader, @@ -1703,7 +1729,9 @@ impl NakamotoChainState { &block.header.signer_signature_hash().0, aggregate_public_key, )? { - let msg = format!("Received block, but the stacker signature does not match the active stacking cycle"); + let msg = format!( + "Received block, but the signer signature does not match the active stacking cycle" + ); warn!("{}", msg; "aggregate_key" => %aggregate_public_key); return Err(ChainstateError::InvalidStacksBlock(msg)); } @@ -2318,11 +2346,7 @@ impl NakamotoChainState { tenure_fees, )?; if let Some(block_reward) = block_reward { - StacksChainState::insert_miner_payment_schedule( - headers_tx.deref_mut(), - block_reward, - &[], - )?; + StacksChainState::insert_miner_payment_schedule(headers_tx.deref_mut(), block_reward)?; } StacksChainState::store_burnchain_txids( headers_tx.deref(), diff --git a/stackslib/src/chainstate/nakamoto/signer_set.rs b/stackslib/src/chainstate/nakamoto/signer_set.rs index 0eeae3d13..504928690 100644 --- a/stackslib/src/chainstate/nakamoto/signer_set.rs +++ b/stackslib/src/chainstate/nakamoto/signer_set.rs @@ -46,7 +46,7 @@ use stacks_common::util::hash::{to_hex, Hash160, MerkleHashFunc, MerkleTree, Sha use stacks_common::util::retry::BoundReader; use stacks_common::util::secp256k1::MessageSignature; use stacks_common::util::vrf::{VRFProof, VRFPublicKey, VRF}; -use wsts::curve::point::Point; +use wsts::curve::point::{Compressed, Point}; use crate::burnchains::{Burnchain, PoxConstants, Txid}; use crate::chainstate::burn::db::sortdb::{ @@ -63,9 +63,8 @@ use crate::chainstate::stacks::address::PoxAddress; use crate::chainstate::stacks::boot::{ PoxVersions, RawRewardSetEntry, RewardSet, BOOT_TEST_POX_4_AGG_KEY_CONTRACT, BOOT_TEST_POX_4_AGG_KEY_FNAME, POX_4_NAME, SIGNERS_MAX_LIST_SIZE, SIGNERS_NAME, SIGNERS_PK_LEN, - SIGNERS_UPDATE_STATE, + SIGNERS_UPDATE_STATE, SIGNERS_VOTING_FUNCTION_NAME, SIGNERS_VOTING_NAME, }; -use crate::chainstate::stacks::db::blocks::StagingUserBurnSupport; use crate::chainstate::stacks::db::{ ChainstateTx, ClarityTx, DBConfig as ChainstateConfig, MinerPaymentSchedule, MinerPaymentTxFees, MinerRewardInfo, StacksBlockHeaderTypes, StacksChainState, StacksDBTx, @@ -100,6 +99,13 @@ pub struct SignerCalculation { pub events: Vec, } +pub struct AggregateKeyVoteParams { + pub signer_index: u64, + pub aggregate_key: Point, + pub voting_round: u64, + pub reward_cycle: u64, +} + impl RawRewardSetEntry { pub fn from_pox_4_tuple(is_mainnet: bool, tuple: TupleData) -> Result { let mut tuple_data = tuple.data_map; @@ -445,4 +451,132 @@ impl NakamotoSigners { let name = Self::make_signers_db_name(reward_cycle, message_id); boot_code_id(&name, mainnet) } + + /// Get the signer addresses and corresponding weights for a given reward cycle + pub fn get_signers_weights( + chainstate: &mut StacksChainState, + sortdb: &SortitionDB, + block_id: &StacksBlockId, + reward_cycle: u64, + ) -> Result, ChainstateError> { + let signers_opt = chainstate + .eval_boot_code_read_only( + sortdb, + block_id, + SIGNERS_NAME, + &format!("(get-signers u{})", reward_cycle), + )? + .expect_optional()?; + let mut signers = HashMap::new(); + if let Some(signers_list) = signers_opt { + for signer in signers_list.expect_list()? { + let signer_tuple = signer.expect_tuple()?; + let principal_data = signer_tuple.get("signer")?.clone().expect_principal()?; + let signer_address = if let PrincipalData::Standard(signer) = principal_data { + signer.into() + } else { + panic!( + "FATAL: Signer returned from get-signers is not a standard principal: {:?}", + principal_data + ); + }; + let weight = u64::try_from(signer_tuple.get("weight")?.to_owned().expect_u128()?) + .expect("FATAL: Signer weight greater than a u64::MAX"); + signers.insert(signer_address, weight); + } + } + if signers.is_empty() { + error!( + "No signers found for reward cycle"; + "reward_cycle" => reward_cycle, + ); + return Err(ChainstateError::NoRegisteredSigners(reward_cycle)); + } + Ok(signers) + } + + /// Verify that the transaction is a valid vote for the aggregate public key + /// Note: it does not verify the function arguments, only that the transaction is validly formed + /// and has a valid nonce from an expected address + pub fn valid_vote_transaction( + account_nonces: &HashMap, + transaction: &StacksTransaction, + is_mainnet: bool, + ) -> bool { + let origin_address = transaction.origin_address(); + let origin_nonce = transaction.get_origin_nonce(); + let Some(account_nonce) = account_nonces.get(&origin_address) else { + debug!("valid_vote_transaction: Unrecognized origin address ({origin_address}).",); + return false; + }; + if transaction.is_mainnet() != is_mainnet { + debug!("valid_vote_transaction: Received a transaction for an unexpected network.",); + return false; + } + if origin_nonce < *account_nonce { + debug!("valid_vote_transaction: Received a transaction with an outdated nonce ({account_nonce} < {origin_nonce})."); + return false; + } + Self::parse_vote_for_aggregate_public_key(transaction).is_some() + } + + pub fn parse_vote_for_aggregate_public_key( + transaction: &StacksTransaction, + ) -> Option { + let TransactionPayload::ContractCall(payload) = &transaction.payload else { + // Not a contract call so not a special cased vote for aggregate public key transaction + return None; + }; + if payload.contract_identifier() + != boot_code_id(SIGNERS_VOTING_NAME, transaction.is_mainnet()) + || payload.function_name != SIGNERS_VOTING_FUNCTION_NAME.into() + { + // This is not a special cased transaction. + return None; + } + if payload.function_args.len() != 4 { + return None; + } + let signer_index_value = payload.function_args.first()?; + let signer_index = u64::try_from(signer_index_value.clone().expect_u128().ok()?).ok()?; + let point_value = payload.function_args.get(1)?; + let point_bytes = point_value.clone().expect_buff(33).ok()?; + let compressed_data = Compressed::try_from(point_bytes.as_slice()).ok()?; + let aggregate_key = Point::try_from(&compressed_data).ok()?; + let round_value = payload.function_args.get(2)?; + let voting_round = u64::try_from(round_value.clone().expect_u128().ok()?).ok()?; + let reward_cycle = + u64::try_from(payload.function_args.get(3)?.clone().expect_u128().ok()?).ok()?; + Some(AggregateKeyVoteParams { + signer_index, + aggregate_key, + voting_round, + reward_cycle, + }) + } + + /// Update the map of filtered valid transactions, selecting one per address based first on lowest nonce, then txid + pub fn update_filtered_transactions( + filtered_transactions: &mut HashMap, + account_nonces: &HashMap, + mainnet: bool, + transactions: Vec, + ) { + for transaction in transactions { + if NakamotoSigners::valid_vote_transaction(&account_nonces, &transaction, mainnet) { + let origin_address = transaction.origin_address(); + let origin_nonce = transaction.get_origin_nonce(); + if let Some(entry) = filtered_transactions.get_mut(&origin_address) { + let entry_nonce = entry.get_origin_nonce(); + if entry_nonce > origin_nonce + || (entry_nonce == origin_nonce && entry.txid() > transaction.txid()) + { + *entry = transaction; + } + } else { + filtered_transactions.insert(origin_address, transaction); + } + } + } + } } diff --git a/stackslib/src/chainstate/nakamoto/tenure.rs b/stackslib/src/chainstate/nakamoto/tenure.rs index 914a2cf49..7f0fad030 100644 --- a/stackslib/src/chainstate/nakamoto/tenure.rs +++ b/stackslib/src/chainstate/nakamoto/tenure.rs @@ -103,7 +103,6 @@ use crate::chainstate::nakamoto::{ NakamotoChainState, }; use crate::chainstate::stacks::db::accounts::MinerReward; -use crate::chainstate::stacks::db::blocks::StagingUserBurnSupport; use crate::chainstate::stacks::db::{ ChainstateTx, ClarityTx, DBConfig as ChainstateConfig, MinerPaymentSchedule, MinerPaymentTxFees, MinerRewardInfo, StacksBlockHeaderTypes, StacksChainState, StacksDBTx, diff --git a/stackslib/src/chainstate/nakamoto/test_signers.rs b/stackslib/src/chainstate/nakamoto/test_signers.rs index 02b38136d..e797a66ba 100644 --- a/stackslib/src/chainstate/nakamoto/test_signers.rs +++ b/stackslib/src/chainstate/nakamoto/test_signers.rs @@ -39,7 +39,7 @@ use crate::burnchains::bitcoin::indexer::BitcoinIndexer; use crate::burnchains::*; use crate::chainstate::burn::db::sortdb::*; use crate::chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, + BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, }; use crate::chainstate::burn::*; use crate::chainstate::coordinator::{ diff --git a/stackslib/src/chainstate/nakamoto/tests/mod.rs b/stackslib/src/chainstate/nakamoto/tests/mod.rs index 284a0af64..28d620b81 100644 --- a/stackslib/src/chainstate/nakamoto/tests/mod.rs +++ b/stackslib/src/chainstate/nakamoto/tests/mod.rs @@ -15,17 +15,22 @@ // along with this program. If not, see . use std::borrow::BorrowMut; +use std::collections::HashMap; use std::fs; use clarity::types::chainstate::{PoxId, SortitionId, StacksBlockId}; use clarity::vm::clarity::ClarityConnection; use clarity::vm::costs::ExecutionCost; use clarity::vm::types::StacksAddressExtensions; +use clarity::vm::Value; +use rand::{thread_rng, RngCore}; use rusqlite::Connection; use stacks_common::address::AddressHashMode; use stacks_common::bitvec::BitVec; use stacks_common::codec::StacksMessageCodec; -use stacks_common::consts::{FIRST_BURNCHAIN_CONSENSUS_HASH, FIRST_STACKS_BLOCK_HASH}; +use stacks_common::consts::{ + CHAIN_ID_MAINNET, CHAIN_ID_TESTNET, FIRST_BURNCHAIN_CONSENSUS_HASH, FIRST_STACKS_BLOCK_HASH, +}; use stacks_common::types::chainstate::{ BlockHeaderHash, BurnchainHeaderHash, ConsensusHash, StacksAddress, StacksPrivateKey, StacksPublicKey, StacksWorkScore, TrieHash, VRFSeed, @@ -37,6 +42,8 @@ use stacks_common::util::secp256k1::{MessageSignature, Secp256k1PublicKey}; use stacks_common::util::vrf::{VRFPrivateKey, VRFProof, VRFPublicKey, VRF}; use stdext::prelude::Integer; use stx_genesis::GenesisData; +use wsts::curve::point::Point; +use wsts::curve::scalar::Scalar; use crate::burnchains::{BurnchainSigner, PoxConstants, Txid}; use crate::chainstate::burn::db::sortdb::tests::make_fork_run; @@ -52,13 +59,16 @@ use crate::chainstate::coordinator::tests::{ }; use crate::chainstate::nakamoto::coordinator::tests::boot_nakamoto; use crate::chainstate::nakamoto::miner::NakamotoBlockBuilder; +use crate::chainstate::nakamoto::signer_set::NakamotoSigners; use crate::chainstate::nakamoto::tenure::NakamotoTenure; use crate::chainstate::nakamoto::test_signers::TestSigners; use crate::chainstate::nakamoto::tests::node::TestStacker; use crate::chainstate::nakamoto::{ NakamotoBlock, NakamotoBlockHeader, NakamotoChainState, SortitionHandle, FIRST_STACKS_BLOCK_ID, }; -use crate::chainstate::stacks::boot::MINERS_NAME; +use crate::chainstate::stacks::boot::{ + MINERS_NAME, SIGNERS_VOTING_FUNCTION_NAME, SIGNERS_VOTING_NAME, +}; use crate::chainstate::stacks::db::{ ChainStateBootData, ChainstateAccountBalance, ChainstateAccountLockup, ChainstateBNSName, ChainstateBNSNamespace, StacksAccount, StacksBlockHeaderTypes, StacksChainState, @@ -67,13 +77,15 @@ use crate::chainstate::stacks::db::{ use crate::chainstate::stacks::{ CoinbasePayload, StacksBlock, StacksBlockHeader, StacksTransaction, StacksTransactionSigner, TenureChangeCause, TenureChangePayload, ThresholdSignature, TokenTransferMemo, - TransactionAnchorMode, TransactionAuth, TransactionPayload, TransactionVersion, + TransactionAnchorMode, TransactionAuth, TransactionContractCall, TransactionPayload, + TransactionPostConditionMode, TransactionSmartContract, TransactionVersion, }; use crate::core; use crate::core::{StacksEpochExtension, STACKS_EPOCH_3_0_MARKER}; use crate::net::codec::test::check_codec_and_corruption; use crate::util_lib::boot::boot_code_id; use crate::util_lib::db::Error as db_error; +use crate::util_lib::strings::StacksString; /// Get an address's account pub fn get_account( @@ -2068,3 +2080,743 @@ fn test_make_miners_stackerdb_config() { assert_eq!(miner_hashbytes[8].1, miner_hash160s[8]); assert_eq!(miner_hashbytes[9].1, miner_hash160s[8]); } + +#[test] +fn parse_vote_for_aggregate_public_key_valid() { + let signer_private_key = StacksPrivateKey::new(); + let mainnet = false; + let chainid = CHAIN_ID_TESTNET; + let vote_contract_id = boot_code_id(SIGNERS_VOTING_NAME, mainnet); + let contract_addr = vote_contract_id.issuer.into(); + let contract_name = vote_contract_id.name.clone(); + + let signer_index = thread_rng().next_u64(); + let signer_index_arg = Value::UInt(signer_index as u128); + + let point = Point::from(Scalar::random(&mut thread_rng())); + let point_arg = + Value::buff_from(point.compress().data.to_vec()).expect("Failed to create buff"); + let round = thread_rng().next_u64(); + let round_arg = Value::UInt(round as u128); + + let reward_cycle = thread_rng().next_u64(); + let reward_cycle_arg = Value::UInt(reward_cycle as u128); + + let valid_function_args = vec![ + signer_index_arg.clone(), + point_arg.clone(), + round_arg.clone(), + reward_cycle_arg.clone(), + ]; + let valid_tx = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr, + contract_name, + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: valid_function_args, + }), + }; + let params = NakamotoSigners::parse_vote_for_aggregate_public_key(&valid_tx).unwrap(); + assert_eq!(params.signer_index, signer_index); + assert_eq!(params.aggregate_key, point); + assert_eq!(params.voting_round, round); + assert_eq!(params.reward_cycle, reward_cycle); +} + +#[test] +fn parse_vote_for_aggregate_public_key_invalid() { + let signer_private_key = StacksPrivateKey::new(); + let mainnet = false; + let chainid = CHAIN_ID_TESTNET; + let vote_contract_id = boot_code_id(SIGNERS_VOTING_NAME, mainnet); + let contract_addr: StacksAddress = vote_contract_id.issuer.into(); + let contract_name = vote_contract_id.name.clone(); + + let signer_index = thread_rng().next_u32(); + let signer_index_arg = Value::UInt(signer_index as u128); + + let point = Point::from(Scalar::random(&mut thread_rng())); + let point_arg = + Value::buff_from(point.compress().data.to_vec()).expect("Failed to create buff"); + let round = thread_rng().next_u64(); + let round_arg = Value::UInt(round as u128); + + let reward_cycle = thread_rng().next_u64(); + let reward_cycle_arg = Value::UInt(reward_cycle as u128); + + let valid_function_args = vec![ + signer_index_arg.clone(), + point_arg.clone(), + round_arg.clone(), + reward_cycle_arg.clone(), + ]; + + let mut invalid_contract_address = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: StacksAddress::p2pkh( + false, + &StacksPublicKey::from_private(&signer_private_key), + ), + contract_name: contract_name.clone(), + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: valid_function_args.clone(), + }), + }; + invalid_contract_address.set_origin_nonce(1); + + let mut invalid_contract_name = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone(), + contract_name: "bad-signers-contract-name".into(), + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: valid_function_args.clone(), + }), + }; + invalid_contract_name.set_origin_nonce(1); + + let mut invalid_signers_vote_function = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone(), + contract_name: contract_name.clone(), + function_name: "some-other-function".into(), + function_args: valid_function_args.clone(), + }), + }; + invalid_signers_vote_function.set_origin_nonce(1); + + let mut invalid_function_arg_signer_index = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone(), + contract_name: contract_name.clone(), + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: vec![ + point_arg.clone(), + point_arg.clone(), + round_arg.clone(), + reward_cycle_arg.clone(), + ], + }), + }; + invalid_function_arg_signer_index.set_origin_nonce(1); + + let mut invalid_function_arg_key = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone(), + contract_name: contract_name.clone(), + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: vec![ + signer_index_arg.clone(), + signer_index_arg.clone(), + round_arg.clone(), + reward_cycle_arg.clone(), + ], + }), + }; + invalid_function_arg_key.set_origin_nonce(1); + + let mut invalid_function_arg_round = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone(), + contract_name: contract_name.clone(), + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: vec![ + signer_index_arg.clone(), + point_arg.clone(), + point_arg.clone(), + reward_cycle_arg.clone(), + ], + }), + }; + invalid_function_arg_round.set_origin_nonce(1); + + let mut invalid_function_arg_reward_cycle = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone(), + contract_name: contract_name.clone(), + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: vec![ + signer_index_arg.clone(), + point_arg.clone(), + round_arg.clone(), + point_arg.clone(), + ], + }), + }; + invalid_function_arg_reward_cycle.set_origin_nonce(1); + + let mut account_nonces = std::collections::HashMap::new(); + account_nonces.insert(invalid_contract_name.origin_address(), 1); + for (i, tx) in vec![ + invalid_contract_address, + invalid_contract_name, + invalid_signers_vote_function, + invalid_function_arg_signer_index, + invalid_function_arg_key, + invalid_function_arg_round, + invalid_function_arg_reward_cycle, + ] + .iter() + .enumerate() + { + assert!( + NakamotoSigners::parse_vote_for_aggregate_public_key(&tx).is_none(), + "{}", + format!("parsed the {i}th transaction: {tx:?}") + ); + } +} + +#[test] +fn valid_vote_transaction() { + let signer_private_key = StacksPrivateKey::new(); + let mainnet = false; + let chainid = CHAIN_ID_TESTNET; + let vote_contract_id = boot_code_id(SIGNERS_VOTING_NAME, mainnet); + let contract_addr = vote_contract_id.issuer.into(); + let contract_name = vote_contract_id.name.clone(); + + let signer_index = thread_rng().next_u32(); + let signer_index_arg = Value::UInt(signer_index as u128); + + let point = Point::from(Scalar::random(&mut thread_rng())); + let point_arg = + Value::buff_from(point.compress().data.to_vec()).expect("Failed to create buff"); + let round = thread_rng().next_u64(); + let round_arg = Value::UInt(round as u128); + + let reward_cycle = thread_rng().next_u64(); + let reward_cycle_arg = Value::UInt(reward_cycle as u128); + + let valid_function_args = vec![ + signer_index_arg.clone(), + point_arg.clone(), + round_arg.clone(), + reward_cycle_arg.clone(), + ]; + let mut valid_tx = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr, + contract_name: contract_name, + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: valid_function_args, + }), + }; + valid_tx.set_origin_nonce(1); + let mut account_nonces = std::collections::HashMap::new(); + account_nonces.insert(valid_tx.origin_address(), 1); + assert!(NakamotoSigners::valid_vote_transaction( + &account_nonces, + &valid_tx, + mainnet + )); +} + +#[test] +fn valid_vote_transaction_malformed_transactions() { + let signer_private_key = StacksPrivateKey::new(); + let mainnet = false; + let chainid = CHAIN_ID_TESTNET; + let vote_contract_id = boot_code_id(SIGNERS_VOTING_NAME, mainnet); + let contract_addr: StacksAddress = vote_contract_id.issuer.into(); + let contract_name = vote_contract_id.name.clone(); + + let signer_index = thread_rng().next_u32(); + let signer_index_arg = Value::UInt(signer_index as u128); + + let point = Point::from(Scalar::random(&mut thread_rng())); + let point_arg = + Value::buff_from(point.compress().data.to_vec()).expect("Failed to create buff"); + let round = thread_rng().next_u64(); + let round_arg = Value::UInt(round as u128); + + let reward_cycle = thread_rng().next_u64(); + let reward_cycle_arg = Value::UInt(reward_cycle as u128); + + let valid_function_args = vec![ + signer_index_arg.clone(), + point_arg.clone(), + round_arg.clone(), + reward_cycle_arg.clone(), + ]; + // Create a invalid transaction that is not a contract call + let mut invalid_not_contract_call = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::SmartContract( + TransactionSmartContract { + name: "test-contract".into(), + code_body: StacksString::from_str("(/ 1 0)").unwrap(), + }, + None, + ), + }; + invalid_not_contract_call.set_origin_nonce(1); + + let mut invalid_contract_address = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: StacksAddress::p2pkh( + mainnet, + &StacksPublicKey::from_private(&signer_private_key), + ), + contract_name: contract_name.clone(), + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: valid_function_args.clone(), + }), + }; + invalid_contract_address.set_origin_nonce(1); + + let mut invalid_contract_name = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone(), + contract_name: "bad-signers-contract-name".into(), + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: valid_function_args.clone(), + }), + }; + invalid_contract_name.set_origin_nonce(1); + + let mut invalid_network = StacksTransaction { + version: TransactionVersion::Mainnet, + chain_id: CHAIN_ID_MAINNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone(), + contract_name: contract_name.clone(), + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: valid_function_args.clone(), + }), + }; + invalid_network.set_origin_nonce(1); + + let mut invalid_signers_vote_function = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone(), + contract_name: contract_name.clone(), + function_name: "some-other-function".into(), + function_args: valid_function_args.clone(), + }), + }; + invalid_signers_vote_function.set_origin_nonce(1); + + let mut invalid_function_arg_signer_index = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone(), + contract_name: contract_name.clone(), + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: vec![ + point_arg.clone(), + point_arg.clone(), + round_arg.clone(), + reward_cycle_arg.clone(), + ], + }), + }; + invalid_function_arg_signer_index.set_origin_nonce(1); + + let mut invalid_function_arg_key = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone(), + contract_name: contract_name.clone(), + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: vec![ + signer_index_arg.clone(), + signer_index_arg.clone(), + round_arg.clone(), + reward_cycle_arg.clone(), + ], + }), + }; + invalid_function_arg_key.set_origin_nonce(1); + + let mut invalid_function_arg_round = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone(), + contract_name: contract_name.clone(), + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: vec![ + signer_index_arg.clone(), + point_arg.clone(), + point_arg.clone(), + reward_cycle_arg.clone(), + ], + }), + }; + invalid_function_arg_round.set_origin_nonce(1); + + let mut invalid_function_arg_reward_cycle = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone(), + contract_name: contract_name.clone(), + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: vec![ + signer_index_arg.clone(), + point_arg.clone(), + round_arg.clone(), + point_arg.clone(), + ], + }), + }; + invalid_function_arg_reward_cycle.set_origin_nonce(1); + + let mut invalid_nonce = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone(), + contract_name: contract_name.clone(), + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: valid_function_args.clone(), + }), + }; + invalid_nonce.set_origin_nonce(0); // old nonce + + let mut account_nonces = std::collections::HashMap::new(); + account_nonces.insert(invalid_not_contract_call.origin_address(), 1); + for tx in vec![ + invalid_not_contract_call, + invalid_contract_address, + invalid_contract_name, + invalid_signers_vote_function, + invalid_function_arg_signer_index, + invalid_function_arg_key, + invalid_function_arg_round, + invalid_function_arg_reward_cycle, + invalid_nonce, + invalid_network, + ] { + assert!(!NakamotoSigners::valid_vote_transaction( + &account_nonces, + &tx, + mainnet + )); + } +} + +#[test] +fn filter_one_transaction_per_signer_multiple_addresses() { + let signer_private_key_1 = StacksPrivateKey::new(); + let signer_private_key_2 = StacksPrivateKey::new(); + let mainnet = false; + let chainid = CHAIN_ID_TESTNET; + let vote_contract_id = boot_code_id(SIGNERS_VOTING_NAME, mainnet); + let contract_addr: StacksAddress = vote_contract_id.issuer.into(); + let contract_name = vote_contract_id.name.clone(); + + let signer_index = thread_rng().next_u32(); + let signer_index_arg = Value::UInt(signer_index as u128); + + let point = Point::from(Scalar::random(&mut thread_rng())); + let point_arg = + Value::buff_from(point.compress().data.to_vec()).expect("Failed to create buff"); + let round = thread_rng().next_u64(); + let round_arg = Value::UInt(round as u128); + + let reward_cycle = thread_rng().next_u64(); + let reward_cycle_arg = Value::UInt(reward_cycle as u128); + + let function_args = vec![ + signer_index_arg.clone(), + point_arg.clone(), + round_arg.clone(), + reward_cycle_arg.clone(), + ]; + + let mut valid_tx_1_address_1 = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key_1).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone(), + contract_name: contract_name.clone(), + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: function_args.clone(), + }), + }; + valid_tx_1_address_1.set_origin_nonce(1); + + let mut valid_tx_2_address_1 = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key_1).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone(), + contract_name: contract_name.clone(), + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: function_args.clone(), + }), + }; + valid_tx_2_address_1.set_origin_nonce(2); + + let mut valid_tx_3_address_1 = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key_1).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone(), + contract_name: contract_name.clone(), + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: function_args.clone(), + }), + }; + valid_tx_3_address_1.set_origin_nonce(3); + + let mut valid_tx_1_address_2 = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key_2).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone(), + contract_name: contract_name.clone(), + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: function_args.clone(), + }), + }; + valid_tx_1_address_2.set_origin_nonce(1); + + let mut valid_tx_2_address_2 = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key_2).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr, + contract_name, + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args, + }), + }; + valid_tx_2_address_2.set_origin_nonce(2); + let mut filtered_transactions = HashMap::new(); + let mut account_nonces = std::collections::HashMap::new(); + account_nonces.insert(valid_tx_1_address_1.origin_address(), 1); + account_nonces.insert(valid_tx_1_address_2.origin_address(), 1); + NakamotoSigners::update_filtered_transactions( + &mut filtered_transactions, + &account_nonces, + false, + vec![ + valid_tx_1_address_1.clone(), + valid_tx_3_address_1, + valid_tx_1_address_2.clone(), + valid_tx_2_address_2, + valid_tx_2_address_1, + ], + ); + let txs: Vec<_> = filtered_transactions.into_values().collect(); + assert_eq!(txs.len(), 2); + assert!(txs.contains(&valid_tx_1_address_1)); + assert!(txs.contains(&valid_tx_1_address_2)); +} + +#[test] +fn filter_one_transaction_per_signer_duplicate_nonces() { + let signer_private_key = StacksPrivateKey::new(); + let mainnet = false; + let chainid = CHAIN_ID_TESTNET; + let vote_contract_id = boot_code_id(SIGNERS_VOTING_NAME, mainnet); + let contract_addr: StacksAddress = vote_contract_id.issuer.into(); + let contract_name = vote_contract_id.name.clone(); + + let signer_index = thread_rng().next_u32(); + let signer_index_arg = Value::UInt(signer_index as u128); + + let point = Point::from(Scalar::random(&mut thread_rng())); + let point_arg = + Value::buff_from(point.compress().data.to_vec()).expect("Failed to create buff"); + let round = thread_rng().next_u64(); + let round_arg = Value::UInt(round as u128); + + let reward_cycle = thread_rng().next_u64(); + let reward_cycle_arg = Value::UInt(reward_cycle as u128); + + let function_args = vec![ + signer_index_arg.clone(), + point_arg.clone(), + round_arg.clone(), + reward_cycle_arg.clone(), + ]; + + let mut valid_tx_1 = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone(), + contract_name: contract_name.clone(), + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: function_args.clone(), + }), + }; + valid_tx_1.set_origin_nonce(0); + + let mut valid_tx_2 = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone(), + contract_name: contract_name.clone(), + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args: function_args.clone(), + }), + }; + valid_tx_2.set_origin_nonce(0); + + let mut valid_tx_3 = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr, + contract_name, + function_name: SIGNERS_VOTING_FUNCTION_NAME.into(), + function_args, + }), + }; + valid_tx_3.set_origin_nonce(0); + + let mut account_nonces = std::collections::HashMap::new(); + account_nonces.insert(valid_tx_1.origin_address(), 0); + let mut txs = vec![valid_tx_2, valid_tx_1, valid_tx_3]; + let mut filtered_transactions = HashMap::new(); + NakamotoSigners::update_filtered_transactions( + &mut filtered_transactions, + &account_nonces, + false, + txs.clone(), + ); + let filtered_txs: Vec<_> = filtered_transactions.into_values().collect(); + txs.sort_by(|a, b| a.txid().cmp(&b.txid())); + assert_eq!(filtered_txs.len(), 1); + assert!(filtered_txs.contains(&txs.first().expect("failed to get first tx"))); +} diff --git a/stackslib/src/chainstate/nakamoto/tests/node.rs b/stackslib/src/chainstate/nakamoto/tests/node.rs index b34cd130c..a9da6c537 100644 --- a/stackslib/src/chainstate/nakamoto/tests/node.rs +++ b/stackslib/src/chainstate/nakamoto/tests/node.rs @@ -40,7 +40,7 @@ use crate::burnchains::tests::*; use crate::burnchains::*; use crate::chainstate::burn::db::sortdb::*; use crate::chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, + BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, }; use crate::chainstate::burn::*; use crate::chainstate::coordinator::{ diff --git a/stackslib/src/chainstate/stacks/address.rs b/stackslib/src/chainstate/stacks/address.rs index e11730bca..c3706a256 100644 --- a/stackslib/src/chainstate/stacks/address.rs +++ b/stackslib/src/chainstate/stacks/address.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::cmp::{Ord, Ordering}; +use std::cmp::Ordering; use std::io::prelude::*; use std::io::{Read, Write}; use std::{fmt, io}; diff --git a/stackslib/src/chainstate/stacks/boot/contract_tests.rs b/stackslib/src/chainstate/stacks/boot/contract_tests.rs index ce12011c7..23f2c9200 100644 --- a/stackslib/src/chainstate/stacks/boot/contract_tests.rs +++ b/stackslib/src/chainstate/stacks/boot/contract_tests.rs @@ -1,5 +1,4 @@ use std::collections::{HashMap, VecDeque}; -use std::convert::{TryFrom, TryInto}; use clarity::vm::analysis::arithmetic_checker::ArithmeticOnlyChecker; use clarity::vm::analysis::mem_type_check; diff --git a/stackslib/src/chainstate/stacks/boot/docs.rs b/stackslib/src/chainstate/stacks/boot/docs.rs index 08a203122..62580f384 100644 --- a/stackslib/src/chainstate/stacks/boot/docs.rs +++ b/stackslib/src/chainstate/stacks/boot/docs.rs @@ -1,5 +1,3 @@ -use std::iter::FromIterator; - use clarity::vm::docs::contracts::{produce_docs_refs, ContractSupportDocs}; use hashbrown::{HashMap, HashSet}; diff --git a/stackslib/src/chainstate/stacks/boot/mod.rs b/stackslib/src/chainstate/stacks/boot/mod.rs index c2970be7d..97fa8d76b 100644 --- a/stackslib/src/chainstate/stacks/boot/mod.rs +++ b/stackslib/src/chainstate/stacks/boot/mod.rs @@ -14,10 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::boxed::Box; use std::cmp; use std::collections::BTreeMap; -use std::convert::{TryFrom, TryInto}; use clarity::vm::analysis::CheckErrors; use clarity::vm::ast::ASTRules; @@ -80,6 +78,7 @@ 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"; +pub const SIGNERS_VOTING_FUNCTION_NAME: &str = "vote-for-aggregate-public-key"; /// 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"; @@ -92,7 +91,7 @@ const POX_4_BODY: &'static str = std::include_str!("pox-4.clar"); pub const SIGNERS_BODY: &'static str = std::include_str!("signers.clar"); pub const SIGNERS_DB_0_BODY: &'static str = std::include_str!("signers-0-xxx.clar"); pub const SIGNERS_DB_1_BODY: &'static str = std::include_str!("signers-1-xxx.clar"); -const SIGNERS_VOTING_BODY: &'static str = std::include_str!("signers-voting.clar"); +pub 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"; @@ -121,7 +120,6 @@ 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), @@ -1340,7 +1338,6 @@ pub mod signers_voting_tests; #[cfg(test)] pub mod test { use std::collections::{HashMap, HashSet}; - use std::convert::From; use std::fs; use clarity::boot_util::boot_code_addr; @@ -1950,7 +1947,7 @@ pub mod test { let payload = TransactionPayload::new_contract_call( boot_code_test_addr(), SIGNERS_VOTING_NAME, - "vote-for-aggregate-public-key", + SIGNERS_VOTING_FUNCTION_NAME, vec![ Value::UInt(signer_index), aggregate_public_key, diff --git a/stackslib/src/chainstate/stacks/boot/pox-4.clar b/stackslib/src/chainstate/stacks/boot/pox-4.clar index d54f3b8d5..77c8ef255 100644 --- a/stackslib/src/chainstate/stacks/boot/pox-4.clar +++ b/stackslib/src/chainstate/stacks/boot/pox-4.clar @@ -32,6 +32,7 @@ (define-constant ERR_DELEGATION_ALREADY_REVOKED 34) (define-constant ERR_INVALID_SIGNATURE_PUBKEY 35) (define-constant ERR_INVALID_SIGNATURE_RECOVER 36) +(define-constant ERR_INVALID_REWARD_CYCLE 37) ;; Valid values for burnchain address versions. ;; These first four correspond to address hash modes in Stacks 2.1, @@ -1368,6 +1369,10 @@ (asserts! (is-eq (unwrap! (principal-construct? (if is-in-mainnet STACKS_ADDR_VERSION_MAINNET STACKS_ADDR_VERSION_TESTNET) (hash160 signer-key)) (err ERR_INVALID_SIGNER_KEY)) tx-sender) (err ERR_NOT_ALLOWED)) + ;; Must be called with positive period + (asserts! (>= period u1) (err ERR_STACKING_INVALID_LOCK_PERIOD)) + ;; Must be current or future reward cycle + (asserts! (>= reward-cycle (current-pox-reward-cycle)) (err ERR_INVALID_REWARD_CYCLE)) (map-set signer-key-authorizations { pox-addr: pox-addr, period: period, reward-cycle: reward-cycle, topic: topic, signer-key: signer-key } allowed) (ok allowed))) diff --git a/stackslib/src/chainstate/stacks/boot/pox_2_tests.rs b/stackslib/src/chainstate/stacks/boot/pox_2_tests.rs index 119d4e418..07d34a04c 100644 --- a/stackslib/src/chainstate/stacks/boot/pox_2_tests.rs +++ b/stackslib/src/chainstate/stacks/boot/pox_2_tests.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::collections::{HashMap, HashSet, VecDeque}; -use std::convert::{TryFrom, TryInto}; use clarity::vm::clarity::ClarityConnection; use clarity::vm::contexts::OwnedEnvironment; diff --git a/stackslib/src/chainstate/stacks/boot/pox_3_tests.rs b/stackslib/src/chainstate/stacks/boot/pox_3_tests.rs index 65aedb130..f0c7a9ef7 100644 --- a/stackslib/src/chainstate/stacks/boot/pox_3_tests.rs +++ b/stackslib/src/chainstate/stacks/boot/pox_3_tests.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::collections::{HashMap, HashSet, VecDeque}; -use std::convert::{TryFrom, TryInto}; use clarity::vm::clarity::ClarityConnection; use clarity::vm::contexts::OwnedEnvironment; diff --git a/stackslib/src/chainstate/stacks/boot/pox_4_tests.rs b/stackslib/src/chainstate/stacks/boot/pox_4_tests.rs index d9857c55d..4f51b81e8 100644 --- a/stackslib/src/chainstate/stacks/boot/pox_4_tests.rs +++ b/stackslib/src/chainstate/stacks/boot/pox_4_tests.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::collections::{HashMap, HashSet, VecDeque}; -use std::convert::{TryFrom, TryInto}; use clarity::vm::clarity::ClarityConnection; use clarity::vm::contexts::OwnedEnvironment; @@ -2643,8 +2642,11 @@ fn test_set_signer_key_auth() { let mut signer_nonce = 0; let signer_key = &keys[1]; let signer_public_key = StacksPublicKey::from_private(signer_key); + let signer_addr = key_to_stacks_addr(&signer_key); let pox_addr = pox_addr_from(&signer_key); + let current_reward_cycle = get_current_reward_cycle(&peer, &burnchain); + // Only the address associated with `signer-key` can enable auth for that key let invalid_enable_nonce = alice_nonce; let invalid_enable_tx = make_pox_4_set_signer_key_auth( @@ -2658,21 +2660,55 @@ fn test_set_signer_key_auth() { Some(&alice_key), ); - // Disable auth for `signer-key` - let disable_auth_nonce = signer_nonce; - let disable_auth_tx: StacksTransaction = make_pox_4_set_signer_key_auth( + // Test that period is at least u1 + let signer_invalid_period_nonce = signer_nonce; + signer_nonce += 1; + let invalid_tx_period: StacksTransaction = make_pox_4_set_signer_key_auth( + &pox_addr, + &signer_key, + current_reward_cycle, + &Pox4SignatureTopic::StackStx, + 0, + false, + signer_invalid_period_nonce, + Some(&signer_key), + ); + + let signer_invalid_cycle_nonce = signer_nonce; + signer_nonce += 1; + // Test that confirmed reward cycle is at least current reward cycle + let invalid_tx_cycle: StacksTransaction = make_pox_4_set_signer_key_auth( &pox_addr, &signer_key, 1, &Pox4SignatureTopic::StackStx, + 1, + false, + signer_invalid_cycle_nonce, + Some(&signer_key), + ); + + // Disable auth for `signer-key` + let disable_auth_tx: StacksTransaction = make_pox_4_set_signer_key_auth( + &pox_addr, + &signer_key, + current_reward_cycle, + &Pox4SignatureTopic::StackStx, lock_period, false, - disable_auth_nonce, + signer_nonce, None, ); - let latest_block = - peer.tenure_with_txs(&[invalid_enable_tx, disable_auth_tx], &mut coinbase_nonce); + let latest_block = peer.tenure_with_txs( + &[ + invalid_enable_tx, + invalid_tx_period, + invalid_tx_cycle, + disable_auth_tx, + ], + &mut coinbase_nonce, + ); let alice_txs = get_last_block_sender_transactions(&observer, alice_addr); let invalid_enable_tx_result = alice_txs @@ -2683,11 +2719,39 @@ fn test_set_signer_key_auth() { let expected_error = Value::error(Value::Int(19)).unwrap(); assert_eq!(invalid_enable_tx_result, expected_error); + let signer_txs = get_last_block_sender_transactions(&observer, signer_addr); + + let invalid_tx_period_result = signer_txs + .clone() + .get(signer_invalid_period_nonce as usize) + .unwrap() + .result + .clone(); + + // Check for invalid lock period err + assert_eq!( + invalid_tx_period_result, + Value::error(Value::Int(2)).unwrap() + ); + + let invalid_tx_cycle_result = signer_txs + .clone() + .get(signer_invalid_cycle_nonce as usize) + .unwrap() + .result + .clone(); + + // Check for invalid cycle err + assert_eq!( + invalid_tx_cycle_result, + Value::error(Value::Int(37)).unwrap() + ); + let signer_key_enabled = get_signer_key_authorization_pox_4( &mut peer, &latest_block, &pox_addr, - 1, + current_reward_cycle.clone() as u64, &Pox4SignatureTopic::StackStx, lock_period.try_into().unwrap(), &signer_public_key, @@ -2701,7 +2765,7 @@ fn test_set_signer_key_auth() { let enable_auth_tx = make_pox_4_set_signer_key_auth( &pox_addr, &signer_key, - 1, + current_reward_cycle, &Pox4SignatureTopic::StackStx, lock_period, true, @@ -2715,7 +2779,7 @@ fn test_set_signer_key_auth() { &mut peer, &latest_block, &pox_addr, - 1, + current_reward_cycle.clone() as u64, &Pox4SignatureTopic::StackStx, lock_period.try_into().unwrap(), &signer_public_key, @@ -2729,7 +2793,7 @@ fn test_set_signer_key_auth() { let disable_auth_tx = make_pox_4_set_signer_key_auth( &pox_addr, &signer_key, - 1, + current_reward_cycle, &Pox4SignatureTopic::StackStx, lock_period, false, @@ -2743,7 +2807,7 @@ fn test_set_signer_key_auth() { &mut peer, &latest_block, &pox_addr, - 1, + current_reward_cycle.clone() as u64, &Pox4SignatureTopic::StackStx, lock_period.try_into().unwrap(), &signer_public_key, diff --git a/stackslib/src/chainstate/stacks/boot/signers_voting_tests.rs b/stackslib/src/chainstate/stacks/boot/signers_voting_tests.rs index cc7226e6e..5ac7d461c 100644 --- a/stackslib/src/chainstate/stacks/boot/signers_voting_tests.rs +++ b/stackslib/src/chainstate/stacks/boot/signers_voting_tests.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::collections::{HashMap, HashSet, VecDeque}; -use std::convert::{TryFrom, TryInto}; use clarity::boot_util::boot_code_addr; use clarity::vm::clarity::ClarityConnection; diff --git a/stackslib/src/chainstate/stacks/db/accounts.rs b/stackslib/src/chainstate/stacks/db/accounts.rs index 68cf1cf37..69dd14a1a 100644 --- a/stackslib/src/chainstate/stacks/db/accounts.rs +++ b/stackslib/src/chainstate/stacks/db/accounts.rs @@ -396,7 +396,6 @@ impl StacksChainState { pub fn insert_miner_payment_schedule( tx: &mut DBTx, block_reward: &MinerPaymentSchedule, - user_burns: &[StagingUserBurnSupport], ) -> Result<(), Error> { assert!(block_reward.burnchain_commit_burn < i64::MAX as u64); assert!(block_reward.burnchain_sortition_burn < i64::MAX as u64); @@ -459,53 +458,6 @@ impl StacksChainState { ) .map_err(|e| Error::DBError(db_error::SqliteError(e)))?; - for user_support in user_burns.iter() { - assert!(user_support.burn_amount < i64::MAX as u64); - - let args: &[&dyn ToSql] = &[ - &user_support.address.to_string(), - &user_support.address.to_string(), - &block_reward.block_hash, - &block_reward.consensus_hash, - &block_reward.parent_block_hash, - &block_reward.parent_consensus_hash, - &format!("{}", block_reward.coinbase), - &"0".to_string(), - &"0".to_string(), - &u64_to_sql(user_support.burn_amount)?, - &u64_to_sql(block_reward.burnchain_sortition_burn)?, - &u64_to_sql(block_reward.stacks_block_height)?, - &false, - &user_support.vtxindex, - &index_block_hash, - &"0".to_string(), - ]; - - tx.execute( - "INSERT INTO payments ( - address, - recipient, - block_hash, - consensus_hash, - parent_block_hash, - parent_consensus_hash, - coinbase, - tx_fees_anchored, - tx_fees_streamed, - burnchain_commit_burn, - burnchain_sortition_burn, - stacks_block_height, - miner, - vtxindex, - index_block_hash, - stx_burns - ) - VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16)", - args, - ) - .map_err(|e| Error::DBError(db_error::SqliteError(e)))?; - } - Ok(()) } @@ -1147,23 +1099,10 @@ mod test { sched } - impl StagingUserBurnSupport { - pub fn from_miner_payment_schedule(user: &MinerPaymentSchedule) -> StagingUserBurnSupport { - StagingUserBurnSupport { - consensus_hash: user.consensus_hash.clone(), - anchored_block_hash: user.block_hash.clone(), - address: user.address.clone(), - burn_amount: user.burnchain_commit_burn, - vtxindex: user.vtxindex, - } - } - } - fn advance_tip( chainstate: &mut StacksChainState, parent_header_info: &StacksHeaderInfo, block_reward: &mut MinerPaymentSchedule, - user_burns: &mut Vec, ) -> StacksHeaderInfo { let mut new_tip = parent_header_info.clone(); @@ -1191,11 +1130,6 @@ mod test { block_reward.block_hash = new_tip.anchored_header.block_hash(); block_reward.consensus_hash = new_tip.consensus_hash.clone(); - for ref mut user_burn in user_burns.iter_mut() { - user_burn.anchored_block_hash = new_tip.anchored_header.block_hash(); - user_burn.consensus_hash = new_tip.consensus_hash.clone(); - } - let mut tx = chainstate.index_tx_begin().unwrap(); let tip = StacksChainState::advance_tip( &mut tx, @@ -1211,7 +1145,6 @@ mod test { new_tip.burn_header_timestamp, new_tip.microblock_tail.clone(), &block_reward, - &user_burns, None, &ExecutionCost::zero(), 123, @@ -1251,9 +1184,6 @@ mod test { 0, ); - let user_support = StagingUserBurnSupport::from_miner_payment_schedule(&user_reward); - let mut user_supports = vec![user_support]; - { let mut tx = chainstate.index_tx_begin().unwrap(); let ancestor_0 = StacksChainState::get_tip_ancestor( @@ -1269,7 +1199,6 @@ mod test { &mut chainstate, &StacksHeaderInfo::regtest_genesis(), &mut miner_reward, - &mut user_supports, ); { @@ -1283,7 +1212,7 @@ mod test { assert_eq!(ancestor_1.unwrap().stacks_block_height, 1); } - let tip = advance_tip(&mut chainstate, &parent_tip, &mut tip_reward, &mut vec![]); + let tip = advance_tip(&mut chainstate, &parent_tip, &mut tip_reward); { let mut tx = chainstate.index_tx_begin().unwrap(); @@ -1306,23 +1235,15 @@ mod test { let miner_1 = StacksAddress::from_string(&"SP1A2K3ENNA6QQ7G8DVJXM24T6QMBDVS7D0TRTAR5".to_string()) .unwrap(); - let user_1 = - StacksAddress::from_string(&"SP2837ZMC89J40K4YTS64B00M7065C6X46JX6ARG0".to_string()) - .unwrap(); let mut miner_reward = make_dummy_miner_payment_schedule(&miner_1, 500, 0, 0, 1000, 1000); - let user_reward = make_dummy_user_payment_schedule(&user_1, 500, 0, 0, 750, 1000, 1); let initial_tip = StacksHeaderInfo::regtest_genesis(); - let user_support = StagingUserBurnSupport::from_miner_payment_schedule(&user_reward); - let mut user_supports = vec![user_support]; - let parent_tip = advance_tip( &mut chainstate, &StacksHeaderInfo::regtest_genesis(), &mut miner_reward, - &mut user_supports, ); // dummy reward @@ -1337,7 +1258,7 @@ mod test { 0, 0, ); - let tip = advance_tip(&mut chainstate, &parent_tip, &mut tip_reward, &mut vec![]); + let tip = advance_tip(&mut chainstate, &parent_tip, &mut tip_reward); { let mut tx = chainstate.index_tx_begin().unwrap(); @@ -1351,15 +1272,8 @@ mod test { StacksChainState::get_scheduled_block_rewards_in_fork_at_height(&mut tx, &tip, 2) .unwrap(); - let mut expected_user_support = user_reward.clone(); - expected_user_support.consensus_hash = miner_reward.consensus_hash.clone(); - expected_user_support.parent_consensus_hash = - miner_reward.parent_consensus_hash.clone(); - expected_user_support.block_hash = miner_reward.block_hash.clone(); - expected_user_support.parent_block_hash = miner_reward.parent_block_hash.clone(); - assert_eq!(payments_0, vec![]); - assert_eq!(payments_1, vec![miner_reward, expected_user_support]); + assert_eq!(payments_1, vec![miner_reward]); assert_eq!(payments_2, vec![tip_reward]); }; } @@ -1380,7 +1294,6 @@ mod test { &mut chainstate, &StacksHeaderInfo::regtest_genesis(), &mut miner_reward, - &mut vec![], ); // dummy reward @@ -1395,7 +1308,7 @@ mod test { 0, 0, ); - let tip = advance_tip(&mut chainstate, &parent_tip, &mut tip_reward, &mut vec![]); + let tip = advance_tip(&mut chainstate, &parent_tip, &mut tip_reward); { let mut tx = chainstate.index_tx_begin().unwrap(); diff --git a/stackslib/src/chainstate/stacks/db/blocks.rs b/stackslib/src/chainstate/stacks/db/blocks.rs index 7030b9259..f55168171 100644 --- a/stackslib/src/chainstate/stacks/db/blocks.rs +++ b/stackslib/src/chainstate/stacks/db/blocks.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::collections::{HashMap, HashSet}; -use std::convert::From; use std::io::prelude::*; use std::io::{Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; @@ -110,15 +109,6 @@ pub struct StagingBlock { pub block_data: Vec, } -#[derive(Debug, Clone, PartialEq)] -pub struct StagingUserBurnSupport { - pub consensus_hash: ConsensusHash, - pub anchored_block_hash: BlockHeaderHash, - pub address: StacksAddress, - pub burn_amount: u64, - pub vtxindex: u32, -} - #[derive(Debug)] pub enum MemPoolRejection { SerializationFailure(codec_error), @@ -416,25 +406,6 @@ impl FromRow for StagingBlock { } } -impl FromRow for StagingUserBurnSupport { - fn from_row<'a>(row: &'a Row) -> Result { - let anchored_block_hash: BlockHeaderHash = - BlockHeaderHash::from_column(row, "anchored_block_hash")?; - let consensus_hash: ConsensusHash = ConsensusHash::from_column(row, "consensus_hash")?; - let address: StacksAddress = StacksAddress::from_column(row, "address")?; - let burn_amount = u64::from_column(row, "burn_amount")?; - let vtxindex: u32 = row.get_unwrap("vtxindex"); - - Ok(StagingUserBurnSupport { - anchored_block_hash, - consensus_hash, - address, - burn_amount, - vtxindex, - }) - } -} - impl StagingMicroblock { #[cfg(test)] pub fn try_into_microblock(self) -> Result { @@ -1074,19 +1045,6 @@ impl StacksChainState { } } - /// Load up the list of users who burned for an unprocessed block. - fn load_staging_block_user_supports( - block_conn: &DBConn, - consensus_hash: &ConsensusHash, - block_hash: &BlockHeaderHash, - ) -> Result, Error> { - let sql = "SELECT * FROM staging_user_burn_support WHERE anchored_block_hash = ?1 AND consensus_hash = ?2".to_string(); - let args: &[&dyn ToSql] = &[&block_hash, &consensus_hash]; - let rows = query_rows::(block_conn, &sql, args) - .map_err(Error::DBError)?; - Ok(rows) - } - /// Load up a queued block's queued pubkey hash fn load_staging_block_pubkey_hash( block_conn: &DBConn, @@ -1752,34 +1710,6 @@ impl StacksChainState { Ok(()) } - /// Store users who burned in support of a block - fn store_staging_block_user_burn_supports<'a>( - tx: &mut DBTx<'a>, - consensus_hash: &ConsensusHash, - block_hash: &BlockHeaderHash, - burn_supports: &[UserBurnSupportOp], - ) -> Result<(), Error> { - for burn_support in burn_supports.iter() { - assert!(burn_support.burn_fee < u64::try_from(i64::MAX).expect("unreachable")); - } - - for burn_support in burn_supports.iter() { - let sql = "INSERT OR REPLACE INTO staging_user_burn_support (anchored_block_hash, consensus_hash, address, burn_amount, vtxindex) VALUES (?1, ?2, ?3, ?4, ?5)"; - let args: &[&dyn ToSql] = &[ - &consensus_hash, - &block_hash, - &burn_support.address.to_string(), - &u64_to_sql(burn_support.burn_fee)?, - &burn_support.vtxindex, - ]; - - tx.execute(&sql, args) - .map_err(|e| Error::DBError(db_error::SqliteError(e)))?; - } - - Ok(()) - } - /// Read all the i64 values from a query (possibly none). fn read_i64s(conn: &DBConn, query: &str, args: &[&dyn ToSql]) -> Result, Error> { let mut stmt = conn @@ -3484,9 +3414,6 @@ impl StacksChainState { return Ok(false); } - // find all user burns that supported this block - let user_burns = sort_handle.get_winning_user_burns_by_block()?; - // does this block match the burnchain state? skip if not let validation_res = StacksChainState::validate_anchored_block_burnchain( &block_tx, @@ -3541,14 +3468,6 @@ impl StacksChainState { download_time, )?; - // store users who burned for this block so they'll get rewarded if we process it - StacksChainState::store_staging_block_user_burn_supports( - &mut block_tx, - consensus_hash, - &block.block_hash(), - &user_burns, - )?; - block_tx.commit()?; debug!( @@ -5277,7 +5196,6 @@ impl StacksChainState { microblocks: &Vec, // parent microblocks burnchain_commit_burn: u64, burnchain_sortition_burn: u64, - user_burns: &[StagingUserBurnSupport], affirmation_weight: u64, do_not_advance: bool, ) -> Result< @@ -5693,7 +5611,6 @@ impl StacksChainState { chain_tip_burn_header_timestamp, microblock_tail_opt, &scheduled_miner_reward, - user_burns, miner_payouts_opt, &block_execution_cost, block_size, @@ -6066,13 +5983,6 @@ impl StacksChainState { last_microblock_seq ); - // find users that burned in support of this block, so we can calculate the miner reward - let user_supports = StacksChainState::load_staging_block_user_supports( - chainstate_tx.deref().deref(), - &next_staging_block.consensus_hash, - &next_staging_block.anchored_block_hash, - )?; - test_debug!( "About to load affirmation map for {}/{}", &next_staging_block.consensus_hash, @@ -6110,7 +6020,6 @@ impl StacksChainState { &next_microblocks, next_staging_block.commit_burn, next_staging_block.sortition_burn, - &user_supports, block_am.weight(), false, ) { diff --git a/stackslib/src/chainstate/stacks/db/mod.rs b/stackslib/src/chainstate/stacks/db/mod.rs index b44dc9e6f..3e48a3f47 100644 --- a/stackslib/src/chainstate/stacks/db/mod.rs +++ b/stackslib/src/chainstate/stacks/db/mod.rs @@ -790,14 +790,6 @@ const CHAINSTATE_INITIAL_SCHEMA: &'static [&'static str] = &[ PRIMARY KEY(anchored_block_hash,consensus_hash) );"#, r#" - -- users who burned in support of a block - CREATE TABLE staging_user_burn_support(anchored_block_hash TEXT NOT NULL, - consensus_hash TEXT NOT NULL, - address TEXT NOT NULL, - burn_amount INT NOT NULL, - vtxindex INT NOT NULL - );"#, - r#" CREATE TABLE transactions( id INTEGER PRIMARY KEY, txid TEXT NOT NULL, @@ -890,7 +882,6 @@ const CHAINSTATE_INDEXES: &'static [&'static str] = &[ "CREATE INDEX IF NOT EXISTS parent_consensus_hashes ON staging_blocks(parent_consensus_hash);", "CREATE INDEX IF NOT EXISTS index_block_hashes ON staging_blocks(index_block_hash);", "CREATE INDEX IF NOT EXISTS height_stacks_blocks ON staging_blocks(height);", - "CREATE INDEX IF NOT EXISTS index_staging_user_burn_support ON staging_user_burn_support(anchored_block_hash,consensus_hash);", "CREATE INDEX IF NOT EXISTS txid_tx_index ON transactions(txid);", "CREATE INDEX IF NOT EXISTS index_block_hash_tx_index ON transactions(index_block_hash);", "CREATE INDEX IF NOT EXISTS index_block_header_by_affirmation_weight ON block_headers(affirmation_weight);", @@ -2534,7 +2525,6 @@ impl StacksChainState { new_burnchain_timestamp: u64, microblock_tail_opt: Option, block_reward: &MinerPaymentSchedule, - user_burns: &[StagingUserBurnSupport], mature_miner_payouts: Option<(MinerReward, Vec, MinerReward, MinerRewardInfo)>, // (miner, [users], parent, matured rewards) anchor_block_cost: &ExecutionCost, anchor_block_size: u64, @@ -2599,11 +2589,7 @@ impl StacksChainState { anchor_block_cost, affirmation_weight, )?; - StacksChainState::insert_miner_payment_schedule( - headers_tx.deref_mut(), - block_reward, - user_burns, - )?; + StacksChainState::insert_miner_payment_schedule(headers_tx.deref_mut(), block_reward)?; StacksChainState::store_burnchain_txids( headers_tx.deref(), &index_block_hash, diff --git a/stackslib/src/chainstate/stacks/db/transactions.rs b/stackslib/src/chainstate/stacks/db/transactions.rs index b0822b817..9297252d1 100644 --- a/stackslib/src/chainstate/stacks/db/transactions.rs +++ b/stackslib/src/chainstate/stacks/db/transactions.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::collections::{HashMap, HashSet}; -use std::convert::{TryFrom, TryInto}; use std::io::prelude::*; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; diff --git a/stackslib/src/chainstate/stacks/index/cache.rs b/stackslib/src/chainstate/stacks/index/cache.rs index 3763f15c6..a71160344 100644 --- a/stackslib/src/chainstate/stacks/index/cache.rs +++ b/stackslib/src/chainstate/stacks/index/cache.rs @@ -16,10 +16,8 @@ use std::char::from_digit; use std::collections::{HashMap, HashSet, VecDeque}; -use std::convert::{TryFrom, TryInto}; use std::hash::{Hash, Hasher}; use std::io::{BufWriter, Cursor, Read, Seek, SeekFrom, Write}; -use std::iter::FromIterator; use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; diff --git a/stackslib/src/chainstate/stacks/index/file.rs b/stackslib/src/chainstate/stacks/index/file.rs index 99df76016..1477f9a7d 100644 --- a/stackslib/src/chainstate/stacks/index/file.rs +++ b/stackslib/src/chainstate/stacks/index/file.rs @@ -16,11 +16,9 @@ use std::char::from_digit; use std::collections::{HashMap, HashSet, VecDeque}; -use std::convert::{TryFrom, TryInto}; use std::fs::OpenOptions; use std::hash::{Hash, Hasher}; use std::io::{BufWriter, Cursor, Read, Seek, SeekFrom, Write}; -use std::iter::FromIterator; use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; diff --git a/stackslib/src/chainstate/stacks/index/storage.rs b/stackslib/src/chainstate/stacks/index/storage.rs index c39976419..97f7ca999 100644 --- a/stackslib/src/chainstate/stacks/index/storage.rs +++ b/stackslib/src/chainstate/stacks/index/storage.rs @@ -16,10 +16,8 @@ use std::char::from_digit; use std::collections::{HashMap, HashSet, VecDeque}; -use std::convert::{TryFrom, TryInto}; use std::hash::{Hash, Hasher}; use std::io::{BufWriter, Cursor, Read, Seek, SeekFrom, Write}; -use std::iter::FromIterator; use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; diff --git a/stackslib/src/chainstate/stacks/index/trie_sql.rs b/stackslib/src/chainstate/stacks/index/trie_sql.rs index d538ed7e5..be1ae91c2 100644 --- a/stackslib/src/chainstate/stacks/index/trie_sql.rs +++ b/stackslib/src/chainstate/stacks/index/trie_sql.rs @@ -19,9 +19,7 @@ use std::char::from_digit; use std::collections::{HashMap, HashSet, VecDeque}; -use std::convert::{TryFrom, TryInto}; use std::io::{BufWriter, Cursor, Read, Seek, SeekFrom, Write}; -use std::iter::FromIterator; use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; diff --git a/stackslib/src/chainstate/stacks/miner.rs b/stackslib/src/chainstate/stacks/miner.rs index b4dbcc554..c04b03dcd 100644 --- a/stackslib/src/chainstate/stacks/miner.rs +++ b/stackslib/src/chainstate/stacks/miner.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::collections::{HashMap, HashSet}; -use std::convert::From; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::thread::ThreadId; diff --git a/stackslib/src/chainstate/stacks/mod.rs b/stackslib/src/chainstate/stacks/mod.rs index 0224631db..7247a28f7 100644 --- a/stackslib/src/chainstate/stacks/mod.rs +++ b/stackslib/src/chainstate/stacks/mod.rs @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::{From, TryFrom}; use std::io::prelude::*; use std::io::{Read, Write}; use std::ops::{Deref, DerefMut}; @@ -128,6 +127,7 @@ pub enum Error { ChannelClosed(String), /// This error indicates a Epoch2 block attempted to build off of a Nakamoto block. InvalidChildOfNakomotoBlock, + NoRegisteredSigners(u64), } impl From for Error { @@ -221,11 +221,15 @@ impl fmt::Display for Error { f, "Block has a different tenure than parent, but no tenure change transaction" ), + Error::NoRegisteredSigners(reward_cycle) => { + write!(f, "No registered signers for reward cycle {reward_cycle}") + } } } } impl error::Error for Error { + #[cfg_attr(test, mutants::skip)] fn cause(&self) -> Option<&dyn error::Error> { match *self { Error::InvalidFee => None, @@ -263,11 +267,13 @@ impl error::Error for Error { Error::ChannelClosed(ref _s) => None, Error::InvalidChildOfNakomotoBlock => None, Error::ExpectedTenureChange => None, + Error::NoRegisteredSigners(_) => None, } } } impl Error { + #[cfg_attr(test, mutants::skip)] fn name(&self) -> &'static str { match self { Error::InvalidFee => "InvalidFee", @@ -305,9 +311,11 @@ impl Error { Error::ChannelClosed(ref _s) => "ChannelClosed", Error::InvalidChildOfNakomotoBlock => "InvalidChildOfNakomotoBlock", Error::ExpectedTenureChange => "ExpectedTenureChange", + Error::NoRegisteredSigners(_) => "NoRegisteredSigners", } } + #[cfg_attr(test, mutants::skip)] pub fn into_json(&self) -> serde_json::Value { let reason_code = self.name(); let reason_data = format!("{:?}", &self); diff --git a/stackslib/src/chainstate/stacks/tests/accounting.rs b/stackslib/src/chainstate/stacks/tests/accounting.rs index d1fc9a46f..e11224ab6 100644 --- a/stackslib/src/chainstate/stacks/tests/accounting.rs +++ b/stackslib/src/chainstate/stacks/tests/accounting.rs @@ -37,7 +37,7 @@ use crate::burnchains::tests::*; use crate::burnchains::*; use crate::chainstate::burn::db::sortdb::*; use crate::chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, + BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, }; use crate::chainstate::burn::*; use crate::chainstate::coordinator::Error as CoordinatorError; diff --git a/stackslib/src/chainstate/stacks/tests/block_construction.rs b/stackslib/src/chainstate/stacks/tests/block_construction.rs index 7ca20d3db..7e241bad4 100644 --- a/stackslib/src/chainstate/stacks/tests/block_construction.rs +++ b/stackslib/src/chainstate/stacks/tests/block_construction.rs @@ -43,7 +43,7 @@ use crate::burnchains::tests::*; use crate::burnchains::*; use crate::chainstate::burn::db::sortdb::*; use crate::chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, + BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, }; use crate::chainstate::burn::*; use crate::chainstate::coordinator::Error as CoordinatorError; diff --git a/stackslib/src/chainstate/stacks/tests/chain_histories.rs b/stackslib/src/chainstate/stacks/tests/chain_histories.rs index 1ce598dbc..fae7a66b4 100644 --- a/stackslib/src/chainstate/stacks/tests/chain_histories.rs +++ b/stackslib/src/chainstate/stacks/tests/chain_histories.rs @@ -42,7 +42,7 @@ use crate::burnchains::tests::*; use crate::burnchains::*; use crate::chainstate::burn::db::sortdb::*; use crate::chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, + BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, }; use crate::chainstate::burn::*; use crate::chainstate::coordinator::Error as CoordinatorError; diff --git a/stackslib/src/chainstate/stacks/tests/mod.rs b/stackslib/src/chainstate/stacks/tests/mod.rs index e0b48f8d7..22a331b19 100644 --- a/stackslib/src/chainstate/stacks/tests/mod.rs +++ b/stackslib/src/chainstate/stacks/tests/mod.rs @@ -34,7 +34,7 @@ use crate::burnchains::tests::*; use crate::burnchains::*; use crate::chainstate::burn::db::sortdb::*; use crate::chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, + BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, }; use crate::chainstate::burn::*; use crate::chainstate::coordinator::Error as CoordinatorError; diff --git a/stackslib/src/chainstate/stacks/transaction.rs b/stackslib/src/chainstate/stacks/transaction.rs index 0c764ec83..4ede285e4 100644 --- a/stackslib/src/chainstate/stacks/transaction.rs +++ b/stackslib/src/chainstate/stacks/transaction.rs @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryFrom; use std::io; use std::io::prelude::*; use std::io::{Read, Write}; diff --git a/stackslib/src/clarity_cli.rs b/stackslib/src/clarity_cli.rs index 426b45bc8..00a067408 100644 --- a/stackslib/src/clarity_cli.rs +++ b/stackslib/src/clarity_cli.rs @@ -14,10 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::{TryFrom, TryInto}; use std::ffi::OsStr; use std::io::{Read, Write}; -use std::iter::Iterator; use std::path::PathBuf; use std::str::FromStr; use std::{env, fs, io, process}; diff --git a/stackslib/src/clarity_vm/clarity.rs b/stackslib/src/clarity_vm/clarity.rs index 835141211..ac764e0e9 100644 --- a/stackslib/src/clarity_vm/clarity.rs +++ b/stackslib/src/clarity_vm/clarity.rs @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryFrom; use std::{error, fmt, thread}; use clarity::vm::analysis::errors::{CheckError, CheckErrors}; @@ -51,7 +50,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_DB_0_BODY, - SIGNERS_DB_1_BODY, SIGNERS_NAME, SIGNERS_VOTING_NAME, SIGNER_VOTING_CODE, + SIGNERS_DB_1_BODY, SIGNERS_NAME, SIGNERS_VOTING_BODY, SIGNERS_VOTING_NAME, }; use crate::chainstate::stacks::db::{StacksAccount, StacksChainState}; use crate::chainstate::stacks::events::{StacksTransactionEvent, StacksTransactionReceipt}; @@ -1458,59 +1457,12 @@ impl<'a, 'b> ClarityBlockConnection<'a, 'b> { } } - let initialized_agg_key = if !mainnet { - let agg_key_value_opt = self - .with_readonly_clarity_env( - false, - self.chain_id, - ClarityVersion::Clarity2, - StacksAddress::burn_address(false).into(), - None, - LimitedCostTracker::Free, - |vm_env| { - vm_env.execute_contract_allow_private( - &boot_code_id(BOOT_TEST_POX_4_AGG_KEY_CONTRACT, false), - BOOT_TEST_POX_4_AGG_KEY_FNAME, - &[], - true, - ) - }, - ) - .map(|agg_key_value| { - agg_key_value - .expect_buff(33) - .expect("FATAL: test aggregate pub key must be a buffer") - }) - .ok(); - agg_key_value_opt - } else { - None - }; - - let mut signers_voting_code = SIGNER_VOTING_CODE.clone(); - if !mainnet { - if let Some(ref agg_pub_key) = initialized_agg_key { - let hex_agg_pub_key = to_hex(agg_pub_key); - for set_in_reward_cycle in 0..pox_4_first_cycle { - info!( - "Setting initial aggregate-public-key in PoX-4"; - "agg_pub_key" => &hex_agg_pub_key, - "reward_cycle" => set_in_reward_cycle, - "pox_4_first_cycle" => pox_4_first_cycle, - ); - let set_str = format!("(map-set aggregate-public-keys u{set_in_reward_cycle} 0x{hex_agg_pub_key})"); - signers_voting_code.push_str("\n"); - signers_voting_code.push_str(&set_str); - } - } - } - 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) + code_body: StacksString::from_str(SIGNERS_VOTING_BODY) .expect("FATAL: invalid boot code body"), }, Some(ClarityVersion::Clarity2), diff --git a/stackslib/src/core/mempool.rs b/stackslib/src/core/mempool.rs index c29d3a686..a1135989a 100644 --- a/stackslib/src/core/mempool.rs +++ b/stackslib/src/core/mempool.rs @@ -1197,6 +1197,7 @@ impl CandidateCache { } /// Total length of the cache. + #[cfg_attr(test, mutants::skip)] fn len(&self) -> usize { self.cache.len() + self.next.len() } @@ -1301,6 +1302,7 @@ impl MemPoolDB { } /// Add indexes + #[cfg_attr(test, mutants::skip)] fn add_indexes(tx: &mut DBTx) -> Result<(), db_error> { for cmd in MEMPOOL_INDEXES { tx.execute_batch(cmd).map_err(db_error::SqliteError)?; @@ -1309,6 +1311,7 @@ impl MemPoolDB { } /// Instantiate the on-disk counting bloom filter + #[cfg_attr(test, mutants::skip)] fn instantiate_bloom_state(tx: &mut DBTx) -> Result<(), db_error> { let node_hasher = BloomNodeHasher::new_random(); let _ = BloomCounter::new( @@ -1326,6 +1329,7 @@ impl MemPoolDB { } /// Instantiate the cost estimator schema + #[cfg_attr(test, mutants::skip)] fn instantiate_cost_estimator(tx: &DBTx) -> Result<(), db_error> { for sql_exec in MEMPOOL_SCHEMA_2_COST_ESTIMATOR { tx.execute_batch(sql_exec)?; @@ -1344,6 +1348,7 @@ impl MemPoolDB { } /// Instantiate the tx blacklist schema + #[cfg_attr(test, mutants::skip)] fn instantiate_tx_blacklist(tx: &DBTx) -> Result<(), db_error> { for sql_exec in MEMPOOL_SCHEMA_4_BLACKLIST { tx.execute_batch(sql_exec)?; @@ -1353,6 +1358,7 @@ impl MemPoolDB { } /// Add the nonce table + #[cfg_attr(test, mutants::skip)] fn instantiate_nonces(tx: &DBTx) -> Result<(), db_error> { for sql_exec in MEMPOOL_SCHEMA_6_NONCES { tx.execute_batch(sql_exec)?; @@ -1361,6 +1367,7 @@ impl MemPoolDB { Ok(()) } + #[cfg_attr(test, mutants::skip)] pub fn db_path(chainstate_root_path: &str) -> Result { let mut path = PathBuf::from(chainstate_root_path); @@ -1468,6 +1475,7 @@ impl MemPoolDB { MemPoolDB::open_db(&db_path, cost_estimator, metric) } + #[cfg_attr(test, mutants::skip)] pub fn reset_nonce_cache(&mut self) -> Result<(), db_error> { debug!("reset nonce cache"); let sql = "DELETE FROM nonces"; diff --git a/stackslib/src/core/mod.rs b/stackslib/src/core/mod.rs index 2280fe6d7..45556adf7 100644 --- a/stackslib/src/core/mod.rs +++ b/stackslib/src/core/mod.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::collections::HashSet; -use std::convert::TryFrom; use clarity::vm::costs::ExecutionCost; use lazy_static::lazy_static; @@ -34,8 +33,7 @@ pub mod mempool; #[cfg(test)] pub mod tests; -use std::cmp::{Ord, Ordering, PartialOrd}; - +use std::cmp::Ordering; pub type StacksEpoch = GenericStacksEpoch; // fork set identifier -- to be mixed with the consensus hash (encodes the version) diff --git a/stackslib/src/cost_estimates/fee_medians.rs b/stackslib/src/cost_estimates/fee_medians.rs index f3a7282a1..88ab0e9c2 100644 --- a/stackslib/src/cost_estimates/fee_medians.rs +++ b/stackslib/src/cost_estimates/fee_medians.rs @@ -1,7 +1,5 @@ use std::cmp; use std::cmp::Ordering; -use std::convert::TryFrom; -use std::iter::FromIterator; use std::path::Path; use clarity::vm::costs::ExecutionCost; diff --git a/stackslib/src/cost_estimates/fee_scalar.rs b/stackslib/src/cost_estimates/fee_scalar.rs index 895d47ed8..1c0349e42 100644 --- a/stackslib/src/cost_estimates/fee_scalar.rs +++ b/stackslib/src/cost_estimates/fee_scalar.rs @@ -1,6 +1,4 @@ use std::cmp; -use std::convert::TryFrom; -use std::iter::FromIterator; use std::path::Path; use clarity::vm::costs::ExecutionCost; diff --git a/stackslib/src/cost_estimates/mod.rs b/stackslib/src/cost_estimates/mod.rs index 1d799607c..fc4aa5b1b 100644 --- a/stackslib/src/cost_estimates/mod.rs +++ b/stackslib/src/cost_estimates/mod.rs @@ -2,7 +2,6 @@ use std::cmp; use std::collections::HashMap; use std::error::Error; use std::fmt::Display; -use std::iter::FromIterator; use std::ops::{Add, Div, Mul, Rem, Sub}; use std::path::Path; diff --git a/stackslib/src/cost_estimates/pessimistic.rs b/stackslib/src/cost_estimates/pessimistic.rs index 7e222f5de..b986d54dc 100644 --- a/stackslib/src/cost_estimates/pessimistic.rs +++ b/stackslib/src/cost_estimates/pessimistic.rs @@ -1,6 +1,4 @@ use std::cmp; -use std::convert::TryFrom; -use std::iter::FromIterator; use std::path::Path; use clarity::vm::costs::ExecutionCost; diff --git a/stackslib/src/main.rs b/stackslib/src/main.rs index 9468f28ad..fa4833cdc 100644 --- a/stackslib/src/main.rs +++ b/stackslib/src/main.rs @@ -34,7 +34,6 @@ use tikv_jemallocator::Jemalloc; static GLOBAL: Jemalloc = Jemalloc; use std::collections::{HashMap, HashSet}; -use std::convert::TryFrom; use std::fs::{File, OpenOptions}; use std::io::prelude::*; use std::io::BufReader; @@ -1690,9 +1689,6 @@ fn replay_block(stacks_path: &str, index_block_hash_hex: &str) { last_microblock_seq ); - // user supports were never activated - let user_supports = vec![]; - let block_am = StacksChainState::find_stacks_tip_affirmation_map( &burnchain_blocks_db, sort_tx.tx(), @@ -1718,7 +1714,6 @@ fn replay_block(stacks_path: &str, index_block_hash_hex: &str) { &next_microblocks, next_staging_block.commit_burn, next_staging_block.sortition_burn, - &user_supports, block_am.weight(), true, ) { diff --git a/stackslib/src/monitoring/mod.rs b/stackslib/src/monitoring/mod.rs index ff718ba30..302ff4d0d 100644 --- a/stackslib/src/monitoring/mod.rs +++ b/stackslib/src/monitoring/mod.rs @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryInto; use std::error::Error; use std::path::PathBuf; use std::sync::atomic::{AtomicUsize, Ordering}; diff --git a/stackslib/src/net/api/getblock.rs b/stackslib/src/net/api/getblock.rs index 924c165de..9596fbe9f 100644 --- a/stackslib/src/net/api/getblock.rs +++ b/stackslib/src/net/api/getblock.rs @@ -206,6 +206,7 @@ impl HttpChunkGenerator for StacksBlockStream { 4096 } + #[cfg_attr(test, mutants::skip)] fn generate_next_chunk(&mut self) -> Result, String> { let block_path = StacksChainState::get_index_block_path(&self.blocks_path, &self.index_block_hash) diff --git a/stackslib/src/net/api/getheaders.rs b/stackslib/src/net/api/getheaders.rs index b2a3e4dc9..95558e300 100644 --- a/stackslib/src/net/api/getheaders.rs +++ b/stackslib/src/net/api/getheaders.rs @@ -231,6 +231,7 @@ impl HttpChunkGenerator for StacksHeaderStream { 4096 } + #[cfg_attr(test, mutants::skip)] fn generate_next_chunk(&mut self) -> Result, String> { if self.total_bytes == 0 { // headers are a JSON array. Start by writing '[', then write each header, and diff --git a/stackslib/src/net/api/getmicroblocks_indexed.rs b/stackslib/src/net/api/getmicroblocks_indexed.rs index 8f5eb7bc5..5481cb3cf 100644 --- a/stackslib/src/net/api/getmicroblocks_indexed.rs +++ b/stackslib/src/net/api/getmicroblocks_indexed.rs @@ -223,6 +223,7 @@ impl HttpChunkGenerator for StacksIndexedMicroblockStream { /// Stream back microblock chunks. /// The first chunk is a 4-byte length prefix /// Subsequent chunks are microblocks + #[cfg_attr(test, mutants::skip)] fn generate_next_chunk(&mut self) -> Result, String> { if self.num_items_ptr == 0 { // send length prefix diff --git a/stackslib/src/net/api/getpoxinfo.rs b/stackslib/src/net/api/getpoxinfo.rs index 2d583da93..b46360984 100644 --- a/stackslib/src/net/api/getpoxinfo.rs +++ b/stackslib/src/net/api/getpoxinfo.rs @@ -17,12 +17,13 @@ use std::io::{Read, Write}; use clarity::vm::clarity::ClarityConnection; -use clarity::vm::costs::LimitedCostTracker; +use clarity::vm::costs::{ExecutionCost, LimitedCostTracker}; use clarity::vm::types::{PrincipalData, StandardPrincipalData}; use clarity::vm::ClarityVersion; use regex::{Captures, Regex}; use stacks_common::types::chainstate::StacksBlockId; use stacks_common::types::net::PeerHost; +use stacks_common::types::StacksEpochId; use stacks_common::util::hash::Sha256Sum; use crate::burnchains::Burnchain; @@ -31,6 +32,7 @@ use crate::chainstate::stacks::boot::{POX_1_NAME, POX_2_NAME, POX_3_NAME, POX_4_ use crate::chainstate::stacks::db::StacksChainState; use crate::chainstate::stacks::Error as ChainError; use crate::core::mempool::MemPoolDB; +use crate::core::StacksEpoch; use crate::net::http::{ parse_json, Error, HttpNotFound, HttpRequest, HttpRequestContents, HttpRequestPreamble, HttpResponse, HttpResponseContents, HttpResponsePayload, HttpResponsePreamble, HttpServerError, @@ -80,6 +82,27 @@ pub struct RPCPoxContractVersion { pub first_reward_cycle_id: u64, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RPCPoxEpoch { + pub epoch_id: StacksEpochId, + pub start_height: u64, + pub end_height: u64, + pub block_limit: ExecutionCost, + pub network_epoch: u8, +} + +impl From for RPCPoxEpoch { + fn from(epoch: StacksEpoch) -> Self { + Self { + epoch_id: epoch.epoch_id, + start_height: epoch.start_height, + end_height: epoch.end_height, + block_limit: epoch.block_limit, + network_epoch: epoch.network_epoch, + } + } +} + /// The data we return on GET /v2/pox #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RPCPoxInfoData { @@ -94,6 +117,7 @@ pub struct RPCPoxInfoData { pub total_liquid_supply_ustx: u64, pub current_cycle: RPCPoxCurrentCycleInfo, pub next_cycle: RPCPoxNextCycleInfo, + pub epochs: Vec, // below are included for backwards-compatibility pub min_amount_ustx: u64, @@ -202,12 +226,6 @@ impl RPCPoxInfoData { .to_owned() .expect_u128()? as u64; - let reward_cycle_id = res - .get("reward-cycle-id") - .unwrap_or_else(|_| panic!("FATAL: no 'reward-cycle-id'")) - .to_owned() - .expect_u128()? as u64; - let reward_cycle_length = res .get("reward-cycle-length") .unwrap_or_else(|_| panic!("FATAL: no 'reward-cycle-length'")) @@ -268,7 +286,16 @@ impl RPCPoxInfoData { return Err(NetError::DBError(DBError::Corruption)); } + // Manually calculate `reward_cycle_id` so that clients don't get an "off by one" view at + // reward cycle boundaries (because if the reward cycle is loaded from clarity, its + // evaluated in the last mined Stacks block, not the most recent burn block). + let reward_cycle_id = burnchain + .block_height_to_reward_cycle(burnchain_tip.block_height) + .ok_or_else(|| { + NetError::ChainstateError("Current burn block height is before stacks start".into()) + })?; let effective_height = burnchain_tip.block_height - first_burnchain_block_height; + let next_reward_cycle_in = reward_cycle_length - (effective_height % reward_cycle_length); let next_rewards_start = burnchain_tip.block_height + next_reward_cycle_in; @@ -331,6 +358,10 @@ impl RPCPoxInfoData { as u64; let cur_cycle_pox_active = sortdb.is_pox_active(burnchain, &burnchain_tip)?; + let epochs: Vec<_> = SortitionDB::get_stacks_epochs(sortdb.conn())? + .into_iter() + .map(|epoch| RPCPoxEpoch::from(epoch)) + .collect(); Ok(RPCPoxInfoData { contract_id: boot_code_id(cur_block_pox_contract, chainstate.mainnet).to_string(), @@ -359,6 +390,7 @@ impl RPCPoxInfoData { blocks_until_reward_phase: next_reward_cycle_in, ustx_until_pox_rejection: rejection_votes_left_required, }, + epochs, min_amount_ustx: next_threshold, prepare_cycle_length, reward_cycle_id, diff --git a/stackslib/src/net/api/mod.rs b/stackslib/src/net/api/mod.rs index 4b45c9f4e..c1a042aef 100644 --- a/stackslib/src/net/api/mod.rs +++ b/stackslib/src/net/api/mod.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::From; - use clarity::vm::costs::ExecutionCost; use stacks_common::codec::read_next; use stacks_common::types::chainstate::{BlockHeaderHash, StacksBlockId}; @@ -114,7 +112,9 @@ impl StacksHttp { liststackerdbreplicas::RPCListStackerDBReplicasRequestHandler::new(), ); self.register_rpc_endpoint(postblock::RPCPostBlockRequestHandler::new()); - self.register_rpc_endpoint(postblock_proposal::RPCBlockProposalRequestHandler::new()); + self.register_rpc_endpoint(postblock_proposal::RPCBlockProposalRequestHandler::new( + self.block_proposal_token.clone(), + )); self.register_rpc_endpoint(postfeerate::RPCPostFeeRateRequestHandler::new()); self.register_rpc_endpoint(postmempoolquery::RPCMempoolQueryRequestHandler::new()); self.register_rpc_endpoint(postmicroblock::RPCPostMicroblockRequestHandler::new()); diff --git a/stackslib/src/net/api/postblock_proposal.rs b/stackslib/src/net/api/postblock_proposal.rs index 72b85c577..50b041529 100644 --- a/stackslib/src/net/api/postblock_proposal.rs +++ b/stackslib/src/net/api/postblock_proposal.rs @@ -342,11 +342,15 @@ impl NakamotoBlockProposal { #[derive(Clone, Default)] pub struct RPCBlockProposalRequestHandler { pub block_proposal: Option, + pub auth: Option, } impl RPCBlockProposalRequestHandler { - pub fn new() -> Self { - Self::default() + pub fn new(auth: Option) -> Self { + Self { + block_proposal: None, + auth, + } } /// Decode a JSON-encoded block proposal @@ -375,24 +379,22 @@ impl HttpRequest for RPCBlockProposalRequestHandler { query: Option<&str>, body: &[u8], ) -> Result { - // Only accept requests from localhost - let is_loopback = match preamble.host { - // Should never be DNS - PeerHost::DNS(..) => false, - PeerHost::IP(addr, ..) => addr.is_loopback(), + // If no authorization is set, then the block proposal endpoint is not enabled + let Some(password) = &self.auth else { + return Err(Error::Http(400, "Bad Request.".into())); }; - - if !is_loopback { - return Err(Error::Http(403, "Forbidden".into())); + let Some(auth_header) = preamble.headers.get("authorization") else { + return Err(Error::Http(401, "Unauthorized".into())); + }; + if auth_header != password { + return Err(Error::Http(401, "Unauthorized".into())); } - if preamble.get_content_length() == 0 { return Err(Error::DecodeError( "Invalid Http request: expected non-zero-length body for block proposal endpoint" .to_string(), )); } - if preamble.get_content_length() > MAX_PAYLOAD_LEN { return Err(Error::DecodeError( "Invalid Http request: BlockProposal body is too big".to_string(), diff --git a/stackslib/src/net/api/postmempoolquery.rs b/stackslib/src/net/api/postmempoolquery.rs index d1e8ce990..7c9cecec5 100644 --- a/stackslib/src/net/api/postmempoolquery.rs +++ b/stackslib/src/net/api/postmempoolquery.rs @@ -127,10 +127,12 @@ impl StacksMemPoolStream { } impl HttpChunkGenerator for StacksMemPoolStream { + #[cfg_attr(test, mutants::skip)] fn hint_chunk_size(&self) -> usize { 4096 } + #[cfg_attr(test, mutants::skip)] fn generate_next_chunk(&mut self) -> Result, String> { if self.corked { test_debug!( diff --git a/stackslib/src/net/api/poststackerdbchunk.rs b/stackslib/src/net/api/poststackerdbchunk.rs index 0caae735d..d7901534e 100644 --- a/stackslib/src/net/api/poststackerdbchunk.rs +++ b/stackslib/src/net/api/poststackerdbchunk.rs @@ -129,6 +129,7 @@ impl StackerDBErrorCodes { } } + #[cfg_attr(test, mutants::skip)] pub fn reason(&self) -> &'static str { match self { Self::DataAlreadyExists => "Data for this slot and version already exist", diff --git a/stackslib/src/net/atlas/db.rs b/stackslib/src/net/atlas/db.rs index 10f48a611..784bff963 100644 --- a/stackslib/src/net/atlas/db.rs +++ b/stackslib/src/net/atlas/db.rs @@ -34,7 +34,6 @@ //! use std::collections::HashSet; -use std::convert::{From, TryFrom}; use std::fs; use clarity::vm::types::QualifiedContractIdentifier; diff --git a/stackslib/src/net/atlas/mod.rs b/stackslib/src/net/atlas/mod.rs index ffeb569f9..45100d984 100644 --- a/stackslib/src/net/atlas/mod.rs +++ b/stackslib/src/net/atlas/mod.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::collections::{HashMap, HashSet}; -use std::convert::TryFrom; use std::hash::{Hash, Hasher}; use clarity::vm::types::{QualifiedContractIdentifier, SequenceData, TupleData, Value}; diff --git a/stackslib/src/net/atlas/tests.rs b/stackslib/src/net/atlas/tests.rs index 567d49fe6..2ebcb7131 100644 --- a/stackslib/src/net/atlas/tests.rs +++ b/stackslib/src/net/atlas/tests.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::collections::{BinaryHeap, HashMap, HashSet}; -use std::convert::TryFrom; use std::{thread, time}; use clarity::vm::types::QualifiedContractIdentifier; diff --git a/stackslib/src/net/chat.rs b/stackslib/src/net/chat.rs index 1ce9d3545..1b5424119 100644 --- a/stackslib/src/net/chat.rs +++ b/stackslib/src/net/chat.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::collections::{HashMap, HashSet, VecDeque}; -use std::convert::TryFrom; use std::io::{Read, Write}; use std::net::SocketAddr; use std::{cmp, mem}; @@ -668,6 +667,7 @@ impl ConversationP2P { /// Does the given services bitfield mempool query interface? It will if it has both /// RELAY and RPC bits set. + #[cfg_attr(test, mutants::skip)] pub fn supports_mempool_query(peer_services: u16) -> bool { let expected_bits = (ServiceFlags::RELAY as u16) | (ServiceFlags::RPC as u16); (peer_services & expected_bits) == expected_bits diff --git a/stackslib/src/net/codec.rs b/stackslib/src/net/codec.rs index e4ba530f2..c0496aa14 100644 --- a/stackslib/src/net/codec.rs +++ b/stackslib/src/net/codec.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::collections::HashSet; -use std::convert::TryFrom; use std::io::prelude::*; use std::io::Read; use std::{io, mem}; diff --git a/stackslib/src/net/connection.rs b/stackslib/src/net/connection.rs index 026fd744e..878ab04ef 100644 --- a/stackslib/src/net/connection.rs +++ b/stackslib/src/net/connection.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::collections::VecDeque; -use std::convert::TryFrom; use std::io::{Read, Write}; use std::ops::{Deref, DerefMut}; use std::sync::mpsc::{ @@ -280,6 +279,7 @@ impl Write for NetworkReplyHandle

{ } } + #[cfg_attr(test, mutants::skip)] fn flush(&mut self) -> io::Result<()> { self.pipe_flush() } @@ -415,6 +415,8 @@ pub struct ConnectionOptions { /// the reward cycle in which Nakamoto activates, and thus needs to run both the epoch /// 2.x and Nakamoto state machines. pub force_nakamoto_epoch_transition: bool, + /// The authorization token to enable the block proposal RPC endpoint + pub block_proposal_token: Option, } impl std::default::Default for ConnectionOptions { @@ -508,6 +510,7 @@ impl std::default::Default for ConnectionOptions { disable_stackerdb_get_chunks: false, force_disconnect_interval: None, force_nakamoto_epoch_transition: false, + block_proposal_token: None, } } } @@ -565,6 +568,7 @@ impl ConnectionInbox

{ /// try to consume buffered data to form a message preamble. /// returns an option of the preamble consumed and the number of bytes used from the bytes slice + #[cfg_attr(test, mutants::skip)] fn consume_preamble( &mut self, protocol: &mut P, @@ -626,6 +630,7 @@ impl ConnectionInbox

{ } /// buffer up bytes for a message + #[cfg_attr(test, mutants::skip)] fn buffer_message_bytes(&mut self, bytes: &[u8], message_len_opt: Option) -> usize { let message_len = message_len_opt.unwrap_or(MAX_MESSAGE_LEN as usize); let buffered_so_far = self.buf[self.message_ptr..].len(); @@ -1201,6 +1206,7 @@ impl ConnectionOutbox

{ } /// How many queued messsages do we have? + #[cfg_attr(test, mutants::skip)] pub fn num_messages(&self) -> usize { self.outbox.len() } @@ -1361,6 +1367,7 @@ impl NetworkConnection

{ } /// Receive data + #[cfg_attr(test, mutants::skip)] pub fn recv_data(&mut self, fd: &mut R) -> Result { self.inbox.recv_bytes(&mut self.protocol, fd) } diff --git a/stackslib/src/net/db.rs b/stackslib/src/net/db.rs index 02d448967..1c116a617 100644 --- a/stackslib/src/net/db.rs +++ b/stackslib/src/net/db.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::collections::HashSet; -use std::convert::{From, TryFrom}; use std::{fmt, fs}; use clarity::vm::types::{ @@ -518,6 +517,7 @@ impl PeerDB { Ok(version) } + #[cfg_attr(test, mutants::skip)] fn apply_schema_2(tx: &Transaction) -> Result<(), db_error> { test_debug!("Apply schema 2 to peer DB"); for row_text in PEERDB_SCHEMA_2 { @@ -1698,6 +1698,7 @@ impl PeerDB { /// Get a randomized set of peers for walking the peer graph. /// -- selects peers at random even if not allowed + #[cfg_attr(test, mutants::skip)] pub fn get_random_walk_neighbors( conn: &DBConn, network_id: u32, @@ -1755,6 +1756,7 @@ impl PeerDB { } /// Classify an IP address to its AS number + #[cfg_attr(test, mutants::skip)] pub fn asn_lookup(conn: &DBConn, addrbits: &PeerAddress) -> Result, db_error> { if addrbits.is_ipv4() { PeerDB::asn4_lookup(conn, addrbits) @@ -1765,6 +1767,7 @@ impl PeerDB { } /// Count the number of nodes in a given AS + #[cfg_attr(test, mutants::skip)] pub fn asn_count(conn: &DBConn, asn: u32) -> Result { let qry = "SELECT COUNT(*) FROM frontier WHERE asn = ?1"; let args = [&asn as &dyn ToSql]; @@ -1772,6 +1775,7 @@ impl PeerDB { Ok(count as u64) } + #[cfg_attr(test, mutants::skip)] pub fn get_frontier_size(conn: &DBConn) -> Result { let qry = "SELECT COUNT(*) FROM frontier"; let count = query_count(conn, &qry, NO_PARAMS)?; diff --git a/stackslib/src/net/download.rs b/stackslib/src/net/download.rs index d44efef4a..f19d6f47d 100644 --- a/stackslib/src/net/download.rs +++ b/stackslib/src/net/download.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::collections::{HashMap, HashSet, VecDeque}; -use std::convert::TryFrom; use std::hash::{Hash, Hasher}; use std::io::{Read, Write}; use std::net::{IpAddr, SocketAddr}; @@ -2507,7 +2506,6 @@ impl PeerNetwork { #[cfg(test)] pub mod test { use std::collections::HashMap; - use std::convert::TryFrom; use clarity::vm::clarity::ClarityConnection; use clarity::vm::costs::ExecutionCost; diff --git a/stackslib/src/net/http/request.rs b/stackslib/src/net/http/request.rs index 36df8235a..287acb116 100644 --- a/stackslib/src/net/http/request.rs +++ b/stackslib/src/net/http/request.rs @@ -16,7 +16,6 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::io::{Read, Write}; -use std::string::ToString; use percent_encoding::percent_decode_str; use rand::{thread_rng, Rng}; diff --git a/stackslib/src/net/http/response.rs b/stackslib/src/net/http/response.rs index 6c2a61066..f6f177621 100644 --- a/stackslib/src/net/http/response.rs +++ b/stackslib/src/net/http/response.rs @@ -114,6 +114,7 @@ impl HttpResponseContents { /// Write data for this to a pipe writer, which buffers it up. /// Return Ok(Some(..)) if there is mroe data to send. /// Once all data is sent, return Ok(None) + #[cfg_attr(test, mutants::skip)] pub fn pipe_out(&mut self, fd: &mut PipeWrite) -> Result { match self { HttpResponseContents::Stream(ref mut inner_stream) => { diff --git a/stackslib/src/net/http/stream.rs b/stackslib/src/net/http/stream.rs index 4f5d9f55c..a14fcb74a 100644 --- a/stackslib/src/net/http/stream.rs +++ b/stackslib/src/net/http/stream.rs @@ -40,6 +40,7 @@ pub trait HttpChunkGenerator: Send { /// Returns Ok(num-bytes > 0) if there are more chunks (i.e. the caller should call this again) /// Returns Ok(0) if there are no more chunks (i.e. the caller should not call this again) /// Returns Err(..) on irrecoverable I/O error + #[cfg_attr(test, mutants::skip)] fn stream_to( &mut self, encoder_state: &mut HttpChunkedTransferWriterState, diff --git a/stackslib/src/net/httpcore.rs b/stackslib/src/net/httpcore.rs index 3ea90efe6..04dd185e1 100644 --- a/stackslib/src/net/httpcore.rs +++ b/stackslib/src/net/httpcore.rs @@ -871,6 +871,8 @@ pub struct StacksHttp { pub maximum_call_argument_size: u32, /// Maximum execution budget of a read-only call pub read_only_call_limit: ExecutionCost, + /// The authorization token to enable the block proposal RPC endpoint + pub block_proposal_token: Option, } impl StacksHttp { @@ -886,6 +888,7 @@ impl StacksHttp { request_handlers: vec![], maximum_call_argument_size: conn_opts.maximum_call_argument_size, read_only_call_limit: conn_opts.read_only_call_limit.clone(), + block_proposal_token: conn_opts.block_proposal_token.clone(), }; http.register_rpc_methods(); http @@ -1568,6 +1571,7 @@ impl ProtocolFamily for StacksHttp { impl PeerNetwork { /// Send a (non-blocking) HTTP request to a remote peer. /// Returns the event ID on success. + #[cfg_attr(test, mutants::skip)] pub fn connect_or_send_http_request( &mut self, data_url: UrlString, diff --git a/stackslib/src/net/inv/epoch2x.rs b/stackslib/src/net/inv/epoch2x.rs index 21f5c5f7e..480743a36 100644 --- a/stackslib/src/net/inv/epoch2x.rs +++ b/stackslib/src/net/inv/epoch2x.rs @@ -16,7 +16,6 @@ use std::cmp; use std::collections::{BTreeMap, HashMap, HashSet}; -use std::convert::TryFrom; use std::io::{Read, Write}; use std::net::SocketAddr; @@ -147,6 +146,7 @@ impl PeerBlocksInv { } /// Does this remote neighbor have certainty about the ith PoX anchor block? + #[cfg_attr(test, mutants::skip)] pub fn has_ith_anchor_block(&self, reward_cycle: u64) -> bool { if self.num_reward_cycles <= reward_cycle { return false; @@ -282,6 +282,7 @@ impl PeerBlocksInv { /// Invalidate PoX inventories as a result of learning a new reward cycle's status /// Returns how many bits were dropped + #[cfg_attr(test, mutants::skip)] pub fn truncate_pox_inventory(&mut self, burnchain: &Burnchain, reward_cycle: u64) -> u64 { let highest_agreed_block_height = burnchain.reward_cycle_to_block_height(reward_cycle); @@ -385,6 +386,7 @@ impl PeerBlocksInv { } /// Clear a block bit + #[cfg_attr(test, mutants::skip)] pub fn clear_block_bit(&mut self, block_height: u64) { self.merge_blocks_inv(block_height, 1, vec![0x01], vec![0x00], true); } @@ -580,6 +582,7 @@ impl NeighborBlockStats { ); } + #[cfg_attr(test, mutants::skip)] pub fn reset_block_scan(&mut self, block_reward_cycle: u64) { self.block_reward_cycle = block_reward_cycle; self.request = None; diff --git a/stackslib/src/net/mod.rs b/stackslib/src/net/mod.rs index 5c2e43f26..d212aa50f 100644 --- a/stackslib/src/net/mod.rs +++ b/stackslib/src/net/mod.rs @@ -15,9 +15,7 @@ // along with this program. If not, see . use std::borrow::Borrow; -use std::cmp::PartialEq; use std::collections::{HashMap, HashSet}; -use std::convert::{From, TryFrom}; use std::hash::{Hash, Hasher}; use std::io::prelude::*; use std::io::{Read, Write}; @@ -1672,7 +1670,6 @@ pub mod test { match self { BlockstackOperationType::LeaderKeyRegister(ref op) => op.consensus_serialize(fd), BlockstackOperationType::LeaderBlockCommit(ref op) => op.consensus_serialize(fd), - BlockstackOperationType::UserBurnSupport(ref op) => op.consensus_serialize(fd), BlockstackOperationType::TransferStx(_) | BlockstackOperationType::DelegateStx(_) | BlockstackOperationType::PreStx(_) @@ -2735,9 +2732,6 @@ pub mod test { BlockstackOperationType::LeaderKeyRegister(ref mut data) => { data.consensus_hash = (*ch).clone(); } - BlockstackOperationType::UserBurnSupport(ref mut data) => { - data.consensus_hash = (*ch).clone(); - } _ => {} } } diff --git a/stackslib/src/net/neighbors/comms.rs b/stackslib/src/net/neighbors/comms.rs index 38c59461f..157c79e9d 100644 --- a/stackslib/src/net/neighbors/comms.rs +++ b/stackslib/src/net/neighbors/comms.rs @@ -493,6 +493,7 @@ impl NeighborComms for PeerNetworkComms { self.events.contains(&event_id) } + #[cfg_attr(test, mutants::skip)] fn add_batch_request(&mut self, naddr: NeighborAddress, rh: ReplyHandleP2P) { if let Some(ref mut batch) = self.ongoing_batch_request.as_mut() { batch.add(naddr, rh); @@ -679,6 +680,7 @@ impl NeighborCommsRequest { } /// How many inflight requests remaining? + #[cfg_attr(test, mutants::skip)] pub fn count_inflight(&self) -> usize { self.state.len() } diff --git a/stackslib/src/net/p2p.rs b/stackslib/src/net/p2p.rs index ed5584009..345426aa3 100644 --- a/stackslib/src/net/p2p.rs +++ b/stackslib/src/net/p2p.rs @@ -574,6 +574,7 @@ impl PeerNetwork { } /// start serving. + #[cfg_attr(test, mutants::skip)] pub fn bind(&mut self, my_addr: &SocketAddr, http_addr: &SocketAddr) -> Result<(), net_error> { let mut net = NetworkState::new(self.connection_opts.max_sockets)?; @@ -2457,6 +2458,7 @@ impl PeerNetwork { } /// Do a mempool sync. Return any transactions we might receive. + #[cfg_attr(test, mutants::skip)] fn do_network_mempool_sync( &mut self, dns_client_opt: &mut Option<&mut DNSClient>, @@ -2507,6 +2509,7 @@ impl PeerNetwork { /// Begin the process of learning this peer's public IP address. /// Return Ok(finished with this step) /// Return Err(..) on failure + #[cfg_attr(test, mutants::skip)] fn begin_learn_public_ip(&mut self) -> Result { if self.peers.len() == 0 { return Err(net_error::NoSuchNeighbor); @@ -3443,6 +3446,7 @@ impl PeerNetwork { /// Returns Ok(Some(..)) if we're not done, and can proceed /// Returns the new sync state -- either ResolveURL if we need to resolve a data URL, /// or SendQuery if we got the IP address and can just issue the query. + #[cfg_attr(test, mutants::skip)] fn mempool_sync_pick_outbound_peer( &mut self, dns_client_opt: &mut Option<&mut DNSClient>, @@ -3502,6 +3506,7 @@ impl PeerNetwork { /// Returns Ok(Some(..)) if we're not done, and can proceed /// Returns the new sync state -- either ResolveURL if we need to resolve a data URL, /// or SendQuery if we got the IP address and can just issue the query. + #[cfg_attr(test, mutants::skip)] fn mempool_sync_begin_resolve_data_url( &self, url_str: UrlString, @@ -3558,6 +3563,7 @@ impl PeerNetwork { /// Returns Ok(true, ..) if we're done syncing the mempool. /// Returns Ok(false, ..) if there's more to do /// Returns the socket addr if we ever succeed in resolving it. + #[cfg_attr(test, mutants::skip)] fn mempool_sync_resolve_data_url( &mut self, url_str: &UrlString, @@ -3605,6 +3611,7 @@ impl PeerNetwork { /// Returns Ok((true, ..)) if we're done mempool syncing /// Returns Ok((false, ..)) if there's more to do /// Returns the event ID on success + #[cfg_attr(test, mutants::skip)] fn mempool_sync_send_query( &mut self, url: &UrlString, @@ -3630,6 +3637,7 @@ impl PeerNetwork { /// Return Ok(true, ..) if we're done with the mempool sync. /// Return Ok(false, ..) if we have more work to do. /// Returns the page ID of the next request to make, and the list of transactions we got + #[cfg_attr(test, mutants::skip)] fn mempool_sync_recv_response( &mut self, event_id: usize, @@ -3681,6 +3689,7 @@ impl PeerNetwork { /// Do a mempool sync /// Return true if we're done and can advance to the next state. /// Returns the transactions as well if the sync ran to completion. + #[cfg_attr(test, mutants::skip)] fn do_mempool_sync( &mut self, dns_client_opt: &mut Option<&mut DNSClient>, @@ -5518,6 +5527,7 @@ impl PeerNetwork { /// Store a single transaction /// Return true if stored; false if it was a dup or if it's temporarily blacklisted. /// Has to be done here, since only the p2p network has the unconfirmed state. + #[cfg_attr(test, mutants::skip)] fn store_transaction( mempool: &mut MemPoolDB, sortdb: &SortitionDB, @@ -5567,6 +5577,7 @@ impl PeerNetwork { /// Store all inbound transactions, and return the ones that we actually stored so they can be /// relayed. + #[cfg_attr(test, mutants::skip)] pub fn store_transactions( mempool: &mut MemPoolDB, chainstate: &mut StacksChainState, diff --git a/stackslib/src/net/poll.rs b/stackslib/src/net/poll.rs index c508d0193..bdda12e6d 100644 --- a/stackslib/src/net/poll.rs +++ b/stackslib/src/net/poll.rs @@ -83,6 +83,7 @@ impl NetworkState { }) } + #[cfg_attr(test, mutants::skip)] pub fn num_events(&self) -> usize { self.event_map.len() } diff --git a/stackslib/src/net/rpc.rs b/stackslib/src/net/rpc.rs index 879769f66..e2f93d728 100644 --- a/stackslib/src/net/rpc.rs +++ b/stackslib/src/net/rpc.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::collections::{HashMap, HashSet, VecDeque}; -use std::convert::TryFrom; use std::io::prelude::*; use std::io::{Read, Seek, SeekFrom, Write}; use std::net::SocketAddr; @@ -512,6 +511,7 @@ impl ConversationHttp { } /// When was this converation conencted? + #[cfg_attr(test, mutants::skip)] pub fn get_connection_time(&self) -> u64 { self.connection_time } @@ -597,6 +597,7 @@ impl ConversationHttp { } /// Remove all timed-out messages, and ding the remote peer as unhealthy + #[cfg_attr(test, mutants::skip)] pub fn clear_timeouts(&mut self) -> () { self.connection.drain_timeouts(); } @@ -625,6 +626,7 @@ impl ConversationHttp { } /// Write data out of our HTTP connection. Write as much as we can + #[cfg_attr(test, mutants::skip)] pub fn send(&mut self, w: &mut W) -> Result { let mut total_sz = 0; loop { diff --git a/stackslib/src/net/server.rs b/stackslib/src/net/server.rs index e93819e34..c920a3cef 100644 --- a/stackslib/src/net/server.rs +++ b/stackslib/src/net/server.rs @@ -87,6 +87,7 @@ impl HttpPeer { } /// Is there a HTTP conversation open to this data_url that is not in progress? + #[cfg_attr(test, mutants::skip)] pub fn find_free_conversation(&self, data_url: &UrlString) -> Option { for (event_id, convo) in self.peers.iter() { if let Some(ref url) = convo.get_url() { @@ -99,6 +100,7 @@ impl HttpPeer { } /// Get a mut ref to a conversation + #[cfg_attr(test, mutants::skip)] pub fn get_conversation(&mut self, event_id: usize) -> Option<&mut ConversationHttp> { self.peers.get_mut(&event_id) } @@ -167,6 +169,7 @@ impl HttpPeer { } /// Can we register this socket? + #[cfg_attr(test, mutants::skip)] fn can_register_http( &self, peer_addr: &SocketAddr, @@ -208,6 +211,7 @@ impl HttpPeer { /// Low-level method to register a socket/event pair on the p2p network interface. /// Call only once the socket is connected (called once the socket triggers ready). /// Will destroy the socket if we can't register for whatever reason. + #[cfg_attr(test, mutants::skip)] fn register_http( &mut self, network_state: &mut NetworkState, @@ -281,6 +285,7 @@ impl HttpPeer { } /// Deregister a socket/event pair + #[cfg_attr(test, mutants::skip)] pub fn deregister_http(&mut self, network_state: &mut NetworkState, event_id: usize) -> () { self.peers.remove(&event_id); @@ -544,6 +549,7 @@ impl HttpPeer { /// Advance the state of all such conversations with remote peers. /// Return the list of events that correspond to failed conversations, as well as the list of /// peer network messages we'll need to forward + #[cfg_attr(test, mutants::skip)] fn process_ready_sockets( &mut self, poll_state: &mut NetworkPollState, @@ -601,6 +607,7 @@ impl HttpPeer { /// Flush outgoing replies, but don't block. /// Drop broken handles. /// Return the list of conversation event IDs to close (i.e. they're broken, or the request is done) + #[cfg_attr(test, mutants::skip)] fn flush_conversations(&mut self) -> Vec { let mut close = vec![]; @@ -626,6 +633,7 @@ impl HttpPeer { /// -- receive data on ready sockets /// -- clear out timed-out requests /// Returns the list of messages to forward along to the peer network. + #[cfg_attr(test, mutants::skip)] pub fn run( &mut self, network_state: &mut NetworkState, diff --git a/stackslib/src/net/stackerdb/mod.rs b/stackslib/src/net/stackerdb/mod.rs index 8d54c0dee..0213c0f96 100644 --- a/stackslib/src/net/stackerdb/mod.rs +++ b/stackslib/src/net/stackerdb/mod.rs @@ -197,6 +197,7 @@ impl StackerDBConfig { } /// How many slots are in this DB total? + #[cfg_attr(test, mutants::skip)] pub fn num_slots(&self) -> u32 { self.signers.iter().fold(0, |acc, s| acc + s.1) } diff --git a/stackslib/src/net/tests/mod.rs b/stackslib/src/net/tests/mod.rs index acff183d9..9111f10fc 100644 --- a/stackslib/src/net/tests/mod.rs +++ b/stackslib/src/net/tests/mod.rs @@ -46,7 +46,9 @@ use crate::chainstate::stacks::address::PoxAddress; use crate::chainstate::stacks::boot::test::{ key_to_stacks_addr, make_pox_4_lockup, make_signer_key_signature, with_sortdb, }; -use crate::chainstate::stacks::boot::MINERS_NAME; +use crate::chainstate::stacks::boot::{ + MINERS_NAME, SIGNERS_VOTING_FUNCTION_NAME, SIGNERS_VOTING_NAME, +}; use crate::chainstate::stacks::db::{MinerPaymentTxFees, StacksAccount, StacksChainState}; use crate::chainstate::stacks::events::TransactionOrigin; use crate::chainstate::stacks::{ @@ -185,9 +187,9 @@ impl NakamotoBootPlan { function_name, .. }) => { - if contract_name.as_str() == "signers-voting" + if contract_name.as_str() == SIGNERS_VOTING_NAME && address.is_burn() - && function_name.as_str() == "vote-for-aggregate-public-key" + && function_name.as_str() == SIGNERS_VOTING_FUNCTION_NAME { false } else { diff --git a/stackslib/src/util_lib/boot.rs b/stackslib/src/util_lib/boot.rs index af3f44327..95cfca9c4 100644 --- a/stackslib/src/util_lib/boot.rs +++ b/stackslib/src/util_lib/boot.rs @@ -1,5 +1,3 @@ -use std::convert::TryFrom; - use clarity::vm::database::STXBalance; use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier}; use clarity::vm::ContractName; diff --git a/stackslib/src/util_lib/db.rs b/stackslib/src/util_lib/db.rs index 584423ffb..a06c23408 100644 --- a/stackslib/src/util_lib/db.rs +++ b/stackslib/src/util_lib/db.rs @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryInto; use std::io::Error as IOError; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; diff --git a/stackslib/src/util_lib/strings.rs b/stackslib/src/util_lib/strings.rs index 3efb1fc7d..0486e6bf8 100644 --- a/stackslib/src/util_lib/strings.rs +++ b/stackslib/src/util_lib/strings.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . use std::borrow::Borrow; -use std::convert::TryFrom; use std::io::prelude::*; use std::io::{Read, Write}; use std::ops::{Deref, DerefMut}; diff --git a/testnet/stacks-node/Cargo.toml b/testnet/stacks-node/Cargo.toml index 5d9d964a1..71f8808a1 100644 --- a/testnet/stacks-node/Cargo.toml +++ b/testnet/stacks-node/Cargo.toml @@ -47,6 +47,7 @@ stacks-signer = { path = "../../stacks-signer" } tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } wsts = {workspace = true} +mutants = "0.0.3" [dependencies.rusqlite] version = "=0.24.2" diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index bb04fc4b9..f2e6f6954 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -29,7 +29,7 @@ use stacks::burnchains::{ use stacks::chainstate::burn::db::sortdb::SortitionDB; use stacks::chainstate::burn::operations::{ BlockstackOperationType, DelegateStxOp, LeaderBlockCommitOp, LeaderKeyRegisterOp, PreStxOp, - TransferStxOp, UserBurnSupportOp, + TransferStxOp, }; #[cfg(test)] use stacks::chainstate::burn::Opcodes; @@ -900,8 +900,7 @@ impl BitcoinRegtestController { BlockstackOperationType::LeaderBlockCommit(_) | BlockstackOperationType::LeaderKeyRegister(_) | BlockstackOperationType::StackStx(_) - | BlockstackOperationType::DelegateStx(_) - | BlockstackOperationType::UserBurnSupport(_) => { + | BlockstackOperationType::DelegateStx(_) => { unimplemented!(); } BlockstackOperationType::PreStx(payload) => { @@ -1664,16 +1663,6 @@ impl BitcoinRegtestController { true } - fn build_user_burn_support_tx( - &mut self, - _epoch_id: StacksEpochId, - _payload: UserBurnSupportOp, - _signer: &mut BurnchainOpSigner, - _attempt: u64, - ) -> Option { - unimplemented!() - } - /// Send a serialized tx to the Bitcoin node. Return Some(txid) on successful send; None on /// failure. pub fn send_transaction(&self, transaction: SerializedTx) -> Option { @@ -1830,9 +1819,6 @@ impl BitcoinRegtestController { BlockstackOperationType::LeaderKeyRegister(payload) => { self.build_leader_key_register_tx(epoch_id, payload, op_signer, attempt) } - BlockstackOperationType::UserBurnSupport(payload) => { - self.build_user_burn_support_tx(epoch_id, payload, op_signer, attempt) - } BlockstackOperationType::PreStx(payload) => { self.build_pre_stacks_tx(epoch_id, payload, op_signer) } diff --git a/testnet/stacks-node/src/burnchains/mocknet_controller.rs b/testnet/stacks-node/src/burnchains/mocknet_controller.rs index 0c1ae9c84..7b43ce1c8 100644 --- a/testnet/stacks-node/src/burnchains/mocknet_controller.rs +++ b/testnet/stacks-node/src/burnchains/mocknet_controller.rs @@ -10,7 +10,7 @@ use stacks::chainstate::burn::db::sortdb::{SortitionDB, SortitionHandleTx}; use stacks::chainstate::burn::operations::leader_block_commit::BURN_BLOCK_MINED_AT_MODULUS; use stacks::chainstate::burn::operations::{ BlockstackOperationType, DelegateStxOp, LeaderBlockCommitOp, LeaderKeyRegisterOp, PreStxOp, - StackStxOp, TransferStxOp, UserBurnSupportOp, + StackStxOp, TransferStxOp, }; use stacks::chainstate::burn::BlockSnapshot; use stacks::core::{ @@ -221,21 +221,6 @@ impl BurnchainController for MocknetController { burn_header_hash: next_block_header.block_hash, }) } - BlockstackOperationType::UserBurnSupport(payload) => { - BlockstackOperationType::UserBurnSupport(UserBurnSupportOp { - address: payload.address, - consensus_hash: payload.consensus_hash, - public_key: payload.public_key, - key_block_ptr: payload.key_block_ptr, - key_vtxindex: payload.key_vtxindex, - block_header_hash_160: payload.block_header_hash_160, - burn_fee: payload.burn_fee, - txid: payload.txid, - vtxindex: payload.vtxindex, - block_height: next_block_header.block_height, - burn_header_hash: next_block_header.block_hash, - }) - } BlockstackOperationType::PreStx(payload) => { BlockstackOperationType::PreStx(PreStxOp { block_height: next_block_header.block_height, diff --git a/testnet/stacks-node/src/chain_data.rs b/testnet/stacks-node/src/chain_data.rs index ac1ff4199..4170cf6f6 100644 --- a/testnet/stacks-node/src/chain_data.rs +++ b/testnet/stacks-node/src/chain_data.rs @@ -657,7 +657,6 @@ pub mod tests { 0x41a3ed94d3cb0a84, ]), candidate: block_commit_1.clone(), - user_burns: vec![], }, BurnSamplePoint { burns: block_commit_2.burn_fee.into(), @@ -675,7 +674,6 @@ pub mod tests { 0x8347db29a7961508, ]), candidate: block_commit_2.clone(), - user_burns: vec![], }, BurnSamplePoint { burns: (block_commit_3.burn_fee).into(), @@ -688,7 +686,6 @@ pub mod tests { ]), range_end: Uint256::max(), candidate: block_commit_3.clone(), - user_burns: vec![], }, ]; diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 0b41b9c6a..18640a5f4 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -1,10 +1,9 @@ use std::collections::HashSet; -use std::convert::TryInto; -use std::fs; use std::net::{SocketAddr, ToSocketAddrs}; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use std::time::Duration; +use std::{fs, thread}; use clarity::vm::costs::ExecutionCost; use clarity::vm::types::{AssetIdentifier, PrincipalData, QualifiedContractIdentifier}; @@ -13,7 +12,6 @@ use rand::RngCore; use stacks::burnchains::bitcoin::BitcoinNetworkType; use stacks::burnchains::{Burnchain, MagicBytes, BLOCKSTACK_MAGIC_MAINNET}; use stacks::chainstate::nakamoto::signer_set::NakamotoSigners; -use stacks::chainstate::nakamoto::test_signers::TestSigners; use stacks::chainstate::stacks::boot::MINERS_NAME; use stacks::chainstate::stacks::index::marf::MARFOpenOpts; use stacks::chainstate::stacks::index::storage::TrieHashCalculationMode; @@ -34,7 +32,6 @@ use stacks::net::connection::ConnectionOptions; use stacks::net::{Neighbor, NeighborKey}; use stacks::util_lib::boot::boot_code_id; use stacks::util_lib::db::Error as DBError; -use stacks_common::address::{AddressHashMode, C32_ADDRESS_VERSION_TESTNET_SINGLESIG}; use stacks_common::consts::SIGNER_SLOTS_PER_USER; use stacks_common::types::chainstate::StacksAddress; use stacks_common::types::net::PeerAddress; @@ -181,6 +178,25 @@ mod tests { "ST2TFVBMRPS5SSNP98DQKQ5JNB2B6NZM91C4K3P7B" ); } + + #[test] + fn should_load_block_proposal_token() { + let config = Config::from_config_file( + ConfigFile::from_str( + r#" + [connection_options] + block_proposal_token = "password" + "#, + ) + .unwrap(), + ) + .expect("Expected to be able to parse block proposal token from file"); + + assert_eq!( + config.connection_options.block_proposal_token, + Some("password".to_string()) + ); + } } impl ConfigFile { @@ -276,102 +292,6 @@ impl ConfigFile { } } - pub fn mockamoto() -> ConfigFile { - let epochs = vec![ - StacksEpochConfigFile { - epoch_name: "1.0".into(), - start_height: 0, - }, - StacksEpochConfigFile { - epoch_name: "2.0".into(), - start_height: 0, - }, - StacksEpochConfigFile { - epoch_name: "2.05".into(), - start_height: 1, - }, - StacksEpochConfigFile { - epoch_name: "2.1".into(), - start_height: 2, - }, - StacksEpochConfigFile { - epoch_name: "2.2".into(), - start_height: 3, - }, - StacksEpochConfigFile { - epoch_name: "2.3".into(), - start_height: 4, - }, - StacksEpochConfigFile { - epoch_name: "2.4".into(), - start_height: 5, - }, - StacksEpochConfigFile { - epoch_name: "2.5".into(), - start_height: 6, - }, - StacksEpochConfigFile { - epoch_name: "3.0".into(), - start_height: 7, - }, - ]; - - let burnchain = BurnchainConfigFile { - mode: Some("mockamoto".into()), - rpc_port: Some(8332), - peer_port: Some(8333), - peer_host: Some("localhost".into()), - username: Some("blockstack".into()), - password: Some("blockstacksystem".into()), - magic_bytes: Some("M3".into()), - epochs: Some(epochs), - pox_prepare_length: Some(3), - pox_reward_length: Some(36), - ..BurnchainConfigFile::default() - }; - - let node = NodeConfigFile { - bootstrap_node: None, - miner: Some(true), - stacker: Some(true), - ..NodeConfigFile::default() - }; - - let mining_key = Secp256k1PrivateKey::new(); - let miner = MinerConfigFile { - mining_key: Some(mining_key.to_hex()), - ..MinerConfigFile::default() - }; - - let mock_private_key = Secp256k1PrivateKey::from_seed(&[0]); - let mock_public_key = Secp256k1PublicKey::from_private(&mock_private_key); - let mock_address = StacksAddress::from_public_keys( - C32_ADDRESS_VERSION_TESTNET_SINGLESIG, - &AddressHashMode::SerializeP2PKH, - 1, - &vec![mock_public_key], - ) - .unwrap(); - - info!( - "Mockamoto starting. Initial balance set to mock_private_key = {}", - mock_private_key.to_hex() - ); - - let ustx_balance = vec![InitialBalanceFile { - address: mock_address.to_string(), - amount: 1_000_000_000_000, - }]; - - ConfigFile { - burnchain: Some(burnchain), - node: Some(node), - miner: Some(miner), - ustx_balance: Some(ustx_balance), - ..ConfigFile::default() - } - } - pub fn helium() -> ConfigFile { // ## Settings for local testnet, relying on a local bitcoind server // ## running with the following bitcoin.conf: @@ -506,19 +426,6 @@ lazy_static! { } impl Config { - #[cfg(any(test, feature = "testing"))] - pub fn self_signing(&self) -> Option { - if !(self.burnchain.mode == "nakamoto-neon" || self.burnchain.mode == "mockamoto") { - return None; - } - self.miner.self_signing_key.clone() - } - - #[cfg(not(any(test, feature = "testing")))] - pub fn self_signing(&self) -> Option { - return None; - } - /// get the up-to-date burnchain options from the config. /// If the config file can't be loaded, then return the existing config pub fn get_burnchain_config(&self) -> BurnchainConfig { @@ -550,6 +457,7 @@ impl Config { } /// Apply any test settings to this burnchain config struct + #[cfg_attr(test, mutants::skip)] fn apply_test_settings(&self, burnchain: &mut Burnchain) { if self.burnchain.get_bitcoin_network().1 == BitcoinNetworkType::Mainnet { return; @@ -644,7 +552,7 @@ impl Config { } // check if the Epoch 3.0 burnchain settings as configured are going to be valid. - if self.burnchain.mode == "nakamoto-neon" || self.burnchain.mode == "mockamoto" { + if self.burnchain.mode == "nakamoto-neon" { self.check_nakamoto_config(&burnchain); } } @@ -876,15 +784,7 @@ impl Config { } pub fn from_config_file(config_file: ConfigFile) -> Result { - if config_file.burnchain.as_ref().map(|b| b.mode.clone()) == Some(Some("mockamoto".into())) - { - // in the case of mockamoto, use `ConfigFile::mockamoto()` as the default for - // processing a user-supplied config - let default = Self::from_config_default(ConfigFile::mockamoto(), Config::default())?; - Self::from_config_default(config_file, default) - } else { - Self::from_config_default(config_file, Config::default()) - } + Self::from_config_default(config_file, Config::default()) } fn from_config_default(config_file: ConfigFile, default: Config) -> Result { @@ -910,7 +810,6 @@ impl Config { "krypton", "xenon", "mainnet", - "mockamoto", "nakamoto-neon", ]; @@ -969,26 +868,11 @@ impl Config { node.require_affirmed_anchor_blocks = false; } - let miners_contract_id = boot_code_id(MINERS_NAME, is_mainnet); - if (node.stacker || node.miner) - && burnchain.mode == "nakamoto-neon" - && !node.stacker_dbs.contains(&miners_contract_id) - { - debug!("A miner/stacker must subscribe to the {miners_contract_id} stacker db contract. Forcibly subscribing..."); - node.stacker_dbs.push(miners_contract_id); + if (node.stacker || node.miner) && burnchain.mode == "nakamoto-neon" { + node.add_miner_stackerdb(is_mainnet); } if (node.stacker || node.miner) && burnchain.mode == "nakamoto-neon" { - for signer_set in 0..2 { - for message_id in 0..SIGNER_SLOTS_PER_USER { - let contract_id = NakamotoSigners::make_signers_db_contract_id( - signer_set, message_id, is_mainnet, - ); - if !node.stacker_dbs.contains(&contract_id) { - debug!("A miner/stacker must subscribe to the {contract_id} stacker db contract. Forcibly subscribing..."); - node.stacker_dbs.push(contract_id); - } - } - } + node.add_signers_stackerdbs(is_mainnet); } let miner = match config_file.miner { @@ -1364,7 +1248,7 @@ impl BurnchainConfig { match self.mode.as_str() { "mainnet" => ("mainnet".to_string(), BitcoinNetworkType::Mainnet), "xenon" => ("testnet".to_string(), BitcoinNetworkType::Testnet), - "helium" | "neon" | "argon" | "krypton" | "mocknet" | "mockamoto" | "nakamoto-neon" => { + "helium" | "neon" | "argon" | "krypton" | "mocknet" | "nakamoto-neon" => { ("regtest".to_string(), BitcoinNetworkType::Regtest) } other => panic!("Invalid stacks-node mode: {other}"), @@ -1595,9 +1479,6 @@ pub struct NodeConfig { pub chain_liveness_poll_time_secs: u64, /// stacker DBs we replicate pub stacker_dbs: Vec, - /// if running in mockamoto mode, how long to wait between each - /// simulated bitcoin block - pub mockamoto_time_ms: u64, } #[derive(Clone, Debug)] @@ -1878,12 +1759,32 @@ impl Default for NodeConfig { fault_injection_hide_blocks: false, chain_liveness_poll_time_secs: 300, stacker_dbs: vec![], - mockamoto_time_ms: 3_000, } } } impl NodeConfig { + pub fn add_signers_stackerdbs(&mut self, is_mainnet: bool) { + for signer_set in 0..2 { + for message_id in 0..SIGNER_SLOTS_PER_USER { + let contract_name = NakamotoSigners::make_signers_db_name(signer_set, message_id); + let contract_id = boot_code_id(contract_name.as_str(), is_mainnet); + if !self.stacker_dbs.contains(&contract_id) { + debug!("A miner/stacker must subscribe to the {contract_id} stacker db contract. Forcibly subscribing..."); + self.stacker_dbs.push(contract_id); + } + } + } + } + + pub fn add_miner_stackerdb(&mut self, is_mainnet: bool) { + let miners_contract_id = boot_code_id(MINERS_NAME, is_mainnet); + if !self.stacker_dbs.contains(&miners_contract_id) { + debug!("A miner/stacker must subscribe to the {miners_contract_id} stacker db contract. Forcibly subscribing..."); + self.stacker_dbs.push(miners_contract_id); + } + } + fn default_neighbor( addr: SocketAddr, pubk: Secp256k1PublicKey, @@ -1921,7 +1822,42 @@ impl NodeConfig { let pubkey = Secp256k1PublicKey::from_hex(pubkey_str) .unwrap_or_else(|_| panic!("Invalid public key '{pubkey_str}'")); debug!("Resolve '{}'", &hostport); - let sockaddr = hostport.to_socket_addrs().unwrap().next().unwrap(); + + let mut attempts = 0; + let max_attempts = 5; + let mut delay = Duration::from_secs(2); + + let sockaddr = loop { + match hostport.to_socket_addrs() { + Ok(mut addrs) => { + if let Some(addr) = addrs.next() { + break addr; + } else { + panic!("No addresses found for '{}'", hostport); + } + } + Err(e) => { + if attempts >= max_attempts { + panic!( + "Failed to resolve '{}' after {} attempts: {}", + hostport, max_attempts, e + ); + } else { + error!( + "Attempt {} - Failed to resolve '{}': {}. Retrying in {:?}...", + attempts + 1, + hostport, + e, + delay + ); + thread::sleep(delay); + attempts += 1; + delay *= 2; + } + } + } + }; + let neighbor = NodeConfig::default_neighbor(sockaddr, pubkey, chain_id, peer_version); self.bootstrap_node.push(neighbor); } @@ -1992,7 +1928,6 @@ pub struct MinerConfig { pub candidate_retry_cache_size: u64, pub unprocessed_block_deadline_secs: u64, pub mining_key: Option, - pub self_signing_key: Option, /// Amount of time while mining in nakamoto to wait in between mining interim blocks pub wait_on_interim_blocks: Duration, /// minimum number of transactions that must be in a block if we're going to replace a pending @@ -2040,7 +1975,6 @@ impl Default for MinerConfig { candidate_retry_cache_size: 1024 * 1024, unprocessed_block_deadline_secs: 30, mining_key: None, - self_signing_key: None, wait_on_interim_blocks: Duration::from_millis(2_500), min_tx_count: 0, only_increase_tx_count: false, @@ -2053,7 +1987,7 @@ impl Default for MinerConfig { filter_origins: HashSet::new(), max_reorg_depth: 3, // TODO: update to a sane value based on stackerdb benchmarking - wait_on_signers: Duration::from_millis(10_000), + wait_on_signers: Duration::from_secs(200), } } } @@ -2100,6 +2034,7 @@ pub struct ConnectionOptionsFile { pub force_disconnect_interval: Option, pub antientropy_public: Option, pub private_neighbors: Option, + pub block_proposal_token: Option, } impl ConnectionOptionsFile { @@ -2223,6 +2158,7 @@ impl ConnectionOptionsFile { max_sockets: self.max_sockets.unwrap_or(800) as usize, antientropy_public: self.antientropy_public.unwrap_or(true), private_neighbors: self.private_neighbors.unwrap_or(true), + block_proposal_token: self.block_proposal_token, ..ConnectionOptions::default() }) } @@ -2260,9 +2196,6 @@ pub struct NodeConfigFile { pub chain_liveness_poll_time_secs: Option, /// Stacker DBs we replicate pub stacker_dbs: Option>, - /// if running in mockamoto mode, how long to wait between each - /// simulated bitcoin block - pub mockamoto_time_ms: Option, } impl NodeConfigFile { @@ -2338,9 +2271,6 @@ impl NodeConfigFile { .iter() .filter_map(|contract_id| QualifiedContractIdentifier::parse(contract_id).ok()) .collect(), - mockamoto_time_ms: self - .mockamoto_time_ms - .unwrap_or(default_node_config.mockamoto_time_ms), }; Ok(node_config) } @@ -2424,7 +2354,6 @@ impl MinerConfigFile { .as_ref() .map(|x| Secp256k1PrivateKey::from_hex(x)) .transpose()?, - self_signing_key: Some(TestSigners::default()), wait_on_interim_blocks: self .wait_on_interim_blocks_ms .map(Duration::from_millis) diff --git a/testnet/stacks-node/src/globals.rs b/testnet/stacks-node/src/globals.rs index 5e126c271..a6a2fdad3 100644 --- a/testnet/stacks-node/src/globals.rs +++ b/testnet/stacks-node/src/globals.rs @@ -205,11 +205,13 @@ impl Globals { } /// Signal system-wide stop + #[cfg_attr(test, mutants::skip)] pub fn signal_stop(&self) { self.should_keep_running.store(false, Ordering::SeqCst); } /// Should we keep running? + #[cfg_attr(test, mutants::skip)] pub fn keep_running(&self) -> bool { self.should_keep_running.load(Ordering::SeqCst) } diff --git a/testnet/stacks-node/src/main.rs b/testnet/stacks-node/src/main.rs index 29d1fde36..bf54c1601 100644 --- a/testnet/stacks-node/src/main.rs +++ b/testnet/stacks-node/src/main.rs @@ -22,7 +22,6 @@ pub mod event_dispatcher; pub mod genesis_data; pub mod globals; pub mod keychain; -pub mod mockamoto; pub mod nakamoto_node; pub mod neon_node; pub mod node; @@ -32,7 +31,6 @@ pub mod syncctl; pub mod tenure; use std::collections::HashMap; -use std::convert::TryInto; use std::{env, panic, process}; use backtrace::Backtrace; @@ -56,7 +54,6 @@ pub use self::node::{ChainTip, Node}; pub use self::run_loop::{helium, neon}; pub use self::tenure::Tenure; use crate::chain_data::MinerStats; -use crate::mockamoto::MockamotoNode; use crate::neon_node::{BlockMinerThread, TipCandidate}; use crate::run_loop::boot_nakamoto; @@ -323,10 +320,6 @@ fn main() { args.finish(); ConfigFile::mainnet() } - "mockamoto" => { - args.finish(); - ConfigFile::mockamoto() - } "check-config" => { let config_path: String = args.value_from_str("--config").unwrap(); args.finish(); @@ -450,9 +443,6 @@ fn main() { { let mut run_loop = neon::RunLoop::new(conf); run_loop.start(None, mine_start.unwrap_or(0)); - } else if conf.burnchain.mode == "mockamoto" { - let mut mockamoto = MockamotoNode::new(&conf).unwrap(); - mockamoto.run(); } else if conf.burnchain.mode == "nakamoto-neon" { let mut run_loop = boot_nakamoto::BootRunLoop::new(conf).unwrap(); run_loop.start(None, 0); diff --git a/testnet/stacks-node/src/mockamoto.rs b/testnet/stacks-node/src/mockamoto.rs deleted file mode 100644 index b499b6b6e..000000000 --- a/testnet/stacks-node/src/mockamoto.rs +++ /dev/null @@ -1,1109 +0,0 @@ -// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 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 . -use std::sync::atomic::AtomicBool; -use std::sync::mpsc::{sync_channel, Receiver, RecvTimeoutError}; -use std::sync::{Arc, Mutex}; -use std::thread; -use std::thread::{sleep, JoinHandle}; -use std::time::Duration; - -use clarity::vm::ast::ASTRules; -use clarity::vm::Value as ClarityValue; -use lazy_static::lazy_static; -use stacks::burnchains::bitcoin::address::{ - BitcoinAddress, LegacyBitcoinAddress, LegacyBitcoinAddressType, -}; -use stacks::burnchains::bitcoin::{ - BitcoinBlock, BitcoinInputType, BitcoinNetworkType, BitcoinTransaction, - BitcoinTxInputStructured, BitcoinTxOutput, -}; -use stacks::burnchains::db::{BurnchainDB, BurnchainHeaderReader}; -use stacks::burnchains::{ - BurnchainBlock, BurnchainBlockHeader, BurnchainSigner, Error as BurnchainError, Txid, -}; -use stacks::chainstate::burn::db::sortdb::SortitionDB; -use stacks::chainstate::burn::operations::leader_block_commit::BURN_BLOCK_MINED_AT_MODULUS; -use stacks::chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, -}; -use stacks::chainstate::burn::BlockSnapshot; -use stacks::chainstate::coordinator::comm::CoordinatorReceivers; -use stacks::chainstate::coordinator::{ - ChainsCoordinator, ChainsCoordinatorConfig, CoordinatorCommunication, -}; -use stacks::chainstate::nakamoto::test_signers::TestSigners; -use stacks::chainstate::nakamoto::{ - NakamotoBlock, NakamotoBlockHeader, NakamotoChainState, SetupBlockResult, -}; -use stacks::chainstate::stacks::address::PoxAddress; -use stacks::chainstate::stacks::boot::SIGNERS_VOTING_NAME; -use stacks::chainstate::stacks::db::{ChainStateBootData, ClarityTx, StacksChainState}; -use stacks::chainstate::stacks::miner::{ - BlockBuilder, BlockBuilderSettings, BlockLimitFunction, MinerStatus, TransactionResult, -}; -use stacks::chainstate::stacks::{ - CoinbasePayload, Error as ChainstateError, StacksBlockBuilder, StacksTransaction, - StacksTransactionSigner, TenureChangeCause, TenureChangePayload, ThresholdSignature, - TransactionAuth, TransactionContractCall, TransactionPayload, TransactionVersion, - MAX_EPOCH_SIZE, MINER_BLOCK_CONSENSUS_HASH, MINER_BLOCK_HEADER_HASH, -}; -use stacks::core::mempool::MemPoolWalkSettings; -use stacks::core::{ - MemPoolDB, StacksEpoch, BLOCK_LIMIT_MAINNET_10, HELIUM_BLOCK_LIMIT_20, PEER_VERSION_EPOCH_1_0, - PEER_VERSION_EPOCH_2_0, PEER_VERSION_EPOCH_2_05, PEER_VERSION_EPOCH_2_1, - PEER_VERSION_EPOCH_2_2, PEER_VERSION_EPOCH_2_3, PEER_VERSION_EPOCH_2_4, PEER_VERSION_EPOCH_2_5, - PEER_VERSION_EPOCH_3_0, STACKS_EPOCH_3_0_MARKER, TX_BLOCK_LIMIT_PROPORTION_HEURISTIC, -}; -use stacks::net::atlas::{AtlasConfig, AtlasDB}; -use stacks::net::relay::Relayer; -use stacks::net::stackerdb::StackerDBs; -use stacks::util_lib::boot::boot_code_addr; -use stacks::util_lib::db::Error as DBError; -use stacks::util_lib::signed_structured_data::pox4::{ - make_pox_4_signer_key_signature, Pox4SignatureTopic, -}; -use stacks_common::address::{AddressHashMode, C32_ADDRESS_VERSION_TESTNET_SINGLESIG}; -use stacks_common::bitvec::BitVec; -use stacks_common::codec::StacksMessageCodec; -use stacks_common::consts::{ - CHAIN_ID_TESTNET, FIRST_BURNCHAIN_CONSENSUS_HASH, FIRST_STACKS_BLOCK_HASH, STACKS_EPOCH_MAX, -}; -use stacks_common::types::chainstate::{ - BlockHeaderHash, BurnchainHeaderHash, ConsensusHash, StacksAddress, StacksBlockId, - StacksPrivateKey, VRFSeed, -}; -use stacks_common::types::{PrivateKey, StacksEpochId}; -use stacks_common::util::hash::{to_hex, Hash160, MerkleTree, Sha512Trunc256Sum}; -use stacks_common::util::secp256k1::{MessageSignature, Secp256k1PrivateKey, Secp256k1PublicKey}; -use stacks_common::util::vrf::{VRFPrivateKey, VRFProof, VRFPublicKey, VRF}; - -use crate::globals::{NeonGlobals as Globals, RelayerDirective}; -use crate::neon::Counters; -use crate::neon_node::{PeerThread, StacksNode, BLOCK_PROCESSOR_STACK_SIZE}; -use crate::syncctl::PoxSyncWatchdogComms; -use crate::{Config, EventDispatcher}; - -#[cfg(test)] -mod tests; - -lazy_static! { - pub static ref STACKS_EPOCHS_MOCKAMOTO: [StacksEpoch; 9] = [ - StacksEpoch { - epoch_id: StacksEpochId::Epoch10, - start_height: 0, - end_height: 0, - block_limit: BLOCK_LIMIT_MAINNET_10.clone(), - network_epoch: PEER_VERSION_EPOCH_1_0 - }, - StacksEpoch { - epoch_id: StacksEpochId::Epoch20, - start_height: 0, - end_height: 1, - block_limit: HELIUM_BLOCK_LIMIT_20.clone(), - network_epoch: PEER_VERSION_EPOCH_2_0 - }, - StacksEpoch { - epoch_id: StacksEpochId::Epoch2_05, - start_height: 1, - end_height: 2, - block_limit: HELIUM_BLOCK_LIMIT_20.clone(), - network_epoch: PEER_VERSION_EPOCH_2_05 - }, - StacksEpoch { - epoch_id: StacksEpochId::Epoch21, - start_height: 2, - end_height: 3, - block_limit: HELIUM_BLOCK_LIMIT_20.clone(), - network_epoch: PEER_VERSION_EPOCH_2_1 - }, - StacksEpoch { - epoch_id: StacksEpochId::Epoch22, - start_height: 3, - end_height: 4, - block_limit: HELIUM_BLOCK_LIMIT_20.clone(), - network_epoch: PEER_VERSION_EPOCH_2_2 - }, - StacksEpoch { - epoch_id: StacksEpochId::Epoch23, - start_height: 4, - end_height: 5, - block_limit: HELIUM_BLOCK_LIMIT_20.clone(), - network_epoch: PEER_VERSION_EPOCH_2_3 - }, - StacksEpoch { - epoch_id: StacksEpochId::Epoch24, - start_height: 5, - end_height: 6, - block_limit: HELIUM_BLOCK_LIMIT_20.clone(), - network_epoch: PEER_VERSION_EPOCH_2_4 - }, - StacksEpoch { - epoch_id: StacksEpochId::Epoch25, - start_height: 6, - end_height: 7, - block_limit: HELIUM_BLOCK_LIMIT_20.clone(), - network_epoch: PEER_VERSION_EPOCH_2_5 - }, - StacksEpoch { - epoch_id: StacksEpochId::Epoch30, - start_height: 7, - end_height: STACKS_EPOCH_MAX, - block_limit: HELIUM_BLOCK_LIMIT_20.clone(), - network_epoch: PEER_VERSION_EPOCH_3_0 - }, - ]; -} - -/// Produce a mock bitcoin block that is descended from `parent_snapshot` and includes -/// `ops`. This method uses `miner_pkh` to set the inputs and outputs of any supplied -/// block commits or leader key registrations -fn make_burn_block( - parent_snapshot: &BlockSnapshot, - miner_pkh: &Hash160, - ops: Vec, -) -> Result { - let block_height = parent_snapshot.block_height + 1; - let mut mock_burn_hash_contents = [0u8; 32]; - mock_burn_hash_contents[0..8].copy_from_slice((block_height + 1).to_be_bytes().as_ref()); - - let txs = ops.into_iter().map(|op| { - let mut data = match &op { - BlockstackOperationType::LeaderKeyRegister(op) => op.serialize_to_vec(), - BlockstackOperationType::LeaderBlockCommit(op) => op.serialize_to_vec(), - _ => panic!("Attempted to mock unexpected blockstack operation."), - }; - - data.remove(0); - - let (inputs, outputs) = if let BlockstackOperationType::LeaderBlockCommit(ref op) = op { - let burn_output = BitcoinTxOutput { - units: op.burn_fee, - address: BitcoinAddress::Legacy(LegacyBitcoinAddress { - addrtype: LegacyBitcoinAddressType::PublicKeyHash, - network_id: BitcoinNetworkType::Testnet, - bytes: Hash160([0; 20]), - }), - }; - - let change_output = BitcoinTxOutput { - units: 1_000_000_000_000, - address: BitcoinAddress::Legacy(LegacyBitcoinAddress { - addrtype: LegacyBitcoinAddressType::PublicKeyHash, - network_id: BitcoinNetworkType::Testnet, - bytes: miner_pkh.clone(), - }), - }; - - let tx_ref = (parent_snapshot.winning_block_txid.clone(), 3); - - let input = BitcoinTxInputStructured { - keys: vec![], - num_required: 0, - in_type: BitcoinInputType::Standard, - tx_ref, - }; - - ( - vec![input.into()], - vec![burn_output.clone(), burn_output, change_output], - ) - } else { - ( - vec![BitcoinTxInputStructured { - keys: vec![], - num_required: 0, - in_type: BitcoinInputType::Standard, - tx_ref: (Txid([0; 32]), 0), - } - .into()], - vec![BitcoinTxOutput { - units: 1_000_000_000_000, - address: BitcoinAddress::Legacy(LegacyBitcoinAddress { - addrtype: LegacyBitcoinAddressType::PublicKeyHash, - network_id: BitcoinNetworkType::Testnet, - bytes: miner_pkh.clone(), - }), - }], - ) - }; - - BitcoinTransaction { - txid: op.txid(), - vtxindex: op.vtxindex(), - opcode: op.opcode() as u8, - data, - data_amt: 0, - inputs, - outputs, - } - }); - - Ok(BitcoinBlock { - block_height, - block_hash: BurnchainHeaderHash(mock_burn_hash_contents), - parent_block_hash: parent_snapshot.burn_header_hash.clone(), - txs: txs.collect(), - timestamp: 100 * u64::from(block_height + 1), - }) -} - -/// This struct wraps all the state required for operating a -/// stacks-node in `mockamoto` mode. -/// -/// This mode of operation is a single-node network in which bitcoin -/// blocks are simulated: no `bitcoind` is communicated with (either -/// operating as regtest, testnet or mainnet). This operation mode -/// is useful for testing the stacks-only operation of Nakamoto. -/// -/// During operation, the mockamoto node issues `stack-stx` and -/// `stack-extend` contract-calls to ensure that the miner is a member -/// of the current stacking set. This ensures nakamoto blocks can be -/// produced with tenure change txs. -/// -pub struct MockamotoNode { - sortdb: SortitionDB, - mempool: MemPoolDB, - chainstate: StacksChainState, - self_signer: TestSigners, - miner_key: StacksPrivateKey, - vrf_key: VRFPrivateKey, - relay_rcv: Option>, - coord_rcv: Option, - dispatcher: EventDispatcher, - pub globals: Globals, - config: Config, -} - -struct MockamotoBlockBuilder { - txs: Vec, - bytes_so_far: u64, -} - -/// This struct is used by mockamoto to pass the burnchain indexer -/// parameter to the `ChainsCoordinator`. It errors on every -/// invocation except `read_burnchain_headers`. -/// -/// The `ChainsCoordinator` only uses this indexer for evaluating -/// affirmation maps, which should never be evaluated in mockamoto. -/// This is passed to the Burnchain DB block processor, though, which -/// requires `read_burnchain_headers` (to generate affirmation maps) -struct MockBurnchainIndexer(BurnchainDB); - -impl BurnchainHeaderReader for MockBurnchainIndexer { - fn read_burnchain_headers( - &self, - start_height: u64, - end_height: u64, - ) -> Result, DBError> { - let mut output = vec![]; - for i in start_height..end_height { - let header = BurnchainDB::get_burnchain_header(self.0.conn(), i) - .map_err(|e| DBError::Other(e.to_string()))? - .ok_or_else(|| DBError::NotFoundError)?; - output.push(header); - } - Ok(output) - } - fn get_burnchain_headers_height(&self) -> Result { - Err(DBError::NoDBError) - } - fn find_burnchain_header_height( - &self, - _header_hash: &BurnchainHeaderHash, - ) -> Result, DBError> { - Err(DBError::NoDBError) - } -} - -impl BlockBuilder for MockamotoBlockBuilder { - fn try_mine_tx_with_len( - &mut self, - clarity_tx: &mut ClarityTx, - tx: &StacksTransaction, - tx_len: u64, - limit_behavior: &BlockLimitFunction, - ast_rules: ASTRules, - ) -> TransactionResult { - if self.bytes_so_far + tx_len >= MAX_EPOCH_SIZE.into() { - return TransactionResult::skipped(tx, "BlockSizeLimit".into()); - } - - if BlockLimitFunction::NO_LIMIT_HIT != *limit_behavior { - return TransactionResult::skipped(tx, "LimitReached".into()); - } - - let (fee, receipt) = match StacksChainState::process_transaction( - clarity_tx, tx, true, ast_rules, - ) { - Ok(x) => x, - Err(ChainstateError::CostOverflowError(cost_before, cost_after, total_budget)) => { - clarity_tx.reset_cost(cost_before.clone()); - if total_budget.proportion_largest_dimension(&cost_before) - < TX_BLOCK_LIMIT_PROPORTION_HEURISTIC - { - warn!( - "Transaction {} consumed over {}% of block budget, marking as invalid; budget was {}", - tx.txid(), - 100 - TX_BLOCK_LIMIT_PROPORTION_HEURISTIC, - &total_budget - ); - return TransactionResult::error(&tx, ChainstateError::TransactionTooBigError); - } else { - warn!( - "Transaction {} reached block cost {}; budget was {}", - tx.txid(), - &cost_after, - &total_budget - ); - return TransactionResult::skipped_due_to_error( - &tx, - ChainstateError::BlockTooBigError, - ); - } - } - Err(e) => return TransactionResult::error(&tx, e), - }; - - info!("Include tx"; - "tx" => %tx.txid(), - "payload" => tx.payload.name(), - "origin" => %tx.origin_address()); - - self.txs.push(tx.clone()); - self.bytes_so_far += tx_len; - - TransactionResult::success(tx, fee, receipt) - } -} - -impl MockamotoNode { - pub fn new(config: &Config) -> Result { - let miner_key = config - .miner - .mining_key - .clone() - .ok_or("Mockamoto node must be configured with `miner.mining_key`")?; - let vrf_key = VRFPrivateKey::new(); - - let stacker_pk = Secp256k1PublicKey::from_private(&miner_key); - let stacker_pk_hash = Hash160::from_node_public_key(&stacker_pk); - - let stacker = StacksAddress { - version: C32_ADDRESS_VERSION_TESTNET_SINGLESIG, - bytes: stacker_pk_hash, - }; - - let burnchain = config.get_burnchain(); - let (sortdb, _burndb) = burnchain - .connect_db( - true, - BurnchainHeaderHash([0; 32]), - 100, - STACKS_EPOCHS_MOCKAMOTO.to_vec(), - ) - .map_err(|e| e.to_string())?; - - let mut initial_balances: Vec<_> = config - .initial_balances - .iter() - .map(|balance| (balance.address.clone(), balance.amount)) - .collect(); - - initial_balances.push((stacker.into(), 100_000_000_000_000)); - - // Create a boot contract to initialize the aggregate public key prior to Pox-4 activation - let self_signer = TestSigners::default(); - let agg_pub_key = self_signer.aggregate_public_key.clone(); - info!("Mockamoto node setting agg public key"; "agg_pub_key" => %to_hex(&self_signer.aggregate_public_key.compress().data)); - let callback = move |clarity_tx: &mut ClarityTx| { - NakamotoChainState::aggregate_public_key_bootcode(clarity_tx, &agg_pub_key); - }; - let mut boot_data = - ChainStateBootData::new(&burnchain, initial_balances, Some(Box::new(callback))); - let (chainstate, boot_receipts) = StacksChainState::open_and_exec( - config.is_mainnet(), - config.burnchain.chain_id, - &config.get_chainstate_path_str(), - Some(&mut boot_data), - Some(config.node.get_marf_opts()), - ) - .unwrap(); - let mempool = PeerThread::connect_mempool_db(config); - - let (coord_rcv, coord_comms) = CoordinatorCommunication::instantiate(); - let miner_status = Arc::new(Mutex::new(MinerStatus::make_ready(100))); - let (relay_send, relay_rcv) = sync_channel(10); - let counters = Counters::new(); - let should_keep_running = Arc::new(AtomicBool::new(true)); - let sync_comms = PoxSyncWatchdogComms::new(should_keep_running.clone()); - - let globals = Globals::new( - coord_comms, - miner_status, - relay_send, - counters, - sync_comms, - should_keep_running, - 0, - ); - - let mut event_dispatcher = EventDispatcher::new(); - for observer in config.events_observers.iter() { - event_dispatcher.register_observer(observer); - } - - crate::run_loop::announce_boot_receipts( - &mut event_dispatcher, - &chainstate, - &burnchain.pox_constants, - &boot_receipts, - ); - - Ok(MockamotoNode { - sortdb, - self_signer, - chainstate, - miner_key, - vrf_key, - relay_rcv: Some(relay_rcv), - coord_rcv: Some(coord_rcv), - dispatcher: event_dispatcher, - mempool, - globals, - config: config.clone(), - }) - } - - fn spawn_chains_coordinator(&mut self) -> JoinHandle<()> { - let config = self.config.clone(); - let atlas_config = AtlasConfig::new(false); - - let (chainstate, _) = self.chainstate.reopen().unwrap(); - let coord_config = ChainsCoordinatorConfig { - always_use_affirmation_maps: false, - require_affirmed_anchor_blocks: false, - ..ChainsCoordinatorConfig::new() - }; - let mut dispatcher = self.dispatcher.clone(); - let burnchain = self.config.get_burnchain(); - let burndb = burnchain.open_burnchain_db(true).unwrap(); - let coordinator_indexer = MockBurnchainIndexer(burndb); - let atlas_db = AtlasDB::connect( - atlas_config.clone(), - &self.config.get_atlas_db_file_path(), - true, - ) - .unwrap(); - let miner_status = Arc::new(Mutex::new(MinerStatus::make_ready(100))); - let coordinator_receivers = self.coord_rcv.take().unwrap(); - - thread::Builder::new() - .name(format!("chains-coordinator-{}", &config.node.rpc_bind)) - .stack_size(BLOCK_PROCESSOR_STACK_SIZE) - .spawn(move || { - debug!( - "chains-coordinator thread ID is {:?}", - thread::current().id() - ); - ChainsCoordinator::run( - coord_config, - chainstate, - burnchain, - &mut dispatcher, - coordinator_receivers, - atlas_config, - Some(&mut ()), - Some(&mut ()), - miner_status, - coordinator_indexer, - atlas_db, - ); - }) - .expect("FATAL: failed to start chains coordinator thread") - } - - pub fn run(&mut self) { - info!("Starting the mockamoto node by issuing initial empty mock burn blocks"); - let coordinator = self.spawn_chains_coordinator(); - - self.produce_burnchain_block(true).unwrap(); - self.produce_burnchain_block(true).unwrap(); - self.produce_burnchain_block(true).unwrap(); - self.produce_burnchain_block(true).unwrap(); - self.produce_burnchain_block(true).unwrap(); - self.produce_burnchain_block(true).unwrap(); - - let mut p2p_net = StacksNode::setup_peer_network( - &self.config, - &self.config.atlas, - self.config.get_burnchain(), - ); - - let stackerdbs = StackerDBs::connect(&self.config.get_stacker_db_file_path(), true) - .expect("FATAL: failed to connect to stacker DB"); - - let _relayer = Relayer::from_p2p(&mut p2p_net, stackerdbs); - - let relayer_rcv = self.relay_rcv.take().unwrap(); - let relayer_globals = self.globals.clone(); - let mock_relayer_thread = thread::Builder::new() - .name("mock-relayer".into()) - .spawn(move || { - while relayer_globals.keep_running() { - match relayer_rcv.recv_timeout(Duration::from_millis(500)) { - Ok(dir) => { - if let RelayerDirective::Exit = dir { - break; - } - } - Err(RecvTimeoutError::Timeout) => continue, - Err(e) => { - warn!("Error accepting relayer directive: {e:?}"); - break; - } - } - } - }) - .expect("FATAL: failed to start mock relayer thread"); - - let peer_thread = PeerThread::new_all( - self.globals.clone(), - &self.config, - self.config.get_burnchain().pox_constants, - p2p_net, - ); - - let ev_dispatcher = self.dispatcher.clone(); - let peer_thread = thread::Builder::new() - .stack_size(BLOCK_PROCESSOR_STACK_SIZE) - .name("p2p".into()) - .spawn(move || { - StacksNode::p2p_main(peer_thread, ev_dispatcher); - }) - .expect("FATAL: failed to start p2p thread"); - - while self.globals.keep_running() { - self.produce_burnchain_block(false).unwrap(); - let expected_chain_length = self.mine_and_stage_block().unwrap(); - self.globals.coord().announce_new_stacks_block(); - let _ = self.wait_for_stacks_block(expected_chain_length); - sleep(Duration::from_millis(self.config.node.mockamoto_time_ms)); - } - - self.globals.coord().stop_chains_coordinator(); - - if let Err(e) = coordinator.join() { - warn!("Error joining coordinator thread during shutdown: {e:?}"); - } - if let Err(e) = mock_relayer_thread.join() { - warn!("Error joining coordinator thread during shutdown: {e:?}"); - } - if let Err(e) = peer_thread.join() { - warn!("Error joining p2p thread during shutdown: {e:?}"); - } - } - - fn wait_for_stacks_block(&mut self, expected_length: u64) -> Result<(), ChainstateError> { - while self.globals.keep_running() { - let chain_length = match NakamotoChainState::get_canonical_block_header( - self.chainstate.db(), - &self.sortdb, - ) { - Ok(Some(chain_tip)) => chain_tip.stacks_block_height, - Ok(None) | Err(ChainstateError::NoSuchBlockError) => 0, - Err(e) => return Err(e), - }; - if chain_length >= expected_length { - return Ok(()); - } - sleep(Duration::from_millis(100)); - } - Err(ChainstateError::NoSuchBlockError) - } - - fn produce_burnchain_block(&mut self, initializing: bool) -> Result<(), BurnchainError> { - let miner_pk = Secp256k1PublicKey::from_private(&self.miner_key); - let miner_pk_hash = Hash160::from_node_public_key(&miner_pk); - - let parent_snapshot = SortitionDB::get_canonical_burn_chain_tip(&self.sortdb.conn())?; - info!("Mocking bitcoin block"; "parent_height" => parent_snapshot.block_height); - let burn_height = parent_snapshot.block_height + 1; - - let mut ops = vec![]; - - if burn_height == 1 { - let mut txid = [2u8; 32]; - txid[0..8].copy_from_slice((burn_height + 1).to_be_bytes().as_ref()); - let key_register = LeaderKeyRegisterOp { - consensus_hash: ConsensusHash([0; 20]), - public_key: VRFPublicKey::from_private(&self.vrf_key), - memo: miner_pk_hash.as_bytes().to_vec(), - txid: Txid(txid), - vtxindex: 0, - block_height: burn_height, - burn_header_hash: BurnchainHeaderHash([0; 32]), - }; - ops.push(BlockstackOperationType::LeaderKeyRegister(key_register)); - } else if !initializing { - let mut txid = [1u8; 32]; - txid[0..8].copy_from_slice((burn_height + 1).to_be_bytes().as_ref()); - txid[8..16].copy_from_slice((0u64).to_be_bytes().as_ref()); - - let (parent_block_ptr, parent_vtxindex) = - if parent_snapshot.winning_block_txid.as_bytes() == &[0; 32] { - (0, 0) - } else { - (parent_snapshot.block_height.try_into().unwrap(), 0) - }; - - let parent_vrf_proof = NakamotoChainState::get_block_vrf_proof( - self.chainstate.db(), - &parent_snapshot.consensus_hash, - ) - .map_err(|_e| BurnchainError::MissingParentBlock)? - .unwrap_or_else(|| VRFProof::empty()); - - let vrf_seed = VRFSeed::from_proof(&parent_vrf_proof); - let parent_block_id = parent_snapshot.get_canonical_stacks_block_id(); - - let block_commit = LeaderBlockCommitOp { - block_header_hash: BlockHeaderHash(parent_block_id.0), - new_seed: vrf_seed, - parent_block_ptr, - parent_vtxindex, - key_block_ptr: 1, - key_vtxindex: 0, - memo: vec![STACKS_EPOCH_3_0_MARKER], - burn_fee: 5000, - input: (parent_snapshot.winning_block_txid.clone(), 3), - burn_parent_modulus: u8::try_from( - parent_snapshot.block_height % BURN_BLOCK_MINED_AT_MODULUS, - ) - .unwrap(), - apparent_sender: BurnchainSigner(miner_pk_hash.to_string()), - commit_outs: vec![ - PoxAddress::Standard(StacksAddress::burn_address(false), None), - PoxAddress::Standard(StacksAddress::burn_address(false), None), - ], - sunset_burn: 0, - txid: Txid(txid), - vtxindex: 0, - block_height: burn_height, - burn_header_hash: BurnchainHeaderHash([0; 32]), - }; - ops.push(BlockstackOperationType::LeaderBlockCommit(block_commit)) - } - - let new_burn_block = make_burn_block(&parent_snapshot, &miner_pk_hash, ops)?; - - let burnchain = self.config.get_burnchain(); - let burndb = burnchain.open_burnchain_db(true).unwrap(); - let indexer = MockBurnchainIndexer(burndb); - let mut burndb = burnchain.open_burnchain_db(true).unwrap(); - - burndb.store_new_burnchain_block( - &burnchain, - &indexer, - &BurnchainBlock::Bitcoin(new_burn_block), - StacksEpochId::Epoch30, - )?; - - self.globals.coord().announce_new_burn_block(); - let mut cur_snapshot = SortitionDB::get_canonical_burn_chain_tip(&self.sortdb.conn())?; - while cur_snapshot.burn_header_hash == parent_snapshot.burn_header_hash { - thread::sleep(Duration::from_millis(100)); - cur_snapshot = SortitionDB::get_canonical_burn_chain_tip(&self.sortdb.conn())?; - } - - Ok(()) - } - - fn mine_stacks_block(&mut self) -> Result { - let miner_principal = StacksAddress::from_public_keys( - C32_ADDRESS_VERSION_TESTNET_SINGLESIG, - &AddressHashMode::SerializeP2PKH, - 1, - &vec![Secp256k1PublicKey::from_private(&self.miner_key)], - ) - .unwrap() - .into(); - let sortition_tip = SortitionDB::get_canonical_burn_chain_tip(self.sortdb.conn())?; - let chain_id = self.chainstate.chain_id; - let (mut chainstate_tx, clarity_instance) = self.chainstate.chainstate_tx_begin().unwrap(); - - let (is_genesis, chain_tip_bh, chain_tip_ch) = - match NakamotoChainState::get_canonical_block_header(&chainstate_tx, &self.sortdb) { - Ok(Some(chain_tip)) => ( - false, - chain_tip.anchored_header.block_hash(), - chain_tip.consensus_hash, - ), - Ok(None) | Err(ChainstateError::NoSuchBlockError) => - // No stacks tip yet, parent should be genesis - { - ( - true, - FIRST_STACKS_BLOCK_HASH, - FIRST_BURNCHAIN_CONSENSUS_HASH, - ) - } - Err(e) => return Err(e), - }; - - let parent_block_id = StacksBlockId::new(&chain_tip_ch, &chain_tip_bh); - - let (parent_chain_length, parent_burn_height) = if is_genesis { - (0, 0) - } else { - let tip_info = NakamotoChainState::get_block_header(&chainstate_tx, &parent_block_id)? - .ok_or(ChainstateError::NoSuchBlockError)?; - (tip_info.stacks_block_height, tip_info.burn_header_height) - }; - - let miner_nonce = if is_genesis { - 0 - } else { - let sortdb_conn = self.sortdb.index_conn(); - let mut clarity_conn = clarity_instance.read_only_connection_checked( - &parent_block_id, - &chainstate_tx, - &sortdb_conn, - )?; - StacksChainState::get_nonce(&mut clarity_conn, &miner_principal) - }; - - info!( - "Mining block"; "parent_chain_length" => parent_chain_length, "chain_tip_bh" => %chain_tip_bh, - "chain_tip_ch" => %chain_tip_ch, "miner_account" => %miner_principal, "miner_nonce" => %miner_nonce, - ); - - let vrf_proof = VRF::prove(&self.vrf_key, sortition_tip.sortition_hash.as_bytes()); - let coinbase_tx_payload = - TransactionPayload::Coinbase(CoinbasePayload([1; 32]), None, Some(vrf_proof)); - let mut coinbase_tx = StacksTransaction::new( - TransactionVersion::Testnet, - TransactionAuth::from_p2pkh(&self.miner_key).unwrap(), - coinbase_tx_payload, - ); - coinbase_tx.chain_id = chain_id; - coinbase_tx.set_origin_nonce(miner_nonce + 1); - let mut coinbase_tx_signer = StacksTransactionSigner::new(&coinbase_tx); - coinbase_tx_signer.sign_origin(&self.miner_key).unwrap(); - let coinbase_tx = coinbase_tx_signer.get_tx().unwrap(); - - let miner_pk = Secp256k1PublicKey::from_private(&self.miner_key); - let miner_pk_hash = Hash160::from_node_public_key(&miner_pk); - - // Add a tenure change transaction to the block: - // as of now every mockamoto block is a tenure-change. - // If mockamoto mode changes to support non-tenure-changing blocks, this will have - // to be gated. - let tenure_change_tx_payload = TransactionPayload::TenureChange(TenureChangePayload { - tenure_consensus_hash: sortition_tip.consensus_hash.clone(), - prev_tenure_consensus_hash: chain_tip_ch.clone(), - burn_view_consensus_hash: sortition_tip.consensus_hash, - previous_tenure_end: parent_block_id, - previous_tenure_blocks: 1, - cause: TenureChangeCause::BlockFound, - pubkey_hash: miner_pk_hash, - }); - let mut tenure_tx = StacksTransaction::new( - TransactionVersion::Testnet, - TransactionAuth::from_p2pkh(&self.miner_key).unwrap(), - tenure_change_tx_payload, - ); - tenure_tx.chain_id = chain_id; - tenure_tx.set_origin_nonce(miner_nonce); - let mut tenure_tx_signer = StacksTransactionSigner::new(&tenure_tx); - tenure_tx_signer.sign_origin(&self.miner_key).unwrap(); - let tenure_tx = tenure_tx_signer.get_tx().unwrap(); - - let pox_address = PoxAddress::Standard( - StacksAddress::burn_address(false), - Some(AddressHashMode::SerializeP2PKH), - ); - - let signer_sk = Secp256k1PrivateKey::from_seed(&[1, 2, 3, 4]); - let signer_key = Secp256k1PublicKey::from_private(&signer_sk).to_bytes_compressed(); - let signer_addr = StacksAddress::from_public_keys( - C32_ADDRESS_VERSION_TESTNET_SINGLESIG, - &AddressHashMode::SerializeP2PKH, - 1, - &vec![Secp256k1PublicKey::from_private(&signer_sk)], - ) - .unwrap() - .into(); - - let block_height = sortition_tip.block_height; - let reward_cycle = self - .sortdb - .pox_constants - .block_height_to_reward_cycle(self.sortdb.first_block_height, block_height) - .unwrap(); - - let stack_stx_payload = if parent_chain_length < 2 { - let signature = make_pox_4_signer_key_signature( - &pox_address, - &signer_sk, - reward_cycle.into(), - &Pox4SignatureTopic::StackStx, - CHAIN_ID_TESTNET, - 12_u128, - ) - .unwrap() - .to_rsv(); - TransactionPayload::ContractCall(TransactionContractCall { - address: StacksAddress::burn_address(false), - contract_name: "pox-4".try_into().unwrap(), - function_name: "stack-stx".try_into().unwrap(), - function_args: vec![ - ClarityValue::UInt(99_000_000_000_000), - pox_address.as_clarity_tuple().unwrap().into(), - ClarityValue::UInt(u128::from(parent_burn_height)), - ClarityValue::UInt(12), - ClarityValue::some(ClarityValue::buff_from(signature).unwrap()).unwrap(), - ClarityValue::buff_from(signer_key).unwrap(), - ], - }) - } else { - let signature = make_pox_4_signer_key_signature( - &pox_address, - &signer_sk, - reward_cycle.into(), - &Pox4SignatureTopic::StackExtend, - CHAIN_ID_TESTNET, - 5_u128, - ) - .unwrap() - .to_rsv(); - // NOTE: stack-extend doesn't currently work, because the PoX-4 lockup - // special functions have not been implemented. - TransactionPayload::ContractCall(TransactionContractCall { - address: StacksAddress::burn_address(false), - contract_name: "pox-4".try_into().unwrap(), - function_name: "stack-extend".try_into().unwrap(), - function_args: vec![ - ClarityValue::UInt(5), - pox_address.as_clarity_tuple().unwrap().into(), - ClarityValue::some(ClarityValue::buff_from(signature).unwrap()).unwrap(), - ClarityValue::buff_from(signer_key).unwrap(), - ], - }) - }; - let mut stack_stx_tx = StacksTransaction::new( - TransactionVersion::Testnet, - TransactionAuth::from_p2pkh(&self.miner_key).unwrap(), - stack_stx_payload, - ); - stack_stx_tx.chain_id = chain_id; - stack_stx_tx.set_origin_nonce(miner_nonce + 2); - let mut stack_stx_tx_signer = StacksTransactionSigner::new(&stack_stx_tx); - stack_stx_tx_signer.sign_origin(&self.miner_key).unwrap(); - let stacks_stx_tx = stack_stx_tx_signer.get_tx().unwrap(); - - let signer_nonce = if is_genesis { - 0 - } else { - let sortdb_conn = self.sortdb.index_conn(); - let mut clarity_conn = clarity_instance.read_only_connection_checked( - &parent_block_id, - &chainstate_tx, - &sortdb_conn, - )?; - StacksChainState::get_nonce(&mut clarity_conn, &signer_addr) - }; - let mut next_signer = self.self_signer.clone(); - let next_agg_key = next_signer.generate_aggregate_key(reward_cycle + 1); - let aggregate_public_key_val = - ClarityValue::buff_from(next_agg_key.compress().data.to_vec()) - .expect("Failed to serialize aggregate public key"); - let vote_payload = TransactionPayload::new_contract_call( - boot_code_addr(false), - SIGNERS_VOTING_NAME, - "vote-for-aggregate-public-key", - vec![ - ClarityValue::UInt(0), - aggregate_public_key_val, - ClarityValue::UInt(0), - ClarityValue::UInt((reward_cycle + 1).into()), - ], - ) - .unwrap(); - let mut vote_tx = StacksTransaction::new( - TransactionVersion::Testnet, - TransactionAuth::from_p2pkh(&signer_sk).unwrap(), - vote_payload, - ); - vote_tx.chain_id = chain_id; - vote_tx.set_origin_nonce(signer_nonce); - let mut vote_tx_signer = StacksTransactionSigner::new(&vote_tx); - vote_tx_signer.sign_origin(&signer_sk).unwrap(); - let vote_tx = vote_tx_signer.get_tx().unwrap(); - - let sortdb_handle = self.sortdb.index_conn(); - let SetupBlockResult { - mut clarity_tx, - matured_miner_rewards_opt, - .. - } = NakamotoChainState::setup_block( - &mut chainstate_tx, - clarity_instance, - &sortdb_handle, - self.sortdb.first_block_height, - &self.sortdb.pox_constants, - chain_tip_ch.clone(), - chain_tip_bh.clone(), - parent_chain_length, - parent_burn_height, - sortition_tip.burn_header_hash.clone(), - sortition_tip.block_height.try_into().map_err(|_| { - ChainstateError::InvalidStacksBlock("Burn block height exceeded u32".into()) - })?, - true, - parent_chain_length + 1, - false, - )?; - - let txs = vec![tenure_tx, coinbase_tx, stacks_stx_tx, vote_tx]; - - let _ = match StacksChainState::process_block_transactions( - &mut clarity_tx, - &txs, - 0, - ASTRules::PrecheckSize, - ) { - Err(e) => { - let msg = format!("Mined invalid stacks block {e:?}"); - warn!("{msg}"); - - clarity_tx.rollback_block(); - return Err(ChainstateError::InvalidStacksBlock(msg)); - } - Ok((block_fees, _block_burns, txs_receipts)) => (block_fees, txs_receipts), - }; - - let bytes_so_far = txs.iter().map(|tx| tx.tx_len()).sum(); - let mut builder = MockamotoBlockBuilder { txs, bytes_so_far }; - let _ = match StacksBlockBuilder::select_and_apply_transactions( - &mut clarity_tx, - &mut builder, - &mut self.mempool, - parent_chain_length, - &[], - BlockBuilderSettings { - max_miner_time_ms: 15_000, - mempool_settings: MemPoolWalkSettings::default(), - miner_status: Arc::new(Mutex::new(MinerStatus::make_ready(10000))), - }, - None, - ASTRules::PrecheckSize, - ) { - Ok(x) => x, - Err(e) => { - let msg = format!("Mined invalid stacks block {e:?}"); - warn!("{msg}"); - - clarity_tx.rollback_block(); - return Err(ChainstateError::InvalidStacksBlock(msg)); - } - }; - - let _lockup_events = match NakamotoChainState::finish_block( - &mut clarity_tx, - matured_miner_rewards_opt.as_ref(), - ) { - Err(ChainstateError::InvalidStacksBlock(e)) => { - clarity_tx.rollback_block(); - return Err(ChainstateError::InvalidStacksBlock(e)); - } - Err(e) => return Err(e), - Ok(lockup_events) => lockup_events, - }; - - let state_index_root = clarity_tx.seal(); - let tx_merkle_tree: MerkleTree = builder.txs.iter().collect(); - clarity_tx - .commit_mined_block(&StacksBlockId::new( - &MINER_BLOCK_CONSENSUS_HASH, - &MINER_BLOCK_HEADER_HASH, - )) - .unwrap(); - chainstate_tx.commit().unwrap(); - - let mut block = NakamotoBlock { - header: NakamotoBlockHeader { - version: 100, - chain_length: parent_chain_length + 1, - burn_spent: sortition_tip.total_burn, - tx_merkle_root: tx_merkle_tree.root(), - state_index_root, - signer_signature: ThresholdSignature::empty(), - miner_signature: MessageSignature::empty(), - consensus_hash: sortition_tip.consensus_hash.clone(), - parent_block_id: StacksBlockId::new(&chain_tip_ch, &chain_tip_bh), - signer_bitvec: BitVec::zeros(1) - .expect("BUG: bitvec of length-1 failed to construct"), - }, - txs: builder.txs, - }; - - let miner_signature = self - .miner_key - .sign(block.header.miner_signature_hash().as_bytes()) - .unwrap(); - - block.header.miner_signature = miner_signature; - - Ok(block) - } - - fn mine_and_stage_block(&mut self) -> Result { - let mut block = self.mine_stacks_block()?; - let config = self.chainstate.config(); - let chain_length = block.header.chain_length; - let mut sortition_handle = self.sortdb.index_handle_at_tip(); - let burn_tip = SortitionDB::get_canonical_burn_chain_tip(&self.sortdb.conn())?; - let cycle = self - .sortdb - .pox_constants - .block_height_to_reward_cycle(self.sortdb.first_block_height, burn_tip.block_height) - .unwrap(); - self.self_signer.sign_nakamoto_block(&mut block, cycle); - - let aggregate_public_key = if chain_length <= 1 { - self.self_signer.aggregate_public_key - } else { - let aggregate_public_key = NakamotoChainState::get_aggregate_public_key( - &mut self.chainstate, - &self.sortdb, - &sortition_handle, - &block, - )?; - aggregate_public_key - }; - let (headers_conn, staging_tx) = self.chainstate.headers_conn_and_staging_tx_begin()?; - NakamotoChainState::accept_block( - &config, - block, - &mut sortition_handle, - &staging_tx, - headers_conn, - &aggregate_public_key, - )?; - staging_tx.commit()?; - Ok(chain_length) - } -} diff --git a/testnet/stacks-node/src/mockamoto/tests.rs b/testnet/stacks-node/src/mockamoto/tests.rs deleted file mode 100644 index cbbed7607..000000000 --- a/testnet/stacks-node/src/mockamoto/tests.rs +++ /dev/null @@ -1,414 +0,0 @@ -use std::thread; -use std::time::{Duration, Instant}; - -use clarity::vm::costs::ExecutionCost; -use stacks::chainstate::burn::db::sortdb::SortitionDB; -use stacks::chainstate::nakamoto::NakamotoChainState; -use stacks::chainstate::stacks::db::StacksChainState; -use stacks_common::types::chainstate::{StacksAddress, StacksPrivateKey}; -use stacks_common::types::net::PeerAddress; -use stacks_common::types::StacksEpochId; -use stacks_common::util::get_epoch_time_secs; -use stacks_common::util::hash::to_hex; - -use super::MockamotoNode; -use crate::config::{EventKeyType, EventObserverConfig}; -use crate::neon_node::PeerThread; -use crate::tests::neon_integrations::{get_pox_info, submit_tx, test_observer}; -use crate::tests::{make_stacks_transfer, to_addr}; -use crate::{Config, ConfigFile}; - -#[test] -fn observe_100_blocks() { - let mut conf = Config::from_config_file(ConfigFile::mockamoto()).unwrap(); - conf.node.working_dir = format!( - "/tmp/stacks-node-tests/mock_observe_100_blocks-{}", - get_epoch_time_secs() - ); - conf.node.rpc_bind = "127.0.0.1:19343".into(); - conf.node.p2p_bind = "127.0.0.1:19344".into(); - conf.connection_options.public_ip_address = Some((PeerAddress::from_ipv4(127, 0, 0, 1), 20443)); - conf.node.mockamoto_time_ms = 10; - - let submitter_sk = StacksPrivateKey::from_seed(&[1]); - let submitter_addr = to_addr(&submitter_sk); - conf.add_initial_balance(submitter_addr.to_string(), 1_000_000); - let recipient_addr = StacksAddress::burn_address(false).into(); - - let observer_port = 19300; - test_observer::spawn_at(observer_port); - conf.events_observers.insert(EventObserverConfig { - endpoint: format!("localhost:{observer_port}"), - events_keys: vec![EventKeyType::AnyEvent], - }); - - let mut mockamoto = MockamotoNode::new(&conf).unwrap(); - let globals = mockamoto.globals.clone(); - - let mut mempool = PeerThread::connect_mempool_db(&conf); - let (mut chainstate, _) = StacksChainState::open( - conf.is_mainnet(), - conf.burnchain.chain_id, - &conf.get_chainstate_path_str(), - None, - ) - .unwrap(); - let burnchain = conf.get_burnchain(); - let sortdb = burnchain.open_sortition_db(true).unwrap(); - - let start = Instant::now(); - - let node_thread = thread::Builder::new() - .name("mockamoto-main".into()) - .spawn(move || mockamoto.run()) - .expect("FATAL: failed to start mockamoto main thread"); - - // make a transfer tx to test that the mockamoto miner picks up txs from the mempool - let tx_fee = 200; - let transfer_tx = make_stacks_transfer(&submitter_sk, 0, tx_fee, &recipient_addr, 100); - let transfer_tx_hex = format!("0x{}", to_hex(&transfer_tx)); - - let mut sent_tx = false; - - // complete within 2 minutes or abort - let completed = loop { - if Instant::now().duration_since(start) > Duration::from_secs(120) { - break false; - } - let latest_block = test_observer::get_blocks().pop(); - thread::sleep(Duration::from_secs(1)); - let Some(ref latest_block) = latest_block else { - info!("No block observed yet!"); - continue; - }; - let stacks_block_height = latest_block.get("block_height").unwrap().as_u64().unwrap(); - info!("Block height observed: {stacks_block_height}"); - - if stacks_block_height >= 1 && !sent_tx { - let tip = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb) - .unwrap() - .unwrap(); - // Bypass admission checks - mempool - .submit_raw( - &mut chainstate, - &sortdb, - &tip.consensus_hash, - &tip.anchored_header.block_hash(), - transfer_tx.clone(), - &ExecutionCost::max_value(), - &StacksEpochId::Epoch30, - ) - .unwrap(); - - sent_tx = true; - } - - if stacks_block_height >= 100 { - break true; - } - }; - - globals.signal_stop(); - - node_thread - .join() - .expect("Failed to join node thread to exit"); - - let transfer_tx_included = test_observer::get_blocks() - .into_iter() - .find(|block_json| { - block_json["transactions"] - .as_array() - .unwrap() - .iter() - .find(|tx_json| tx_json["raw_tx"].as_str() == Some(&transfer_tx_hex)) - .is_some() - }) - .is_some(); - - assert!( - transfer_tx_included, - "Mockamoto node failed to include the transfer tx" - ); - - assert!( - completed, - "Mockamoto node failed to produce and announce 100 blocks before timeout" - ); -} - -#[test] -fn mempool_rpc_submit() { - let mut conf = Config::from_config_file(ConfigFile::mockamoto()).unwrap(); - conf.node.working_dir = format!( - "/tmp/stacks-node-tests/mempool_rpc_submit-{}", - get_epoch_time_secs() - ); - conf.node.rpc_bind = "127.0.0.1:19743".into(); - conf.node.p2p_bind = "127.0.0.1:19744".into(); - conf.node.mockamoto_time_ms = 10; - - let submitter_sk = StacksPrivateKey::from_seed(&[1]); - let submitter_addr = to_addr(&submitter_sk); - conf.add_initial_balance(submitter_addr.to_string(), 1_000); - let recipient_addr = StacksAddress::burn_address(false).into(); - - let observer_port = 19800; - test_observer::spawn_at(observer_port); - conf.events_observers.insert(EventObserverConfig { - endpoint: format!("localhost:{observer_port}"), - events_keys: vec![EventKeyType::AnyEvent], - }); - - let mut mockamoto = MockamotoNode::new(&conf).unwrap(); - let globals = mockamoto.globals.clone(); - - let http_origin = format!("http://{}", &conf.node.rpc_bind); - - let start = Instant::now(); - - let node_thread = thread::Builder::new() - .name("mockamoto-main".into()) - .spawn(move || mockamoto.run()) - .expect("FATAL: failed to start mockamoto main thread"); - - // make a transfer tx to test that the mockamoto miner picks up txs from the mempool - let tx_fee = 200; - let transfer_tx = make_stacks_transfer(&submitter_sk, 0, tx_fee, &recipient_addr, 100); - let transfer_tx_hex = format!("0x{}", to_hex(&transfer_tx)); - - let mut sent_tx = false; - - // complete within 2 minutes or abort - let completed = loop { - if Instant::now().duration_since(start) > Duration::from_secs(120) { - break false; - } - let latest_block = test_observer::get_blocks().pop(); - thread::sleep(Duration::from_secs(1)); - let Some(ref latest_block) = latest_block else { - info!("No block observed yet!"); - continue; - }; - let stacks_block_height = latest_block.get("block_height").unwrap().as_u64().unwrap(); - info!("Block height observed: {stacks_block_height}"); - - if stacks_block_height >= 1 && !sent_tx { - // Enforce admission checks by utilizing the RPC endpoint - submit_tx(&http_origin, &transfer_tx); - sent_tx = true; - } - - if stacks_block_height >= 100 { - break true; - } - }; - - globals.signal_stop(); - - node_thread - .join() - .expect("Failed to join node thread to exit"); - - let transfer_tx_included = test_observer::get_blocks() - .into_iter() - .find(|block_json| { - block_json["transactions"] - .as_array() - .unwrap() - .iter() - .find(|tx_json| tx_json["raw_tx"].as_str() == Some(&transfer_tx_hex)) - .is_some() - }) - .is_some(); - - assert!( - transfer_tx_included, - "Mockamoto node failed to include the transfer tx" - ); - - assert!( - completed, - "Mockamoto node failed to produce and announce 100 blocks before timeout" - ); -} - -#[test] -fn observe_set_aggregate_key() { - let mut conf = Config::from_config_file(ConfigFile::mockamoto()).unwrap(); - conf.node.mockamoto_time_ms = 10; - conf.node.p2p_bind = "127.0.0.1:20443".into(); - conf.node.rpc_bind = "127.0.0.1:20444".into(); - conf.connection_options.public_ip_address = Some((PeerAddress::from_ipv4(127, 0, 0, 1), 20443)); - - let submitter_sk = StacksPrivateKey::from_seed(&[1]); - let submitter_addr = to_addr(&submitter_sk); - conf.add_initial_balance(submitter_addr.to_string(), 1_000); - - test_observer::spawn(); - let observer_port = test_observer::EVENT_OBSERVER_PORT; - conf.events_observers.insert(EventObserverConfig { - endpoint: format!("localhost:{observer_port}"), - events_keys: vec![EventKeyType::AnyEvent], - }); - - let mut mockamoto = MockamotoNode::new(&conf).unwrap(); - let mut signer = mockamoto.self_signer.clone(); - - let globals = mockamoto.globals.clone(); - - StacksChainState::open( - conf.is_mainnet(), - conf.burnchain.chain_id, - &conf.get_chainstate_path_str(), - None, - ) - .unwrap(); - let sortition_tip = SortitionDB::get_canonical_burn_chain_tip(mockamoto.sortdb.conn()).unwrap(); - - let start = Instant::now(); - // Get the reward cycle of the sortition tip - let reward_cycle = mockamoto - .sortdb - .pox_constants - .block_height_to_reward_cycle( - mockamoto.sortdb.first_block_height, - sortition_tip.block_height, - ) - .unwrap_or_else(|| { - panic!( - "Failed to determine reward cycle of block height: {}", - sortition_tip.block_height - ) - }); - - // Get the aggregate public key of the original reward cycle to compare against - let expected_cur_key = signer.generate_aggregate_key(reward_cycle); - let expected_next_key = signer.generate_aggregate_key(reward_cycle + 1); - - let node_thread = thread::Builder::new() - .name("mockamoto-main".into()) - .spawn(move || { - mockamoto.run(); - let aggregate_key_block_header = NakamotoChainState::get_canonical_block_header( - mockamoto.chainstate.db(), - &mockamoto.sortdb, - ) - .unwrap() - .unwrap(); - // Get the aggregate public key of the original reward cycle - let orig_aggregate_key = mockamoto - .chainstate - .get_aggregate_public_key_pox_4( - &mockamoto.sortdb, - &aggregate_key_block_header.index_block_hash(), - reward_cycle, - ) - .unwrap(); - // Get the aggregate public key of the next reward cycle that we manually overwrote - let new_aggregate_key = mockamoto - .chainstate - .get_aggregate_public_key_pox_4( - &mockamoto.sortdb, - &aggregate_key_block_header.index_block_hash(), - reward_cycle + 1, - ) - .unwrap(); - (orig_aggregate_key, new_aggregate_key) - }) - .expect("FATAL: failed to start mockamoto main thread"); - - // complete within 5 seconds or abort (we are only observing one block) - let completed = loop { - if Instant::now().duration_since(start) > Duration::from_secs(120) { - break false; - } - let latest_block = test_observer::get_blocks().pop(); - thread::sleep(Duration::from_secs(1)); - let Some(ref latest_block) = latest_block else { - info!("No block observed yet!"); - continue; - }; - let stacks_block_height = latest_block.get("block_height").unwrap().as_u64().unwrap(); - info!("Block height observed: {stacks_block_height}"); - if stacks_block_height >= 100 { - break true; - } - }; - - globals.signal_stop(); - - let (orig_aggregate_key, new_aggregate_key) = node_thread - .join() - .expect("Failed to join node thread to exit"); - - assert!( - completed, - "Mockamoto node failed to produce and announce its block before timeout" - ); - - // Did we set and retrieve the aggregate key correctly? - assert_eq!(orig_aggregate_key.unwrap(), expected_cur_key); - assert_eq!(new_aggregate_key.unwrap(), expected_next_key); -} - -#[test] -fn rpc_pox_info() { - let mut conf = Config::from_config_file(ConfigFile::mockamoto()).unwrap(); - conf.node.mockamoto_time_ms = 10; - conf.node.rpc_bind = "127.0.0.1:19543".into(); - conf.node.p2p_bind = "127.0.0.1:19544".into(); - - let observer_port = 19500; - test_observer::spawn_at(observer_port); - conf.events_observers.insert(EventObserverConfig { - endpoint: format!("localhost:{observer_port}"), - events_keys: vec![EventKeyType::AnyEvent], - }); - - let mut mockamoto = MockamotoNode::new(&conf).unwrap(); - let globals = mockamoto.globals.clone(); - - let http_origin = format!("http://{}", &conf.node.rpc_bind); - - let start = Instant::now(); - - let node_thread = thread::Builder::new() - .name("mockamoto-main".into()) - .spawn(move || mockamoto.run()) - .expect("FATAL: failed to start mockamoto main thread"); - - // mine 5 blocks - let completed = loop { - // complete within 2 minutes or abort - if Instant::now().duration_since(start) > Duration::from_secs(120) { - break false; - } - let latest_block = test_observer::get_blocks().pop(); - thread::sleep(Duration::from_secs(1)); - let Some(ref latest_block) = latest_block else { - info!("No block observed yet!"); - continue; - }; - let stacks_block_height = latest_block.get("block_height").unwrap().as_u64().unwrap(); - info!("Block height observed: {stacks_block_height}"); - - if stacks_block_height >= 5 { - break true; - } - }; - - // fetch rpc poxinfo - let _pox_info = get_pox_info(&http_origin); - - globals.signal_stop(); - - assert!( - completed, - "Mockamoto node failed to produce and announce 100 blocks before timeout" - ); - node_thread - .join() - .expect("Failed to join node thread to exit"); -} diff --git a/testnet/stacks-node/src/nakamoto_node/miner.rs b/testnet/stacks-node/src/nakamoto_node/miner.rs index 1f4fb2326..3546e48b9 100644 --- a/testnet/stacks-node/src/nakamoto_node/miner.rs +++ b/testnet/stacks-node/src/nakamoto_node/miner.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation // Copyright (C) 2020-2023 Stacks Open Internet Foundation // @@ -13,24 +14,24 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::convert::TryFrom; use std::thread; use std::thread::JoinHandle; use std::time::{Duration, Instant}; use clarity::boot_util::boot_code_id; -use clarity::vm::types::PrincipalData; +use clarity::vm::clarity::ClarityConnection; +use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier}; use hashbrown::HashSet; use libsigner::{ BlockResponse, RejectCode, SignerMessage, SignerSession, StackerDBSession, BLOCK_MSG_ID, + TRANSACTIONS_MSG_ID, }; use stacks::burnchains::{Burnchain, BurnchainParameters}; use stacks::chainstate::burn::db::sortdb::SortitionDB; use stacks::chainstate::burn::{BlockSnapshot, ConsensusHash}; use stacks::chainstate::nakamoto::miner::{NakamotoBlockBuilder, NakamotoTenureInfo}; use stacks::chainstate::nakamoto::signer_set::NakamotoSigners; -use stacks::chainstate::nakamoto::test_signers::TestSigners; -use stacks::chainstate::nakamoto::{NakamotoBlock, NakamotoChainState}; +use stacks::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockVote, NakamotoChainState}; use stacks::chainstate::stacks::boot::MINERS_NAME; use stacks::chainstate::stacks::db::{StacksChainState, StacksHeaderInfo}; use stacks::chainstate::stacks::{ @@ -40,7 +41,7 @@ use stacks::chainstate::stacks::{ }; use stacks::core::FIRST_BURNCHAIN_CONSENSUS_HASH; use stacks::net::stackerdb::StackerDBs; -use stacks_common::codec::read_next; +use stacks_common::codec::{read_next, StacksMessageCodec}; use stacks_common::types::chainstate::{StacksAddress, StacksBlockId}; use stacks_common::types::{PrivateKey, StacksEpochId}; use stacks_common::util::hash::{Hash160, Sha512Trunc256Sum}; @@ -149,10 +150,6 @@ impl BlockMinerThread { let miners_contract_id = boot_code_id(MINERS_NAME, self.config.is_mainnet()); let stackerdbs = StackerDBs::connect(&self.config.get_stacker_db_file_path(), true) .expect("FATAL: failed to connect to stacker DB"); - let rpc_sock = self.config.node.rpc_bind.parse().expect(&format!( - "Failed to parse socket: {}", - &self.config.node.rpc_bind - )); let Some(miner_privkey) = self.config.miner.mining_key else { warn!("No mining key configured, cannot mine"); return; @@ -160,7 +157,7 @@ impl BlockMinerThread { // now, actually run this tenure loop { let new_block = loop { - match self.mine_block() { + match self.mine_block(&stackerdbs) { Ok(x) => break Some(x), Err(NakamotoNodeError::MiningFailure(ChainstateError::MinerAborted)) => { info!("Miner interrupted while mining, will try again"); @@ -203,7 +200,7 @@ impl BlockMinerThread { // Propose the block to the observing signers through the .miners stackerdb instance let miner_contract_id = boot_code_id(MINERS_NAME, self.config.is_mainnet()); let mut miners_stackerdb = - StackerDBSession::new(rpc_sock, miner_contract_id); + StackerDBSession::new(&self.config.node.rpc_bind, miner_contract_id); match miners_stackerdb.put_chunk(&chunk) { Ok(ack) => { info!("Proposed block to stackerdb: {ack:?}"); @@ -221,21 +218,14 @@ impl BlockMinerThread { warn!("Failed to propose block to stackerdb: {e:?}"); } } + self.globals.counters.bump_naka_proposed_blocks(); - if let Some(self_signer) = self.config.self_signing() { - if let Err(e) = self.self_sign_and_broadcast(self_signer, new_block.clone()) { - warn!("Error self-signing block: {e:?}"); - } else { - self.globals.coord().announce_new_stacks_block(); - } + if let Err(e) = + self.wait_for_signer_signature_and_broadcast(&stackerdbs, new_block.clone()) + { + warn!("Error broadcasting block: {e:?}"); } else { - if let Err(e) = - self.wait_for_signer_signature_and_broadcast(&stackerdbs, new_block.clone()) - { - warn!("Error broadcasting block: {e:?}"); - } else { - self.globals.coord().announce_new_stacks_block(); - } + self.globals.coord().announce_new_stacks_block(); } self.globals.counters.bump_naka_mined_blocks(); @@ -256,24 +246,19 @@ impl BlockMinerThread { } } - fn wait_for_signer_signature( + fn get_stackerdb_contract_and_slots( &self, stackerdbs: &StackerDBs, - aggregate_public_key: &Point, - signer_signature_hash: &Sha512Trunc256Sum, - ) -> Result { + msg_id: u32, + reward_cycle: u64, + ) -> Result<(QualifiedContractIdentifier, HashMap), NakamotoNodeError> { let stackerdb_contracts = stackerdbs .get_stackerdb_contract_ids() .expect("FATAL: could not get the stacker DB contract ids"); - let reward_cycle = self - .burnchain - .block_height_to_reward_cycle(self.burn_block.block_height) - .expect("FATAL: no reward cycle for burn block"); - let signers_contract_id = NakamotoSigners::make_signers_db_contract_id( reward_cycle, - BLOCK_MSG_ID, + msg_id, self.config.is_mainnet(), ); if !stackerdb_contracts.contains(&signers_contract_id) { @@ -281,21 +266,123 @@ impl BlockMinerThread { "No signers contract found, cannot wait for signers", )); }; - // Get the block slot for every signer - let slot_ids = stackerdbs + // Get the slots for every signer + let signers = stackerdbs + .get_signers(&signers_contract_id) + .expect("FATAL: could not get signers from stacker DB"); + let mut slot_ids_addresses = HashMap::with_capacity(signers.len()); + for (slot_id, address) in stackerdbs .get_signers(&signers_contract_id) .expect("FATAL: could not get signers from stacker DB") - .iter() + .into_iter() .enumerate() - .map(|(id, _)| { - u32::try_from(id).expect("FATAL: too many signers to fit into u32 range") - }) - .collect::>(); + { + slot_ids_addresses.insert( + u32::try_from(slot_id).expect("FATAL: too many signers to fit into u32 range"), + address, + ); + } + Ok((signers_contract_id, slot_ids_addresses)) + } + fn get_signer_transactions( + &self, + chainstate: &mut StacksChainState, + sortdb: &SortitionDB, + stackerdbs: &StackerDBs, + ) -> Result, NakamotoNodeError> { + let next_reward_cycle = self + .burnchain + .block_height_to_reward_cycle(self.burn_block.block_height) + .expect("FATAL: no reward cycle for burn block") + .wrapping_add(1); + let (signers_contract_id, slot_ids_addresses) = self.get_stackerdb_contract_and_slots( + stackerdbs, + TRANSACTIONS_MSG_ID, + next_reward_cycle, + )?; + let slot_ids = slot_ids_addresses.keys().cloned().collect::>(); + let addresses = slot_ids_addresses.values().cloned().collect::>(); + // Get the transactions from the signers for the next block + let signer_chunks = stackerdbs + .get_latest_chunks(&signers_contract_id, &slot_ids) + .expect("FATAL: could not get latest chunks from stacker DB"); + let signer_messages: Vec<(u32, SignerMessage)> = slot_ids + .iter() + .zip(signer_chunks.into_iter()) + .filter_map(|(slot_id, chunk)| { + chunk.and_then(|chunk| { + read_next::(&mut &chunk[..]) + .ok() + .map(|msg| (*slot_id, msg)) + }) + }) + .collect(); + + if signer_messages.is_empty() { + return Ok(vec![]); + } + + let (consensus_hash, block_bhh) = + SortitionDB::get_canonical_stacks_chain_tip_hash(sortdb.conn()).unwrap(); + let stacks_block_id = StacksBlockId::new(&consensus_hash, &block_bhh); + + // Get all nonces for the signers from clarity DB to use to validate transactions + let account_nonces = chainstate + .with_read_only_clarity_tx(&sortdb.index_conn(), &stacks_block_id, |clarity_tx| { + clarity_tx.with_clarity_db_readonly(|clarity_db| { + addresses + .iter() + .map(|address| { + ( + address.clone(), + clarity_db + .get_account_nonce(&address.clone().into()) + .unwrap_or(0), + ) + }) + .collect::>() + }) + }) + .unwrap_or_default(); + let mut filtered_transactions: HashMap = HashMap::new(); + for (_slot, signer_message) in signer_messages { + match signer_message { + SignerMessage::Transactions(transactions) => { + NakamotoSigners::update_filtered_transactions( + &mut filtered_transactions, + &account_nonces, + self.config.is_mainnet(), + transactions, + ) + } + _ => {} // Any other message is ignored + } + } + Ok(filtered_transactions.into_values().collect()) + } + + fn wait_for_signer_signature( + &self, + stackerdbs: &StackerDBs, + aggregate_public_key: &Point, + signer_signature_hash: &Sha512Trunc256Sum, + signer_weights: HashMap, + ) -> Result { + let reward_cycle = self + .burnchain + .block_height_to_reward_cycle(self.burn_block.block_height) + .expect("FATAL: no reward cycle for burn block"); + let (signers_contract_id, slot_ids_addresses) = + self.get_stackerdb_contract_and_slots(stackerdbs, BLOCK_MSG_ID, reward_cycle)?; + let slot_ids = slot_ids_addresses.keys().cloned().collect::>(); // If more than a threshold percentage of the signers reject the block, we should not wait any further - let rejection_threshold = slot_ids.len() / 10 * 7; + let weights: u64 = signer_weights.values().sum(); + let rejection_threshold: u64 = (weights as f64 * 7_f64 / 10_f64).ceil() as u64; let mut rejections = HashSet::new(); + let mut rejections_weight: u64 = 0; let now = Instant::now(); + debug!("Miner: waiting for block response from reward cycle {reward_cycle } signers..."); while now.elapsed() < self.config.miner.wait_on_signers { // Get the block responses from the signers for the block we just proposed let signer_chunks = stackerdbs @@ -323,6 +410,7 @@ impl BlockMinerThread { { // The signature is valid across the signer signature hash of the original proposed block // Immediately return and update the block with this new signature before appending it to the chain + debug!("Miner: received a signature accross the proposed block's signer signature hash ({signer_signature_hash:?}): {signature:?}"); return Ok(signature); } // We received an accepted block for some unknown block hash...Useless! Ignore it. @@ -337,8 +425,11 @@ impl BlockMinerThread { } if let RejectCode::SignedRejection(signature) = block_rejection.reason_code { - let mut message = signer_signature_hash.0.to_vec(); - message.push(b'n'); + let block_vote = NakamotoBlockVote { + signer_signature_hash: *signer_signature_hash, + rejected: true, + }; + let message = block_vote.serialize_to_vec(); if signature.0.verify(aggregate_public_key, &message) { // A threshold number of signers signed a denial of the proposed block // Miner will NEVER get a signed block from the signers for this particular block @@ -348,10 +439,24 @@ impl BlockMinerThread { )); } } else { + if rejections.contains(&signer_id) { + // We have already received a rejection from this signer + continue; + } + // We received a rejection that is not signed. We will keep waiting for a threshold number of rejections. // Ensure that we do not double count a rejection from the same signer. rejections.insert(signer_id); - if rejections.len() > rejection_threshold { + rejections_weight = rejections_weight.saturating_add( + *signer_weights + .get( + &slot_ids_addresses + .get(&signer_id) + .expect("FATAL: signer not found in slot ids"), + ) + .expect("FATAL: signer not found in signer weights"), + ); + if rejections_weight > rejection_threshold { // A threshold number of signers rejected the proposed block. // Miner will likely never get a signed block from the signers for this particular block // Return and attempt to mine a new block @@ -368,6 +473,7 @@ impl BlockMinerThread { thread::sleep(Duration::from_millis(WAIT_FOR_SIGNERS_MS)); } // We have waited for the signers for too long: stop waiting so we can propose a new block + debug!("Miner: exceeded signer signature timeout. Will propose a new block"); Err(NakamotoNodeError::SignerSignatureError( "Timed out waiting for signers", )) @@ -394,11 +500,23 @@ impl BlockMinerThread { &sortition_handle, &block, )?; + + let reward_cycle = self + .burnchain + .block_height_to_reward_cycle(self.burn_block.block_height) + .expect("FATAL: no reward cycle for burn block"); + let signer_weights = NakamotoSigners::get_signers_weights( + &mut chain_state, + &sort_db, + &self.parent_tenure_id, + reward_cycle, + )?; let signature = self .wait_for_signer_signature( &stackerdbs, &aggregate_public_key, &block.header.signer_signature_hash(), + signer_weights, ) .map_err(|e| { ChainstateError::InvalidStacksBlock(format!("Invalid Nakamoto block: {e:?}")) @@ -417,54 +535,6 @@ impl BlockMinerThread { Ok(()) } - fn self_sign_and_broadcast( - &self, - mut signer: TestSigners, - mut block: NakamotoBlock, - ) -> Result<(), ChainstateError> { - let mut chain_state = neon_node::open_chainstate_with_faults(&self.config) - .expect("FATAL: could not open chainstate DB"); - let chainstate_config = chain_state.config(); - let sort_db = SortitionDB::open( - &self.config.get_burn_db_file_path(), - true, - self.burnchain.pox_constants.clone(), - ) - .expect("FATAL: could not open sortition DB"); - - let burn_height = self.burn_block.block_height; - let cycle = self - .burnchain - .block_height_to_reward_cycle(burn_height) - .expect("FATAL: no reward cycle for burn block"); - signer.sign_nakamoto_block(&mut block, cycle); - - let mut sortition_handle = sort_db.index_handle_at_tip(); - let aggregate_public_key = if block.header.chain_length <= 1 { - signer.aggregate_public_key.clone() - } else { - let aggregate_public_key = NakamotoChainState::get_aggregate_public_key( - &mut chain_state, - &sort_db, - &sortition_handle, - &block, - )?; - aggregate_public_key - }; - - let (headers_conn, staging_tx) = chain_state.headers_conn_and_staging_tx_begin()?; - NakamotoChainState::accept_block( - &chainstate_config, - block, - &mut sortition_handle, - &staging_tx, - headers_conn, - &aggregate_public_key, - )?; - staging_tx.commit()?; - Ok(()) - } - /// Get the coinbase recipient address, if set in the config and if allowed in this epoch fn get_coinbase_recipient(&self, epoch_id: StacksEpochId) -> Option { if epoch_id < StacksEpochId::Epoch21 && self.config.miner.block_reward_recipient.is_some() { @@ -639,7 +709,7 @@ impl BlockMinerThread { /// Try to mine a Stacks block by assembling one from mempool transactions and sending a /// burnchain block-commit transaction. If we succeed, then return the assembled block. - fn mine_block(&mut self) -> Result { + fn mine_block(&mut self, stackerdbs: &StackerDBs) -> Result { debug!("block miner thread ID is {:?}", thread::current().id()); let burn_db_path = self.config.get_burn_db_file_path(); @@ -709,6 +779,10 @@ impl BlockMinerThread { let block_num = u64::try_from(self.mined_blocks.len()) .map_err(|_| NakamotoNodeError::UnexpectedChainState)? .saturating_add(1); + + let signer_transactions = + self.get_signer_transactions(&mut chain_state, &burn_db, &stackerdbs)?; + // build the block itself let (mut block, _, _) = NakamotoBlockBuilder::build_nakamoto_block( &chain_state, @@ -724,6 +798,7 @@ impl BlockMinerThread { self.globals.get_miner_status(), ), Some(&self.event_dispatcher), + signer_transactions, ) .map_err(|e| { if !matches!( diff --git a/testnet/stacks-node/src/nakamoto_node/peer.rs b/testnet/stacks-node/src/nakamoto_node/peer.rs index 2c8beb774..eeb6789d3 100644 --- a/testnet/stacks-node/src/nakamoto_node/peer.rs +++ b/testnet/stacks-node/src/nakamoto_node/peer.rs @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . use std::collections::VecDeque; -use std::default::Default; use std::net::SocketAddr; use std::sync::mpsc::TrySendError; use std::time::Duration; diff --git a/testnet/stacks-node/src/neon_node.rs b/testnet/stacks-node/src/neon_node.rs index a9c039367..49064d497 100644 --- a/testnet/stacks-node/src/neon_node.rs +++ b/testnet/stacks-node/src/neon_node.rs @@ -140,8 +140,6 @@ use std::cmp; use std::cmp::Ordering as CmpOrdering; use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; -use std::convert::{TryFrom, TryInto}; -use std::default::Default; use std::io::{Read, Write}; use std::net::SocketAddr; use std::sync::mpsc::{Receiver, TrySendError}; diff --git a/testnet/stacks-node/src/node.rs b/testnet/stacks-node/src/node.rs index cdebdbc78..90c212307 100644 --- a/testnet/stacks-node/src/node.rs +++ b/testnet/stacks-node/src/node.rs @@ -1,5 +1,4 @@ use std::collections::{HashMap, HashSet}; -use std::convert::TryFrom; use std::net::SocketAddr; use std::thread::JoinHandle; use std::{env, thread, time}; diff --git a/testnet/stacks-node/src/run_loop/nakamoto.rs b/testnet/stacks-node/src/run_loop/nakamoto.rs index 0b3702a99..dd13b2d32 100644 --- a/testnet/stacks-node/src/run_loop/nakamoto.rs +++ b/testnet/stacks-node/src/run_loop/nakamoto.rs @@ -20,7 +20,7 @@ use std::thread::JoinHandle; use std::{cmp, thread}; use stacks::burnchains::bitcoin::address::{BitcoinAddress, LegacyBitcoinAddressType}; -use stacks::burnchains::Burnchain; +use stacks::burnchains::{Burnchain, Error as burnchain_error}; use stacks::chainstate::burn::db::sortdb::SortitionDB; use stacks::chainstate::burn::BlockSnapshot; use stacks::chainstate::coordinator::comm::{CoordinatorChannels, CoordinatorReceivers}; @@ -400,13 +400,27 @@ impl RunLoop { // setup the termination handler, allow it to error if a prior runloop already set it neon::RunLoop::setup_termination_handler(self.should_keep_running.clone(), true); - let mut burnchain = neon::RunLoop::instantiate_burnchain_state( + + let burnchain_result = neon::RunLoop::instantiate_burnchain_state( &self.config, self.should_keep_running.clone(), burnchain_opt, coordinator_senders.clone(), ); + let mut burnchain = match burnchain_result { + Ok(burnchain_controller) => burnchain_controller, + Err(burnchain_error::ShutdownInitiated) => { + info!("Exiting stacks-node"); + return; + } + Err(e) => { + error!("Error initializing burnchain: {}", e); + info!("Exiting stacks-node"); + return; + } + }; + let burnchain_config = burnchain.get_burnchain(); self.burnchain = Some(burnchain_config.clone()); diff --git a/testnet/stacks-node/src/run_loop/neon.rs b/testnet/stacks-node/src/run_loop/neon.rs index 58910aef8..9a875d178 100644 --- a/testnet/stacks-node/src/run_loop/neon.rs +++ b/testnet/stacks-node/src/run_loop/neon.rs @@ -8,7 +8,7 @@ use std::{cmp, thread}; use libc; use stacks::burnchains::bitcoin::address::{BitcoinAddress, LegacyBitcoinAddressType}; -use stacks::burnchains::Burnchain; +use stacks::burnchains::{Burnchain, Error as burnchain_error}; use stacks::chainstate::burn::db::sortdb::SortitionDB; use stacks::chainstate::burn::BlockSnapshot; use stacks::chainstate::coordinator::comm::{CoordinatorChannels, CoordinatorReceivers}; @@ -17,8 +17,7 @@ use stacks::chainstate::coordinator::{ static_get_heaviest_affirmation_map, static_get_stacks_tip_affirmation_map, ChainsCoordinator, ChainsCoordinatorConfig, CoordinatorCommunication, Error as coord_error, }; -use stacks::chainstate::nakamoto::NakamotoChainState; -use stacks::chainstate::stacks::db::{ChainStateBootData, ClarityTx, StacksChainState}; +use stacks::chainstate::stacks::db::{ChainStateBootData, StacksChainState}; use stacks::chainstate::stacks::miner::{signal_mining_blocked, signal_mining_ready, MinerStatus}; use stacks::core::StacksEpochId; use stacks::net::atlas::{AtlasConfig, AtlasDB, Attachment}; @@ -26,12 +25,12 @@ use stacks::util_lib::db::Error as db_error; use stacks_common::deps_common::ctrlc as termination; use stacks_common::deps_common::ctrlc::SignalId; use stacks_common::types::PublicKey; -use stacks_common::util::hash::{to_hex, Hash160}; +use stacks_common::util::hash::Hash160; use stacks_common::util::{get_epoch_time_secs, sleep_ms}; use stx_genesis::GenesisData; use super::RunLoopCallbacks; -use crate::burnchains::make_bitcoin_indexer; +use crate::burnchains::{make_bitcoin_indexer, Error}; use crate::globals::NeonGlobals as Globals; use crate::monitoring::{start_serving_monitoring_metrics, MonitoringError}; use crate::neon_node::{StacksNode, BLOCK_PROCESSOR_STACK_SIZE, RELAYER_MAX_BUFFER}; @@ -47,10 +46,12 @@ use crate::{ pub const STDERR: i32 = 2; #[cfg(test)] -pub type RunLoopCounter = Arc; +#[derive(Clone)] +pub struct RunLoopCounter(pub Arc); #[cfg(not(test))] -pub type RunLoopCounter = (); +#[derive(Clone)] +pub struct RunLoopCounter(); #[cfg(test)] const UNCONDITIONAL_CHAIN_LIVENESS_CHECK: u64 = 30; @@ -58,7 +59,27 @@ const UNCONDITIONAL_CHAIN_LIVENESS_CHECK: u64 = 30; #[cfg(not(test))] const UNCONDITIONAL_CHAIN_LIVENESS_CHECK: u64 = 300; -#[derive(Clone)] +impl Default for RunLoopCounter { + #[cfg(test)] + fn default() -> Self { + RunLoopCounter(Arc::new(AtomicU64::new(0))) + } + #[cfg(not(test))] + fn default() -> Self { + Self() + } +} + +#[cfg(test)] +impl std::ops::Deref for RunLoopCounter { + type Target = Arc; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Clone, Default)] pub struct Counters { pub blocks_processed: RunLoopCounter, pub microblocks_processed: RunLoopCounter, @@ -69,43 +90,18 @@ pub struct Counters { pub naka_submitted_vrfs: RunLoopCounter, pub naka_submitted_commits: RunLoopCounter, pub naka_mined_blocks: RunLoopCounter, + pub naka_proposed_blocks: RunLoopCounter, pub naka_mined_tenures: RunLoopCounter, } impl Counters { - #[cfg(test)] - pub fn new() -> Counters { - Counters { - blocks_processed: RunLoopCounter::new(AtomicU64::new(0)), - microblocks_processed: RunLoopCounter::new(AtomicU64::new(0)), - missed_tenures: RunLoopCounter::new(AtomicU64::new(0)), - missed_microblock_tenures: RunLoopCounter::new(AtomicU64::new(0)), - cancelled_commits: RunLoopCounter::new(AtomicU64::new(0)), - naka_submitted_vrfs: RunLoopCounter::new(AtomicU64::new(0)), - naka_submitted_commits: RunLoopCounter::new(AtomicU64::new(0)), - naka_mined_blocks: RunLoopCounter::new(AtomicU64::new(0)), - naka_mined_tenures: RunLoopCounter::new(AtomicU64::new(0)), - } - } - - #[cfg(not(test))] - pub fn new() -> Counters { - Counters { - blocks_processed: (), - microblocks_processed: (), - missed_tenures: (), - missed_microblock_tenures: (), - cancelled_commits: (), - naka_submitted_vrfs: (), - naka_submitted_commits: (), - naka_mined_blocks: (), - naka_mined_tenures: (), - } + pub fn new() -> Self { + Self::default() } #[cfg(test)] fn inc(ctr: &RunLoopCounter) { - ctr.fetch_add(1, Ordering::SeqCst); + ctr.0.fetch_add(1, Ordering::SeqCst); } #[cfg(not(test))] @@ -113,7 +109,7 @@ impl Counters { #[cfg(test)] fn set(ctr: &RunLoopCounter, value: u64) { - ctr.store(value, Ordering::SeqCst); + ctr.0.store(value, Ordering::SeqCst); } #[cfg(not(test))] @@ -151,6 +147,10 @@ impl Counters { Counters::inc(&self.naka_mined_blocks); } + pub fn bump_naka_proposed_blocks(&self) { + Counters::inc(&self.naka_proposed_blocks); + } + pub fn bump_naka_mined_tenures(&self) { Counters::inc(&self.naka_mined_tenures); } @@ -217,7 +217,7 @@ impl RunLoop { globals: None, coordinator_channels: Some(channels), callbacks: RunLoopCallbacks::new(), - counters: Counters::new(), + counters: Counters::default(), should_keep_running, event_dispatcher, pox_watchdog: None, @@ -393,13 +393,13 @@ impl RunLoop { should_keep_running: Arc, burnchain_opt: Option, coordinator_senders: CoordinatorChannels, - ) -> BitcoinRegtestController { + ) -> Result { // Initialize and start the burnchain. let mut burnchain_controller = BitcoinRegtestController::with_burnchain( config.clone(), Some(coordinator_senders), burnchain_opt, - Some(should_keep_running), + Some(should_keep_running.clone()), ); let burnchain = burnchain_controller.get_burnchain(); @@ -448,13 +448,21 @@ impl RunLoop { } }; - match burnchain_controller.start(Some(target_burnchain_block_height)) { - Ok(_) => {} - Err(e) => { + burnchain_controller + .start(Some(target_burnchain_block_height)) + .map_err(|e| { + match e { + Error::CoordinatorClosed => { + if !should_keep_running.load(Ordering::SeqCst) { + info!("Shutdown initiated during burnchain initialization: {}", e); + return burnchain_error::ShutdownInitiated; + } + } + Error::IndexerError(_) => {} + } error!("Burnchain controller stopped: {}", e); panic!(); - } - }; + })?; // if the chainstate DBs don't exist, this will instantiate them if let Err(e) = burnchain_controller.connect_dbs() { @@ -464,7 +472,7 @@ impl RunLoop { // TODO (hack) instantiate the sortdb in the burnchain let _ = burnchain_controller.sortdb_mut(); - burnchain_controller + Ok(burnchain_controller) } /// Boot up the stacks chainstate. @@ -481,23 +489,10 @@ impl RunLoop { .map(|e| (e.address.clone(), e.amount)) .collect(); - // TODO: delete this once aggregate public key voting is working - let agg_pubkey_boot_callback = if let Some(self_signer) = self.config.self_signing() { - let agg_pub_key = self_signer.aggregate_public_key.clone(); - info!("Neon node setting agg public key"; "agg_pub_key" => %to_hex(&agg_pub_key.compress().data)); - let callback = Box::new(move |clarity_tx: &mut ClarityTx| { - NakamotoChainState::aggregate_public_key_bootcode(clarity_tx, &agg_pub_key) - }) as Box; - Some(callback) - } else { - warn!("Self-signing is not supported yet"); - None - }; - // instantiate chainstate let mut boot_data = ChainStateBootData { initial_balances, - post_flight_callback: agg_pubkey_boot_callback, + post_flight_callback: None, first_burnchain_block_hash: burnchain_config.first_block_hash, first_burnchain_block_height: burnchain_config.first_block_height as u32, first_burnchain_block_timestamp: burnchain_config.first_block_timestamp, @@ -514,6 +509,7 @@ impl RunLoop { get_bulk_initial_names: Some(Box::new(move || get_names(use_test_genesis_data))), }; + info!("About to call open_and_exec"); let (chain_state_db, receipts) = StacksChainState::open_and_exec( self.config.is_mainnet(), self.config.burnchain.chain_id, @@ -1007,13 +1003,27 @@ impl RunLoop { .expect("Run loop already started, can only start once after initialization."); Self::setup_termination_handler(self.should_keep_running.clone(), false); - let mut burnchain = Self::instantiate_burnchain_state( + + let burnchain_result = Self::instantiate_burnchain_state( &self.config, self.should_keep_running.clone(), burnchain_opt, coordinator_senders.clone(), ); + let mut burnchain = match burnchain_result { + Ok(burnchain_controller) => burnchain_controller, + Err(burnchain_error::ShutdownInitiated) => { + info!("Exiting stacks-node"); + return; + } + Err(e) => { + error!("Error initializing burnchain: {}", e); + info!("Exiting stacks-node"); + return; + } + }; + let burnchain_config = burnchain.get_burnchain(); self.burnchain = Some(burnchain_config.clone()); diff --git a/testnet/stacks-node/src/tests/epoch_205.rs b/testnet/stacks-node/src/tests/epoch_205.rs index 844a314bc..0f689f00e 100644 --- a/testnet/stacks-node/src/tests/epoch_205.rs +++ b/testnet/stacks-node/src/tests/epoch_205.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::convert::TryFrom; use std::sync::atomic::Ordering; use std::{env, thread}; diff --git a/testnet/stacks-node/src/tests/mempool.rs b/testnet/stacks-node/src/tests/mempool.rs index cc1f3d822..8c906cd43 100644 --- a/testnet/stacks-node/src/tests/mempool.rs +++ b/testnet/stacks-node/src/tests/mempool.rs @@ -1,4 +1,3 @@ -use std::convert::{From, TryFrom}; use std::sync::Mutex; use clarity::vm::costs::ExecutionCost; diff --git a/testnet/stacks-node/src/tests/mod.rs b/testnet/stacks-node/src/tests/mod.rs index 485dd524d..7dbabae3e 100644 --- a/testnet/stacks-node/src/tests/mod.rs +++ b/testnet/stacks-node/src/tests/mod.rs @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . use std::collections::HashMap; -use std::convert::TryInto; use std::sync::atomic::AtomicU64; use std::sync::Arc; diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index 40577cf37..46324aebb 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -13,24 +13,30 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use std::collections::{HashMap, HashSet}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; +use std::thread::JoinHandle; use std::time::{Duration, Instant}; use std::{env, thread}; use clarity::vm::ast::ASTRules; use clarity::vm::costs::ExecutionCost; -use clarity::vm::types::PrincipalData; +use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier}; +use http_types::headers::AUTHORIZATION; use lazy_static::lazy_static; -use libsigner::{SignerSession, StackerDBSession}; +use libsigner::{BlockResponse, SignerMessage, SignerSession, StackerDBSession}; use stacks::burnchains::MagicBytes; use stacks::chainstate::burn::db::sortdb::SortitionDB; use stacks::chainstate::coordinator::comm::CoordinatorChannels; use stacks::chainstate::nakamoto::miner::NakamotoBlockBuilder; +use stacks::chainstate::nakamoto::signer_set::NakamotoSigners; use stacks::chainstate::nakamoto::test_signers::TestSigners; use stacks::chainstate::nakamoto::{NakamotoBlock, NakamotoChainState}; use stacks::chainstate::stacks::address::PoxAddress; -use stacks::chainstate::stacks::boot::{MINERS_NAME, SIGNERS_VOTING_NAME}; +use stacks::chainstate::stacks::boot::{ + MINERS_NAME, SIGNERS_VOTING_FUNCTION_NAME, SIGNERS_VOTING_NAME, +}; use stacks::chainstate::stacks::db::StacksChainState; use stacks::chainstate::stacks::miner::{BlockBuilder, BlockLimitFunction, TransactionResult}; use stacks::chainstate::stacks::{StacksTransaction, ThresholdSignature, TransactionPayload}; @@ -40,6 +46,7 @@ use stacks::core::{ PEER_VERSION_EPOCH_2_1, PEER_VERSION_EPOCH_2_2, PEER_VERSION_EPOCH_2_3, PEER_VERSION_EPOCH_2_4, PEER_VERSION_EPOCH_2_5, PEER_VERSION_EPOCH_3_0, }; +use stacks::libstackerdb::{SlotMetadata, StackerDBChunkData}; use stacks::net::api::callreadonly::CallReadOnlyRequestBody; use stacks::net::api::getstackers::GetStackersResponse; use stacks::net::api::postblock_proposal::{ @@ -55,8 +62,8 @@ use stacks_common::consts::{CHAIN_ID_TESTNET, STACKS_EPOCH_MAX}; use stacks_common::types::chainstate::{ BlockHeaderHash, StacksAddress, StacksPrivateKey, StacksPublicKey, }; -use stacks_common::util::hash::to_hex; -use stacks_common::util::secp256k1::{MessageSignature, Secp256k1PrivateKey}; +use stacks_common::util::hash::{to_hex, Sha512Trunc256Sum}; +use stacks_common::util::secp256k1::{MessageSignature, Secp256k1PrivateKey, Secp256k1PublicKey}; use super::bitcoin_regtest::BitcoinCoreController; use crate::config::{EventKeyType, EventObserverConfig, InitialBalance}; @@ -154,6 +161,32 @@ pub fn get_stacker_set(http_origin: &str, cycle: u64) -> GetStackersResponse { res } +pub fn get_stackerdb_slot_version( + http_origin: &str, + contract: &QualifiedContractIdentifier, + slot_id: u64, +) -> Option { + let client = reqwest::blocking::Client::new(); + let path = format!( + "{http_origin}/v2/stackerdb/{}/{}", + &contract.issuer, &contract.name + ); + let res = client + .get(&path) + .send() + .unwrap() + .json::>() + .unwrap(); + debug!("StackerDB metadata response: {res:?}"); + res.iter().find_map(|slot| { + if u64::from(slot.slot_id) == slot_id { + Some(slot.slot_version) + } else { + None + } + }) +} + pub fn add_initial_balances( conf: &mut Config, accounts: usize, @@ -171,6 +204,114 @@ pub fn add_initial_balances( .collect() } +/// Spawn a blind signing thread. `signer` is the private key +/// of the individual signer who broadcasts the response to the StackerDB +pub fn blind_signer( + conf: &Config, + signers: &TestSigners, + signer: &Secp256k1PrivateKey, + proposals_count: RunLoopCounter, +) -> JoinHandle<()> { + let mut signed_blocks = HashSet::new(); + let conf = conf.clone(); + let signers = signers.clone(); + let signer = signer.clone(); + let mut last_count = proposals_count.load(Ordering::SeqCst); + thread::spawn(move || loop { + thread::sleep(Duration::from_millis(100)); + let cur_count = proposals_count.load(Ordering::SeqCst); + if cur_count <= last_count { + continue; + } + last_count = cur_count; + match read_and_sign_block_proposal(&conf, &signers, &signer, &signed_blocks) { + Ok(signed_block) => { + if signed_blocks.contains(&signed_block) { + continue; + } + info!("Signed block"; "signer_sig_hash" => signed_block.to_hex()); + signed_blocks.insert(signed_block); + } + Err(e) => { + warn!("Error reading and signing block proposal: {e}"); + } + } + }) +} + +pub fn read_and_sign_block_proposal( + conf: &Config, + signers: &TestSigners, + signer: &Secp256k1PrivateKey, + signed_blocks: &HashSet, +) -> Result { + let burnchain = conf.get_burnchain(); + let sortdb = burnchain.open_sortition_db(true).unwrap(); + let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap(); + let miner_pubkey = StacksPublicKey::from_private(&conf.get_miner_config().mining_key.unwrap()); + let miner_slot_id = NakamotoChainState::get_miner_slot(&sortdb, &tip, &miner_pubkey) + .map_err(|_| "Unable to get miner slot")? + .ok_or("No miner slot exists")?; + let reward_cycle = burnchain + .block_height_to_reward_cycle(tip.block_height) + .unwrap(); + + let mut proposed_block: NakamotoBlock = { + let miner_contract_id = boot_code_id(MINERS_NAME, false); + let mut miners_stackerdb = StackerDBSession::new(&conf.node.rpc_bind, miner_contract_id); + miners_stackerdb + .get_latest(miner_slot_id) + .map_err(|_| "Failed to get latest chunk from the miner slot ID")? + .ok_or("No chunk found")? + }; + let proposed_block_hash = format!("0x{}", proposed_block.header.block_hash()); + let signer_sig_hash = proposed_block.header.signer_signature_hash(); + if signed_blocks.contains(&signer_sig_hash) { + // already signed off on this block, don't sign again. + return Ok(signer_sig_hash); + } + + info!( + "Fetched proposed block from .miners StackerDB"; + "proposed_block_hash" => &proposed_block_hash, + "signer_sig_hash" => &signer_sig_hash.to_hex(), + ); + + signers + .clone() + .sign_nakamoto_block(&mut proposed_block, reward_cycle); + + let signer_message = SignerMessage::BlockResponse(BlockResponse::Accepted(( + signer_sig_hash.clone(), + proposed_block.header.signer_signature.clone(), + ))); + + let signers_contract_id = + NakamotoSigners::make_signers_db_contract_id(reward_cycle, libsigner::BLOCK_MSG_ID, false); + + let http_origin = format!("http://{}", &conf.node.rpc_bind); + let signers_info = get_stacker_set(&http_origin, reward_cycle); + let signer_index = get_signer_index(&signers_info, &Secp256k1PublicKey::from_private(signer)) + .unwrap() + .try_into() + .unwrap(); + + let next_version = get_stackerdb_slot_version(&http_origin, &signers_contract_id, signer_index) + .map(|x| x + 1) + .unwrap_or(0); + let mut signers_contract_sess = StackerDBSession::new(&conf.node.rpc_bind, signers_contract_id); + let mut chunk_to_put = StackerDBChunkData::new( + u32::try_from(signer_index).unwrap(), + next_version, + signer_message.serialize_to_vec(), + ); + chunk_to_put.sign(signer).unwrap(); + signers_contract_sess + .put_chunk(&chunk_to_put) + .map_err(|e| e.to_string())?; + Ok(signer_sig_hash) +} + /// Return a working nakamoto-neon config and the miner's bitcoin address to fund pub fn naka_neon_integration_conf(seed: Option<&[u8]>) -> (Config, StacksAddress) { let mut conf = super::new_test_conf(); @@ -189,13 +330,9 @@ pub fn naka_neon_integration_conf(seed: Option<&[u8]>) -> (Config, StacksAddress let mining_key = Secp256k1PrivateKey::from_seed(&[1]); conf.miner.mining_key = Some(mining_key); - conf.miner.self_signing_key = Some(TestSigners::default()); conf.node.miner = true; conf.node.wait_time_for_microblocks = 500; - conf.node - .stacker_dbs - .push(boot_code_id(MINERS_NAME, conf.is_mainnet())); conf.burnchain.burn_fee_cap = 20000; conf.burnchain.username = Some("neon-tester".into()); @@ -204,6 +341,8 @@ pub fn naka_neon_integration_conf(seed: Option<&[u8]>) -> (Config, StacksAddress conf.burnchain.local_mining_public_key = Some(keychain.generate_op_signer().get_public_key().to_hex()); conf.burnchain.commit_anchor_block_within = 0; + conf.node.add_signers_stackerdbs(false); + conf.node.add_miner_stackerdb(false); // test to make sure config file parsing is correct let mut cfile = ConfigFile::xenon(); @@ -356,9 +495,10 @@ pub fn setup_stacker(naka_conf: &mut Config) -> Secp256k1PrivateKey { /// for pox-4 to activate pub fn boot_to_epoch_3( naka_conf: &Config, - blocks_processed: &RunLoopCounter, + blocks_processed: &Arc, stacker_sks: &[StacksPrivateKey], signer_sks: &[StacksPrivateKey], + self_signing: Option<&TestSigners>, btc_regtest_controller: &mut BitcoinRegtestController, ) { assert_eq!(stacker_sks.len(), signer_sks.len()); @@ -440,25 +580,29 @@ pub fn boot_to_epoch_3( &naka_conf, ); - // If we are self-signing, then we need to vote on the aggregate public key - if let Some(mut signers) = naka_conf.self_signing() { + // We need to vote on the aggregate public key if this test is self signing + if let Some(signers) = self_signing { // Get the aggregate key - let aggregate_key = signers.generate_aggregate_key(reward_cycle + 1); + let aggregate_key = signers.clone().generate_aggregate_key(reward_cycle + 1); let aggregate_public_key = clarity::vm::Value::buff_from(aggregate_key.compress().data.to_vec()) .expect("Failed to serialize aggregate public key"); - + let signer_sks_unique: HashMap<_, _> = signer_sks.iter().map(|x| (x.to_hex(), x)).collect(); + let signer_set = get_stacker_set(&http_origin, reward_cycle + 1); // Vote on the aggregate public key - for (i, signer_sk) in signer_sks.iter().enumerate() { + for signer_sk in signer_sks_unique.values() { + let signer_index = + get_signer_index(&signer_set, &Secp256k1PublicKey::from_private(signer_sk)) + .unwrap(); let voting_tx = tests::make_contract_call( - &signer_sk, + signer_sk, 0, 300, &StacksAddress::burn_address(false), SIGNERS_VOTING_NAME, - "vote-for-aggregate-public-key", + SIGNERS_VOTING_FUNCTION_NAME, &[ - clarity::vm::Value::UInt(i as u128), + clarity::vm::Value::UInt(u128::try_from(signer_index).unwrap()), aggregate_public_key.clone(), clarity::vm::Value::UInt(0), clarity::vm::Value::UInt(reward_cycle as u128 + 1), @@ -478,6 +622,32 @@ pub fn boot_to_epoch_3( info!("Bootstrapped to Epoch-3.0 boundary, Epoch2x miner should stop"); } +fn get_signer_index( + stacker_set: &GetStackersResponse, + signer_key: &Secp256k1PublicKey, +) -> Result { + let Some(ref signer_set) = stacker_set.stacker_set.signers else { + return Err("Empty signer set for reward cycle".into()); + }; + let signer_key_bytes = signer_key.to_bytes_compressed(); + signer_set + .iter() + .enumerate() + .find_map(|(ix, entry)| { + if entry.signing_key.as_slice() == signer_key_bytes.as_slice() { + Some(ix) + } else { + None + } + }) + .ok_or_else(|| { + format!( + "Signing key not found. {} not found.", + to_hex(&signer_key_bytes) + ) + }) +} + fn is_key_set_for_cycle( reward_cycle: u64, is_mainnet: bool, @@ -518,67 +688,165 @@ fn signer_vote_if_needed( btc_regtest_controller: &BitcoinRegtestController, naka_conf: &Config, signer_sks: &[StacksPrivateKey], // TODO: Is there some way to get this from the TestSigners? + signers: &TestSigners, ) { - if let Some(mut signers) = naka_conf.self_signing() { - // When we reach the next prepare phase, submit new voting transactions - let block_height = btc_regtest_controller.get_headers_height(); - let reward_cycle = btc_regtest_controller - .get_burnchain() - .block_height_to_reward_cycle(block_height) - .unwrap(); - let prepare_phase_start = btc_regtest_controller - .get_burnchain() - .pox_constants - .prepare_phase_start( - btc_regtest_controller.get_burnchain().first_block_height, - reward_cycle, + // When we reach the next prepare phase, submit new voting transactions + let block_height = btc_regtest_controller.get_headers_height(); + let reward_cycle = btc_regtest_controller + .get_burnchain() + .block_height_to_reward_cycle(block_height) + .unwrap(); + let prepare_phase_start = btc_regtest_controller + .get_burnchain() + .pox_constants + .prepare_phase_start( + btc_regtest_controller.get_burnchain().first_block_height, + reward_cycle, + ); + + if block_height >= prepare_phase_start { + // If the key is already set, do nothing. + if is_key_set_for_cycle( + reward_cycle + 1, + naka_conf.is_mainnet(), + &naka_conf.node.rpc_bind, + ) + .unwrap_or(false) + { + return; + } + + // If we are self-signing, then we need to vote on the aggregate public key + let http_origin = format!("http://{}", &naka_conf.node.rpc_bind); + + // Get the aggregate key + let aggregate_key = signers.clone().generate_aggregate_key(reward_cycle + 1); + let aggregate_public_key = + clarity::vm::Value::buff_from(aggregate_key.compress().data.to_vec()) + .expect("Failed to serialize aggregate public key"); + + for (i, signer_sk) in signer_sks.iter().enumerate() { + let signer_nonce = get_account(&http_origin, &to_addr(signer_sk)).nonce; + + // Vote on the aggregate public key + let voting_tx = tests::make_contract_call( + &signer_sk, + signer_nonce, + 300, + &StacksAddress::burn_address(false), + SIGNERS_VOTING_NAME, + "vote-for-aggregate-public-key", + &[ + clarity::vm::Value::UInt(i as u128), + aggregate_public_key.clone(), + clarity::vm::Value::UInt(0), + clarity::vm::Value::UInt(reward_cycle as u128 + 1), + ], ); - - if block_height >= prepare_phase_start { - // If the key is already set, do nothing. - if is_key_set_for_cycle( - reward_cycle + 1, - naka_conf.is_mainnet(), - &naka_conf.node.rpc_bind, - ) - .unwrap_or(false) - { - return; - } - - // If we are self-signing, then we need to vote on the aggregate public key - let http_origin = format!("http://{}", &naka_conf.node.rpc_bind); - - // Get the aggregate key - let aggregate_key = signers.generate_aggregate_key(reward_cycle + 1); - let aggregate_public_key = - clarity::vm::Value::buff_from(aggregate_key.compress().data.to_vec()) - .expect("Failed to serialize aggregate public key"); - - for (i, signer_sk) in signer_sks.iter().enumerate() { - let signer_nonce = get_account(&http_origin, &to_addr(signer_sk)).nonce; - - // Vote on the aggregate public key - let voting_tx = tests::make_contract_call( - &signer_sk, - signer_nonce, - 300, - &StacksAddress::burn_address(false), - SIGNERS_VOTING_NAME, - "vote-for-aggregate-public-key", - &[ - clarity::vm::Value::UInt(i as u128), - aggregate_public_key.clone(), - clarity::vm::Value::UInt(0), - clarity::vm::Value::UInt(reward_cycle as u128 + 1), - ], - ); - submit_tx(&http_origin, &voting_tx); - } + submit_tx(&http_origin, &voting_tx); } } } +/// +/// * `stacker_sks` - must be a private key for sending a large `stack-stx` transaction in order +/// for pox-4 to activate +/// * `signer_pks` - must be the same size as `stacker_sks` +pub fn boot_to_epoch_3_reward_set( + naka_conf: &Config, + blocks_processed: &Arc, + stacker_sks: &[StacksPrivateKey], + signer_sks: &[StacksPrivateKey], + btc_regtest_controller: &mut BitcoinRegtestController, +) { + assert_eq!(stacker_sks.len(), signer_sks.len()); + + let epochs = naka_conf.burnchain.epochs.clone().unwrap(); + let epoch_3 = &epochs[StacksEpoch::find_epoch_by_id(&epochs, StacksEpochId::Epoch30).unwrap()]; + let reward_cycle_len = naka_conf.get_burnchain().pox_constants.reward_cycle_length as u64; + let prepare_phase_len = naka_conf.get_burnchain().pox_constants.prepare_length as u64; + + let epoch_3_start_height = epoch_3.start_height; + assert!( + epoch_3_start_height > 0, + "Epoch 3.0 start height must be greater than 0" + ); + let epoch_3_reward_cycle_boundary = + epoch_3_start_height.saturating_sub(epoch_3_start_height % reward_cycle_len); + let epoch_3_reward_set_calculation_boundary = + epoch_3_reward_cycle_boundary.saturating_sub(prepare_phase_len); + let epoch_3_reward_set_calculation = epoch_3_reward_set_calculation_boundary.wrapping_add(2); // +2 to ensure we are at the second block of the prepare phase + let http_origin = format!("http://{}", &naka_conf.node.rpc_bind); + next_block_and_wait(btc_regtest_controller, &blocks_processed); + next_block_and_wait(btc_regtest_controller, &blocks_processed); + // first mined stacks block + next_block_and_wait(btc_regtest_controller, &blocks_processed); + + // stack enough to activate pox-4 + let block_height = btc_regtest_controller.get_headers_height(); + let reward_cycle = btc_regtest_controller + .get_burnchain() + .block_height_to_reward_cycle(block_height) + .unwrap(); + let lock_period = 12; + debug!("Test Cycle Info"; + "prepare_phase_len" => {prepare_phase_len}, + "reward_cycle_len" => {reward_cycle_len}, + "block_height" => {block_height}, + "reward_cycle" => {reward_cycle}, + "epoch_3_reward_cycle_boundary" => {epoch_3_reward_cycle_boundary}, + "epoch_3_reward_set_calculation" => {epoch_3_reward_set_calculation}, + "epoch_3_start_height" => {epoch_3_start_height}, + ); + for (stacker_sk, signer_sk) in stacker_sks.iter().zip(signer_sks.iter()) { + let pox_addr = PoxAddress::from_legacy( + AddressHashMode::SerializeP2PKH, + tests::to_addr(&stacker_sk).bytes, + ); + let pox_addr_tuple: clarity::vm::Value = + pox_addr.clone().as_clarity_tuple().unwrap().into(); + let signature = make_pox_4_signer_key_signature( + &pox_addr, + &signer_sk, + reward_cycle.into(), + &Pox4SignatureTopic::StackStx, + CHAIN_ID_TESTNET, + lock_period, + ) + .unwrap() + .to_rsv(); + + let signer_pk = StacksPublicKey::from_private(signer_sk); + let stacking_tx = tests::make_contract_call( + &stacker_sk, + 0, + 1000, + &StacksAddress::burn_address(false), + "pox-4", + "stack-stx", + &[ + clarity::vm::Value::UInt(POX_4_DEFAULT_STACKER_STX_AMT), + pox_addr_tuple.clone(), + clarity::vm::Value::UInt(block_height as u128), + clarity::vm::Value::UInt(lock_period), + clarity::vm::Value::some(clarity::vm::Value::buff_from(signature).unwrap()) + .unwrap(), + clarity::vm::Value::buff_from(signer_pk.to_bytes_compressed()).unwrap(), + ], + ); + submit_tx(&http_origin, &stacking_tx); + } + + run_until_burnchain_height( + btc_regtest_controller, + &blocks_processed, + epoch_3_reward_set_calculation, + &naka_conf, + ); + + info!("Bootstrapped to Epoch 3.0 reward set calculation height: {epoch_3_reward_set_calculation}."); +} + #[test] #[ignore] /// This test spins up a nakamoto-neon node. @@ -594,6 +862,7 @@ fn simple_neon_integration() { return; } + let signers = TestSigners::default(); let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None); let prom_bind = format!("{}:{}", "127.0.0.1", 6000); naka_conf.node.prometheus_bind = Some(prom_bind.clone()); @@ -636,6 +905,7 @@ fn simple_neon_integration() { blocks_processed, naka_submitted_vrfs: vrfs_submitted, naka_submitted_commits: commits_submitted, + naka_proposed_blocks: proposals_submitted, .. } = run_loop.counters(); @@ -648,6 +918,7 @@ fn simple_neon_integration() { &blocks_processed, &[stacker_sk], &[sender_signer_sk], + Some(&signers), &mut btc_regtest_controller, ); @@ -685,6 +956,8 @@ fn simple_neon_integration() { } info!("Nakamoto miner started..."); + blind_signer(&naka_conf, &signers, &sender_signer_sk, proposals_submitted); + // first block wakes up the run loop, wait until a key registration has been submitted. next_block_and(&mut btc_regtest_controller, 60, || { let vrf_count = vrfs_submitted.load(Ordering::SeqCst); @@ -709,7 +982,12 @@ fn simple_neon_integration() { ) .unwrap(); - signer_vote_if_needed(&btc_regtest_controller, &naka_conf, &[sender_signer_sk]); + signer_vote_if_needed( + &btc_regtest_controller, + &naka_conf, + &[sender_signer_sk], + &signers, + ); } // Submit a TX @@ -746,7 +1024,12 @@ fn simple_neon_integration() { ) .unwrap(); - signer_vote_if_needed(&btc_regtest_controller, &naka_conf, &[sender_signer_sk]); + signer_vote_if_needed( + &btc_regtest_controller, + &naka_conf, + &[sender_signer_sk], + &signers, + ); } // load the chain tip, and assert that it is a nakamoto block and at least 30 blocks have advanced in epoch 3 @@ -818,6 +1101,7 @@ fn mine_multiple_per_tenure_integration() { return; } + let signers = TestSigners::default(); let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None); let http_origin = format!("http://{}", &naka_conf.node.rpc_bind); naka_conf.miner.wait_on_interim_blocks = Duration::from_secs(1); @@ -862,6 +1146,7 @@ fn mine_multiple_per_tenure_integration() { blocks_processed, naka_submitted_vrfs: vrfs_submitted, naka_submitted_commits: commits_submitted, + naka_proposed_blocks: proposals_submitted, .. } = run_loop.counters(); @@ -877,6 +1162,7 @@ fn mine_multiple_per_tenure_integration() { &blocks_processed, &[stacker_sk], &[sender_signer_sk], + Some(&signers), &mut btc_regtest_controller, ); @@ -899,6 +1185,8 @@ fn mine_multiple_per_tenure_integration() { .stacks_block_height; info!("Nakamoto miner started..."); + blind_signer(&naka_conf, &signers, &sender_signer_sk, proposals_submitted); + // first block wakes up the run loop, wait until a key registration has been submitted. next_block_and(&mut btc_regtest_controller, 60, || { let vrf_count = vrfs_submitted.load(Ordering::SeqCst); @@ -995,6 +1283,7 @@ fn correct_burn_outs() { return; } + let signers = TestSigners::default(); let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None); naka_conf.burnchain.pox_reward_length = Some(10); naka_conf.burnchain.pox_prepare_length = Some(3); @@ -1051,6 +1340,7 @@ fn correct_burn_outs() { blocks_processed, naka_submitted_vrfs: vrfs_submitted, naka_submitted_commits: commits_submitted, + naka_proposed_blocks: proposals_submitted, .. } = run_loop.counters(); @@ -1182,7 +1472,12 @@ fn correct_burn_outs() { &naka_conf, ); - signer_vote_if_needed(&btc_regtest_controller, &naka_conf, &[sender_signer_sk]); + signer_vote_if_needed( + &btc_regtest_controller, + &naka_conf, + &[sender_signer_sk], + &signers, + ); run_until_burnchain_height( &mut btc_regtest_controller, @@ -1192,6 +1487,7 @@ fn correct_burn_outs() { ); info!("Bootstrapped to Epoch-3.0 boundary, Epoch2x miner should stop"); + blind_signer(&naka_conf, &signers, &sender_signer_sk, proposals_submitted); // we should already be able to query the stacker set via RPC let burnchain = naka_conf.get_burnchain(); @@ -1255,7 +1551,12 @@ fn correct_burn_outs() { "The new burnchain tip must have been processed" ); - signer_vote_if_needed(&btc_regtest_controller, &naka_conf, &[sender_signer_sk]); + signer_vote_if_needed( + &btc_regtest_controller, + &naka_conf, + &[sender_signer_sk], + &signers, + ); } coord_channel @@ -1340,7 +1641,10 @@ fn block_proposal_api_endpoint() { return; } + let signers = TestSigners::default(); let (mut conf, _miner_account) = naka_neon_integration_conf(None); + let password = "12345".to_string(); + conf.connection_options.block_proposal_token = Some(password.clone()); let account_keys = add_initial_balances(&mut conf, 10, 1_000_000); let stacker_sk = setup_stacker(&mut conf); let sender_signer_sk = Secp256k1PrivateKey::new(); @@ -1371,6 +1675,7 @@ fn block_proposal_api_endpoint() { blocks_processed, naka_submitted_vrfs: vrfs_submitted, naka_submitted_commits: commits_submitted, + naka_proposed_blocks: proposals_submitted, .. } = run_loop.counters(); @@ -1383,10 +1688,12 @@ fn block_proposal_api_endpoint() { &blocks_processed, &[stacker_sk], &[sender_signer_sk], + Some(&signers), &mut btc_regtest_controller, ); info!("Bootstrapped to Epoch-3.0 boundary, starting nakamoto miner"); + blind_signer(&conf, &signers, &sender_signer_sk, proposals_submitted); let burnchain = conf.get_burnchain(); let sortdb = burnchain.open_sortition_db(true).unwrap(); @@ -1434,9 +1741,6 @@ fn block_proposal_api_endpoint() { // TODO (hack) instantiate the sortdb in the burnchain _ = btc_regtest_controller.sortdb_mut(); - // Set up test signer - let signer = conf.miner.self_signing_key.as_mut().unwrap(); - // ----- Setup boilerplate finished, test block proposal API endpoint ----- let tip = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb) @@ -1465,19 +1769,13 @@ fn block_proposal_api_endpoint() { _ => None, }); - // Apply both miner/stacker signatures - let mut sign = |mut p: NakamotoBlockProposal| { + // Apply miner signature + let sign = |p: &NakamotoBlockProposal| { + let mut p = p.clone(); p.block .header .sign_miner(&privk) .expect("Miner failed to sign"); - let burn_height = burnchain - .get_highest_burnchain_block() - .unwrap() - .unwrap() - .block_height; - let cycle = burnchain.block_height_to_reward_cycle(burn_height).unwrap(); - signer.sign_nakamoto_block(&mut p.block, cycle); p }; @@ -1532,18 +1830,19 @@ fn block_proposal_api_endpoint() { const HTTP_ACCEPTED: u16 = 202; const HTTP_TOO_MANY: u16 = 429; + const HTTP_NOT_AUTHORIZED: u16 = 401; let test_cases = [ ( "Valid Nakamoto block proposal", - sign(proposal.clone()), + sign(&proposal), HTTP_ACCEPTED, Some(Ok(())), ), - ("Must wait", sign(proposal.clone()), HTTP_TOO_MANY, None), + ("Must wait", sign(&proposal), HTTP_TOO_MANY, None), ( "Corrupted (bit flipped after signing)", (|| { - let mut sp = sign(proposal.clone()); + let mut sp = sign(&proposal); sp.block.header.consensus_hash.0[3] ^= 0x07; sp })(), @@ -1555,7 +1854,7 @@ fn block_proposal_api_endpoint() { (|| { let mut p = proposal.clone(); p.chain_id ^= 0xFFFFFFFF; - sign(p) + sign(&p) })(), HTTP_ACCEPTED, Some(Err(ValidateRejectCode::InvalidBlock)), @@ -1563,13 +1862,14 @@ fn block_proposal_api_endpoint() { ( "Invalid `miner_signature`", (|| { - let mut sp = sign(proposal.clone()); + let mut sp = sign(&proposal); sp.block.header.miner_signature.0[1] ^= 0x80; sp })(), HTTP_ACCEPTED, Some(Err(ValidateRejectCode::ChainstateError)), ), + ("Not authorized", sign(&proposal), HTTP_NOT_AUTHORIZED, None), ]; // Build HTTP client @@ -1586,12 +1886,18 @@ fn block_proposal_api_endpoint() { test_cases.iter().enumerate() { // Send POST request - let mut response = client + let request_builder = client .post(&path) .header("Content-Type", "application/json") - .json(block_proposal) - .send() - .expect("Failed to POST"); + .json(block_proposal); + let mut response = if expected_http_code == &HTTP_NOT_AUTHORIZED { + request_builder.send().expect("Failed to POST") + } else { + request_builder + .header(AUTHORIZATION.to_string(), password.to_string()) + .send() + .expect("Failed to POST") + }; let start_time = Instant::now(); while ix != 1 && response.status().as_u16() == HTTP_TOO_MANY { if start_time.elapsed() > Duration::from_secs(30) { @@ -1600,20 +1906,29 @@ fn block_proposal_api_endpoint() { } info!("Waiting for prior request to finish processing, and then resubmitting"); thread::sleep(Duration::from_secs(5)); - response = client + let request_builder = client .post(&path) .header("Content-Type", "application/json") - .json(block_proposal) - .send() - .expect("Failed to POST"); + .json(block_proposal); + response = if expected_http_code == &HTTP_NOT_AUTHORIZED { + request_builder.send().expect("Failed to POST") + } else { + request_builder + .header(AUTHORIZATION.to_string(), password.to_string()) + .send() + .expect("Failed to POST") + }; } let response_code = response.status().as_u16(); - let response_json = response.json::(); - + let response_json = if expected_http_code != &HTTP_NOT_AUTHORIZED { + response.json::().unwrap().to_string() + } else { + "No json response".to_string() + }; info!( "Block proposal submitted and checked for HTTP response"; - "response_json" => %response_json.unwrap(), + "response_json" => response_json, "request_json" => serde_json::to_string(block_proposal).unwrap(), "response_code" => response_code, "test_description" => test_description, @@ -1687,6 +2002,7 @@ fn miner_writes_proposed_block_to_stackerdb() { return; } + let signers = TestSigners::default(); let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None); naka_conf.miner.wait_on_interim_blocks = Duration::from_secs(1000); let sender_sk = Secp256k1PrivateKey::new(); @@ -1727,6 +2043,7 @@ fn miner_writes_proposed_block_to_stackerdb() { blocks_processed, naka_submitted_vrfs: vrfs_submitted, naka_submitted_commits: commits_submitted, + naka_proposed_blocks: proposals_submitted, .. } = run_loop.counters(); @@ -1739,10 +2056,12 @@ fn miner_writes_proposed_block_to_stackerdb() { &blocks_processed, &[stacker_sk], &[sender_signer_sk], + Some(&signers), &mut btc_regtest_controller, ); info!("Nakamoto miner started..."); + blind_signer(&naka_conf, &signers, &sender_signer_sk, proposals_submitted); // first block wakes up the run loop, wait until a key registration has been submitted. next_block_and(&mut btc_regtest_controller, 60, || { let vrf_count = vrfs_submitted.load(Ordering::SeqCst); @@ -1766,13 +2085,6 @@ fn miner_writes_proposed_block_to_stackerdb() { ) .unwrap(); - let rpc_sock = naka_conf - .node - .rpc_bind - .clone() - .parse() - .expect("Failed to parse socket"); - let sortdb = naka_conf.get_burnchain().open_sortition_db(true).unwrap(); let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap(); let miner_pubkey = @@ -1781,20 +2093,15 @@ fn miner_writes_proposed_block_to_stackerdb() { .expect("Unable to get miner slot") .expect("No miner slot exists"); - let chunk = std::thread::spawn(move || { + let proposed_block: NakamotoBlock = { let miner_contract_id = boot_code_id(MINERS_NAME, false); - let mut miners_stackerdb = StackerDBSession::new(rpc_sock, miner_contract_id); + let mut miners_stackerdb = + StackerDBSession::new(&naka_conf.node.rpc_bind, miner_contract_id); miners_stackerdb - .get_latest_chunk(slot_id) + .get_latest(slot_id) .expect("Failed to get latest chunk from the miner slot ID") .expect("No chunk found") - }) - .join() - .expect("Failed to join chunk handle"); - - // We should now successfully deserialize a chunk - let proposed_block = NakamotoBlock::consensus_deserialize(&mut &chunk[..]) - .expect("Failed to deserialize chunk into block"); + }; let proposed_block_hash = format!("0x{}", proposed_block.header.block_hash()); let mut proposed_zero_block = proposed_block.clone(); diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index d751035ac..5ea49c1cc 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -1,5 +1,4 @@ use std::collections::{HashMap, HashSet}; -use std::convert::TryFrom; use std::path::Path; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{mpsc, Arc}; @@ -6083,6 +6082,7 @@ fn pox_integration_test() { let http_origin = format!("http://{}", &conf.node.rpc_bind); btc_regtest_controller.bootstrap_chain(201); + let burnchain = burnchain_config.clone(); eprintln!("Chain bootstrapped..."); @@ -6140,9 +6140,12 @@ fn pox_integration_test() { pox_info.rejection_fraction, Some(pox_constants.pox_rejection_fraction) ); - assert_eq!(pox_info.reward_cycle_id, 0); - assert_eq!(pox_info.current_cycle.id, 0); - assert_eq!(pox_info.next_cycle.id, 1); + let reward_cycle = burnchain + .block_height_to_reward_cycle(sort_height) + .expect("Expected to be able to get reward cycle"); + assert_eq!(pox_info.reward_cycle_id, reward_cycle); + assert_eq!(pox_info.current_cycle.id, reward_cycle); + assert_eq!(pox_info.next_cycle.id, reward_cycle + 1); assert_eq!( pox_info.reward_cycle_length as u32, pox_constants.reward_cycle_length @@ -6185,6 +6188,9 @@ fn pox_integration_test() { } let pox_info = get_pox_info(&http_origin).unwrap(); + let reward_cycle = burnchain + .block_height_to_reward_cycle(sort_height) + .expect("Expected to be able to get reward cycle"); assert_eq!( &pox_info.contract_id, @@ -6208,9 +6214,9 @@ fn pox_integration_test() { pox_info.rejection_fraction, Some(pox_constants.pox_rejection_fraction) ); - assert_eq!(pox_info.reward_cycle_id, 14); - assert_eq!(pox_info.current_cycle.id, 14); - assert_eq!(pox_info.next_cycle.id, 15); + assert_eq!(pox_info.reward_cycle_id, reward_cycle); + assert_eq!(pox_info.current_cycle.id, reward_cycle); + assert_eq!(pox_info.next_cycle.id, reward_cycle + 1); assert_eq!( pox_info.reward_cycle_length as u32, pox_constants.reward_cycle_length diff --git a/testnet/stacks-node/src/tests/signer.rs b/testnet/stacks-node/src/tests/signer.rs index e2cf03b24..54e851be9 100644 --- a/testnet/stacks-node/src/tests/signer.rs +++ b/testnet/stacks-node/src/tests/signer.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::HashSet; use std::net::ToSocketAddrs; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::mpsc::{channel, Receiver, Sender}; @@ -7,49 +7,63 @@ use std::time::{Duration, Instant}; use std::{env, thread}; use clarity::boot_util::boot_code_id; +use clarity::vm::Value; use libsigner::{ BlockResponse, RejectCode, RunningSigner, Signer, SignerEventReceiver, SignerMessage, - BLOCK_MSG_ID, TRANSACTIONS_MSG_ID, + BLOCK_MSG_ID, }; +use rand::thread_rng; +use rand_core::RngCore; +use stacks::burnchains::Txid; use stacks::chainstate::coordinator::comm::CoordinatorChannels; use stacks::chainstate::nakamoto::signer_set::NakamotoSigners; -use stacks::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader}; -use stacks::chainstate::stacks::boot::SIGNERS_NAME; +use stacks::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader, NakamotoBlockVote}; +use stacks::chainstate::stacks::boot::{ + SIGNERS_NAME, SIGNERS_VOTING_FUNCTION_NAME, SIGNERS_VOTING_NAME, +}; +use stacks::chainstate::stacks::miner::TransactionEvent; use stacks::chainstate::stacks::{ StacksPrivateKey, StacksTransaction, ThresholdSignature, TransactionAnchorMode, TransactionAuth, TransactionPayload, TransactionPostConditionMode, TransactionSmartContract, TransactionVersion, }; +use stacks::core::StacksEpoch; use stacks::net::api::postblock_proposal::BlockValidateResponse; use stacks::util_lib::strings::StacksString; use stacks_common::bitvec::BitVec; -use stacks_common::codec::read_next; -use stacks_common::consts::SIGNER_SLOTS_PER_USER; +use stacks_common::codec::{read_next, StacksMessageCodec}; +use stacks_common::consts::{CHAIN_ID_TESTNET, SIGNER_SLOTS_PER_USER}; use stacks_common::types::chainstate::{ ConsensusHash, StacksAddress, StacksBlockId, StacksPublicKey, TrieHash, }; +use stacks_common::types::StacksEpochId; use stacks_common::util::hash::{MerkleTree, Sha512Trunc256Sum}; use stacks_common::util::secp256k1::MessageSignature; use stacks_signer::client::{StackerDB, StacksClient}; -use stacks_signer::config::{Config as SignerConfig, Network}; -use stacks_signer::runloop::{calculate_coordinator, RunLoopCommand}; -use stacks_signer::utils::build_signer_config_tomls; +use stacks_signer::config::{build_signer_config_tomls, GlobalConfig as SignerConfig, Network}; +use stacks_signer::runloop::RunLoopCommand; +use stacks_signer::signer::{Command as SignerCommand, SignerSlotID}; use tracing_subscriber::prelude::*; use tracing_subscriber::{fmt, EnvFilter}; +use wsts::common::Signature; +use wsts::compute::tweaked_public_key; use wsts::curve::point::Point; -use wsts::state_machine::coordinator::fire::Coordinator as FireCoordinator; +use wsts::curve::scalar::Scalar; use wsts::state_machine::OperationResult; -use wsts::v2; +use wsts::taproot::SchnorrProof; use crate::config::{Config as NeonConfig, EventKeyType, EventObserverConfig, InitialBalance}; +use crate::event_dispatcher::MinedNakamotoBlockEvent; use crate::neon::Counters; use crate::run_loop::boot_nakamoto; use crate::tests::bitcoin_regtest::BitcoinCoreController; use crate::tests::nakamoto_integrations::{ - boot_to_epoch_3, naka_neon_integration_conf, next_block_and, next_block_and_mine_commit, - POX_4_DEFAULT_STACKER_BALANCE, + boot_to_epoch_3_reward_set, naka_neon_integration_conf, next_block_and, + next_block_and_mine_commit, POX_4_DEFAULT_STACKER_BALANCE, +}; +use crate::tests::neon_integrations::{ + next_block_and_wait, run_until_burnchain_height, test_observer, wait_for_runloop, }; -use crate::tests::neon_integrations::{next_block_and_wait, test_observer, wait_for_runloop}; use crate::tests::to_addr; use crate::{BitcoinRegtestController, BurnchainController}; @@ -70,16 +84,12 @@ struct RunningNodes { struct SignerTest { // The stx and bitcoin nodes and their run loops pub running_nodes: RunningNodes, - // The channel for sending commands to the coordinator - pub coordinator_cmd_sender: Sender, // The channels for sending commands to the signers - pub _signer_cmd_senders: HashMap>, - // The channels for receiving results from both the coordinator and the signers + pub signer_cmd_senders: Vec>, + // The channels for receiving results from the signers pub result_receivers: Vec>>, - // The running coordinator and its threads - pub running_coordinator: RunningSigner>, // The running signer and its threads - pub running_signers: HashMap>>, + pub running_signers: Vec>>, // the private keys of the signers pub signer_stacks_private_keys: Vec, // link to the stacks node @@ -87,81 +97,663 @@ struct SignerTest { } impl SignerTest { - fn new(num_signers: u32, num_keys: u32) -> Self { + fn new(num_signers: usize) -> Self { // Generate Signer Data let signer_stacks_private_keys = (0..num_signers) .map(|_| StacksPrivateKey::new()) .collect::>(); - // Build the stackerdb signers contract - let signers_stacker_db_contract_id = boot_code_id(SIGNERS_NAME.into(), false); - - let (naka_conf, _miner_account) = naka_neon_integration_conf(None); + let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None); + // So the combination is... one, two, three, four, five? That's the stupidest combination I've ever heard in my life! + // That's the kind of thing an idiot would have on his luggage! + let password = "12345"; + naka_conf.connection_options.block_proposal_token = Some(password.to_string()); // Setup the signer and coordinator configurations let signer_configs = build_signer_config_tomls( &signer_stacks_private_keys, - num_keys, &naka_conf.node.rpc_bind, - &signers_stacker_db_contract_id.to_string(), Some(Duration::from_millis(128)), // Timeout defaults to 5 seconds. Let's override it to 128 milliseconds. &Network::Testnet, + password, ); - let mut running_signers = HashMap::new(); - let mut signer_cmd_senders = HashMap::new(); + let mut running_signers = Vec::new(); + let mut signer_cmd_senders = Vec::new(); let mut result_receivers = Vec::new(); - // Spawn all signers before the node to ensure their listening ports are open for the node event observer to bind to - for i in (0..num_signers).rev() { + for i in 0..num_signers { let (cmd_send, cmd_recv) = channel(); let (res_send, res_recv) = channel(); info!("spawn signer"); - running_signers.insert( - i, - spawn_signer(&signer_configs[i as usize], cmd_recv, res_send), - ); - signer_cmd_senders.insert(i, cmd_send); + running_signers.push(spawn_signer( + &signer_configs[i as usize], + cmd_recv, + res_send, + )); + signer_cmd_senders.push(cmd_send); result_receivers.push(res_recv); } // Setup the nodes and deploy the contract to it - let node = setup_stx_btc_node( - naka_conf, - num_signers, - &signer_stacks_private_keys, - &signer_configs, - ); - - // Calculate which signer will be selected as the coordinator - let config = stacks_signer::config::Config::load_from_str(&signer_configs[0]).unwrap(); + let node = setup_stx_btc_node(naka_conf, &signer_stacks_private_keys, &signer_configs); + let config = SignerConfig::load_from_str(&signer_configs[0]).unwrap(); let stacks_client = StacksClient::from(&config); - let (coordinator_id, coordinator_pk) = - calculate_coordinator(&config.signer_ids_public_keys, &stacks_client); - info!( - "Selected coordinator id: {:?} with pk: {:?}", - &coordinator_id, &coordinator_pk - ); - - // Fetch the selected coordinator and its cmd_sender - let running_coordinator = running_signers - .remove(&coordinator_id) - .expect("Coordinator not found"); - let coordinator_cmd_sender = signer_cmd_senders - .remove(&coordinator_id) - .expect("Command sender not found"); Self { running_nodes: node, result_receivers, - _signer_cmd_senders: signer_cmd_senders, - coordinator_cmd_sender, - running_coordinator, + signer_cmd_senders, running_signers, signer_stacks_private_keys, stacks_client, } } + fn boot_to_epoch_3(&mut self, timeout: Duration) -> Point { + boot_to_epoch_3_reward_set( + &self.running_nodes.conf, + &self.running_nodes.blocks_processed, + &self.signer_stacks_private_keys, + &self.signer_stacks_private_keys, + &mut self.running_nodes.btc_regtest_controller, + ); + let dkg_vote = self.wait_for_dkg(timeout); + + // Advance and mine the DKG key block + self.run_until_epoch_3_boundary(); + + let reward_cycle = self.get_current_reward_cycle(); + let set_dkg = self + .stacks_client + .get_approved_aggregate_key(reward_cycle) + .expect("Failed to get approved aggregate key") + .expect("No approved aggregate key found"); + assert_eq!(set_dkg, dkg_vote); + + let (vrfs_submitted, commits_submitted) = ( + self.running_nodes.vrfs_submitted.clone(), + self.running_nodes.commits_submitted.clone(), + ); + // first block wakes up the run loop, wait until a key registration has been submitted. + next_block_and(&mut self.running_nodes.btc_regtest_controller, 60, || { + let vrf_count = vrfs_submitted.load(Ordering::SeqCst); + Ok(vrf_count >= 1) + }) + .unwrap(); + + info!("Successfully triggered first block to wake up the miner runloop."); + // second block should confirm the VRF register, wait until a block commit is submitted + next_block_and(&mut self.running_nodes.btc_regtest_controller, 60, || { + let commits_count = commits_submitted.load(Ordering::SeqCst); + Ok(commits_count >= 1) + }) + .unwrap(); + info!("Ready to mine Nakamoto blocks!"); + set_dkg + } + + fn nmb_blocks_to_reward_set_calculation(&mut self) -> u64 { + let prepare_phase_len = self + .running_nodes + .conf + .get_burnchain() + .pox_constants + .prepare_length as u64; + let current_block_height = self + .running_nodes + .btc_regtest_controller + .get_headers_height(); + let curr_reward_cycle = self.get_current_reward_cycle(); + let next_reward_cycle = curr_reward_cycle.saturating_add(1); + let next_reward_cycle_height = self + .running_nodes + .btc_regtest_controller + .get_burnchain() + .reward_cycle_to_block_height(next_reward_cycle); + let next_reward_cycle_reward_set_calculation = next_reward_cycle_height + .saturating_sub(prepare_phase_len) + .saturating_add(1); // +1 as the reward calculation occurs in the SECOND block of the prepare phase/ + + next_reward_cycle_reward_set_calculation.saturating_sub(current_block_height) + } + + fn nmb_blocks_to_reward_cycle_boundary(&mut self, reward_cycle: u64) -> u64 { + let current_block_height = self + .running_nodes + .btc_regtest_controller + .get_headers_height(); + let reward_cycle_height = self + .running_nodes + .btc_regtest_controller + .get_burnchain() + .reward_cycle_to_block_height(reward_cycle); + reward_cycle_height + .saturating_sub(current_block_height) + .saturating_sub(1) + } + + // Only call after already past the epoch 3.0 boundary + fn run_to_dkg(&mut self, timeout: Duration) -> Option { + let curr_reward_cycle = self.get_current_reward_cycle(); + let set_dkg = self + .stacks_client + .get_approved_aggregate_key(curr_reward_cycle) + .expect("Failed to get approved aggregate key") + .expect("No approved aggregate key found"); + let nmb_blocks_to_mine_to_dkg = self.nmb_blocks_to_reward_set_calculation(); + let end_block_height = self + .running_nodes + .btc_regtest_controller + .get_headers_height() + .saturating_add(nmb_blocks_to_mine_to_dkg); + info!("Mining {nmb_blocks_to_mine_to_dkg} Nakamoto block(s) to reach DKG calculation at block height {end_block_height}"); + for i in 1..=nmb_blocks_to_mine_to_dkg { + info!("Mining Nakamoto block #{i} of {nmb_blocks_to_mine_to_dkg}"); + self.mine_nakamoto_block(timeout); + let hash = self.wait_for_validate_ok_response(timeout); + let signatures = self.wait_for_frost_signatures(timeout); + // Verify the signers accepted the proposed block and are using the new DKG to sign it + for signature in &signatures { + assert!(signature.verify(&set_dkg, hash.0.as_slice())); + } + } + if nmb_blocks_to_mine_to_dkg == 0 { + None + } else { + Some(self.wait_for_dkg(timeout)) + } + } + + // Only call after already past the epoch 3.0 boundary + fn run_until_burnchain_height_nakamoto( + &mut self, + timeout: Duration, + burnchain_height: u64, + ) -> Vec { + let mut points = vec![]; + let current_block_height = self + .running_nodes + .btc_regtest_controller + .get_headers_height(); + let mut total_nmb_blocks_to_mine = burnchain_height.saturating_sub(current_block_height); + debug!("Mining {total_nmb_blocks_to_mine} Nakamoto block(s) to reach burnchain height {burnchain_height}"); + let mut nmb_blocks_to_reward_cycle = 0; + let mut blocks_to_dkg = self.nmb_blocks_to_reward_set_calculation(); + while total_nmb_blocks_to_mine > 0 && blocks_to_dkg > 0 { + if blocks_to_dkg > 0 && total_nmb_blocks_to_mine >= blocks_to_dkg { + let dkg = self.run_to_dkg(timeout); + total_nmb_blocks_to_mine -= blocks_to_dkg; + if dkg.is_some() { + points.push(dkg.unwrap()); + } + blocks_to_dkg = 0; + nmb_blocks_to_reward_cycle = self.nmb_blocks_to_reward_cycle_boundary( + self.get_current_reward_cycle().saturating_add(1), + ) + } + if total_nmb_blocks_to_mine >= nmb_blocks_to_reward_cycle { + debug!("Mining {nmb_blocks_to_reward_cycle} Nakamoto block(s) to reach the next reward cycle boundary."); + for i in 1..=nmb_blocks_to_reward_cycle { + debug!("Mining Nakamoto block #{i} of {nmb_blocks_to_reward_cycle}"); + let curr_reward_cycle = self.get_current_reward_cycle(); + let set_dkg = self + .stacks_client + .get_approved_aggregate_key(curr_reward_cycle) + .expect("Failed to get approved aggregate key") + .expect("No approved aggregate key found"); + self.mine_nakamoto_block(timeout); + let hash = self.wait_for_validate_ok_response(timeout); + let signatures = self.wait_for_frost_signatures(timeout); + // Verify the signers accepted the proposed block and are using the new DKG to sign it + for signature in &signatures { + assert!(signature.verify(&set_dkg, hash.0.as_slice())); + } + } + total_nmb_blocks_to_mine -= nmb_blocks_to_reward_cycle; + nmb_blocks_to_reward_cycle = 0; + blocks_to_dkg = self.nmb_blocks_to_reward_set_calculation(); + } + } + for _ in 1..=total_nmb_blocks_to_mine { + let curr_reward_cycle = self.get_current_reward_cycle(); + let set_dkg = self + .stacks_client + .get_approved_aggregate_key(curr_reward_cycle) + .expect("Failed to get approved aggregate key") + .expect("No approved aggregate key found"); + self.mine_nakamoto_block(timeout); + let hash = self.wait_for_validate_ok_response(timeout); + let signatures = self.wait_for_frost_signatures(timeout); + // Verify the signers accepted the proposed block and are using the new DKG to sign it + for signature in &signatures { + assert!(signature.verify(&set_dkg, hash.0.as_slice())); + } + } + points + } + + fn mine_nakamoto_block(&mut self, timeout: Duration) -> MinedNakamotoBlockEvent { + let commits_submitted = self.running_nodes.commits_submitted.clone(); + let mined_block_time = Instant::now(); + next_block_and_mine_commit( + &mut self.running_nodes.btc_regtest_controller, + timeout.as_secs(), + &self.running_nodes.coord_channel, + &commits_submitted, + ) + .unwrap(); + + let t_start = Instant::now(); + while test_observer::get_mined_nakamoto_blocks().is_empty() { + assert!( + t_start.elapsed() < timeout, + "Timed out while waiting for mined nakamoto block event" + ); + thread::sleep(Duration::from_secs(1)); + } + let mined_block_elapsed_time = mined_block_time.elapsed(); + info!( + "Nakamoto block mine time elapsed: {:?}", + mined_block_elapsed_time + ); + test_observer::get_mined_nakamoto_blocks().pop().unwrap() + } + + fn wait_for_validate_ok_response(&mut self, timeout: Duration) -> Sha512Trunc256Sum { + // Wait for the block to show up in the test observer (Don't have to wait long as if we have received a mined block already, + // we know that the signers have already received their block proposal events via their event observers) + let t_start = Instant::now(); + while test_observer::get_proposal_responses().is_empty() { + assert!( + t_start.elapsed() < timeout, + "Timed out while waiting for block proposal event" + ); + thread::sleep(Duration::from_secs(1)); + } + let validate_response = test_observer::get_proposal_responses() + .pop() + .expect("No block proposal"); + match validate_response { + BlockValidateResponse::Ok(block_validated) => block_validated.signer_signature_hash, + _ => panic!("Unexpected response"), + } + } + + fn wait_for_dkg(&mut self, timeout: Duration) -> Point { + debug!("Waiting for DKG..."); + let mut key = Point::default(); + let dkg_now = Instant::now(); + for recv in self.result_receivers.iter() { + let mut aggregate_public_key = None; + loop { + let results = recv + .recv_timeout(timeout) + .expect("failed to recv dkg results"); + for result in results { + match result { + OperationResult::Sign(sig) => { + panic!("Received Signature ({},{})", &sig.R, &sig.z); + } + OperationResult::SignTaproot(proof) => { + panic!("Received SchnorrProof ({},{})", &proof.r, &proof.s); + } + OperationResult::DkgError(dkg_error) => { + panic!("Received DkgError {:?}", dkg_error); + } + OperationResult::SignError(sign_error) => { + panic!("Received SignError {}", sign_error); + } + OperationResult::Dkg(point) => { + info!("Received aggregate_group_key {point}"); + aggregate_public_key = Some(point); + } + } + } + if aggregate_public_key.is_some() || dkg_now.elapsed() > timeout { + break; + } + } + key = aggregate_public_key.expect(&format!( + "Failed to get aggregate public key within {timeout:?}" + )); + } + debug!("Finished waiting for DKG!"); + key + } + + fn wait_for_frost_signatures(&mut self, timeout: Duration) -> Vec { + debug!("Waiting for frost signatures..."); + let mut results = Vec::new(); + let sign_now = Instant::now(); + for recv in self.result_receivers.iter() { + let mut frost_signature = None; + loop { + let results = recv + .recv_timeout(timeout) + .expect("failed to recv signature results"); + for result in results { + match result { + OperationResult::Sign(sig) => { + info!("Received Signature ({},{})", &sig.R, &sig.z); + frost_signature = Some(sig); + } + OperationResult::SignTaproot(proof) => { + panic!("Received SchnorrProof ({},{})", &proof.r, &proof.s); + } + OperationResult::DkgError(dkg_error) => { + panic!("Received DkgError {:?}", dkg_error); + } + OperationResult::SignError(sign_error) => { + panic!("Received SignError {}", sign_error); + } + OperationResult::Dkg(point) => { + panic!("Received aggregate_group_key {point}"); + } + } + } + if frost_signature.is_some() || sign_now.elapsed() > timeout { + break; + } + } + + let frost_signature = frost_signature + .expect(&format!("Failed to get frost signature within {timeout:?}")); + results.push(frost_signature); + } + debug!("Finished waiting for frost signatures!"); + results + } + + fn wait_for_taproot_signatures(&mut self, timeout: Duration) -> Vec { + debug!("Waiting for taproot signatures..."); + let mut results = vec![]; + let sign_now = Instant::now(); + for recv in self.result_receivers.iter() { + let mut schnorr_proof = None; + loop { + let results = recv + .recv_timeout(timeout) + .expect("failed to recv signature results"); + for result in results { + match result { + OperationResult::Sign(sig) => { + panic!("Received Signature ({},{})", &sig.R, &sig.z); + } + OperationResult::SignTaproot(proof) => { + info!("Received SchnorrProof ({},{})", &proof.r, &proof.s); + schnorr_proof = Some(proof); + } + OperationResult::DkgError(dkg_error) => { + panic!("Received DkgError {:?}", dkg_error); + } + OperationResult::SignError(sign_error) => { + panic!("Received SignError {}", sign_error); + } + OperationResult::Dkg(point) => { + panic!("Received aggregate_group_key {point}"); + } + } + } + if schnorr_proof.is_some() || sign_now.elapsed() > timeout { + break; + } + } + let schnorr_proof = schnorr_proof.expect(&format!( + "Failed to get schnorr proof signature within {timeout:?}" + )); + results.push(schnorr_proof); + } + debug!("Finished waiting for taproot signatures!"); + results + } + + fn run_until_epoch_3_boundary(&mut self) { + let epochs = self.running_nodes.conf.burnchain.epochs.clone().unwrap(); + let epoch_3 = + &epochs[StacksEpoch::find_epoch_by_id(&epochs, StacksEpochId::Epoch30).unwrap()]; + + let epoch_30_boundary = epoch_3.start_height - 1; + // advance to epoch 3.0 and trigger a sign round (cannot vote on blocks in pre epoch 3.0) + run_until_burnchain_height( + &mut self.running_nodes.btc_regtest_controller, + &self.running_nodes.blocks_processed, + epoch_30_boundary, + &self.running_nodes.conf, + ); + info!("Advanced to Nakamoto epoch 3.0 boundary {epoch_30_boundary}! Ready to Sign Blocks!"); + } + + fn get_current_reward_cycle(&self) -> u64 { + let block_height = self + .running_nodes + .btc_regtest_controller + .get_headers_height(); + self.running_nodes + .btc_regtest_controller + .get_burnchain() + .block_height_to_reward_cycle(block_height) + .unwrap() + } + + fn get_signer_index(&self, reward_cycle: u64) -> SignerSlotID { + let valid_signer_set = + u32::try_from(reward_cycle % 2).expect("FATAL: reward_cycle % 2 exceeds u32::MAX"); + let signer_stackerdb_contract_id = boot_code_id(SIGNERS_NAME, false); + + self.stacks_client + .get_stackerdb_signer_slots(&signer_stackerdb_contract_id, valid_signer_set) + .expect("FATAL: failed to get signer slots from stackerdb") + .iter() + .position(|(address, _)| address == self.stacks_client.get_signer_address()) + .map(|pos| { + SignerSlotID(u32::try_from(pos).expect("FATAL: number of signers exceeds u32::MAX")) + }) + .expect("FATAL: signer not registered") + } + + fn generate_invalid_transactions(&self) -> Vec { + let host = self + .running_nodes + .conf + .node + .rpc_bind + .to_socket_addrs() + .unwrap() + .next() + .unwrap(); + // Get the signer indices + let reward_cycle = self.get_current_reward_cycle(); + + let signer_private_key = self.signer_stacks_private_keys[0]; + + let vote_contract_id = boot_code_id(SIGNERS_VOTING_NAME, false); + let contract_addr = vote_contract_id.issuer.into(); + let contract_name = vote_contract_id.name.clone(); + + let signer_index = thread_rng().next_u64(); + let signer_index_arg = Value::UInt(signer_index as u128); + + let point = Point::from(Scalar::random(&mut thread_rng())); + let point_arg = + Value::buff_from(point.compress().data.to_vec()).expect("Failed to create buff"); + + let round = thread_rng().next_u64(); + let round_arg = Value::UInt(round as u128); + + let reward_cycle_arg = Value::UInt(reward_cycle as u128); + let valid_function_args = vec![ + signer_index_arg.clone(), + point_arg.clone(), + round_arg.clone(), + reward_cycle_arg.clone(), + ]; + + // Create a invalid transaction that is not a contract call + let invalid_not_contract_call = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: CHAIN_ID_TESTNET, + auth: TransactionAuth::from_p2pkh(&signer_private_key).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::SmartContract( + TransactionSmartContract { + name: "test-contract".into(), + code_body: StacksString::from_str("(/ 1 0)").unwrap(), + }, + None, + ), + }; + let invalid_contract_address = StacksClient::build_signed_contract_call_transaction( + &StacksAddress::p2pkh(false, &StacksPublicKey::from_private(&signer_private_key)), + contract_name.clone(), + SIGNERS_VOTING_FUNCTION_NAME.into(), + &valid_function_args, + &signer_private_key, + TransactionVersion::Testnet, + CHAIN_ID_TESTNET, + 1, + 10, + ) + .unwrap(); + + let invalid_contract_name = StacksClient::build_signed_contract_call_transaction( + &contract_addr, + "bad-signers-contract-name".into(), + SIGNERS_VOTING_FUNCTION_NAME.into(), + &valid_function_args, + &signer_private_key, + TransactionVersion::Testnet, + CHAIN_ID_TESTNET, + 1, + 10, + ) + .unwrap(); + + let invalid_signers_vote_function = StacksClient::build_signed_contract_call_transaction( + &contract_addr, + contract_name.clone(), + "some-other-function".into(), + &valid_function_args, + &signer_private_key, + TransactionVersion::Testnet, + CHAIN_ID_TESTNET, + 1, + 10, + ) + .unwrap(); + + let invalid_function_arg_signer_index = + StacksClient::build_signed_contract_call_transaction( + &contract_addr, + contract_name.clone(), + SIGNERS_VOTING_FUNCTION_NAME.into(), + &[ + point_arg.clone(), + point_arg.clone(), + round_arg.clone(), + reward_cycle_arg.clone(), + ], + &signer_private_key, + TransactionVersion::Testnet, + CHAIN_ID_TESTNET, + 1, + 10, + ) + .unwrap(); + + let invalid_function_arg_key = StacksClient::build_signed_contract_call_transaction( + &contract_addr, + contract_name.clone(), + SIGNERS_VOTING_FUNCTION_NAME.into(), + &[ + signer_index_arg.clone(), + signer_index_arg.clone(), + round_arg.clone(), + reward_cycle_arg.clone(), + ], + &signer_private_key, + TransactionVersion::Testnet, + CHAIN_ID_TESTNET, + 1, + 10, + ) + .unwrap(); + + let invalid_function_arg_round = StacksClient::build_signed_contract_call_transaction( + &contract_addr, + contract_name.clone(), + SIGNERS_VOTING_FUNCTION_NAME.into(), + &[ + signer_index_arg.clone(), + point_arg.clone(), + point_arg.clone(), + reward_cycle_arg.clone(), + ], + &signer_private_key, + TransactionVersion::Testnet, + CHAIN_ID_TESTNET, + 1, + 10, + ) + .unwrap(); + + let invalid_function_arg_reward_cycle = + StacksClient::build_signed_contract_call_transaction( + &contract_addr, + contract_name.clone(), + SIGNERS_VOTING_FUNCTION_NAME.into(), + &[ + signer_index_arg.clone(), + point_arg.clone(), + round_arg.clone(), + point_arg.clone(), + ], + &signer_private_key, + TransactionVersion::Testnet, + CHAIN_ID_TESTNET, + 1, + 10, + ) + .unwrap(); + + let invalid_nonce = StacksClient::build_signed_contract_call_transaction( + &contract_addr, + contract_name.clone(), + SIGNERS_VOTING_FUNCTION_NAME.into(), + &valid_function_args, + &signer_private_key, + TransactionVersion::Testnet, + CHAIN_ID_TESTNET, + 0, // Old nonce + 10, + ) + .unwrap(); + + let invalid_stacks_client = StacksClient::new( + StacksPrivateKey::new(), + host, + "12345".to_string(), // That's amazing. I've got the same combination on my luggage! + false, + ); + let invalid_signer_tx = invalid_stacks_client + .build_vote_for_aggregate_public_key(0, round, point, reward_cycle, None, 0) + .expect("FATAL: failed to build vote for aggregate public key"); + + vec![ + invalid_nonce, + invalid_not_contract_call, + invalid_contract_name, + invalid_contract_address, + invalid_signers_vote_function, + invalid_function_arg_key, + invalid_function_arg_reward_cycle, + invalid_function_arg_round, + invalid_function_arg_signer_index, + invalid_signer_tx, + ] + } + fn shutdown(self) { self.running_nodes .coord_channel @@ -172,14 +764,11 @@ impl SignerTest { self.running_nodes .run_loop_stopper .store(false, Ordering::SeqCst); - - self.running_nodes.run_loop_thread.join().unwrap(); - // Stop the signers - for (_id, signer) in self.running_signers { + // Stop the signers before the node to prevent hanging + for signer in self.running_signers { assert!(signer.stop().is_none()); } - // Stop the coordinator - assert!(self.running_coordinator.stop().is_none()); + self.running_nodes.run_loop_thread.join().unwrap(); } } @@ -188,30 +777,24 @@ fn spawn_signer( receiver: Receiver, sender: Sender>, ) -> RunningSigner> { - let config = stacks_signer::config::Config::load_from_str(data).unwrap(); - let is_mainnet = config.network.is_mainnet(); - let ev = SignerEventReceiver::new(vec![config.stackerdb_contract_id.clone()], is_mainnet); - let runloop: stacks_signer::runloop::RunLoop> = - stacks_signer::runloop::RunLoop::from(&config); + let config = SignerConfig::load_from_str(data).unwrap(); + let ev = SignerEventReceiver::new(config.network.is_mainnet()); + let endpoint = config.endpoint; + let runloop: stacks_signer::runloop::RunLoop = stacks_signer::runloop::RunLoop::from(config); let mut signer: Signer< RunLoopCommand, Vec, - stacks_signer::runloop::RunLoop>, + stacks_signer::runloop::RunLoop, SignerEventReceiver, > = Signer::new(runloop, ev, receiver, sender); - let endpoint = config.endpoint; - info!( - "Spawning signer {} on endpoint {}", - config.signer_id, endpoint - ); + info!("Spawning signer on endpoint {}", endpoint); signer.spawn(endpoint).unwrap() } fn setup_stx_btc_node( mut naka_conf: NeonConfig, - num_signers: u32, signer_stacks_private_keys: &[StacksPrivateKey], - signer_config_tomls: &Vec, + signer_config_tomls: &[String], ) -> RunningNodes { // Spawn the endpoints for observing signers for toml in signer_config_tomls { @@ -228,16 +811,20 @@ fn setup_stx_btc_node( let observer_port = test_observer::EVENT_OBSERVER_PORT; naka_conf.events_observers.insert(EventObserverConfig { endpoint: format!("localhost:{observer_port}"), - events_keys: vec![EventKeyType::StackerDBChunks, EventKeyType::BlockProposal], + events_keys: vec![ + EventKeyType::StackerDBChunks, + EventKeyType::BlockProposal, + EventKeyType::MinedBlocks, + ], }); // The signers need some initial balances in order to pay for epoch 2.5 transaction votes let mut initial_balances = Vec::new(); // TODO: separate keys for stacking and signing (because they'll be different in prod) - for i in 0..num_signers { + for key in signer_stacks_private_keys { initial_balances.push(InitialBalance { - address: to_addr(&signer_stacks_private_keys[i as usize]).into(), + address: to_addr(key).into(), amount: POX_4_DEFAULT_STACKER_BALANCE, }); } @@ -255,7 +842,6 @@ fn setup_stx_btc_node( } } } - info!("Make new BitcoinCoreController"); let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone()); btcd_controller @@ -299,24 +885,14 @@ fn setup_stx_btc_node( info!("Mine third block..."); next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); - info!("Boot to epoch 3.0 to activate pox-4..."); - boot_to_epoch_3( - &naka_conf, - &blocks_processed, - signer_stacks_private_keys, - signer_stacks_private_keys, - &mut btc_regtest_controller, - ); - - info!("Pox 4 activated and ready for signers to perform DKG and sign!"); RunningNodes { btcd_controller, btc_regtest_controller, run_loop_thread, run_loop_stopper, - vrfs_submitted, - commits_submitted, - blocks_processed, + vrfs_submitted: vrfs_submitted.0, + commits_submitted: commits_submitted.0, + blocks_processed: blocks_processed.0, coord_channel, conf: naka_conf, } @@ -325,8 +901,57 @@ fn setup_stx_btc_node( #[test] #[ignore] /// Test the signer can respond to external commands to perform DKG -/// and sign a block with both taproot and non-taproot signatures -fn stackerdb_dkg_sign() { +fn stackerdb_dkg() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::from_default_env()) + .init(); + + info!("------------------------- Test Setup -------------------------"); + let timeout = Duration::from_secs(200); + let mut signer_test = SignerTest::new(10); + info!("Boot to epoch 3.0 reward calculation..."); + boot_to_epoch_3_reward_set( + &signer_test.running_nodes.conf, + &signer_test.running_nodes.blocks_processed, + &signer_test.signer_stacks_private_keys, + &signer_test.signer_stacks_private_keys, + &mut signer_test.running_nodes.btc_regtest_controller, + ); + + info!("Pox 4 activated and at epoch 3.0 reward set calculation (2nd block of its prepare phase)! Ready for signers to perform DKG and Sign!"); + // First wait for the automatically triggered DKG to complete + let key = signer_test.wait_for_dkg(timeout); + + info!("------------------------- Test DKG -------------------------"); + let reward_cycle = signer_test.get_current_reward_cycle().saturating_add(1); + + // Determine the coordinator of the current node height + info!("signer_runloop: spawn send commands to do dkg"); + let dkg_now = Instant::now(); + for sender in signer_test.signer_cmd_senders.iter() { + sender + .send(RunLoopCommand { + reward_cycle, + command: SignerCommand::Dkg, + }) + .expect("failed to send DKG command"); + } + let new_key = signer_test.wait_for_dkg(timeout); + let dkg_elapsed = dkg_now.elapsed(); + assert_ne!(new_key, key); + + info!("DKG Time Elapsed: {:.2?}", dkg_elapsed); +} + +#[test] +#[ignore] +/// Test the signer can respond to external commands to perform DKG +fn stackerdb_sign() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; } @@ -366,126 +991,100 @@ fn stackerdb_dkg_sign() { }; block.header.tx_merkle_root = tx_merkle_root; - // The block is invalid so the signers should return a signature across its hash + b'n' - let mut msg = block.header.signer_signature_hash().0.to_vec(); - msg.push(b'n'); + // The block is invalid so the signers should return a signature across a rejection + let block_vote = NakamotoBlockVote { + signer_signature_hash: block.header.signer_signature_hash(), + rejected: true, + }; + let msg = block_vote.serialize_to_vec(); - let signer_test = SignerTest::new(10, 400); - - info!("------------------------- Test DKG -------------------------"); - info!("signer_runloop: spawn send commands to do dkg"); - let dkg_now = Instant::now(); - signer_test - .coordinator_cmd_sender - .send(RunLoopCommand::Dkg) - .expect("failed to send Dkg command"); - let mut key = Point::default(); - for recv in signer_test.result_receivers.iter() { - let mut aggregate_public_key = None; - loop { - let results = recv - .recv_timeout(Duration::from_secs(30)) - .expect("failed to recv dkg results"); - for result in results { - match result { - OperationResult::Sign(sig) => { - panic!("Received Signature ({},{})", &sig.R, &sig.z); - } - OperationResult::SignTaproot(proof) => { - panic!("Received SchnorrProof ({},{})", &proof.r, &proof.s); - } - OperationResult::DkgError(dkg_error) => { - panic!("Received DkgError {:?}", dkg_error); - } - OperationResult::SignError(sign_error) => { - panic!("Received SignError {}", sign_error); - } - OperationResult::Dkg(point) => { - info!("Received aggregate_group_key {point}"); - aggregate_public_key = Some(point); - } - } - } - if aggregate_public_key.is_some() || dkg_now.elapsed() > Duration::from_secs(100) { - break; - } - } - key = aggregate_public_key.expect("Failed to get aggregate public key within 100 seconds"); - } - let dkg_elapsed = dkg_now.elapsed(); + let timeout = Duration::from_secs(200); + let mut signer_test = SignerTest::new(10); + let key = signer_test.boot_to_epoch_3(timeout); info!("------------------------- Test Sign -------------------------"); + let reward_cycle = signer_test.get_current_reward_cycle(); + // Determine the coordinator of the current node height + info!("signer_runloop: spawn send commands to do sign"); let sign_now = Instant::now(); - info!("signer_runloop: spawn send commands to do dkg and then sign"); - signer_test - .coordinator_cmd_sender - .send(RunLoopCommand::Sign { + let sign_command = RunLoopCommand { + reward_cycle, + command: SignerCommand::Sign { block: block.clone(), is_taproot: false, merkle_root: None, - }) - .expect("failed to send non taproot Sign command"); - signer_test - .coordinator_cmd_sender - .send(RunLoopCommand::Sign { - block, + }, + }; + let sign_taproot_command = RunLoopCommand { + reward_cycle, + command: SignerCommand::Sign { + block: block.clone(), is_taproot: true, merkle_root: None, - }) - .expect("failed to send taproot Sign command"); - for recv in signer_test.result_receivers.iter() { - let mut frost_signature = None; - let mut schnorr_proof = None; - loop { - let results = recv - .recv_timeout(Duration::from_secs(30)) - .expect("failed to recv signature results"); - for result in results { - match result { - OperationResult::Sign(sig) => { - info!("Received Signature ({},{})", &sig.R, &sig.z); - frost_signature = Some(sig); - } - OperationResult::SignTaproot(proof) => { - info!("Received SchnorrProof ({},{})", &proof.r, &proof.s); - schnorr_proof = Some(proof); - } - OperationResult::DkgError(dkg_error) => { - panic!("Received DkgError {:?}", dkg_error); - } - OperationResult::SignError(sign_error) => { - panic!("Received SignError {}", sign_error); - } - OperationResult::Dkg(point) => { - panic!("Received aggregate_group_key {point}"); - } - } - } - if (frost_signature.is_some() && schnorr_proof.is_some()) - || sign_now.elapsed() > Duration::from_secs(100) - { - break; - } - } - let frost_signature = - frost_signature.expect("Failed to get frost signature within 100 seconds"); + }, + }; + for sender in signer_test.signer_cmd_senders.iter() { + sender + .send(sign_command.clone()) + .expect("failed to send sign command"); + sender + .send(sign_taproot_command.clone()) + .expect("failed to send sign taproot command"); + } + let frost_signatures = signer_test.wait_for_frost_signatures(timeout); + let schnorr_proofs = signer_test.wait_for_taproot_signatures(timeout); + + for frost_signature in frost_signatures { + assert!(frost_signature.verify(&key, &msg)); + } + for schnorr_proof in schnorr_proofs { + let tweaked_key = tweaked_public_key(&key, None); assert!( - frost_signature.verify(&key, msg.as_slice()), - "Signature verification failed" - ); - let schnorr_proof = - schnorr_proof.expect("Failed to get schnorr proof signature within 100 seconds"); - let tweaked_key = wsts::compute::tweaked_public_key(&key, None); - assert!( - schnorr_proof.verify(&tweaked_key.x(), &msg.as_slice()), + schnorr_proof.verify(&tweaked_key.x(), &msg), "Schnorr proof verification failed" ); } let sign_elapsed = sign_now.elapsed(); - info!("DKG Time Elapsed: {:.2?}", dkg_elapsed); + info!("------------------------- Test Block Accepted -------------------------"); + + // Verify the signers rejected the proposed block + let t_start = Instant::now(); + let mut chunk = None; + while chunk.is_none() { + assert!( + t_start.elapsed() < Duration::from_secs(30), + "Timed out while waiting for signers block response stacker db event" + ); + + let nakamoto_blocks = test_observer::get_stackerdb_chunks(); + for event in nakamoto_blocks { + // Only care about the miners block slot + if event.contract_id.name == format!("signers-1-{}", BLOCK_MSG_ID).as_str().into() + || event.contract_id.name == format!("signers-0-{}", BLOCK_MSG_ID).as_str().into() + { + for slot in event.modified_slots { + chunk = Some(slot.data); + break; + } + if chunk.is_some() { + break; + } + } + } + thread::sleep(Duration::from_secs(1)); + } + let chunk = chunk.unwrap(); + let signer_message = read_next::(&mut &chunk[..]).unwrap(); + if let SignerMessage::BlockResponse(BlockResponse::Rejected(rejection)) = signer_message { + assert!(matches!( + rejection.reason_code, + RejectCode::ValidationFailed(_) + )); + } else { + panic!("Received unexpected message: {:?}", &signer_message); + } info!("Sign Time Elapsed: {:.2?}", sign_elapsed); - signer_test.shutdown(); } #[test] @@ -494,19 +1093,19 @@ fn stackerdb_dkg_sign() { /// /// Test Setup: /// The test spins up five stacks signers, one miner Nakamoto node, and a corresponding bitcoind. -/// The stacks node is advanced to epoch 3.0. and signers perform a DKG round (this should be removed -/// once we have proper casting of the vote during epoch 2.5). +/// The stacks node is advanced to epoch 2.5. forcibly triggering DKG to set the key correctly +/// The stacks node is next advanced to epoch 3.0 boundary to allow block signing. /// /// Test Execution: -/// The node attempts to mine a Nakamoto tenure, sending a block to the observing signers via the +/// The node attempts to mine a Nakamoto block, sending a block to the observing signers via the /// .miners stacker db instance. The signers submit the block to the stacks node for verification. /// Upon receiving a Block Validation response approving the block, the signers perform a signing -/// round across its signature hash. +/// round across its signature hash and return it back to the miner. /// /// Test Assertion: /// Signers return an operation result containing a valid signature across the miner's Nakamoto block's signature hash. /// Signers broadcasted a signature across the miner's proposed block back to the respective .signers-XXX-YYY contract. -/// TODO: update test to check miner received the signed block and appended it to the chain +/// Miner appends the signature to the block and finishes mininig it. fn stackerdb_block_proposal() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; @@ -518,126 +1117,33 @@ fn stackerdb_block_proposal() { .init(); info!("------------------------- Test Setup -------------------------"); - let mut signer_test = SignerTest::new(5, 5); + let mut signer_test = SignerTest::new(5); + let timeout = Duration::from_secs(200); + let short_timeout = Duration::from_secs(30); - // First run DKG in order to sign the block that arrives from the miners following a nakamoto block production - // TODO: remove this forcibly running DKG once we have casting of the vote automagically happening during epoch 2.5 - info!("signer_runloop: spawn send commands to do dkg"); - signer_test - .coordinator_cmd_sender - .send(RunLoopCommand::Dkg) - .expect("failed to send Dkg command"); - let mut aggregate_public_key = None; - let recv = signer_test - .result_receivers - .last() - .expect("Failed to get coordinator recv"); - let results = recv - .recv_timeout(Duration::from_secs(30)) - .expect("failed to recv dkg results"); - for result in results { - match result { - OperationResult::Dkg(point) => { - info!("Received aggregate_group_key {point}"); - aggregate_public_key = Some(point); - break; - } - _ => { - panic!("Received Unexpected result"); - } - } - } - let aggregate_public_key = aggregate_public_key.expect("Failed to get aggregate public key"); + let key = signer_test.boot_to_epoch_3(timeout); + signer_test.mine_nakamoto_block(timeout); - let (vrfs_submitted, commits_submitted) = ( - signer_test.running_nodes.vrfs_submitted.clone(), - signer_test.running_nodes.commits_submitted.clone(), - ); + info!("------------------------- Test Block Proposal -------------------------"); + // Verify that the signers accepted the proposed block, sending back a validate ok response + let proposed_signer_signature_hash = signer_test.wait_for_validate_ok_response(short_timeout); - info!("Mining a Nakamoto tenure..."); - - // first block wakes up the run loop, wait until a key registration has been submitted. - next_block_and( - &mut signer_test.running_nodes.btc_regtest_controller, - 60, - || { - let vrf_count = vrfs_submitted.load(Ordering::SeqCst); - Ok(vrf_count >= 1) - }, - ) - .unwrap(); - - // second block should confirm the VRF register, wait until a block commit is submitted - next_block_and( - &mut signer_test.running_nodes.btc_regtest_controller, - 60, - || { - let commits_count = commits_submitted.load(Ordering::SeqCst); - Ok(commits_count >= 1) - }, - ) - .unwrap(); - - // Mine 1 nakamoto tenure - next_block_and_mine_commit( - &mut signer_test.running_nodes.btc_regtest_controller, - 60, - &signer_test.running_nodes.coord_channel, - &commits_submitted, - ) - .unwrap(); - - info!("------------------------- Test Block Processed -------------------------"); - let recv = signer_test - .result_receivers - .last() - .expect("Failed to retreive coordinator recv"); - let results = recv - .recv_timeout(Duration::from_secs(30)) - .expect("failed to recv signature results"); - let mut signature = None; - for result in results { - match result { - OperationResult::Sign(sig) => { - info!("Received Signature ({},{})", &sig.R, &sig.z); - signature = Some(sig); - break; - } - _ => { - panic!("Unexpected operation result"); - } - } - } - let signature = signature.expect("Failed to get signature"); - // Wait for the block to show up in the test observer (Don't have to wait long as if we have received a signature, - // we know that the signers have already received their block proposal events via their event observers) - let t_start = Instant::now(); - while test_observer::get_proposal_responses().is_empty() { + info!("------------------------- Test Block Signed -------------------------"); + // Verify that the signers signed the proposed block + let frost_signatures = signer_test.wait_for_frost_signatures(short_timeout); + for signature in &frost_signatures { assert!( - t_start.elapsed() < Duration::from_secs(30), - "Timed out while waiting for block proposal event" + signature.verify(&key, proposed_signer_signature_hash.0.as_slice()), + "Signature verification failed" ); - thread::sleep(Duration::from_secs(1)); } - let validate_responses = test_observer::get_proposal_responses(); - let proposed_signer_signature_hash = - match validate_responses.first().expect("No block proposal") { - BlockValidateResponse::Ok(block_validated) => block_validated.signer_signature_hash, - _ => panic!("Unexpected response"), - }; - assert!( - signature.verify( - &aggregate_public_key, - proposed_signer_signature_hash.0.as_slice() - ), - "Signature verification failed" - ); + info!("------------------------- Test Signers Broadcast Block -------------------------"); // Verify that the signers broadcasted a signed NakamotoBlock back to the .signers contract let t_start = Instant::now(); let mut chunk = None; while chunk.is_none() { assert!( - t_start.elapsed() < Duration::from_secs(30), + t_start.elapsed() < short_timeout, "Timed out while waiting for signers block response stacker db event" ); @@ -668,7 +1174,10 @@ fn stackerdb_block_proposal() { ))) = signer_message { assert_eq!(block_signer_signature_hash, proposed_signer_signature_hash); - assert_eq!(block_signature, ThresholdSignature(signature)); + assert_eq!( + block_signature, + ThresholdSignature(frost_signatures.first().expect("No signature").clone()) + ); } else { panic!("Received unexpected message"); } @@ -677,22 +1186,19 @@ fn stackerdb_block_proposal() { #[test] #[ignore] -/// Test that signers will reject a miners block proposal if it is missing expected transactions +/// Test that signers can handle a transition between Nakamoto reward cycles /// /// Test Setup: /// The test spins up five stacks signers, one miner Nakamoto node, and a corresponding bitcoind. -/// The stacks node is advanced to epoch 3.0. and signers perform a DKG round (this should be removed -/// once we have proper casting of the vote during epoch 2.5). +/// The stacks node is advanced to epoch 2.5, triggering a DKG round. The stacks node is then advanced +/// to Epoch 3.0 boundary to allow block signing. /// /// Test Execution: -/// The node attempts to mine a Nakamoto tenure, sending a block to the observing signers via the -/// .miners stacker db instance. The signers submit the block to the stacks node for verification. -/// Upon receiving a Block Validation response approving the block, the signers verify that it contains -/// all expected transactions. As it does not, the signers reject the block and do not sign it. +/// The node mines 2 full Nakamoto reward cycles, sending blocks to observing signers to sign and return. /// /// Test Assertion: -/// Signers broadcast rejections with the list of missing transactions back to the miners stackerdb instance -fn stackerdb_block_proposal_missing_transactions() { +/// Signers can perform DKG and sign blocks across Nakamoto reward cycles. +fn stackerdb_mine_2_nakamoto_reward_cycles() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; } @@ -703,217 +1209,143 @@ fn stackerdb_block_proposal_missing_transactions() { .init(); info!("------------------------- Test Setup -------------------------"); - let mut signer_test = SignerTest::new(5, 5); + let nmb_reward_cycles = 2; + let mut signer_test = SignerTest::new(5); + let timeout = Duration::from_secs(200); + let first_dkg = signer_test.boot_to_epoch_3(timeout); + let curr_reward_cycle = signer_test.get_current_reward_cycle(); + // Mine 2 full Nakamoto reward cycles (epoch 3 starts in the middle of one, hence the + 1) + let next_reward_cycle = curr_reward_cycle.saturating_add(1); + let final_reward_cycle = next_reward_cycle.saturating_add(nmb_reward_cycles); + let final_reward_cycle_height_boundary = signer_test + .running_nodes + .btc_regtest_controller + .get_burnchain() + .reward_cycle_to_block_height(final_reward_cycle) + .saturating_sub(1); - let host = signer_test - .running_nodes - .conf - .node - .rpc_bind - .to_socket_addrs() - .unwrap() - .next() - .unwrap(); - let signer_stacker_db_1 = signer_test - .running_nodes - .conf - .node - .stacker_dbs - .iter() - .find(|id| { - id.name.to_string() == NakamotoSigners::make_signers_db_name(1, TRANSACTIONS_MSG_ID) - }) - .unwrap() + info!("------------------------- Test Mine 2 Nakamoto Reward Cycles -------------------------"); + let dkgs = signer_test + .run_until_burnchain_height_nakamoto(timeout, final_reward_cycle_height_boundary); + assert_eq!(dkgs.len() as u64, nmb_reward_cycles.saturating_add(1)); // We will have mined the DKG vote for the following reward cycle + let last_dkg = dkgs + .last() + .expect(&format!( + "Failed to reach DKG for reward cycle {final_reward_cycle_height_boundary}" + )) .clone(); + assert_ne!(first_dkg, last_dkg); - let signer_id = 0; - - let signer_addresses_1: Vec<_> = signer_test + let set_dkg = signer_test .stacks_client - .get_stackerdb_signer_slots(&boot_code_id(SIGNERS_NAME, false), 1) - .unwrap() - .into_iter() - .map(|(address, _)| address) - .collect(); + .get_approved_aggregate_key(final_reward_cycle) + .expect("Failed to get approved aggregate key") + .expect("No approved aggregate key found"); + assert_eq!(set_dkg, last_dkg); - let signer_address_1 = signer_addresses_1.get(signer_id).cloned().unwrap(); + let current_burnchain_height = signer_test + .running_nodes + .btc_regtest_controller + .get_headers_height(); + assert_eq!(current_burnchain_height, final_reward_cycle_height_boundary); + signer_test.shutdown(); +} - let signer_private_key_1 = signer_test +#[test] +#[ignore] +/// Test that signers will accept a miners block proposal and sign it if it contains all expected transactions, +/// filtering invalid transactions from the block requirements +/// +/// Test Setup: +/// The test spins up five stacks signers, one miner Nakamoto node, and a corresponding bitcoind. +/// The stacks node is advanced to epoch 2.5, triggering a DKG round. The stacks node is then advanced +/// to Epoch 3.0 boundary to allow block signing. It then advances to the prepare phase of the next reward cycle +/// to enable Nakamoto signers to look at the next signer transactions to compare against a proposed block. +/// +/// Test Execution: +/// The node attempts to mine a Nakamoto tenure, sending a block to the observing signers via the +/// .miners stacker db instance. The signers submit the block to the stacks node for verification. +/// Upon receiving a Block Validation response approving the block, the signers verify that it contains +/// all of the NEXT signers' expected transactions, being sure to filter out any invalid transactions +/// from stackerDB as well. +/// +/// Test Assertion: +/// Miner proposes a block to the signers containing all expected transactions. +/// Signers broadcast block approval with a signature back to the waiting miner. +/// Miner includes the signers' signature in the block and finishes mining it. +fn stackerdb_filter_bad_transactions() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::from_default_env()) + .init(); + + info!("------------------------- Test Setup -------------------------"); + // Advance to the prepare phase of a post epoch 3.0 reward cycle to force signers to look at the next signer transactions to compare against a proposed block + let mut signer_test = SignerTest::new(5); + let timeout = Duration::from_secs(200); + let current_signers_dkg = signer_test.boot_to_epoch_3(timeout); + let next_signers_dkg = signer_test + .run_to_dkg(timeout) + .expect("Failed to run to DKG"); + assert_ne!(current_signers_dkg, next_signers_dkg); + + info!("------------------------- Submit Invalid Transactions -------------------------"); + + let signer_private_key = signer_test .signer_stacks_private_keys .iter() .find(|pk| { - let addr = StacksAddress::p2pkh(false, &StacksPublicKey::from_private(pk)); - addr == signer_address_1 + let addr = to_addr(pk); + addr == *signer_test.stacks_client.get_signer_address() }) .cloned() .expect("Cannot find signer private key for signer id 1"); - - let mut stackerdb_1 = StackerDB::new(host, signer_stacker_db_1, signer_private_key_1, 0); - - stackerdb_1.set_signer_set(1); - - debug!("Signer address is {}", &signer_address_1); - assert_eq!( - signer_address_1, - StacksAddress::p2pkh(false, &StacksPublicKey::from_private(&signer_private_key_1)) + let next_reward_cycle = signer_test.get_current_reward_cycle().saturating_add(1); + // Must submit to the NEXT reward cycle slots as they are the ones looked at by the CURRENT miners + let signer_index = signer_test.get_signer_index(next_reward_cycle); + let mut stackerdb = StackerDB::new( + &signer_test.running_nodes.conf.node.rpc_bind, + signer_private_key, + false, + next_reward_cycle, + signer_index, ); - // Create a valid transaction signed by the signer private key coresponding to the slot into which it is being inserted (signer id 0) - let mut valid_tx = StacksTransaction { - version: TransactionVersion::Testnet, - chain_id: 0x80000000, - auth: TransactionAuth::from_p2pkh(&signer_private_key_1).unwrap(), - anchor_mode: TransactionAnchorMode::Any, - post_condition_mode: TransactionPostConditionMode::Allow, - post_conditions: vec![], - payload: TransactionPayload::SmartContract( - TransactionSmartContract { - name: "test-contract".into(), - code_body: StacksString::from_str("(/ 1 0)").unwrap(), - }, - None, - ), - }; - valid_tx.set_origin_nonce(2); - - // Create a transaction signed by a different private key - // This transaction will be invalid as it is signed by a non signer private key - let invalid_signer_private_key = StacksPrivateKey::new(); debug!( - "Invalid address is {}", - &StacksAddress::p2pkh( - false, - &StacksPublicKey::from_private(&invalid_signer_private_key) - ) + "Signer address is {}", + &signer_test.stacks_client.get_signer_address() ); - let mut invalid_tx = StacksTransaction { - version: TransactionVersion::Testnet, - chain_id: 0, - auth: TransactionAuth::from_p2pkh(&invalid_signer_private_key).unwrap(), - anchor_mode: TransactionAnchorMode::Any, - post_condition_mode: TransactionPostConditionMode::Allow, - post_conditions: vec![], - payload: TransactionPayload::SmartContract( - TransactionSmartContract { - name: "test-contract".into(), - code_body: StacksString::from_str("(/ 1 0)").unwrap(), - }, - None, - ), - }; - invalid_tx.set_origin_nonce(0); - // First run DKG in order to sign the block that arrives from the miners following a nakamoto block production - // TODO: remove this forcibly running DKG once we have casting of the vote automagically happening during epoch 2.5 - info!("signer_runloop: spawn send commands to do dkg"); - signer_test - .coordinator_cmd_sender - .send(RunLoopCommand::Dkg) - .expect("failed to send Dkg command"); - let recv = signer_test - .result_receivers - .last() - .expect("Failed to get coordinator recv"); - let results = recv - .recv_timeout(Duration::from_secs(30)) - .expect("failed to recv dkg results"); - for result in results { - match result { - OperationResult::Dkg(point) => { - info!("Received aggregate_group_key {point}"); - break; - } - _ => { - panic!("Received Unexpected result"); - } - } + let invalid_txs = signer_test.generate_invalid_transactions(); + let invalid_txids: HashSet = invalid_txs.iter().map(|tx| tx.txid()).collect(); + + // Submit transactions to stackerdb for the signers and miners to pick up during block verification + stackerdb + .send_message_with_retry(SignerMessage::Transactions(invalid_txs)) + .expect("Failed to write expected transactions to stackerdb"); + + info!("------------------------- Verify Nakamoto Block Mined -------------------------"); + let mined_block_event = signer_test.mine_nakamoto_block(timeout); + let hash = signer_test.wait_for_validate_ok_response(timeout); + let signatures = signer_test.wait_for_frost_signatures(timeout); + // Verify the signers accepted the proposed block and are using the previously determined dkg to sign it + for signature in &signatures { + assert!(signature.verify(¤t_signers_dkg, hash.0.as_slice())); } - - // Following stacker DKG, submit transactions to stackerdb for the signers to pick up during block verification - stackerdb_1 - .send_message_with_retry(SignerMessage::Transactions(vec![ - valid_tx.clone(), - invalid_tx.clone(), - ])) - .expect("Failed to write expected transactions to stackerdb_1"); - - let (vrfs_submitted, commits_submitted) = ( - signer_test.running_nodes.vrfs_submitted.clone(), - signer_test.running_nodes.commits_submitted.clone(), - ); - - info!("------------------------- Test Block Rejected -------------------------"); - - info!("Mining a Nakamoto tenure..."); - - // first block wakes up the run loop, wait until a key registration has been submitted. - next_block_and( - &mut signer_test.running_nodes.btc_regtest_controller, - 60, - || { - let vrf_count = vrfs_submitted.load(Ordering::SeqCst); - Ok(vrf_count >= 1) - }, - ) - .unwrap(); - - // second block should confirm the VRF register, wait until a block commit is submitted - next_block_and( - &mut signer_test.running_nodes.btc_regtest_controller, - 60, - || { - let commits_count = commits_submitted.load(Ordering::SeqCst); - Ok(commits_count >= 1) - }, - ) - .unwrap(); - - // Mine 1 nakamoto tenure - next_block_and_mine_commit( - &mut signer_test.running_nodes.btc_regtest_controller, - 60, - &signer_test.running_nodes.coord_channel, - &commits_submitted, - ) - .unwrap(); - - // Verify that the signers broadcasted a series of rejections with missing transactions back to the miner - let t_start = Instant::now(); - let mut chunk = None; - while chunk.is_none() { + for tx_event in &mined_block_event.tx_events { + let TransactionEvent::Success(tx_success) = tx_event else { + panic!("Received unexpected transaction event"); + }; + // Since we never broadcast the "invalid" transaction to the mempool and the transaction did not come from a signer or had an invalid nonce + // the miner should never construct a block that contains them and signers should still approve it assert!( - t_start.elapsed() < Duration::from_secs(30), - "Timed out while waiting for signers block response stacker db event" + !invalid_txids.contains(&tx_success.txid), + "Miner included an invalid transaction in the block" ); - - let nakamoto_blocks = test_observer::get_stackerdb_chunks(); - for event in nakamoto_blocks { - // Only care about the miners block slot - if event.contract_id.name == format!("signers-1-{}", BLOCK_MSG_ID).as_str().into() - || event.contract_id.name == format!("signers-0-{}", BLOCK_MSG_ID).as_str().into() - { - for slot in event.modified_slots { - chunk = Some(slot.data); - break; - } - if chunk.is_some() { - break; - } - } - } - thread::sleep(Duration::from_secs(1)); - } - let chunk = chunk.unwrap(); - let signer_message = read_next::(&mut &chunk[..]).unwrap(); - if let SignerMessage::BlockResponse(BlockResponse::Rejected(block_rejection)) = signer_message { - // Verify we are missing the valid tx that we expect to see in the block - if let RejectCode::MissingTransactions(missing_txs) = block_rejection.reason_code { - assert_eq!(missing_txs, vec![valid_tx]); - } else { - panic!("Received unexpected rejection reason"); - } - } else { - panic!("Received unexpected message: {:?}", &signer_message); } signer_test.shutdown(); }