diff --git a/.cargo/config.toml b/.cargo/config.toml
index 440c785d471..7f5642cf5e4 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -1,7 +1,7 @@
[alias]
stacks-node = "run --package stacks-node --"
fmt-stacks = "fmt -- --config group_imports=StdExternalCrate,imports_granularity=Module"
-clippy-stacks = "clippy -p stx-genesis -p libstackerdb -p stacks-signer -p pox-locking -p clarity-types -p clarity -p libsigner -p stacks-common -p clarity-cli -p stacks-cli -p stacks-inspect --no-deps --tests --all-features -- -D warnings"
+clippy-stacks = "clippy -p stx-genesis -p libstackerdb -p stacks-signer -p pox-locking -p clarity-types -p clarity -p libsigner -p stacks-common -p clarity-cli -p stacks-cli -p stacks-inspect --no-deps --tests --all-features -- -D warnings -Aclippy::unnecessary_lazy_evaluations"
clippy-stackslib = "clippy -p stackslib --no-deps -- -Aclippy::all -Wclippy::indexing_slicing -Wclippy::nonminimal_bool -Wclippy::clone_on_copy"
# Uncomment to improve performance slightly, at the cost of portability
diff --git a/CHANGELOG.md b/CHANGELOG.md
index adc282f18ba..bcf131c5379 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to the versioning scheme outlined in the [README.md](README.md).
-## [Unreleased]
+## [3.3.0.0.4]
+
+### Added
+
+- New `/v3/tenures/tip_metadata` endpoint for returning some metadata along with the normal tenure tip information.
+
+
+## [3.3.0.0.3]
### Added
diff --git a/Cargo.lock b/Cargo.lock
index 756c1f1388b..186a415f610 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -610,9 +610,9 @@ dependencies = [
[[package]]
name = "clarity"
version = "0.0.1"
-source = "git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5#b3efa588571e168d2d154790e1c57d9e3e64adc5"
+source = "git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed#7baa99ac3120111d1166c1a6c91abbd7aee8dbed"
dependencies = [
- "clarity-types 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "clarity-types 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
"integer-sqrt",
"lazy_static",
"regex",
@@ -624,7 +624,7 @@ dependencies = [
"serde_json",
"serde_stacker",
"slog",
- "stacks-common 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "stacks-common 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
]
[[package]]
@@ -663,7 +663,7 @@ dependencies = [
[[package]]
name = "clarity-types"
version = "0.0.1"
-source = "git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5#b3efa588571e168d2d154790e1c57d9e3e64adc5"
+source = "git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed#7baa99ac3120111d1166c1a6c91abbd7aee8dbed"
dependencies = [
"lazy_static",
"regex",
@@ -672,7 +672,7 @@ dependencies = [
"serde_derive",
"serde_json",
"slog",
- "stacks-common 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "stacks-common 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
]
[[package]]
@@ -1875,19 +1875,19 @@ dependencies = [
[[package]]
name = "libsigner"
version = "0.0.1"
-source = "git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5#b3efa588571e168d2d154790e1c57d9e3e64adc5"
+source = "git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed#7baa99ac3120111d1166c1a6c91abbd7aee8dbed"
dependencies = [
- "clarity 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "clarity 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
"hashbrown 0.15.2",
"lazy_static",
"libc",
- "libstackerdb 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "libstackerdb 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
"serde",
"serde_json",
"sha2 0.10.8",
"slog",
- "stacks-common 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
- "stackslib 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "stacks-common 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
+ "stackslib 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
"thiserror",
"tiny_http",
]
@@ -1916,12 +1916,12 @@ dependencies = [
[[package]]
name = "libstackerdb"
version = "0.0.1"
-source = "git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5#b3efa588571e168d2d154790e1c57d9e3e64adc5"
+source = "git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed#7baa99ac3120111d1166c1a6c91abbd7aee8dbed"
dependencies = [
- "clarity 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "clarity 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
"serde",
"sha2 0.10.8",
- "stacks-common 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "stacks-common 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
]
[[package]]
@@ -2431,11 +2431,11 @@ dependencies = [
[[package]]
name = "pox-locking"
version = "2.4.0"
-source = "git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5#b3efa588571e168d2d154790e1c57d9e3e64adc5"
+source = "git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed#7baa99ac3120111d1166c1a6c91abbd7aee8dbed"
dependencies = [
- "clarity 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "clarity 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
"slog",
- "stacks-common 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "stacks-common 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
]
[[package]]
@@ -3363,7 +3363,7 @@ dependencies = [
[[package]]
name = "stacks-common"
version = "0.0.1"
-source = "git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5#b3efa588571e168d2d154790e1c57d9e3e64adc5"
+source = "git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed#7baa99ac3120111d1166c1a6c91abbd7aee8dbed"
dependencies = [
"chrono",
"curve25519-dalek",
@@ -3421,7 +3421,7 @@ dependencies = [
"lazy_static",
"libc",
"libsigner 0.0.1",
- "libsigner 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "libsigner 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
"madhouse",
"mockito",
"mutants",
@@ -3438,12 +3438,12 @@ dependencies = [
"serial_test",
"slog",
"stacks-common 0.0.1",
- "stacks-common 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "stacks-common 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
"stacks-inspect",
"stacks-signer 0.0.1",
- "stacks-signer 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "stacks-signer 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
"stackslib 0.0.1",
- "stackslib 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "stackslib 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
"stdext",
"stx-genesis",
"tempfile",
@@ -3493,15 +3493,15 @@ dependencies = [
[[package]]
name = "stacks-signer"
version = "0.0.1"
-source = "git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5#b3efa588571e168d2d154790e1c57d9e3e64adc5"
+source = "git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed#7baa99ac3120111d1166c1a6c91abbd7aee8dbed"
dependencies = [
"backoff",
"clap",
- "clarity 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "clarity 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
"hashbrown 0.15.2",
"lazy_static",
- "libsigner 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
- "libstackerdb 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "libsigner 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
+ "libstackerdb 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
"rand 0.8.5",
"rand_core 0.6.4",
"reqwest",
@@ -3510,8 +3510,8 @@ dependencies = [
"serde_json",
"slog",
"slog-term",
- "stacks-common 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
- "stackslib 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "stacks-common 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
+ "stackslib 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
"thiserror",
"toml",
"tracing",
@@ -3566,17 +3566,17 @@ dependencies = [
[[package]]
name = "stackslib"
version = "0.0.1"
-source = "git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5#b3efa588571e168d2d154790e1c57d9e3e64adc5"
+source = "git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed#7baa99ac3120111d1166c1a6c91abbd7aee8dbed"
dependencies = [
"chrono",
- "clarity 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "clarity 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
"ed25519-dalek",
"lazy_static",
- "libstackerdb 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "libstackerdb 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
"mio 0.6.23",
"nix",
"percent-encoding",
- "pox-locking 2.4.0 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "pox-locking 2.4.0 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
"rand 0.8.5",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
@@ -3589,7 +3589,7 @@ dependencies = [
"sha2 0.10.8",
"siphasher",
"slog",
- "stacks-common 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=b3efa588571e168d2d154790e1c57d9e3e64adc5)",
+ "stacks-common 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=7baa99ac3120111d1166c1a6c91abbd7aee8dbed)",
"time",
"toml",
"url",
diff --git a/clarity-types/src/errors/mod.rs b/clarity-types/src/errors/mod.rs
index 4db200c3121..668f7d01043 100644
--- a/clarity-types/src/errors/mod.rs
+++ b/clarity-types/src/errors/mod.rs
@@ -190,6 +190,8 @@ pub enum RuntimeError {
PoxAlreadyLocked,
/// Block time unavailable during execution.
BlockTimeNotAvailable,
+ /// A Clarity string used as a token name for a post-condition is not a valid Clarity name.
+ BadTokenName(String),
}
#[derive(Debug, PartialEq)]
diff --git a/clarity-types/src/tests/types/signatures.rs b/clarity-types/src/tests/types/signatures.rs
index a41dbf6ed38..ba64e1ff17a 100644
--- a/clarity-types/src/tests/types/signatures.rs
+++ b/clarity-types/src/tests/types/signatures.rs
@@ -12,7 +12,7 @@
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-use std::collections::HashSet;
+use std::collections::BTreeSet;
use crate::errors::analysis::CommonCheckErrorKind;
use crate::representations::CONTRACT_MAX_NAME_LENGTH;
@@ -370,7 +370,7 @@ fn test_least_supertype() {
}),
];
let list_union2 = ListUnionType(callables2.clone().into());
- let list_union_merged = ListUnionType(HashSet::from_iter(
+ let list_union_merged = ListUnionType(BTreeSet::from_iter(
[callables, callables2].concat().iter().cloned(),
));
let callable_principals = [
diff --git a/clarity-types/src/types/signatures.rs b/clarity-types/src/types/signatures.rs
index 13ce2becc0a..2afd7d5e495 100644
--- a/clarity-types/src/types/signatures.rs
+++ b/clarity-types/src/types/signatures.rs
@@ -14,7 +14,7 @@
// along with this program. If not, see .
use std::collections::btree_map::Entry;
-use std::collections::{BTreeMap, HashSet};
+use std::collections::{BTreeMap, BTreeSet};
use std::hash::Hash;
use std::sync::Arc;
use std::{cmp, fmt};
@@ -293,7 +293,7 @@ pub enum TypeSignature {
// data structure to maintain the set of types in the list, so that when
// we reach the place where the coercion needs to happen, we can perform
// the check -- see `concretize` method.
- ListUnionType(HashSet),
+ ListUnionType(BTreeSet),
// This is used only below epoch 2.1. It has been replaced by CallableType.
TraitReferenceType(TraitIdentifier),
}
@@ -326,7 +326,7 @@ pub enum StringSubtype {
UTF8(StringUTF8Length),
}
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, PartialOrd, Ord)]
pub enum CallableSubtype {
Principal(QualifiedContractIdentifier),
Trait(TraitIdentifier),
@@ -1223,7 +1223,7 @@ impl TypeSignature {
if x == y {
Ok(a.clone())
} else {
- Ok(ListUnionType(HashSet::from([x.clone(), y.clone()])))
+ Ok(ListUnionType(BTreeSet::from([x.clone(), y.clone()])))
}
}
(ListUnionType(l), CallableType(c)) | (CallableType(c), ListUnionType(l)) => {
diff --git a/clarity/src/vm/functions/post_conditions.rs b/clarity/src/vm/functions/post_conditions.rs
index 59a91f9d03a..9beb952528c 100644
--- a/clarity/src/vm/functions/post_conditions.rs
+++ b/clarity/src/vm/functions/post_conditions.rs
@@ -15,7 +15,9 @@
use std::collections::HashMap;
+use clarity_types::errors::RuntimeError;
use clarity_types::types::{AssetIdentifier, PrincipalData, StandardPrincipalData};
+use clarity_types::ClarityName;
use crate::vm::analysis::type_checker::v2_1::natives::post_conditions::MAX_ALLOWANCES;
use crate::vm::contexts::AssetMap;
@@ -135,7 +137,12 @@ fn eval_allowance(
};
let asset_name = eval(&rest[1], env, context)?;
- let asset_name = asset_name.expect_string_ascii()?.as_str().into();
+ let asset_name = match ClarityName::try_from(asset_name.expect_string_ascii()?) {
+ Ok(name) => name,
+ Err(_) => {
+ return Err(RuntimeError::BadTokenName(rest[1].to_string()).into());
+ }
+ };
let asset = AssetIdentifier {
contract_identifier,
@@ -165,7 +172,12 @@ fn eval_allowance(
};
let asset_name = eval(&rest[1], env, context)?;
- let asset_name = asset_name.expect_string_ascii()?.as_str().into();
+ let asset_name = match ClarityName::try_from(asset_name.expect_string_ascii()?) {
+ Ok(name) => name,
+ Err(_) => {
+ return Err(RuntimeError::BadTokenName(rest[1].to_string()).into());
+ }
+ };
let asset = AssetIdentifier {
contract_identifier,
diff --git a/docs/rpc/components/schemas/tenure-tip-metadata.schema.yaml b/docs/rpc/components/schemas/tenure-tip-metadata.schema.yaml
new file mode 100644
index 00000000000..08fc53401fe
--- /dev/null
+++ b/docs/rpc/components/schemas/tenure-tip-metadata.schema.yaml
@@ -0,0 +1,124 @@
+description: |
+ JSON encoding of `BlockHeaderWithMetadata` returned by /v3/tenures/tip_metadata.
+ Exactly one variant property will be present: either `Epoch2` or `Nakamoto`.
+type: object
+required:
+ - anchored_header
+properties:
+ burn_view:
+ type: string
+ description: Hex-encoded bitcoin block hash
+ anchored_header:
+ oneOf:
+ - title: Epoch2HeaderVariant
+ type: object
+ required: [Epoch2]
+ additionalProperties: false
+ properties:
+ Epoch2:
+ type: object
+ description: Header structure for a Stacks 2.x anchored block.
+ required:
+ - version
+ - total_work
+ - proof
+ - parent_block
+ - parent_microblock
+ - parent_microblock_sequence
+ - tx_merkle_root
+ - state_index_root
+ - microblock_pubkey_hash
+ properties:
+ version:
+ type: integer
+ minimum: 0
+ total_work:
+ type: object
+ required: [burn, work]
+ properties:
+ burn:
+ type: integer
+ format: uint64
+ work:
+ type: integer
+ format: uint64
+ proof:
+ type: string
+ description: Hex-encoded VRF proof
+ parent_block:
+ type: string
+ description: 32-byte hex of the parent block header hash
+ parent_microblock:
+ type: string
+ description: 32-byte hex of the parent microblock header hash
+ parent_microblock_sequence:
+ type: integer
+ tx_merkle_root:
+ type: string
+ description: Hex-encoded merkle root of the transactions in the block
+ state_index_root:
+ type: string
+ description: Hex-encoded MARF trie root after this block
+ microblock_pubkey_hash:
+ type: string
+ description: Hash160 (20-byte hex) of the microblock public key
+ additionalProperties: false
+ - title: NakamotoHeaderVariant
+ type: object
+ required: [Nakamoto]
+ additionalProperties: false
+ properties:
+ Nakamoto:
+ type: object
+ description: Header structure for a Nakamoto-epoch Stacks block.
+ required:
+ - version
+ - chain_length
+ - burn_spent
+ - consensus_hash
+ - parent_block_id
+ - tx_merkle_root
+ - state_index_root
+ - timestamp
+ - miner_signature
+ - signer_signature
+ - pox_treatment
+ properties:
+ version:
+ type: integer
+ minimum: 0
+ chain_length:
+ type: integer
+ format: uint64
+ description: Number of ancestor blocks including Stacks 2.x blocks
+ burn_spent:
+ type: integer
+ format: uint64
+ description: Total BTC spent by the sortition that elected this block
+ consensus_hash:
+ type: string
+ description: 20-byte hex consensus hash that identifies the tenure
+ parent_block_id:
+ type: string
+ description: 32-byte hex identifier of the parent block (hash+consensus)
+ tx_merkle_root:
+ type: string
+ description: Hex-encoded merkle root of all transactions in the block
+ state_index_root:
+ type: string
+ description: Hex-encoded MARF trie root after this block
+ timestamp:
+ type: integer
+ description: Unix timestamp (seconds)
+ miner_signature:
+ type: string
+ description: Recoverable ECDSA signature from the miner
+ signer_signature:
+ type: array
+ description: Signer-set signatures over the block header
+ items:
+ type: string
+ pox_treatment:
+ type: string
+ description: Bit-vector, hex-encoded, indicating PoX reward treatment
+ additionalProperties: false
diff --git a/docs/rpc/openapi.yaml b/docs/rpc/openapi.yaml
index 4c52d2eff70..22876be7837 100644
--- a/docs/rpc/openapi.yaml
+++ b/docs/rpc/openapi.yaml
@@ -156,6 +156,8 @@ components:
$ref: ./components/schemas/tenure-info.schema.yaml
TenureTip:
$ref: ./components/schemas/tenure-tip.schema.yaml
+ TenureTipMetadata:
+ $ref: ./components/schemas/tenure-tip-metadata.schema.yaml
GetStackerSet:
$ref: ./components/schemas/get-stacker-set.schema.yaml
TenureBlocks:
@@ -2062,6 +2064,37 @@ paths:
"500":
$ref: "#/components/responses/InternalServerError"
+ /v3/tenures/tip_metadata/{consensus_hash}:
+ get:
+ summary: Get tenure tip with metadata
+ tags:
+ - Blocks
+ security: []
+ operationId: getTenureTipMetadata
+ description: |
+ Get the tip block and associated metadata of a tenure identified by consensus hash.
+ parameters:
+ - name: consensus_hash
+ in: path
+ required: true
+ description: Consensus hash (40 characters)
+ schema:
+ type: string
+ pattern: "^[0-9a-f]{40}$"
+ responses:
+ "200":
+ description: Tenure tip block information
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TenureTipMetadata"
+ "400":
+ $ref: "#/components/responses/BadRequest"
+ "404":
+ $ref: "#/components/responses/NotFound"
+ "500":
+ $ref: "#/components/responses/InternalServerError"
+
/v2/transactions/unconfirmed/{txid}:
get:
summary: Get unconfirmed transaction
diff --git a/libsigner/src/v0/messages.rs b/libsigner/src/v0/messages.rs
index 36a5dd05a6f..700a892cddf 100644
--- a/libsigner/src/v0/messages.rs
+++ b/libsigner/src/v0/messages.rs
@@ -52,7 +52,7 @@ use stacks_common::types::chainstate::StacksBlockId;
use stacks_common::util::hash::{Hash160, Sha512Trunc256Sum};
use crate::stacks_common::types::PublicKey;
-use crate::v0::signer_state::ReplayTransactionSet;
+use crate::v0::signer_state::{ReplayTransactionSet, SignerStateMachine};
use crate::{
BlockProposal, MessageSlotID as MessageSlotIDTrait, SignerMessage as SignerMessageTrait,
VERSION_STRING,
@@ -614,18 +614,58 @@ pub enum StateMachineUpdateMinerState {
/// The signer doesn't believe there's any valid miner
NoValidMiner,
}
-
impl StateMachineUpdate {
- /// Construct a StateMachineUpdate message, checking to ensure that the
- /// supplied content is supported by the supplied protocol versions.
+ /// Create a `StateMachineUpdate` for **outbound** messages.
+ ///
+ /// The content version **must exactly match** the negotiated protocol version:
+ /// `min(active_signer_protocol_version, local_supported_signer_protocol_version)`.
+ ///
+ /// This ensures the message we send conforms strictly to what both sides have agreed upon.
pub fn new(
active_signer_protocol_version: u64,
local_supported_signer_protocol_version: u64,
content: StateMachineUpdateContent,
) -> Result {
- if !content.is_protocol_version_compatible(active_signer_protocol_version) {
- return Err(CodecError::DeserializeError(format!("StateMachineUpdateContent is incompatible with protocol version: {active_signer_protocol_version}")));
+ let negotiated =
+ active_signer_protocol_version.min(local_supported_signer_protocol_version);
+ let version = content.version();
+
+ if version != negotiated {
+ return Err(CodecError::DeserializeError(format!(
+ "Outbound content version {version} does not match negotiated protocol version {negotiated} \
+ (active={active_signer_protocol_version}, local_supported={local_supported_signer_protocol_version})"
+ )));
+ }
+
+ Ok(Self {
+ active_signer_protocol_version,
+ local_supported_signer_protocol_version,
+ content,
+ no_manual_construct: PhantomData,
+ })
+ }
+
+ /// Create a `StateMachineUpdate` for **inbound** messages.
+ ///
+ /// The content version must be **less than or equal to** the negotiated protocol version
+ /// (`min(active_signer_protocol_version, local_supported_signer_protocol_version)`).
+ /// Older versions are explicitly allowed for backward compatibility.
+ fn new_inbound(
+ active_signer_protocol_version: u64,
+ local_supported_signer_protocol_version: u64,
+ content: StateMachineUpdateContent,
+ ) -> Result {
+ let negotiated =
+ active_signer_protocol_version.min(local_supported_signer_protocol_version);
+ let version = content.version();
+
+ if content.version() > negotiated {
+ return Err(CodecError::DeserializeError(format!(
+ "Inbound content version {version} exceeds negotiated protocol version {negotiated} \
+ (active={active_signer_protocol_version}, local_supported={local_supported_signer_protocol_version})"
+ )));
}
+
Ok(Self {
active_signer_protocol_version,
local_supported_signer_protocol_version,
@@ -692,12 +732,45 @@ impl StacksMessageCodec for StateMachineUpdateMinerState {
}
impl StateMachineUpdateContent {
- // Is the protocol version specified one that uses self's content?
- fn is_protocol_version_compatible(&self, version: u64) -> bool {
+ /// Attempt to create a new state machine update content with the specified version
+ pub fn new(
+ version: u64,
+ current_miner: StateMachineUpdateMinerState,
+ state_machine: &SignerStateMachine,
+ ) -> Result {
+ let content = match version {
+ 0 => StateMachineUpdateContent::V0 {
+ burn_block: state_machine.burn_block.clone(),
+ burn_block_height: state_machine.burn_block_height,
+ current_miner,
+ },
+ 1 => StateMachineUpdateContent::V1 {
+ burn_block: state_machine.burn_block.clone(),
+ burn_block_height: state_machine.burn_block_height,
+ current_miner,
+ replay_transactions: state_machine.tx_replay_set.clone().unwrap_or_default(),
+ },
+ 2 => StateMachineUpdateContent::V2 {
+ burn_block: state_machine.burn_block.clone(),
+ burn_block_height: state_machine.burn_block_height,
+ current_miner,
+ replay_transactions: state_machine.tx_replay_set.clone().unwrap_or_default(),
+ },
+ other => {
+ return Err(CodecError::DeserializeError(format!(
+ "Signer protocol version is unknown: {other}"
+ )))
+ }
+ };
+ Ok(content)
+ }
+
+ /// Get the underlying version of the state machine update content
+ pub fn version(&self) -> u64 {
match self {
- Self::V0 { .. } => version == 0,
- Self::V1 { .. } => version == 1,
- Self::V2 { .. } => version == 2,
+ Self::V0 { .. } => 0,
+ Self::V1 { .. } => 1,
+ Self::V2 { .. } => 2,
}
}
@@ -832,8 +905,8 @@ impl StacksMessageCodec for StateMachineUpdate {
}
fn consensus_deserialize(fd: &mut R) -> Result {
- let active_signer_protocol_version = read_next(fd)?;
- let local_supported_signer_protocol_version = read_next(fd)?;
+ let active_signer_protocol_version: u64 = read_next(fd)?;
+ let local_supported_signer_protocol_version: u64 = read_next(fd)?;
let content_len: u32 = read_next(fd)?;
if content_len > STATE_MACHINE_UPDATE_MAX_SIZE {
return Err(CodecError::DeserializeError(format!(
@@ -844,12 +917,12 @@ impl StacksMessageCodec for StateMachineUpdate {
.expect("FATAL: cannot process signer messages when usize < u32");
let mut buffer = vec![0u8; buffer_len];
fd.read_exact(&mut buffer).map_err(CodecError::ReadError)?;
- let content = StateMachineUpdateContent::deserialize(
- &mut buffer.as_slice(),
- active_signer_protocol_version,
- )?;
+ let negotiated =
+ active_signer_protocol_version.min(local_supported_signer_protocol_version);
+ let content = StateMachineUpdateContent::deserialize(&mut buffer.as_slice(), negotiated)?;
- Self::new(
+ // We use the inbound constructor here as we need to allow for older versions
+ Self::new_inbound(
active_signer_protocol_version,
local_supported_signer_protocol_version,
content,
@@ -2424,23 +2497,65 @@ mod test {
#[test]
fn version_check_state_machine_update() {
- let error = StateMachineUpdate::new(
- 1,
- 3,
- StateMachineUpdateContent::V0 {
- burn_block: ConsensusHash([0x55; 20]),
- burn_block_height: 100,
- current_miner: StateMachineUpdateMinerState::ActiveMiner {
- current_miner_pkh: Hash160([0xab; 20]),
- tenure_id: ConsensusHash([0x44; 20]),
- parent_tenure_id: ConsensusHash([0x22; 20]),
- parent_tenure_last_block: StacksBlockId([0x33; 32]),
- parent_tenure_last_block_height: 1,
- },
+ let content = StateMachineUpdateContent::V1 {
+ burn_block: ConsensusHash([0x55; 20]),
+ burn_block_height: 100,
+ current_miner: StateMachineUpdateMinerState::ActiveMiner {
+ current_miner_pkh: Hash160([0xab; 20]),
+ tenure_id: ConsensusHash([0x44; 20]),
+ parent_tenure_id: ConsensusHash([0x22; 20]),
+ parent_tenure_last_block: StacksBlockId([0x33; 32]),
+ parent_tenure_last_block_height: 1,
},
- )
- .unwrap_err();
+ replay_transactions: vec![],
+ };
+ // We active version does not support the content
+ let error = StateMachineUpdate::new(0, 1, content.clone()).unwrap_err();
+ assert!(matches!(error, CodecError::DeserializeError(_)));
+ // The content should be the min of the active/local versions but it is lower
+ let error = StateMachineUpdate::new(2, 3, content.clone()).unwrap_err();
+ assert!(matches!(error, CodecError::DeserializeError(_)));
+ // The content should be the min of the active/local versions but it is greater
+ let error = StateMachineUpdate::new(2, 0, content.clone()).unwrap_err();
+ assert!(matches!(error, CodecError::DeserializeError(_)));
+ // the content version is equal to the min of the active/local versions
+ assert!(StateMachineUpdate::new(1, 2, content.clone()).is_ok())
+ }
+
+ #[test]
+ fn version_check_state_machine_update_inbound() {
+ let content_v1 = StateMachineUpdateContent::V1 {
+ burn_block: ConsensusHash([0x55; 20]),
+ burn_block_height: 100,
+ current_miner: StateMachineUpdateMinerState::ActiveMiner {
+ current_miner_pkh: Hash160([0xab; 20]),
+ tenure_id: ConsensusHash([0x44; 20]),
+ parent_tenure_id: ConsensusHash([0x22; 20]),
+ parent_tenure_last_block: StacksBlockId([0x33; 32]),
+ parent_tenure_last_block_height: 1,
+ },
+ replay_transactions: vec![],
+ };
+
+ // Inbound: content version exceeds negotiated version → should reject
+
+ // Case 1: Content version (1) > negotiated (0) → reject
+ let error = StateMachineUpdate::new_inbound(0, 1, content_v1.clone()).unwrap_err();
assert!(matches!(error, CodecError::DeserializeError(_)));
+
+ // Case 2: Content version (1) > negotiated (0) → reject (swapped active/local)
+ let error = StateMachineUpdate::new_inbound(1, 0, content_v1.clone()).unwrap_err();
+ assert!(matches!(error, CodecError::DeserializeError(_)));
+
+ // Case 3: Content version (1) == negotiated (1) → accept
+ assert!(StateMachineUpdate::new_inbound(1, 1, content_v1.clone()).is_ok());
+ assert!(StateMachineUpdate::new_inbound(1, 2, content_v1.clone()).is_ok());
+ assert!(StateMachineUpdate::new_inbound(3, 1, content_v1.clone()).is_ok());
+
+ // Case 4: Content version (1) < negotiated (2) → accept (backward compatibility)
+ assert!(StateMachineUpdate::new_inbound(2, 2, content_v1.clone()).is_ok());
+ assert!(StateMachineUpdate::new_inbound(2, 5, content_v1.clone()).is_ok());
+ assert!(StateMachineUpdate::new_inbound(10, 10, content_v1.clone()).is_ok());
}
#[test]
diff --git a/stacks-node/Cargo.toml b/stacks-node/Cargo.toml
index a43393aa622..84d60b6624b 100644
--- a/stacks-node/Cargo.toml
+++ b/stacks-node/Cargo.toml
@@ -32,11 +32,11 @@ async-std = { version = "1.6", optional = true, features = ["attributes"] }
http-types = { version = "2.12", default-features = false, optional = true }
thiserror = { workspace = true }
-# This dependency is used for the multiversion integration tests which live behind the build-v3-1-0-0-13 feature flag
-signer_v3_3_0_0_1 = { package = "stacks-signer", git = "https://github.com/stacks-network/stacks-core.git", rev="b3efa588571e168d2d154790e1c57d9e3e64adc5", optional = true, features = ["testing", "default"]}
-libsigner_v3_3_0_0_1 = { package = "libsigner", git = "https://github.com/stacks-network/stacks-core.git", rev="b3efa588571e168d2d154790e1c57d9e3e64adc5", optional = true}
-stacks_v3_3_0_0_1 = { package = "stackslib", git = "https://github.com/stacks-network/stacks-core.git", rev="b3efa588571e168d2d154790e1c57d9e3e64adc5", optional = true, features = ["testing", "default"]}
-stacks_common_v3_3_0_0_1 = { package = "stacks-common", git = "https://github.com/stacks-network/stacks-core.git", rev="b3efa588571e168d2d154790e1c57d9e3e64adc5", optional = true, features = ["testing", "default"]}
+# This dependency is used for the multiversion integration tests which live behind the build-v3-1-0-0-4 feature flag
+signer_v3_3_0_0_4 = { package = "stacks-signer", git = "https://github.com/stacks-network/stacks-core.git", rev="7baa99ac3120111d1166c1a6c91abbd7aee8dbed", optional = true, features = ["testing", "default"]}
+libsigner_v3_3_0_0_4 = { package = "libsigner", git = "https://github.com/stacks-network/stacks-core.git", rev="7baa99ac3120111d1166c1a6c91abbd7aee8dbed", optional = true}
+stacks_v3_3_0_0_4 = { package = "stackslib", git = "https://github.com/stacks-network/stacks-core.git", rev="7baa99ac3120111d1166c1a6c91abbd7aee8dbed", optional = true, features = ["testing", "default"]}
+stacks_common_v3_3_0_0_4 = { package = "stacks-common", git = "https://github.com/stacks-network/stacks-core.git", rev="7baa99ac3120111d1166c1a6c91abbd7aee8dbed", optional = true, features = ["testing", "default"]}
[target.'cfg(not(any(target_os = "macos", target_os="windows", target_arch = "arm")))'.dependencies]
tikv-jemallocator = {workspace = true}
@@ -68,7 +68,7 @@ slog_json = ["stacks/slog_json", "stacks-common/slog_json", "clarity/slog_json"]
prod-genesis-chainstate = []
default = []
testing = ["stacks-common/testing", "stacks/testing", "clarity/testing"]
-build-signer-v3-3-0-0-1 = ["signer_v3_3_0_0_1", "libsigner_v3_3_0_0_1", "stacks_v3_3_0_0_1", "stacks_common_v3_3_0_0_1"]
+build-signer-v3-3-0-0-4 = ["signer_v3_3_0_0_4", "libsigner_v3_3_0_0_4", "stacks_v3_3_0_0_4", "stacks_common_v3_3_0_0_4"]
[package.metadata.pinny]
allowed = ["bitcoind", "flaky", "slow"]
diff --git a/stacks-node/src/tests/signer/mod.rs b/stacks-node/src/tests/signer/mod.rs
index d8db0e960d0..973d63834a7 100644
--- a/stacks-node/src/tests/signer/mod.rs
+++ b/stacks-node/src/tests/signer/mod.rs
@@ -13,7 +13,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
mod commands;
-#[cfg(feature = "build-signer-v3-3-0-0-1")]
+#[cfg(feature = "build-signer-v3-3-0-0-4")]
pub mod multiversion;
pub mod v0;
@@ -63,6 +63,7 @@ use stacks_signer::config::{build_signer_config_tomls, GlobalConfig as SignerCon
use stacks_signer::runloop::{SignerResult, State, StateInfo};
use stacks_signer::signerdb::SignerDb;
use stacks_signer::v0::signer_state::LocalStateMachine;
+use stacks_signer::v0::tests::TEST_PIN_SUPPORTED_SIGNER_PROTOCOL_VERSION;
use stacks_signer::{Signer, SpawnedSigner};
use super::nakamoto_integrations::{
@@ -585,7 +586,8 @@ impl SignerTest {
let latest_block = self
.stacks_client
.get_tenure_tip(&sortition_prior.consensus_hash)
- .unwrap();
+ .unwrap()
+ .anchored_header;
let latest_block_id =
StacksBlockId::new(&sortition_prior.consensus_hash, &latest_block.block_hash());
@@ -642,7 +644,8 @@ impl SignerTest {
let latest_block = self
.stacks_client
.get_tenure_tip(sortition_prior.stacks_parent_ch.as_ref().unwrap())
- .unwrap();
+ .unwrap()
+ .anchored_header;
let latest_block_id = StacksBlockId::new(
sortition_prior.stacks_parent_ch.as_ref().unwrap(),
&latest_block.block_hash(),
@@ -904,7 +907,8 @@ impl SignerTest {
let latest_block = self
.stacks_client
.get_tenure_tip(&sortition_prior.consensus_hash)
- .unwrap();
+ .unwrap()
+ .anchored_header;
let latest_block_id =
StacksBlockId::new(&sortition_prior.consensus_hash, &latest_block.block_hash());
@@ -984,7 +988,8 @@ impl SignerTest {
let latest_block = self
.stacks_client
.get_tenure_tip(&sortition_parent.consensus_hash)
- .unwrap();
+ .unwrap()
+ .anchored_header;
let latest_block_id =
StacksBlockId::new(&sortition_parent.consensus_hash, &latest_block.block_hash());
@@ -1414,10 +1419,14 @@ impl SignerTest {
.iter()
.zip(self.signer_configs.clone())
.map(|(privk, config)| {
- (
- StacksAddress::p2pkh(false, &StacksPublicKey::from_private(privk)),
- config.supported_signer_protocol_version,
- )
+ let public_key = StacksPublicKey::from_private(privk);
+ let pinned_versions = TEST_PIN_SUPPORTED_SIGNER_PROTOCOL_VERSION.get();
+ let version = if let Some(pinned_version) = pinned_versions.get(&public_key) {
+ *pinned_version
+ } else {
+ config.supported_signer_protocol_version
+ };
+ (StacksAddress::p2pkh(false, &public_key), version)
})
.collect()
}
@@ -1705,6 +1714,22 @@ impl SignerTest {
.txid;
Some(Txid::from_bitcoin_tx_hash(parent_txid))
}
+ /// Restart the signer at `idx` with a new supported protocol version.
+ pub fn restart_signer_with_supported_version(&mut self, idx: usize, version: u64) {
+ let mut cfg = self.stop_signer(idx);
+ cfg.supported_signer_protocol_version = version;
+ self.restart_signer(idx, cfg);
+ }
+
+ /// Restart the first `n` signers with a new supported protocol version.
+ /// Restarts in reverse index order so removals/insertions don't shift upcoming indices.
+ /// Waits for all signers to re-register after restarts.
+ pub fn restart_first_n_signers_with_supported_version(&mut self, n: usize, version: u64) {
+ for idx in (0..n).rev() {
+ self.restart_signer_with_supported_version(idx, version);
+ }
+ self.wait_for_registered();
+ }
}
fn setup_stx_btc_node(
diff --git a/stacks-node/src/tests/signer/multiversion.rs b/stacks-node/src/tests/signer/multiversion.rs
index 152b9747357..b02686b2c6b 100644
--- a/stacks-node/src/tests/signer/multiversion.rs
+++ b/stacks-node/src/tests/signer/multiversion.rs
@@ -1,4 +1,4 @@
-// Copyright (C) 2025 Stacks Open Internet Foundation
+// Copyright (C) 2026 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
@@ -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::sync::mpsc::TryRecvError;
-use std::thread;
use std::time::Duration;
use libsigner::v0::messages::{
@@ -21,16 +20,19 @@ use libsigner::v0::messages::{
SignerMessageMetadata,
};
use libsigner::v0::signer_state::{MinerState, ReplayTransactionSet, SignerStateMachine};
-use libsigner_v3_3_0_0_1::v0::messages::SignerMessage as OldSignerMessage;
+use libsigner_v3_3_0_0_4::v0::messages::SignerMessage as OldSignerMessage;
+use signer_v3_3_0_0_4::v0::signer_state::SUPPORTED_SIGNER_PROTOCOL_VERSION as OldSupportedVersion;
use stacks::chainstate::stacks::StacksTransaction;
use stacks::util::hash::{Hash160, Sha512Trunc256Sum};
use stacks::util::secp256k1::{MessageSignature, Secp256k1PrivateKey};
use stacks_common::types::chainstate::{ConsensusHash, StacksBlockId};
-use stacks_common_v3_3_0_0_1::codec::StacksMessageCodec as OldStacksMessageCodec;
+use stacks_common_v3_3_0_0_4::codec::StacksMessageCodec as OldStacksMessageCodec;
use stacks_signer::runloop::{RewardCycleInfo, State, StateInfo};
-use stacks_signer::v0::signer_state::LocalStateMachine;
+use stacks_signer::v0::signer_state::{
+ LocalStateMachine, SUPPORTED_SIGNER_PROTOCOL_VERSION as NewSupportedVersion,
+};
use stacks_signer::v0::SpawnedSigner;
-use {libsigner_v3_3_0_0_1, signer_v3_3_0_0_1, stacks_common_v3_3_0_0_1, stacks_v3_3_0_0_1};
+use {libsigner_v3_3_0_0_4, signer_v3_3_0_0_4, stacks_common_v3_3_0_0_4, stacks_v3_3_0_0_4};
use super::SpawnedSignerTrait;
use crate::stacks_common::codec::StacksMessageCodec;
@@ -41,24 +43,24 @@ use crate::tests::{self};
use crate::Keychain;
pub enum MultiverSpawnedSigner {
- V33001(signer_v3_3_0_0_1::v0::SpawnedSigner),
+ V33004(signer_v3_3_0_0_4::v0::SpawnedSigner),
Current(SpawnedSigner),
}
pub enum ReceiveResult {
- V33001(Result),
+ V33004(Result),
Current(Result),
}
-// Helper function to convert libsigner_v3_3_0_0_1 miner state to current miner state
-pub fn miner_state_v3_3_0_0_1_to_current(
- miner_state: &libsigner_v3_3_0_0_1::v0::signer_state::MinerState,
+// Helper function to convert libsigner_v3_3_0_0_4 miner state to current miner state
+pub fn miner_state_v3_3_0_0_4_to_current(
+ miner_state: &libsigner_v3_3_0_0_4::v0::signer_state::MinerState,
) -> MinerState {
match miner_state {
- libsigner_v3_3_0_0_1::v0::signer_state::MinerState::NoValidMiner => {
+ libsigner_v3_3_0_0_4::v0::signer_state::MinerState::NoValidMiner => {
MinerState::NoValidMiner
}
- libsigner_v3_3_0_0_1::v0::signer_state::MinerState::ActiveMiner {
+ libsigner_v3_3_0_0_4::v0::signer_state::MinerState::ActiveMiner {
current_miner_pkh,
tenure_id,
parent_tenure_id,
@@ -75,29 +77,29 @@ pub fn miner_state_v3_3_0_0_1_to_current(
}
// Helper function to convert from one to the other
-pub fn stacks_transaction_v3_3_0_0_1_to_current(
- tx: &stacks_v3_3_0_0_1::chainstate::stacks::StacksTransaction,
+pub fn stacks_transaction_v3_3_0_0_4_to_current(
+ tx: &stacks_v3_3_0_0_4::chainstate::stacks::StacksTransaction,
) -> StacksTransaction {
let tx_bytes = tx.serialize_to_vec();
StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap()
}
-// Helper function to convert libsigner_v3_1_0_0_13 signer state machine to current signer state machine
-pub fn signer_state_update_v3_3_0_0_1_to_current(
- update: &signer_v3_3_0_0_1::v0::signer_state::StateMachineUpdate,
+// Helper function to convert libsigner_v3_3_0_0_4 signer state machine to current signer state machine
+pub fn signer_state_update_v3_3_0_0_4_to_current(
+ update: &signer_v3_3_0_0_4::v0::signer_state::StateMachineUpdate,
) -> stacks_signer::v0::signer_state::StateMachineUpdate {
let serialized = serde_json::to_string(update).unwrap();
serde_json::from_str(&serialized).unwrap()
}
-// Helper function to convert libsigner_v3_1_0_0_13 signer state machine to current signer state machine
-pub fn signer_state_machine_v3_3_0_0_1_to_current(
- machine: &libsigner_v3_3_0_0_1::v0::signer_state::SignerStateMachine,
+// Helper function to convert libsigner_v3_3_0_0_4 signer state machine to current signer state machine
+pub fn signer_state_machine_v3_3_0_0_4_to_current(
+ machine: &libsigner_v3_3_0_0_4::v0::signer_state::SignerStateMachine,
) -> SignerStateMachine {
SignerStateMachine {
burn_block: ConsensusHash(machine.burn_block.0),
burn_block_height: machine.burn_block_height,
- current_miner: miner_state_v3_3_0_0_1_to_current(&machine.current_miner),
+ current_miner: miner_state_v3_3_0_0_4_to_current(&machine.current_miner),
active_signer_protocol_version: machine.active_signer_protocol_version,
tx_replay_set: ReplayTransactionSet::new(
machine
@@ -105,28 +107,28 @@ pub fn signer_state_machine_v3_3_0_0_1_to_current(
.clone()
.unwrap_or_default()
.iter()
- .map(stacks_transaction_v3_3_0_0_1_to_current)
+ .map(stacks_transaction_v3_3_0_0_4_to_current)
.collect(),
),
}
}
-// Helper function to convert signer_v3_1_0_0_13 local state machines to current local state machines
-pub fn local_state_machine_v3_3_0_0_1_to_current(
- state_machine: &signer_v3_3_0_0_1::v0::signer_state::LocalStateMachine,
+// Helper function to convert signer_v3_3_0_0_4 local state machines to current local state machines
+pub fn local_state_machine_v3_3_0_0_4_to_current(
+ state_machine: &signer_v3_3_0_0_4::v0::signer_state::LocalStateMachine,
) -> LocalStateMachine {
match state_machine {
- signer_v3_3_0_0_1::v0::signer_state::LocalStateMachine::Uninitialized => {
+ signer_v3_3_0_0_4::v0::signer_state::LocalStateMachine::Uninitialized => {
LocalStateMachine::Uninitialized
}
- signer_v3_3_0_0_1::v0::signer_state::LocalStateMachine::Pending { prior, update } => {
+ signer_v3_3_0_0_4::v0::signer_state::LocalStateMachine::Pending { prior, update } => {
LocalStateMachine::Pending {
- prior: signer_state_machine_v3_3_0_0_1_to_current(prior),
- update: signer_state_update_v3_3_0_0_1_to_current(update),
+ prior: signer_state_machine_v3_3_0_0_4_to_current(prior),
+ update: signer_state_update_v3_3_0_0_4_to_current(update),
}
}
- signer_v3_3_0_0_1::v0::signer_state::LocalStateMachine::Initialized(machine) => {
- LocalStateMachine::Initialized(signer_state_machine_v3_3_0_0_1_to_current(machine))
+ signer_v3_3_0_0_4::v0::signer_state::LocalStateMachine::Initialized(machine) => {
+ LocalStateMachine::Initialized(signer_state_machine_v3_3_0_0_4_to_current(machine))
}
}
}
@@ -137,9 +139,17 @@ impl SpawnedSignerTrait for MultiverSpawnedSigner {
fn new(c: stacks_signer::config::GlobalConfig) -> Self {
if c.endpoint.port() % 2 == 0 {
+ debug!(
+ "Spawning current version signer for endpoint {}",
+ c.endpoint
+ );
Self::Current(SpawnedSigner::new(c))
} else {
- let config = signer_v3_3_0_0_1::config::GlobalConfig {
+ debug!(
+ "Spawning v3_3_0_0_4 version signer for endpoint {}",
+ c.endpoint
+ );
+ let config = signer_v3_3_0_0_4::config::GlobalConfig {
node_host: c.node_host,
endpoint: c.endpoint,
stacks_private_key: serde_json::from_value(
@@ -150,7 +160,7 @@ impl SpawnedSignerTrait for MultiverSpawnedSigner {
serde_json::to_value(&c.stacks_address).unwrap(),
)
.unwrap(),
- network: signer_v3_3_0_0_1::config::Network::Testnet,
+ network: signer_v3_3_0_0_4::config::Network::Testnet,
event_timeout: c.event_timeout,
auth_password: c.auth_password,
db_path: c.db_path,
@@ -171,16 +181,17 @@ impl SpawnedSignerTrait for MultiverSpawnedSigner {
reset_replay_set_after_fork_blocks: c.reset_replay_set_after_fork_blocks,
stackerdb_timeout: c.stackerdb_timeout,
supported_signer_protocol_version: c.supported_signer_protocol_version,
+ read_count_idle_timeout: c.read_count_idle_timeout,
};
- Self::V33001(signer_v3_3_0_0_1::v0::SpawnedSigner::new(config))
+ Self::V33004(signer_v3_3_0_0_4::v0::SpawnedSigner::new(config))
}
}
fn try_recv(&self) -> Self::ReceiveResult {
match self {
- MultiverSpawnedSigner::V33001(spawned_signer) => {
+ MultiverSpawnedSigner::V33004(spawned_signer) => {
let result = spawned_signer.res_recv.try_recv().map_err(|_| ());
- ReceiveResult::V33001(result)
+ ReceiveResult::V33004(result)
}
MultiverSpawnedSigner::Current(spawned_signer) => {
ReceiveResult::Current(spawned_signer.res_recv.try_recv())
@@ -190,7 +201,7 @@ impl SpawnedSignerTrait for MultiverSpawnedSigner {
fn stop(self) -> Option {
match self {
- MultiverSpawnedSigner::V33001(spawned_signer) => spawned_signer.stop().map(|_| ()),
+ MultiverSpawnedSigner::V33004(spawned_signer) => spawned_signer.stop().map(|_| ()),
MultiverSpawnedSigner::Current(spawned_signer) => spawned_signer.stop().map(|_| ()),
}
}
@@ -199,13 +210,13 @@ impl SpawnedSignerTrait for MultiverSpawnedSigner {
result: Self::ReceiveResult,
) -> Option {
match result {
- ReceiveResult::V33001(signer_result) => {
- let Ok(signer_v3_3_0_0_1::runloop::SignerResult::StatusCheck(state_info)) =
+ ReceiveResult::V33004(signer_result) => {
+ let Ok(signer_v3_3_0_0_4::runloop::SignerResult::StatusCheck(state_info)) =
signer_result
else {
return None;
};
- let signer_v3_3_0_0_1::runloop::StateInfo {
+ let signer_v3_3_0_0_4::runloop::StateInfo {
runloop_state,
reward_cycle_info,
running_signers,
@@ -215,11 +226,11 @@ impl SpawnedSignerTrait for MultiverSpawnedSigner {
} = state_info;
Some(StateInfo {
runloop_state: match runloop_state {
- signer_v3_3_0_0_1::runloop::State::Uninitialized => State::Uninitialized,
- signer_v3_3_0_0_1::runloop::State::NoRegisteredSigners => {
+ signer_v3_3_0_0_4::runloop::State::Uninitialized => State::Uninitialized,
+ signer_v3_3_0_0_4::runloop::State::NoRegisteredSigners => {
State::NoRegisteredSigners
}
- signer_v3_3_0_0_1::runloop::State::RegisteredSigners => {
+ signer_v3_3_0_0_4::runloop::State::RegisteredSigners => {
State::RegisteredSigners
}
},
@@ -247,7 +258,7 @@ impl SpawnedSignerTrait for MultiverSpawnedSigner {
*i,
machine
.as_ref()
- .map(local_state_machine_v3_3_0_0_1_to_current),
+ .map(local_state_machine_v3_3_0_0_4_to_current),
)
})
.collect(),
@@ -319,13 +330,10 @@ fn old_version_parses_new_messages() {
as_block_accepted.response_data.reject_reason.to_string(),
new_msg.response_data.reject_reason.to_string()
);
+ let empty_vec: Vec = vec![];
assert_eq!(
- as_block_accepted.response_data.unknown_bytes,
- new_msg
- .response_data
- .tenure_extend_read_count_timestamp
- .to_be_bytes()
- .to_vec()
+ as_block_accepted.response_data.unknown_bytes, // No difference between versions at the moment
+ empty_vec
);
let serialized_old_msg = old_msg.serialize_to_vec();
@@ -403,7 +411,6 @@ fn with_new_miners(supported_signer_protocol_version: u64
.submit_transfer_tx(&sender_sk, send_fee, send_amt)
.unwrap();
wait_for(120, || {
- thread::sleep(Duration::from_secs(1));
Ok(get_account(&signer_test.running_nodes.rpc_origin(), &sender_addr).nonce > nonce)
})
.expect("Timed out waiting for interim block to be mined");
@@ -455,3 +462,297 @@ fn with_new_miners(supported_signer_protocol_version: u64
get_chain_info(&signer_test.running_nodes.conf)
);
}
+
+#[test]
+#[ignore] // Remove when ready to run in CI
+/// Test with 40% new signers and 60% old signers that the chain still progresses
+/// with ALL signers signing the proposed blocks.
+fn mixed_signer_set_40_percent_new_60_percent_old() {
+ // We want: 40% new (current), 60% old (v3.3.0.0.4)
+ // Let's use 10 signers total for clean percentages: 4 new, 6 old
+ assert!(
+ OldSupportedVersion < NewSupportedVersion,
+ "Test setup error: old supported version should be less than new supported version"
+ );
+ let num_signers = 10;
+ let num_new_signers = 4; // 40%
+ let num_old_signers = 6; // 60%
+
+ // Let's just do 3 tenures
+ let nmb_tenures = 3;
+
+ let btc_miner_seed = vec![2, 2, 2, 2];
+ let btc_miner_pk = Keychain::default(btc_miner_seed.clone()).get_pub_key();
+
+ let localhost = "127.0.0.1";
+ let node_rpc_port = 30500;
+ let node_p2p_port = 30501;
+
+ // Custom signer config: decide old vs new based on signer index
+ let signer_test: SignerTest = SignerTest::new_with_config_modifications(
+ num_signers,
+ vec![],
+ |signer_config| {
+ // Use port to determine version: even port -> current (new), odd -> old
+ // Initially the signer port number is simply sequential
+ let signer_index = signer_config.endpoint.port() % num_signers as u16;
+ // Update the ports to enforce versioning
+ let base_port = 40000 + signer_index * 10;
+ let endpoint_port = if signer_index < num_new_signers as u16 {
+ signer_config.supported_signer_protocol_version = NewSupportedVersion;
+ base_port // even: new version
+ } else {
+ signer_config.supported_signer_protocol_version = OldSupportedVersion;
+ base_port + 1 // odd: old version
+ };
+ signer_config.endpoint = format!("{localhost}:{endpoint_port}").parse().unwrap();
+ },
+ |node_config| {
+ node_config.node.rpc_bind = format!("{localhost}:{node_rpc_port}");
+ node_config.node.p2p_bind = format!("{localhost}:{node_p2p_port}");
+ node_config.node.data_url = format!("http://{localhost}:{node_rpc_port}");
+ node_config.node.p2p_address = format!("{localhost}:{node_p2p_port}");
+
+ node_config.node.seed = btc_miner_seed.clone();
+ node_config.node.local_peer_seed = btc_miner_seed.clone();
+ node_config.burnchain.local_mining_public_key = Some(btc_miner_pk.to_hex());
+
+ node_config.miner.wait_on_interim_blocks = Some(Duration::from_secs(10));
+ node_config.burnchain.pox_reward_length = Some(30);
+ node_config.node.pox_sync_sample_secs = 30;
+ },
+ Some(vec![btc_miner_pk.clone()]),
+ None,
+ );
+ info!("--------------- Started mixed signer test: {num_new_signers} new, {num_old_signers} old signers ---------------");
+ // Boot to Nakamoto (Epoch 3.0)
+ signer_test.boot_to_epoch_3();
+ let start_chain = get_chain_info(&signer_test.running_nodes.conf);
+ test_observer::clear();
+
+ // Mine several Nakamoto tenures to trigger signing rounds
+ for tenure in 1..=nmb_tenures {
+ info!("--------------- Mining Nakamoto tenure #{tenure} ---------------");
+
+ // Mine a tenure-start block (should trigger signer voting)
+ signer_test.mine_nakamoto_block(Duration::from_secs(60), false);
+ }
+ // Wait for all signatures to come in from every signer per tenure
+ wait_for(60, || {
+ let stackerdb_events = test_observer::get_stackerdb_chunks();
+ let nmb_signatures = stackerdb_events
+ .iter()
+ .map(|ev| ev.modified_slots.iter())
+ .flatten()
+ .filter_map(|chunk| {
+ let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice())
+ else {
+ return None;
+ };
+ let SignerMessage::BlockResponse(BlockResponse::Accepted(accepted)) = message
+ else {
+ return None;
+ };
+ Some(accepted)
+ })
+ .count();
+ Ok(nmb_signatures >= num_signers * nmb_tenures)
+ })
+ .expect("Expected every signer to sign every tenure-start block");
+ // Now analyze stackerdb events to count old vs new BlockResponse versions
+ let stackerdb_events = test_observer::get_stackerdb_chunks();
+ let state_machine_updates = stackerdb_events
+ .iter()
+ .map(|ev| ev.modified_slots.iter())
+ .flatten()
+ .filter_map(|chunk| {
+ let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice())
+ else {
+ return None;
+ };
+ let SignerMessage::StateMachineUpdate(update) = message else {
+ return None;
+ };
+ Some(update)
+ })
+ .collect::>();
+ info!("--------------- StackerDB State Machine Updates ---------------");
+ let nmb_old_versions = state_machine_updates
+ .iter()
+ .filter(|update| update.local_supported_signer_protocol_version == OldSupportedVersion)
+ .count();
+ let nmb_current_versions = state_machine_updates
+ .iter()
+ .filter(|update| update.local_supported_signer_protocol_version == NewSupportedVersion)
+ .count();
+ assert!(
+ nmb_old_versions >= state_machine_updates.len() * 6 / 10,
+ "Expected 60% of signers to be configured to support only the old protocol version"
+ );
+ assert!(
+ nmb_current_versions >= state_machine_updates.len() * 4 / 10,
+ "Expected 40% of signers to be configured to support the new protocol version"
+ );
+
+ let end_chain = get_chain_info(&signer_test.running_nodes.conf);
+ info!("Final chain info: {end_chain:#?}",);
+
+ assert_eq!(
+ end_chain.burn_block_height,
+ start_chain.burn_block_height + nmb_tenures as u64,
+ "Chain should have progressed by {nmb_tenures} burn blocks"
+ );
+ assert_eq!(
+ end_chain.stacks_tip_height,
+ start_chain.stacks_tip_height + nmb_tenures as u64,
+ "Chain should have progressed by {nmb_tenures} stacks blocks",
+ );
+ assert_ne!(end_chain.stacks_tip, start_chain.stacks_tip);
+}
+
+#[test]
+#[ignore] // Remove when ready to run in CI
+/// Test with 80% new signers and 20% old signers that the chain still progresses
+/// with ALL signers signing the proposed blocks.
+fn mixed_signer_set_80_percent_new_20_percent_old() {
+ // We want: 80% new (current), 20% old (v3.3.0.0.4)
+ // Let's use 10 signers total for clean percentages: 8 new, 2 old
+ assert!(
+ OldSupportedVersion < NewSupportedVersion,
+ "Test setup error: old supported version should be less than new supported version"
+ );
+ let num_signers = 10;
+ let num_new_signers = 8; // 80%
+ let num_old_signers = 2; // 20%
+
+ // Let's just do 3 tenures
+ let nmb_tenures = 3;
+
+ let btc_miner_seed = vec![2, 2, 2, 2];
+ let btc_miner_pk = Keychain::default(btc_miner_seed.clone()).get_pub_key();
+
+ let localhost = "127.0.0.1";
+ let node_rpc_port = 30500;
+ let node_p2p_port = 30501;
+
+ // Custom signer config: decide old vs new based on signer index
+ let signer_test: SignerTest = SignerTest::new_with_config_modifications(
+ num_signers,
+ vec![],
+ |signer_config| {
+ // Use port to determine version: even port -> current (new), odd -> old
+ // Initially the signer port number is simply sequential
+ let signer_index = signer_config.endpoint.port() % num_signers as u16;
+ // Update the ports to enforce versioning
+ let base_port = 40000 + signer_index * 10;
+ let endpoint_port = if signer_index < num_new_signers as u16 {
+ signer_config.supported_signer_protocol_version = NewSupportedVersion;
+ base_port // even: new version
+ } else {
+ signer_config.supported_signer_protocol_version = OldSupportedVersion;
+ base_port + 1 // odd: old version
+ };
+ signer_config.endpoint = format!("{localhost}:{endpoint_port}").parse().unwrap();
+ },
+ |node_config| {
+ node_config.node.rpc_bind = format!("{localhost}:{node_rpc_port}");
+ node_config.node.p2p_bind = format!("{localhost}:{node_p2p_port}");
+ node_config.node.data_url = format!("http://{localhost}:{node_rpc_port}");
+ node_config.node.p2p_address = format!("{localhost}:{node_p2p_port}");
+
+ node_config.node.seed = btc_miner_seed.clone();
+ node_config.node.local_peer_seed = btc_miner_seed.clone();
+ node_config.burnchain.local_mining_public_key = Some(btc_miner_pk.to_hex());
+
+ node_config.miner.wait_on_interim_blocks = Some(Duration::from_secs(10));
+ node_config.burnchain.pox_reward_length = Some(30);
+ node_config.node.pox_sync_sample_secs = 30;
+ },
+ Some(vec![btc_miner_pk.clone()]),
+ None,
+ );
+ info!("--------------- Started mixed signer test: {num_new_signers} new, {num_old_signers} old signers ---------------");
+ // Boot to Nakamoto (Epoch 3.0)
+ signer_test.boot_to_epoch_3();
+ let start_chain = get_chain_info(&signer_test.running_nodes.conf);
+ test_observer::clear();
+
+ // Mine several Nakamoto tenures to trigger signing rounds
+ for tenure in 1..=nmb_tenures {
+ info!("--------------- Mining Nakamoto tenure #{tenure} ---------------");
+
+ // Mine a tenure-start block (should trigger signer voting)
+ signer_test.mine_nakamoto_block(Duration::from_secs(60), false);
+ }
+ // Wait for all signatures to come in from every signer per tenure
+ wait_for(60, || {
+ let stackerdb_events = test_observer::get_stackerdb_chunks();
+ let nmb_signatures = stackerdb_events
+ .iter()
+ .map(|ev| ev.modified_slots.iter())
+ .flatten()
+ .filter_map(|chunk| {
+ let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice())
+ else {
+ return None;
+ };
+ let SignerMessage::BlockResponse(BlockResponse::Accepted(accepted)) = message
+ else {
+ return None;
+ };
+ Some(accepted)
+ })
+ .count();
+ Ok(nmb_signatures >= num_signers * nmb_tenures)
+ })
+ .expect("Expected every signer to sign every tenure-start block");
+ // Now analyze stackerdb events to count old vs new BlockResponse versions
+ let stackerdb_events = test_observer::get_stackerdb_chunks();
+ let state_machine_updates = stackerdb_events
+ .iter()
+ .map(|ev| ev.modified_slots.iter())
+ .flatten()
+ .filter_map(|chunk| {
+ let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice())
+ else {
+ return None;
+ };
+ let SignerMessage::StateMachineUpdate(update) = message else {
+ return None;
+ };
+ Some(update)
+ })
+ .collect::>();
+ info!("--------------- StackerDB State Machine Updates ---------------");
+ let nmb_old_versions = state_machine_updates
+ .iter()
+ .filter(|update| update.local_supported_signer_protocol_version == OldSupportedVersion)
+ .count();
+ let nmb_current_versions = state_machine_updates
+ .iter()
+ .filter(|update| update.local_supported_signer_protocol_version == NewSupportedVersion)
+ .count();
+ assert!(
+ nmb_old_versions >= state_machine_updates.len() * 2 / 10,
+ "Expected 20% of signers to be configured to support only the old protocol version"
+ );
+ assert!(
+ nmb_current_versions >= state_machine_updates.len() * 8 / 10,
+ "Expected 80% of signers to be configured to support the new protocol version"
+ );
+
+ let end_chain = get_chain_info(&signer_test.running_nodes.conf);
+ info!("Final chain info: {end_chain:#?}",);
+
+ assert_eq!(
+ end_chain.burn_block_height,
+ start_chain.burn_block_height + nmb_tenures as u64,
+ "Chain should have progressed by {nmb_tenures} burn blocks"
+ );
+ assert_eq!(
+ end_chain.stacks_tip_height,
+ start_chain.stacks_tip_height + nmb_tenures as u64,
+ "Chain should have progressed by {nmb_tenures} stacks blocks",
+ );
+ assert_ne!(end_chain.stacks_tip, start_chain.stacks_tip);
+}
diff --git a/stacks-node/src/tests/signer/v0.rs b/stacks-node/src/tests/signer/v0.rs
index 9b6f50e5da5..c16fe821138 100644
--- a/stacks-node/src/tests/signer/v0.rs
+++ b/stacks-node/src/tests/signer/v0.rs
@@ -1,4 +1,4 @@
-// Copyright (C) 2020-2024 Stacks Open Internet Foundation
+// Copyright (C) 2020-2026 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
@@ -1571,24 +1571,35 @@ pub fn wait_for_state_machine_update(
signer_addresses: &[(StacksAddress, u64)],
) -> Result<(), String> {
wait_for(timeout_secs, || {
- let mut found_updates = HashSet::new();
+ let mut found_updates: HashSet = HashSet::new();
let stackerdb_events = test_observer::get_stackerdb_chunks();
for chunk in stackerdb_events
.into_iter()
.flat_map(|chunk| chunk.modified_slots)
{
- let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice())
- .expect("Failed to deserialize SignerMessage");
+ let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice())
+ else {
+ continue;
+ };
let SignerMessage::StateMachineUpdate(update) = message else {
continue;
};
let Some((address, version)) = signer_addresses
.iter()
- .find(|(addr, _)| chunk.verify(addr).unwrap())
+ .find(|(addr, _)| chunk.verify(addr).unwrap_or(false))
else {
continue;
};
- let (burn_block, burn_block_height, current_miner) = match (version, &update.content) {
+
+ let negotiated = update
+ .active_signer_protocol_version
+ .min(update.local_supported_signer_protocol_version);
+ if negotiated != *version {
+ continue;
+ }
+
+ let (burn_block, burn_block_height, current_miner) = match (negotiated, &update.content)
+ {
(
0,
StateMachineUpdateContent::V0 {
@@ -1643,7 +1654,7 @@ pub fn wait_for_state_machine_update(
}
};
// We only need one update to match our conditions
- found_updates.insert(address);
+ found_updates.insert(address.clone());
}
Ok(found_updates.len() > signer_addresses.len() * 7 / 10)
})
@@ -1656,24 +1667,34 @@ pub fn wait_for_state_machine_update_by_miner_tenure_id(
signer_addresses: &[(StacksAddress, u64)],
) -> Result<(), String> {
wait_for(timeout_secs, || {
- let mut found_updates = HashSet::new();
+ let mut found_updates: HashSet = HashSet::new();
let stackerdb_events = test_observer::get_stackerdb_chunks();
for chunk in stackerdb_events
.into_iter()
.flat_map(|chunk| chunk.modified_slots)
{
- let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice())
- .expect("Failed to deserialize SignerMessage");
+ let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice())
+ else {
+ continue;
+ };
let SignerMessage::StateMachineUpdate(update) = message else {
continue;
};
let Some((address, version)) = signer_addresses
.iter()
- .find(|(addr, _)| chunk.verify(addr).unwrap())
+ .find(|(addr, _)| chunk.verify(addr).unwrap_or(false))
else {
continue;
};
- match (version, &update.content) {
+ // Version sanity + expectation:
+ let negotiated = update
+ .active_signer_protocol_version
+ .min(update.local_supported_signer_protocol_version);
+ debug!("Found update from address {address} with version {version}. We expect version {negotiated}.");
+ if negotiated != *version {
+ continue;
+ }
+ match (negotiated, &update.content) {
(
0,
StateMachineUpdateContent::V0 {
@@ -1696,10 +1717,10 @@ pub fn wait_for_state_machine_update_by_miner_tenure_id(
},
) => {
if tenure_id == expected_tenure_id {
- found_updates.insert(address);
+ found_updates.insert(address.clone());
}
}
- (_, _) => {}
+ (_, _) => continue,
};
}
Ok(found_updates.len() > signer_addresses.len() * 7 / 10)
@@ -1813,7 +1834,14 @@ fn block_proposal_rejection() {
if signer_signature_hash == block_signer_signature_hash_1 {
found_signer_signature_hash_1 = true;
assert_eq!(reason_code, RejectCode::SortitionViewMismatch,);
- assert_eq!(response_data.reject_reason, RejectReason::InvalidBitvec);
+ assert!(
+ matches!(
+ response_data.reject_reason,
+ RejectReason::ConsensusHashMismatch { .. }
+ ),
+ "Unexpected reject reason: {}",
+ response_data.reject_reason
+ );
} else if signer_signature_hash == block_signer_signature_hash_2 {
found_signer_signature_hash_2 = true;
assert!(matches!(
@@ -7429,7 +7457,8 @@ fn empty_tenure_delayed() {
.collect();
assert_eq!(signer_slot_ids.len(), num_signers);
- // The miner's proposed block should get rejected by all the signers
+ // The miner's proposed block should get rejected by all the signers as the consensus hash does not match
+ // the active miner's tenure id
let mut found_rejections = Vec::new();
wait_for(short_timeout.as_secs(), || {
for slot_id in signer_slot_ids.iter() {
@@ -7455,7 +7484,7 @@ fn empty_tenure_delayed() {
})) = latest_msg
{
assert_eq!(reason_code, RejectCode::SortitionViewMismatch);
- assert_eq!(response_data.reject_reason, RejectReason::InvalidMiner);
+ assert!(matches!(response_data.reject_reason, RejectReason::ConsensusHashMismatch { .. }), "Unexpected reject reason: {}", response_data.reject_reason);
assert_eq!(metadata.server_version, VERSION_STRING.to_string());
found_rejections.push(*slot_id);
} else {
@@ -15559,7 +15588,7 @@ fn non_blocking_minority_configured_to_favour_incoming_miner() {
/// - The stacks tip advances to N+3
#[test]
#[ignore]
-fn non_blocking_minority_configured_to_favour_prev_miner() {
+fn non_blocking_minority_configured_to_favour_prev_miner_v1() {
if env::var("BITCOIND_TEST") != Ok("1".into()) {
return;
}
@@ -15593,7 +15622,10 @@ fn non_blocking_minority_configured_to_favour_prev_miner() {
config.miner.block_commit_delay = Duration::from_secs(0);
},
);
-
+ let all_signers = miners.signer_test.signer_test_pks();
+ // Pin all the signers to version 1;
+ let pinned_signers = all_signers.iter().map(|key| (key.clone(), 1)).collect();
+ TEST_PIN_SUPPORTED_SIGNER_PROTOCOL_VERSION.set(pinned_signers);
let (conf_1, _) = miners.get_node_configs();
let (miner_pk_1, miner_pk_2) = miners.get_miner_public_keys();
let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes();
@@ -15755,6 +15787,247 @@ fn non_blocking_minority_configured_to_favour_prev_miner() {
miners.shutdown();
}
+/// Test a scenario where a non-blocking majority of signers are configured to favour the previous miner
+/// extending their tenure when the incoming miner is slow to propose a block. The incoming miner should succeed
+/// and its subsequent blocks should be be approved.
+/// Two miners boot to Nakamoto.
+/// Miner 1 wins the first tenure A.
+/// Miner 1 proposes a block N with a TenureChangeCause::BlockFound
+/// Signers accept and the stacks tip advances to N
+/// Miner 2 wins the second tenure B.
+/// A minority of signers mark miner 2 as invalid.
+/// Miner 1 proposes block N+1' with a TenureChangeCause::Extended
+/// ALL signers reject block N+1'
+/// Miner 2 proposes block N+1 with a TenureChangeCause::BlockFound
+/// ALL signers accept block N+1.
+/// Miner 2 proposes block N+2 with a transfer tx
+/// ALL signers should accept block N+2.
+/// Miner 1 wins the third tenure C.
+/// Miner 1 proposes block N+3 with a TenureChangeCause::BlockFound
+/// Signers accept and the stacks tip advances to N+3
+///
+/// Asserts:
+/// - Block N contains the TenureChangeCause::BlockFound
+/// - Block N+1' contains a TenureChangeCause::Extended and is rejected
+/// - Block N+1 contains the TenureChangeCause::BlockFound
+/// - Block N+2 is accepted.
+/// - Block N+3 contains the TenureChangeCause::BlockFound.
+/// - The stacks tip advances to N+3
+#[test]
+#[ignore]
+fn non_blocking_minority_configured_to_favour_prev_miner() {
+ if env::var("BITCOIND_TEST") != Ok("1".into()) {
+ return;
+ }
+
+ let num_signers = 5;
+ let non_block_minority = num_signers * 2 / 10;
+ let num_txs = 1;
+
+ let favour_prev_miner_block_proposal_timeout = Duration::from_secs(20);
+ let favour_incoming_miner_block_proposal_timeout = Duration::from_secs(500);
+ // Make sure the miner attempts to extend after the minority mark the incoming as invalid
+ let tenure_extend_wait_timeout = favour_prev_miner_block_proposal_timeout;
+
+ let mut miners = MultipleMinerTest::new_with_config_modifications(
+ num_signers,
+ num_txs,
+ |signer_config| {
+ let port = signer_config.endpoint.port();
+ // Note signer ports are based on the number of them, the first being 3000, the last being 3000 + num_signers - 1
+ if port < 3000 + non_block_minority as u16 {
+ signer_config.block_proposal_timeout = favour_prev_miner_block_proposal_timeout;
+ } else {
+ signer_config.block_proposal_timeout = favour_incoming_miner_block_proposal_timeout;
+ }
+ },
+ |config| {
+ config.miner.tenure_extend_wait_timeout = tenure_extend_wait_timeout;
+ config.miner.block_commit_delay = Duration::from_secs(0);
+ },
+ |config| {
+ config.miner.block_commit_delay = Duration::from_secs(0);
+ },
+ );
+ let all_signers = miners.signer_test.signer_test_pks();
+ let non_blocking_minority_signers = &all_signers[..non_block_minority];
+ let non_blocking_signer_versions: Vec<_> = miners
+ .signer_test
+ .signer_addresses_versions()
+ .into_iter()
+ .filter(|(address, _)| {
+ non_blocking_minority_signers
+ .iter()
+ .find(|pubkey| &StacksAddress::p2pkh(false, pubkey) == address)
+ .is_some()
+ })
+ .collect();
+ let (conf_1, _) = miners.get_node_configs();
+ let (miner_pk_1, miner_pk_2) = miners.get_miner_public_keys();
+ let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes();
+
+ let rl1_skip_commit_op = miners
+ .signer_test
+ .running_nodes
+ .counters
+ .naka_skip_commit_op
+ .clone();
+ let rl2_skip_commit_op = miners.rl2_counters.naka_skip_commit_op.clone();
+
+ info!("------------------------- Pause Miner 2's Block Commits -------------------------");
+
+ // Make sure Miner 2 cannot win a sortition at first.
+ rl2_skip_commit_op.set(true);
+
+ miners.boot_to_epoch_3();
+
+ let burnchain = conf_1.get_burnchain();
+ let sortdb = burnchain.open_sortition_db(true).unwrap();
+
+ let get_burn_height = || {
+ SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())
+ .unwrap()
+ .block_height
+ };
+ let starting_peer_height = get_chain_info(&conf_1).stacks_tip_height;
+ let starting_burn_height = get_burn_height();
+ let mut btc_blocks_mined = 0;
+
+ info!("------------------------- Pause Miner 1's Block Commit -------------------------");
+ // Make sure miner 1 doesn't submit any further block commits for the next tenure BEFORE mining the bitcoin block
+ rl1_skip_commit_op.set(true);
+
+ info!("------------------------- Miner 1 Mines a Normal Tenure A -------------------------");
+ miners
+ .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 30)
+ .expect("Failed to mine BTC block and Tenure Change Tx Block");
+ btc_blocks_mined += 1;
+
+ // assure we have a successful sortition that miner 1 won
+ verify_sortition_winner(&sortdb, &miner_pkh_1);
+
+ info!("------------------------- Submit Miner 2 Block Commit -------------------------");
+ miners.submit_commit_miner_2(&sortdb);
+ // Pause the block proposal broadcast so that miner 2 will be unable to broadcast its
+ // tenure change proposal BEFORE miner 1 attempts to extend.
+ TEST_BROADCAST_PROPOSAL_STALL.set(vec![miner_pk_2.clone()]);
+
+ let stacks_height_before = miners.get_peer_stacks_tip_height();
+ info!("------------------------- Miner 2 Wins Tenure B -------------------------";
+ "stacks_height_before" => %stacks_height_before);
+ test_observer::clear();
+ miners
+ .mine_bitcoin_blocks_and_confirm(&sortdb, 1, 30)
+ .expect("Failed to start Tenure B");
+ btc_blocks_mined += 1;
+
+ assert_eq!(stacks_height_before, miners.get_peer_stacks_tip_height());
+
+ // assure we have a successful sortition that miner 2 won
+ verify_sortition_winner(&sortdb, &miner_pkh_2);
+ info!(
+ "------------------------- Wait for Miner 1 to think Miner 2 is Invalid -------------------------"
+ );
+ // Make sure that miner 1 thinks miner 2 is invalid.
+ std::thread::sleep(tenure_extend_wait_timeout.add(Duration::from_secs(1)));
+ let get_burn_consensus_hash = || {
+ SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())
+ .unwrap()
+ .consensus_hash
+ };
+ // Lets make sure our non blocking minority tries to mark the miner invalid
+ wait_for_state_machine_update(
+ 30,
+ &get_burn_consensus_hash(),
+ miners.get_peer_info().burn_block_height,
+ Some((miner_pkh_1.clone(), stacks_height_before - 1)),
+ &non_blocking_signer_versions,
+ )
+ .expect("Timed out waiting for minority signers to send a state update");
+
+ info!("------------------------- Wait for Miner 1's Block N+1' to be Proposed ------------------------";
+ "stacks_height_before" => %stacks_height_before);
+
+ let miner_1_block_n_1_prime =
+ wait_for_block_proposal(30, stacks_height_before + 1, &miner_pk_1)
+ .expect("Miner 1 failed to propose block N+1'");
+ assert!(miner_1_block_n_1_prime
+ .try_get_tenure_change_payload()
+ .unwrap()
+ .cause
+ .is_eq(&TenureChangeCause::Extended));
+
+ info!("------------------------- Verify that Miner 1's Block N+1' was Rejected by ALL signers ------------------------");
+ wait_for_block_rejections_from_signers(
+ 30,
+ &miner_1_block_n_1_prime.header.signer_signature_hash(),
+ &all_signers,
+ )
+ .expect("Failed to reach rejection consensus for Miner 1's Block N+1'");
+
+ assert_eq!(stacks_height_before, miners.get_peer_stacks_tip_height());
+
+ info!("------------------------- Wait for Miner 2's Block N+1 BlockFound to be Proposed and Approved------------------------";
+ "stacks_height_before" => %stacks_height_before
+ );
+
+ TEST_BROADCAST_PROPOSAL_STALL.set(vec![]);
+
+ let miner_2_block_n_1 =
+ wait_for_block_pushed_by_miner_key(30, stacks_height_before + 1, &miner_pk_2)
+ .expect("Miner 2's block N+1 was not mined");
+ let peer_info = miners.get_peer_info();
+ assert_eq!(peer_info.stacks_tip, miner_2_block_n_1.header.block_hash());
+ assert_eq!(peer_info.stacks_tip_height, stacks_height_before + 1);
+
+ info!("------------------------- Verify ALL the Signer's Accepted Miner 2's Block N+1 -------------------------");
+ wait_for_block_acceptance_from_signers(
+ 30,
+ &miner_2_block_n_1.header.signer_signature_hash(),
+ &all_signers,
+ )
+ .expect("Failed to get expected acceptances for Miner 2's block N+1.");
+ info!(
+ "------------------------- Verify BlockFound in Miner 2's Block N+1 -------------------------"
+ );
+ verify_last_block_contains_tenure_change_tx(TenureChangeCause::BlockFound);
+
+ info!("------------------------- Miner 2 Mines Block N+2 with Transfer Tx -------------------------");
+ let stacks_height_before = miners.get_peer_stacks_tip_height();
+ miners
+ .send_and_mine_transfer_tx(30)
+ .expect("Failed to Mine Block N+2");
+
+ let miner_2_block_n_2 =
+ wait_for_block_pushed_by_miner_key(30, stacks_height_before + 1, &miner_pk_2)
+ .expect("Miner 2's block N+1 was not mined");
+ let peer_info = miners.get_peer_info();
+ assert_eq!(peer_info.stacks_tip, miner_2_block_n_2.header.block_hash());
+ assert_eq!(peer_info.stacks_tip_height, stacks_height_before + 1);
+
+ info!("------------------------- Unpause Miner 1's Block Commits -------------------------");
+ miners.submit_commit_miner_1(&sortdb);
+
+ info!("------------------------- Miner 1 Mines a Normal Tenure C -------------------------");
+ miners
+ .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 30)
+ .expect("Failed to start Tenure C and mine block N+3");
+ btc_blocks_mined += 1;
+
+ // assure we have a successful sortition that miner 1 won
+ verify_sortition_winner(&sortdb, &miner_pkh_1);
+
+ info!(
+ "------------------------- Confirm Burn and Stacks Block Heights -------------------------"
+ );
+ assert_eq!(get_burn_height(), starting_burn_height + btc_blocks_mined);
+ assert_eq!(
+ miners.get_peer_stacks_tip_height(),
+ starting_peer_height + 4
+ );
+ miners.shutdown();
+}
+
/// Test a scenario where:
/// Two miners boot to Nakamoto.
/// Sortition occurs. Miner 1 wins.
@@ -17885,136 +18158,193 @@ fn reorging_signers_capitulate_to_nonreorging_signers_during_tenure_fork() {
);
}
-/// Tests that signers are able to upgrade or downgrade their active protocol version numbers based on
-/// the majority of other signers current local supported version numbers
-#[test]
-#[ignore]
-fn rollover_signer_protocol_version() {
+fn mine_burn_block_and_confirm_signer_rollover(
+ signer_test: &mut SignerTest,
+ expected_versions: &[(StacksAddress, u64)],
+) {
+ test_observer::clear();
+ TEST_MINE_SKIP.set(true);
+ info!("------------------------- Mine Bitcoin Block -------------------------");
+ signer_test.mine_bitcoin_block();
+ let tip = SortitionDB::get_canonical_burn_chain_tip(
+ signer_test
+ .running_nodes
+ .conf
+ .get_burnchain()
+ .open_sortition_db(true)
+ .unwrap()
+ .conn(),
+ )
+ .unwrap();
+ wait_for_state_machine_update(
+ 60,
+ &tip.consensus_hash,
+ tip.block_height,
+ None,
+ &expected_versions,
+ )
+ .expect("Timed out waiting for signers to send their state updates after a bitcoin block");
+ let info = signer_test.get_peer_info();
+ info!(
+ "------------------------- Mine Tenure Change Stacks Transaction -------------------------"
+ );
+ TEST_MINE_SKIP.set(false);
+ let expected_miner = StacksPublicKey::from_private(
+ &signer_test
+ .running_nodes
+ .conf
+ .miner
+ .mining_key
+ .clone()
+ .unwrap(),
+ );
+
+ let block = wait_for_block_pushed_by_miner_key(60, info.stacks_tip_height + 1, &expected_miner)
+ .expect("Failed to mine block after upgraded version number.");
+ wait_for_block_acceptance_from_signers(
+ 30,
+ &block.header.signer_signature_hash(),
+ &signer_test.signer_test_pks(),
+ )
+ .expect("Failed to confirm all signers accepted block");
+}
+
+/// Tests that signers negotiate their **active** signer protocol version based on the
+/// **majority of locally supported** signer protocol versions.
+///
+/// Scenario (10 signers):
+/// 1) Baseline: all signers start on `SUPPORTED_SIGNER_PROTOCOL_VERSION` and can sign blocks.
+/// 2) Downgrade 30%: non-blocking minority downgraded. Stays at `SUPPORTED_SIGNER_PROTOCOL_VERSION`.
+/// 3) Downgrade 40%: blocking minority downgraded. Downgrades to `SUPPORTED_SIGNER_PROTOCOL_VERSION - 1`.
+/// 4) Upgrade 70%: majority upgraded. Upgrades back to `SUPPORTED_SIGNER_PROTOCOL_VERSION`.
+#[test]
+#[ignore]
+fn downgrade_signer_protocol_version() {
if env::var("BITCOIND_TEST") != Ok("1".into()) {
return;
}
- let num_signers = 5;
+ // To make the math easier, use 10 signers
+ let num_signers: usize = 10;
- let signer_test: SignerTest = SignerTest::new(num_signers, vec![]);
+ // Integer percent helper (e.g. 30 => 3 when num_signers=10)
+ fn pct(num_signers: usize, percent: usize) -> usize {
+ (num_signers * percent) / 100
+ }
+
+ // Boot with the most recent supported version
+ let mut signer_test: SignerTest = SignerTest::new(num_signers, vec![]);
signer_test.boot_to_epoch_3();
- let conf = signer_test.running_nodes.conf.clone();
+ info!(
+ "------------------------- Confirm miner tenure continues and all signers use protocol {SUPPORTED_SIGNER_PROTOCOL_VERSION} -------------------------"
+ );
+ let versions = signer_test.signer_addresses_versions();
+ mine_burn_block_and_confirm_signer_rollover(&mut signer_test, &versions);
- let burnchain = conf.get_burnchain();
- let sortdb = burnchain.open_sortition_db(true).unwrap();
+ let downgraded_version = SUPPORTED_SIGNER_PROTOCOL_VERSION.saturating_sub(1);
+ let downgraded_versions = signer_test
+ .signer_addresses_versions()
+ .into_iter()
+ .map(|(addr, _)| (addr, downgraded_version))
+ .collect::>();
+ info!("------------------------- Downgrading Signer Versions to {downgraded_version} for 30 Percent of Signers -------------------------");
+ // Since only 30 percent downgraded. Active protocol version should stay the same
+ signer_test
+ .restart_first_n_signers_with_supported_version(pct(num_signers, 30), downgraded_version);
+ let versions = signer_test.signer_addresses_versions();
+ mine_burn_block_and_confirm_signer_rollover(&mut signer_test, &versions);
+ info!("------------------------- Downgrading Signer Versions to {downgraded_version} for 40 Percent of Signers -------------------------");
+ // A blocking minority has downgraded, so expect everyone to negotiate to downgraded_version.
+ signer_test
+ .restart_first_n_signers_with_supported_version(pct(num_signers, 40), downgraded_version);
+ mine_burn_block_and_confirm_signer_rollover(&mut signer_test, &downgraded_versions);
- let all_signers = signer_test.signer_test_pks();
- info!(
- "------------------------- Miner Tenure Starts and Mines Block N-------------------------"
+ info!("------------------------- Upgrade Signer Versions to {SUPPORTED_SIGNER_PROTOCOL_VERSION} for 70 Percent of Signers -------------------------");
+ signer_test.restart_first_n_signers_with_supported_version(
+ pct(num_signers, 70),
+ SUPPORTED_SIGNER_PROTOCOL_VERSION,
);
- test_observer::clear();
- signer_test.mine_and_verify_confirmed_naka_block(Duration::from_secs(30), num_signers, true);
+ let versions = signer_test.signer_addresses_versions();
+ // A majority of signers have upgraded, expect everyone to negotiate back to the supported version.
+ mine_burn_block_and_confirm_signer_rollover(&mut signer_test, &versions);
+ signer_test.shutdown();
+}
- let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap();
- let burn_consensus_hash = tip.consensus_hash;
- let burn_height = tip.block_height;
+/// Tests the latest activation rollover behavior across different activation
+/// percentages, ensuring that block signing continues as old signers can still communicate with new signers.
+///
+/// Scenario (10 signers):
+/// 1) 30% activated: non-blocking minority activated.
+/// 2) 40% activated: blocking minority activated .
+/// 3) 60% activated: blocking minority unactivated.
+/// 4) 70% activated: non-blocking minority unactivated.
+/// 5) 100% activated: all signers activated.
+#[test]
+#[ignore]
+fn rollover_signer_protocol_version() {
+ if env::var("BITCOIND_TEST") != Ok("1".into()) {
+ return;
+ }
- info!("------------------------- Confirm Miner is the Active Miner in Update and All Signers Are Using Protocol Number {SUPPORTED_SIGNER_PROTOCOL_VERSION} -------------------------");
- // Verify that signers first sent a bitcoin block update
- wait_for_state_machine_update(
- 60,
- &burn_consensus_hash,
- burn_height,
- None,
- &signer_test.signer_addresses_versions(),
- )
- .expect("Timed out waiting for signers to send a state update for block N");
+ // To make the math easier, use 10 signers
+ let num_signers: usize = 10;
- test_observer::clear();
- let downgraded_version = SUPPORTED_SIGNER_PROTOCOL_VERSION.saturating_sub(1);
- info!("------------------------- Downgrading Signer Versions to {downgraded_version} for 20 Percent of Signers -------------------------");
- // Take a non blocking minority of signers (20%) and downgrade their version number
- let pinned_signers: Vec<_> = all_signers
- .iter()
- .take(num_signers * 2 / 10)
- .cloned()
- .collect();
- let pinned_signers_versions: HashMap = pinned_signers
- .iter()
- .map(|signer| (signer.clone(), downgraded_version))
- .collect();
- TEST_PIN_SUPPORTED_SIGNER_PROTOCOL_VERSION.set(pinned_signers_versions);
+ // Integer percent helper (e.g. 30 => 3 when num_signers=10)
+ fn pct(num_signers: usize, percent: usize) -> usize {
+ (num_signers * percent) / 100
+ }
- info!("------------------------- Confirm Signers Still Manage to Sign a Stacks Block With Misaligned Version Numbers -------------------------");
- signer_test.mine_and_verify_confirmed_naka_block(Duration::from_secs(30), num_signers, true);
+ let old_version = SUPPORTED_SIGNER_PROTOCOL_VERSION.saturating_sub(1);
+ // Start with 30% of signers activated
+ let mut signer_test: SignerTest = SignerTest::new_with_config_modifications(
+ num_signers,
+ vec![],
+ |signer_config| {
+ // 30% of 10 is 3 signers
+ let activated_signers = pct(num_signers, 30) as u16;
- let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap();
- let burn_consensus_hash = tip.consensus_hash;
- let burn_height = tip.block_height;
- // Only one signer is downgraded so the active protocol version remains the same.
- wait_for_state_machine_update(
- 60,
- &burn_consensus_hash,
- burn_height,
+ // signer_nmb in [0, num_signers-1]
+ let signer_nmb = signer_config.endpoint.port() % num_signers as u16;
+
+ let signer_version = if signer_nmb < activated_signers {
+ SUPPORTED_SIGNER_PROTOCOL_VERSION
+ } else {
+ old_version
+ };
+
+ signer_config.supported_signer_protocol_version = signer_version;
+ },
+ |_| {},
None,
- &signer_test.signer_addresses_versions(),
- )
- .expect("Timed out waiting for signers to send their downgraded state update for block N+1");
+ None,
+ );
- test_observer::clear();
- info!("------------------------- Confirm Signer Version Downgrades Fully Once 70 percent of Signers Downgrade -------------------------");
- let pinned_signers: Vec<_> = all_signers
- .iter()
- .take(num_signers * 7 / 10)
- .cloned()
- .collect();
- let pinned_signers_versions: HashMap = pinned_signers
- .iter()
- .map(|signer| (signer.clone(), downgraded_version))
- .collect();
- TEST_PIN_SUPPORTED_SIGNER_PROTOCOL_VERSION.set(pinned_signers_versions);
+ signer_test.boot_to_epoch_3();
- // Not strictly necessary, but makes it easier to logic out if miner doesn't send a proposal until signers are on same page...
- TEST_MINE_SKIP.set(true);
- info!("------------------------- Confirm Signers Sent Downgraded State Machine Updates -------------------------");
- // Cannot use any built in functions that call mine_nakamoto_block since it expects signer updates matching the majority version and we are manually messing with these versions
- signer_test.mine_bitcoin_block();
- let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap();
- let burn_consensus_hash = tip.consensus_hash;
- let burn_height = tip.block_height;
- // Confirm ALL signers downgrade their supported version and then send a corresponding message in that version message
- let downgraded_versions: Vec<_> = signer_test
+ let old_versions: Vec<_> = signer_test
.signer_addresses_versions()
.into_iter()
- .map(|(address, _)| (address, downgraded_version))
+ .map(|(address, _)| (address, old_version))
.collect();
- wait_for_state_machine_update(
- 60,
- &burn_consensus_hash,
- burn_height,
- None,
- &downgraded_versions,
- )
- .expect("Timed out waiting for signers to send their state update for block N+2");
- let info = signer_test.get_peer_info();
- info!("------------------------- Confirm Signers Sign The Block After Complete Downgraded Version Number -------------------------");
- TEST_MINE_SKIP.set(false);
- let expected_miner = StacksPublicKey::from_private(
- &signer_test
- .running_nodes
- .conf
- .miner
- .mining_key
- .clone()
- .unwrap(),
- );
- let block = wait_for_block_pushed_by_miner_key(60, info.stacks_tip_height + 1, &expected_miner)
- .expect("Failed to mine block after downgraded version number.");
- // Expect ALL signers even after downgrade to approve the proposed blocks
- wait_for_block_acceptance_from_signers(30, &block.header.signer_signature_hash(), &all_signers)
- .expect("Failed to confirm all signers accepted last block");
+ mine_burn_block_and_confirm_signer_rollover(&mut signer_test, &old_versions);
- info!("------------------------- Reset All Signers to {SUPPORTED_SIGNER_PROTOCOL_VERSION} -------------------------");
- TEST_PIN_SUPPORTED_SIGNER_PROTOCOL_VERSION.set(HashMap::new());
- test_observer::clear();
- info!("------------------------- Confirm Signers Sign The Block After Upgraded Version Number -------------------------");
- signer_test.mine_and_verify_confirmed_naka_block(Duration::from_secs(30), num_signers, true);
+ for percent in [40, 60, 70] {
+ info!("------------------------- Upgrade to {SUPPORTED_SIGNER_PROTOCOL_VERSION} for {percent}% of signers -------------------------");
+ signer_test.restart_first_n_signers_with_supported_version(
+ pct(num_signers, percent),
+ SUPPORTED_SIGNER_PROTOCOL_VERSION,
+ );
+ mine_burn_block_and_confirm_signer_rollover(&mut signer_test, &old_versions);
+ }
+
+ info!("------------------------- Upgrade to {SUPPORTED_SIGNER_PROTOCOL_VERSION} for 100% of signers -------------------------");
+ signer_test.restart_first_n_signers_with_supported_version(
+ num_signers,
+ SUPPORTED_SIGNER_PROTOCOL_VERSION,
+ );
+ let new_versions = signer_test.signer_addresses_versions();
+ mine_burn_block_and_confirm_signer_rollover(&mut signer_test, &new_versions);
signer_test.shutdown();
}
@@ -18867,19 +19197,37 @@ fn signers_do_not_commit_unless_threshold_precommitted() {
let ignore_signers: Vec<_> = ignore_slice.to_vec();
let pre_commit_signers: Vec<_> = pre_commit_slice.to_vec();
TEST_IGNORE_ALL_BLOCK_PROPOSALS.set(ignore_signers);
+ // Pause the miner proposing blocks because we need to make sure that the global signer state updates before we evaluate the proposal to ensure
+ // it is isn't immediately rejected under global signer state conditions
+ TEST_BROADCAST_PROPOSAL_STALL.set(vec![miner_pk.clone()]);
test_observer::clear();
let blocks_before = test_observer::get_mined_nakamoto_blocks().len();
let height_before = signer_test.get_peer_info().stacks_tip_height;
+ info!("------------------------- Start Tenure A -------------------------");
next_block_and(
&mut signer_test.running_nodes.btc_regtest_controller,
30,
|| Ok(test_observer::get_mined_nakamoto_blocks().len() > blocks_before),
)
.unwrap();
+ let peer_after = signer_test.get_peer_info();
+ wait_for_state_machine_update(
+ 30,
+ &peer_after.pox_consensus,
+ peer_after.burn_block_height,
+ None,
+ &signer_test.signer_addresses_versions(),
+ )
+ .expect("Failed to update to tenure A");
+ TEST_BROADCAST_PROPOSAL_STALL.set(vec![]);
+
+ info!("------------------------- Wait for block proposal N -------------------------");
let proposal = wait_for_block_proposal(30, height_before + 1, &miner_pk)
.expect("Timed out waiting for block proposal");
let hash = proposal.header.signer_signature_hash();
+
+ info!("------------------------- Wait for pre-commits for N ({hash})-------------------------");
wait_for_block_pre_commits_from_signers(30, &hash, &pre_commit_signers)
.expect("Timed out waiting for pre-commits");
assert!(
@@ -19535,6 +19883,180 @@ fn tenure_extend_after_stale_commit_same_miner_then_no_winner() {
signer_test.shutdown();
}
+#[test]
+#[ignore]
+fn read_count_extend_after_burn_view_change() {
+ 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 num_signers = 5;
+ let num_txs = 5;
+ let idle_timeout = Duration::from_secs(30);
+ let mut miners = MultipleMinerTest::new_with_config_modifications(
+ num_signers,
+ num_txs,
+ |signer_config| {
+ signer_config.block_proposal_timeout = Duration::from_secs(60);
+ signer_config.first_proposal_burn_block_timing = Duration::from_secs(0);
+ // use a different timeout to ensure that the correct timeout
+ // is read by the miner
+ signer_config.tenure_idle_timeout = Duration::from_secs(36000);
+ signer_config.read_count_idle_timeout = idle_timeout;
+ },
+ |config| {
+ config.miner.block_commit_delay = Duration::from_secs(0);
+ let epochs = config.burnchain.epochs.as_mut().unwrap();
+ let epoch_30_height = epochs[StacksEpochId::Epoch30].start_height;
+ epochs[StacksEpochId::Epoch30].end_height = epoch_30_height;
+ epochs[StacksEpochId::Epoch31].start_height = epoch_30_height;
+ epochs[StacksEpochId::Epoch31].end_height = epoch_30_height;
+ epochs[StacksEpochId::Epoch32].start_height = epoch_30_height;
+ epochs[StacksEpochId::Epoch32].end_height = epoch_30_height;
+ epochs[StacksEpochId::Epoch33].start_height = epoch_30_height;
+ },
+ |config| {
+ config.miner.block_commit_delay = Duration::from_secs(0);
+ config.miner.tenure_extend_cost_threshold = 0;
+ config.miner.read_count_extend_cost_threshold = 0;
+
+ let epochs = config.burnchain.epochs.as_mut().unwrap();
+ let epoch_30_height = epochs[StacksEpochId::Epoch30].start_height;
+ epochs[StacksEpochId::Epoch30].end_height = epoch_30_height;
+ epochs[StacksEpochId::Epoch31].start_height = epoch_30_height;
+ epochs[StacksEpochId::Epoch31].end_height = epoch_30_height;
+ epochs[StacksEpochId::Epoch32].start_height = epoch_30_height;
+ epochs[StacksEpochId::Epoch32].end_height = epoch_30_height;
+ epochs[StacksEpochId::Epoch33].start_height = epoch_30_height;
+ },
+ );
+
+ let (conf_1, _) = miners.get_node_configs();
+ let (miner_pk_1, _) = miners.get_miner_public_keys();
+ let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes();
+
+ miners.pause_commits_miner_2();
+ miners.boot_to_epoch_3();
+
+ miners.pause_commits_miner_1();
+
+ let sortdb = conf_1.get_burnchain().open_sortition_db(true).unwrap();
+
+ miners
+ .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 60)
+ .unwrap();
+
+ miners.submit_commit_miner_1(&sortdb);
+
+ info!("------------------------- Miner 1 Wins Tenure A -------------------------");
+ miners
+ .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 60)
+ .unwrap();
+ verify_sortition_winner(&sortdb, &miner_pkh_1);
+ miners.send_and_mine_transfer_tx(60).unwrap();
+ let tip_a_height = miners.get_peer_stacks_tip_height();
+ let prev_tip = get_chain_info(&conf_1);
+
+ info!("------------------------- Miner 2 Wins Tenure B -------------------------");
+ miners.submit_commit_miner_2(&sortdb);
+ miners
+ .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 60)
+ .unwrap();
+ verify_sortition_winner(&sortdb, &miner_pkh_2);
+ miners.send_and_mine_transfer_tx(60).unwrap();
+ let tip_b_height = miners.get_peer_stacks_tip_height();
+ let tenure_b_ch = miners.get_peer_stacks_tip_ch();
+
+ info!("------------------------- Miner 1 Wins Tenure C with stale commit -------------------------");
+
+ // We can't use `submit_commit_miner_1` here because we are using the stale view
+ {
+ TEST_MINER_COMMIT_TIP.set(Some((prev_tip.pox_consensus, prev_tip.stacks_tip)));
+ let rl1_commits_before = miners
+ .signer_test
+ .running_nodes
+ .counters
+ .naka_submitted_commits
+ .load(Ordering::SeqCst);
+
+ miners
+ .signer_test
+ .running_nodes
+ .counters
+ .naka_skip_commit_op
+ .set(false);
+
+ wait_for(30, || {
+ let commits_after = miners
+ .signer_test
+ .running_nodes
+ .counters
+ .naka_submitted_commits
+ .load(Ordering::SeqCst);
+ let last_commit_tip = miners
+ .signer_test
+ .running_nodes
+ .counters
+ .naka_submitted_commit_last_stacks_tip
+ .load(Ordering::SeqCst);
+
+ Ok(commits_after > rl1_commits_before && last_commit_tip == prev_tip.stacks_tip_height)
+ })
+ .expect("Timed out waiting for miner 1 to submit a commit op");
+
+ miners
+ .signer_test
+ .running_nodes
+ .counters
+ .naka_skip_commit_op
+ .set(true);
+ TEST_MINER_COMMIT_TIP.set(None);
+ }
+
+ miners
+ .mine_bitcoin_blocks_and_confirm(&sortdb, 1, 60)
+ .unwrap();
+ verify_sortition_winner(&sortdb, &miner_pkh_1);
+
+ info!(
+ "------------------------- Miner 1's proposal for C is rejected -------------------------"
+ );
+ let proposed_block = wait_for_block_proposal(60, tip_a_height + 1, &miner_pk_1).unwrap();
+ wait_for_block_global_rejection(
+ 60,
+ &proposed_block.header.signer_signature_hash(),
+ num_signers,
+ )
+ .unwrap();
+
+ assert_eq!(miners.get_peer_stacks_tip_ch(), tenure_b_ch);
+
+ info!("------------------------- Miner 2 Extends Tenure B -------------------------");
+ wait_for_tenure_change_tx(60, TenureChangeCause::Extended, tip_b_height + 1).unwrap();
+
+ let final_height = miners.get_peer_stacks_tip_height();
+ assert_eq!(miners.get_peer_stacks_tip_ch(), tenure_b_ch);
+ assert!(final_height >= tip_b_height + 1);
+
+ info!("---- Waiting for a tenure extend ----");
+
+ // Now, wait for a block with a tenure extend
+ wait_for(idle_timeout.as_secs() + 10, || {
+ Ok(last_block_contains_tenure_change_tx(
+ TenureChangeCause::ExtendedReadCount,
+ ))
+ })
+ .expect("Timed out waiting for a block with a tenure extend");
+
+ miners.shutdown();
+}
+
#[test]
#[ignore]
/// Scenario: burn block arrives while miner is in the middle of mining a block.
diff --git a/stacks-signer/CHANGELOG.md b/stacks-signer/CHANGELOG.md
index 632a924a8d7..d9ab23687cf 100644
--- a/stacks-signer/CHANGELOG.md
+++ b/stacks-signer/CHANGELOG.md
@@ -5,11 +5,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to the versioning scheme outlined in the [README.md](README.md).
-## [Unreleased]
+## [3.3.0.0.4.0]
+
+### Fixed
+
+- Correct calculation of burn-view change status using the new tip with metadata endpoint.
+
+## [3.3.0.0.3.0]
### Changed
- Avoid sending duplicate block acceptance messages when additional pre-commits arrive
+- Upgraded `SUPPORTED_SIGNER_PROTOCOL_VERSION` to 2.
+- Set `GLOBAL_SIGNER_STATE_ACTIVATION_VERSION` to 2, activating global signer state agreement block processing.
## [3.3.0.0.2.0]
diff --git a/stacks-signer/src/chainstate/mod.rs b/stacks-signer/src/chainstate/mod.rs
index 4cd59a71244..83b9d81cb95 100644
--- a/stacks-signer/src/chainstate/mod.rs
+++ b/stacks-signer/src/chainstate/mod.rs
@@ -365,7 +365,7 @@ impl SortitionData {
}
let tip = match client.get_tenure_tip(tenure_id) {
- Ok(tip) => tip,
+ Ok(tip) => tip.anchored_header,
Err(e) => {
warn!(
"Failed to fetch the tenure tip for the parent tenure: {e:?}. Assuming proposal is higher than the parent tenure for now.";
diff --git a/stacks-signer/src/chainstate/tests/mod.rs b/stacks-signer/src/chainstate/tests/mod.rs
index 135745390cf..86b2de6bffb 100644
--- a/stacks-signer/src/chainstate/tests/mod.rs
+++ b/stacks-signer/src/chainstate/tests/mod.rs
@@ -13,7 +13,37 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
+use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader};
+use blockstack_lib::net::api::get_tenure_tip_meta::BlockHeaderWithMetadata;
+use stacks_common::util::secp256k1::{MessageSignature, Secp256k1PrivateKey};
+
/// Tests for chainstate v1 implementation
mod v1;
/// Tests for chainstate v2 implementation
mod v2;
+
+pub fn make_parent_header_meta(
+ miner_sk: &Secp256k1PrivateKey,
+ block: &mut NakamotoBlock,
+) -> BlockHeaderWithMetadata {
+ let mut parent_block_header = NakamotoBlockHeader {
+ version: block.header.version,
+ chain_length: block.header.chain_length - 1,
+ burn_spent: block.header.burn_spent,
+ consensus_hash: block.header.consensus_hash.clone(),
+ parent_block_id: block.header.parent_block_id.clone(),
+ tx_merkle_root: block.header.tx_merkle_root.clone(),
+ state_index_root: block.header.state_index_root,
+ timestamp: block.header.timestamp,
+ miner_signature: MessageSignature::empty(),
+ signer_signature: vec![],
+ pox_treatment: block.header.pox_treatment.clone(),
+ };
+
+ parent_block_header.sign_miner(miner_sk).unwrap();
+ block.header.parent_block_id = parent_block_header.block_id();
+ BlockHeaderWithMetadata {
+ anchored_header: parent_block_header.clone().into(),
+ burn_view: Some(block.header.consensus_hash.clone()),
+ }
+}
diff --git a/stacks-signer/src/chainstate/tests/v1.rs b/stacks-signer/src/chainstate/tests/v1.rs
index fbd769f8f7f..14d1a56c43d 100644
--- a/stacks-signer/src/chainstate/tests/v1.rs
+++ b/stacks-signer/src/chainstate/tests/v1.rs
@@ -15,6 +15,8 @@
use std::fs;
use std::net::{Ipv4Addr, SocketAddrV4};
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::Arc;
use std::time::{Duration, SystemTime};
use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader};
@@ -26,6 +28,7 @@ use blockstack_lib::chainstate::stacks::{
TransactionSpendingCondition, TransactionVersion,
};
use blockstack_lib::core::test_util::make_stacks_transfer_tx;
+use blockstack_lib::net::api::get_tenure_tip_meta::BlockHeaderWithMetadata;
use blockstack_lib::net::api::get_tenures_fork_info::TenureForkingInfo;
use blockstack_lib::net::api::getsortition::SortitionInfo;
use clarity::types::chainstate::{BurnchainHeaderHash, SortitionId, StacksAddress};
@@ -43,6 +46,7 @@ use stacks_common::util::hash::{Hash160, Sha512Trunc256Sum};
use stacks_common::util::secp256k1::MessageSignature;
use stacks_common::{function_name, info};
+use crate::chainstate::tests::make_parent_header_meta;
use crate::chainstate::v1::{SortitionMinerStatus, SortitionState, SortitionsView};
use crate::chainstate::{ProposalEvalConfig, SortitionData};
use crate::client::tests::MockServerClient;
@@ -317,9 +321,11 @@ fn reorg_timing_testing(
)
});
header_clone.chain_length -= 1;
- let response = crate::client::tests::build_get_tenure_tip_response(
- &StacksBlockHeaderTypes::Nakamoto(header_clone),
- );
+ let tenure_tip_resp = BlockHeaderWithMetadata {
+ burn_view: Some(header_clone.consensus_hash.clone()),
+ anchored_header: StacksBlockHeaderTypes::Nakamoto(header_clone),
+ };
+ let response = crate::client::tests::build_get_tenure_tip_response(&tenure_tip_resp);
crate::client::tests::write_response(server, response.as_bytes());
server = crate::client::tests::mock_server_from_config(&config);
@@ -444,93 +450,117 @@ fn make_tenure_change_tx(payload: TenureChangePayload) -> StacksTransaction {
}
#[test]
-fn check_proposal_tenure_extend_invalid_conditions() {
- let (stacks_client, mut signer_db, block_sk, mut view, mut block) =
- setup_test_environment(function_name!());
- block.header.consensus_hash = view.cur_sortition.data.consensus_hash.clone();
- let mut extend_payload = make_tenure_change_payload();
- extend_payload.burn_view_consensus_hash = view.cur_sortition.data.consensus_hash.clone();
- extend_payload.tenure_consensus_hash = block.header.consensus_hash.clone();
- extend_payload.prev_tenure_consensus_hash = block.header.consensus_hash.clone();
- let tx = make_tenure_change_tx(extend_payload);
- block.txs = vec![tx];
- block.header.sign_miner(&block_sk).unwrap();
- view.check_proposal(
- &stacks_client,
- &mut signer_db,
- &block,
- false,
- ReplayTransactionSet::none(),
- )
+fn check_tenure_extend_invalid_conditions() {
+ check_tenure_extend(|view, block| {
+ let mut extend_payload = make_tenure_change_payload();
+ extend_payload.burn_view_consensus_hash = view.cur_sortition.data.consensus_hash.clone();
+ extend_payload.tenure_consensus_hash = block.header.consensus_hash.clone();
+ extend_payload.prev_tenure_consensus_hash = block.header.consensus_hash.clone();
+ extend_payload
+ })
.expect_err("Proposal should not validate");
+}
- let mut extend_payload = make_tenure_change_payload();
- extend_payload.burn_view_consensus_hash = ConsensusHash([64; 20]);
- extend_payload.tenure_consensus_hash = block.header.consensus_hash.clone();
- extend_payload.prev_tenure_consensus_hash = block.header.consensus_hash.clone();
- let tx = make_tenure_change_tx(extend_payload);
- block.txs = vec![tx];
- block.header.sign_miner(&block_sk).unwrap();
- view.check_proposal(
- &stacks_client,
- &mut signer_db,
- &block,
- false,
- ReplayTransactionSet::none(),
- )
+#[test]
+fn check_tenure_extend_burn_view_change() {
+ check_tenure_extend(|_view, block| {
+ let mut extend_payload = make_tenure_change_payload();
+ extend_payload.burn_view_consensus_hash = ConsensusHash([64; 20]);
+ extend_payload.tenure_consensus_hash = block.header.consensus_hash.clone();
+ extend_payload.prev_tenure_consensus_hash = block.header.consensus_hash.clone();
+ extend_payload
+ })
.expect("Proposal should validate");
+}
- let mut extend_payload = make_tenure_change_payload();
- extend_payload.cause = TenureChangeCause::ExtendedRuntime;
- extend_payload.burn_view_consensus_hash = ConsensusHash([64; 20]);
- extend_payload.tenure_consensus_hash = block.header.consensus_hash.clone();
- extend_payload.prev_tenure_consensus_hash = block.header.consensus_hash.clone();
- let tx = make_tenure_change_tx(extend_payload);
- block.txs = vec![tx];
- block.header.sign_miner(&block_sk).unwrap();
- view.check_proposal(
- &stacks_client,
- &mut signer_db,
- &block,
- false,
- ReplayTransactionSet::none(),
- )
+#[test]
+fn check_tenure_extend_unsupported_cause() {
+ check_tenure_extend(|_view, block| {
+ let mut extend_payload = make_tenure_change_payload();
+ extend_payload.cause = TenureChangeCause::ExtendedRuntime;
+ extend_payload.burn_view_consensus_hash = ConsensusHash([64; 20]);
+ extend_payload.tenure_consensus_hash = block.header.consensus_hash.clone();
+ extend_payload.prev_tenure_consensus_hash = block.header.consensus_hash.clone();
+ extend_payload
+ })
.expect_err("Proposal should not validate on SIP-034 extension");
+}
- let mut extend_payload = make_tenure_change_payload();
- extend_payload.cause = TenureChangeCause::ExtendedRuntime;
- extend_payload.burn_view_consensus_hash = ConsensusHash([64; 20]);
- extend_payload.tenure_consensus_hash = block.header.consensus_hash.clone();
- extend_payload.prev_tenure_consensus_hash = block.header.consensus_hash.clone();
- let tx = make_tenure_change_tx(extend_payload);
- block.txs = vec![tx];
- block.header.sign_miner(&block_sk).unwrap();
- view.check_proposal(
- &stacks_client,
- &mut signer_db,
- &block,
- false,
- ReplayTransactionSet::none(),
- )
+#[test]
+fn check_tenure_extend_no_burn_change_during_read_count() {
+ check_tenure_extend(|view, block| {
+ // make sure that enough time has passed for the read count to be extended
+ // if the rest of the proposal was valid (in this case, its not)
+ view.config.read_count_idle_timeout = Duration::ZERO;
+
+ let mut extend_payload = make_tenure_change_payload();
+ extend_payload.cause = TenureChangeCause::ExtendedRuntime;
+ extend_payload.burn_view_consensus_hash = ConsensusHash([64; 20]);
+ extend_payload.tenure_consensus_hash = block.header.consensus_hash.clone();
+ extend_payload.prev_tenure_consensus_hash = block.header.consensus_hash.clone();
+ extend_payload
+ })
.expect_err("Proposal should not validate");
+}
- let mut extend_payload = make_tenure_change_payload();
- extend_payload.cause = TenureChangeCause::ExtendedReadCount;
- extend_payload.burn_view_consensus_hash = view.cur_sortition.data.consensus_hash.clone();
- extend_payload.tenure_consensus_hash = block.header.consensus_hash.clone();
- extend_payload.prev_tenure_consensus_hash = block.header.consensus_hash.clone();
- let tx = make_tenure_change_tx(extend_payload);
+#[test]
+fn check_tenure_extend_read_count() {
+ check_tenure_extend(|view, block| {
+ let mut extend_payload = make_tenure_change_payload();
+ // make sure that enough time has passed for the read count to be extended
+ view.config.read_count_idle_timeout = Duration::ZERO;
+ extend_payload.cause = TenureChangeCause::ExtendedReadCount;
+ extend_payload.burn_view_consensus_hash = view.cur_sortition.data.consensus_hash.clone();
+ extend_payload.tenure_consensus_hash = block.header.consensus_hash.clone();
+ extend_payload.prev_tenure_consensus_hash = block.header.consensus_hash.clone();
+ extend_payload
+ })
+ .expect("Proposal should validate");
+}
+
+fn check_tenure_extend(make_payload: F) -> Result<(), RejectReason>
+where
+ F: Fn(&mut SortitionsView, &NakamotoBlock) -> TenureChangePayload,
+{
+ let MockServerClient {
+ server,
+ client: stacks_client,
+ config: _,
+ } = MockServerClient::new();
+ let (_stacks_client, mut signer_db, block_sk, mut view, mut block) =
+ setup_test_environment(function_name!());
+ let mut parent_block_header = make_parent_header_meta(&block_sk, &mut block);
+ parent_block_header.burn_view = Some(view.cur_sortition.data.consensus_hash.clone());
+ let response = crate::client::tests::build_get_tenure_tip_response(&parent_block_header);
+
+ block.header.consensus_hash = view.cur_sortition.data.consensus_hash.clone();
+ let mut payload = make_payload(&mut view, &block);
+ payload.previous_tenure_end = block.header.parent_block_id.clone();
+ let tx = make_tenure_change_tx(payload);
block.txs = vec![tx];
block.header.sign_miner(&block_sk).unwrap();
- view.config.read_count_idle_timeout = Duration::ZERO;
- view.check_proposal(
+ let exit_flag = Arc::new(AtomicBool::new(false));
+ let moved_exit_flag = exit_flag.clone();
+
+ let serve = std::thread::spawn(move || {
+ crate::client::tests::write_response_nonblockinig(
+ &server,
+ response.as_bytes(),
+ moved_exit_flag,
+ );
+ });
+
+ let result = view.check_proposal(
&stacks_client,
&mut signer_db,
&block,
false,
ReplayTransactionSet::none(),
- )
- .expect("Proposal should validate");
+ );
+
+ exit_flag.store(true, Ordering::SeqCst);
+ serve.join().unwrap();
+ result
}
#[test]
@@ -774,8 +804,18 @@ fn check_proposal_refresh() {
#[test]
fn check_proposal_with_extend_during_replay() {
- let (stacks_client, mut signer_db, block_sk, mut view, mut block) =
+ let MockServerClient {
+ server,
+ client: stacks_client,
+ config: _,
+ } = MockServerClient::new();
+
+ let (_stacks_client, mut signer_db, block_sk, mut view, mut block) =
setup_test_environment(function_name!());
+
+ let parent_block_header = make_parent_header_meta(&block_sk, &mut block);
+ let response = crate::client::tests::build_get_tenure_tip_response(&parent_block_header);
+
block.header.consensus_hash = view.cur_sortition.data.consensus_hash.clone();
let mut extend_payload = make_tenure_change_payload();
extend_payload.burn_view_consensus_hash = view.cur_sortition.data.consensus_hash.clone();
@@ -796,6 +836,12 @@ fn check_proposal_with_extend_during_replay() {
);
let replay_set = ReplayTransactionSet::new(vec![replay_tx]);
block.header.sign_miner(&block_sk).unwrap();
- view.check_proposal(&stacks_client, &mut signer_db, &block, false, replay_set)
- .expect("Proposal should validate");
+
+ let j = std::thread::spawn(move || {
+ view.check_proposal(&stacks_client, &mut signer_db, &block, false, replay_set)
+ .expect("Proposal should validate");
+ });
+
+ crate::client::tests::write_response(server, response.as_bytes());
+ j.join().unwrap();
}
diff --git a/stacks-signer/src/chainstate/tests/v2.rs b/stacks-signer/src/chainstate/tests/v2.rs
index 5f541e1e72e..4002ae9058b 100644
--- a/stacks-signer/src/chainstate/tests/v2.rs
+++ b/stacks-signer/src/chainstate/tests/v2.rs
@@ -16,6 +16,8 @@
use std::collections::HashMap;
use std::fs;
use std::net::{Ipv4Addr, SocketAddrV4};
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::Arc;
use std::time::{Duration, SystemTime};
use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader};
@@ -45,6 +47,7 @@ use stacks_common::util::hash::{Hash160, Sha512Trunc256Sum};
use stacks_common::util::secp256k1::MessageSignature;
use stacks_common::{function_name, info};
+use crate::chainstate::tests::make_parent_header_meta;
use crate::chainstate::v2::{GlobalStateView, SortitionState};
use crate::chainstate::{ProposalEvalConfig, SignerChainstateError, SortitionData};
use crate::client::tests::MockServerClient;
@@ -374,80 +377,121 @@ fn make_tenure_change_tx(payload: TenureChangePayload) -> StacksTransaction {
}
}
-#[test]
-fn check_proposal_tenure_extend() {
- let (stacks_client, mut signer_db, block_sk, mut block, cur_sortition, _, mut sortitions_view) =
- setup_test_environment(function_name!());
+fn check_tenure_extend(make_payload: F) -> Result<(), RejectReason>
+where
+ F: Fn(&mut SortitionState, &NakamotoBlock) -> TenureChangePayload,
+{
+ let MockServerClient {
+ server,
+ client: stacks_client,
+ config: _,
+ } = MockServerClient::new();
+ let (
+ _stacks_client,
+ mut signer_db,
+ block_sk,
+ mut block,
+ mut cur_sortition,
+ _,
+ mut sortitions_view,
+ ) = setup_test_environment(function_name!());
block.header.consensus_hash = cur_sortition.data.consensus_hash.clone();
- let mut extend_payload = make_tenure_change_payload();
- extend_payload.burn_view_consensus_hash = cur_sortition.data.consensus_hash.clone();
- extend_payload.tenure_consensus_hash = block.header.consensus_hash.clone();
- extend_payload.prev_tenure_consensus_hash = block.header.consensus_hash.clone();
- let tx = make_tenure_change_tx(extend_payload);
- block.txs = vec![tx];
- sortitions_view
- .check_proposal(&stacks_client, &mut signer_db, &block)
- .expect_err("Proposal should not validate");
+ let mut parent_block_header = make_parent_header_meta(&block_sk, &mut block);
+ parent_block_header.burn_view = Some(cur_sortition.data.consensus_hash.clone());
+ let response = crate::client::tests::build_get_tenure_tip_response(&parent_block_header);
- let mut extend_payload = make_tenure_change_payload();
- extend_payload.burn_view_consensus_hash = ConsensusHash([64; 20]);
- extend_payload.tenure_consensus_hash = block.header.consensus_hash.clone();
- extend_payload.prev_tenure_consensus_hash = block.header.consensus_hash.clone();
- let tx = make_tenure_change_tx(extend_payload);
+ let mut payload = make_payload(&mut cur_sortition, &block);
+ payload.previous_tenure_end = block.header.parent_block_id.clone();
+ let tx = make_tenure_change_tx(payload);
block.txs = vec![tx];
- block.header.miner_signature = block_sk
- .sign(block.header.miner_signature_hash().as_bytes())
- .unwrap();
- sortitions_view
- .check_proposal(&stacks_client, &mut signer_db, &block)
- .expect("Proposal should validate");
+ block.header.sign_miner(&block_sk).unwrap();
- let mut extend_payload = make_tenure_change_payload();
- extend_payload.cause = TenureChangeCause::ExtendedRuntime;
- extend_payload.burn_view_consensus_hash = ConsensusHash([64; 20]);
- extend_payload.tenure_consensus_hash = block.header.consensus_hash.clone();
- extend_payload.prev_tenure_consensus_hash = block.header.consensus_hash.clone();
- let tx = make_tenure_change_tx(extend_payload);
- block.txs = vec![tx];
- block.header.miner_signature = block_sk
- .sign(block.header.miner_signature_hash().as_bytes())
- .unwrap();
- sortitions_view
- .check_proposal(&stacks_client, &mut signer_db, &block)
- .expect_err("Proposal should not validate");
+ let exit_flag = Arc::new(AtomicBool::new(false));
+ let moved_exit_flag = exit_flag.clone();
- let mut extend_payload = make_tenure_change_payload();
- extend_payload.cause = TenureChangeCause::ExtendedRuntime;
- extend_payload.burn_view_consensus_hash = ConsensusHash([64; 20]);
- extend_payload.tenure_consensus_hash = block.header.consensus_hash.clone();
- extend_payload.prev_tenure_consensus_hash = block.header.consensus_hash.clone();
- let tx = make_tenure_change_tx(extend_payload);
- block.txs = vec![tx];
- block.header.miner_signature = block_sk
- .sign(block.header.miner_signature_hash().as_bytes())
- .unwrap();
- sortitions_view
- .check_proposal(&stacks_client, &mut signer_db, &block)
- .expect_err("Proposal should not validate");
+ let serve = std::thread::spawn(move || {
+ crate::client::tests::write_response_nonblockinig(
+ &server,
+ response.as_bytes(),
+ moved_exit_flag,
+ );
+ });
- let mut extend_payload = make_tenure_change_payload();
- extend_payload.cause = TenureChangeCause::ExtendedReadCount;
- extend_payload.burn_view_consensus_hash = cur_sortition.data.consensus_hash;
- extend_payload.tenure_consensus_hash = block.header.consensus_hash.clone();
- extend_payload.prev_tenure_consensus_hash = block.header.consensus_hash.clone();
- let tx = make_tenure_change_tx(extend_payload);
- block.txs = vec![tx];
- block.header.sign_miner(&block_sk).unwrap();
- sortitions_view.config.read_count_idle_timeout = Duration::ZERO;
- sortitions_view
- .check_proposal(&stacks_client, &mut signer_db, &block)
- .expect("Proposal should validate");
+ sortitions_view.config.read_count_idle_timeout = Duration::from_secs(0);
+ let result = sortitions_view.check_proposal(&stacks_client, &mut signer_db, &block);
+
+ exit_flag.store(true, Ordering::SeqCst);
+ serve.join().unwrap();
+ result
+}
+
+#[test]
+fn check_tenure_extend_burn_view_change() {
+ check_tenure_extend(|_, block| {
+ let mut extend_payload = make_tenure_change_payload();
+ extend_payload.burn_view_consensus_hash = ConsensusHash([64; 20]);
+ extend_payload.tenure_consensus_hash = block.header.consensus_hash.clone();
+ extend_payload.prev_tenure_consensus_hash = block.header.consensus_hash.clone();
+ extend_payload
+ })
+ .expect("Proposal should validate");
+}
+
+#[test]
+fn check_tenure_extend_unsupported_cause() {
+ check_tenure_extend(|_, block| {
+ let mut extend_payload = make_tenure_change_payload();
+ extend_payload.cause = TenureChangeCause::ExtendedRuntime;
+ extend_payload.burn_view_consensus_hash = ConsensusHash([64; 20]);
+ extend_payload.tenure_consensus_hash = block.header.consensus_hash.clone();
+ extend_payload.prev_tenure_consensus_hash = block.header.consensus_hash.clone();
+ extend_payload
+ })
+ .expect_err("Proposal should not validate");
+}
+
+#[test]
+fn check_tenure_extend_no_burn_change_during_read_count() {
+ check_tenure_extend(|_, block| {
+ let mut extend_payload = make_tenure_change_payload();
+ extend_payload.cause = TenureChangeCause::ExtendedRuntime;
+ extend_payload.burn_view_consensus_hash = ConsensusHash([64; 20]);
+ extend_payload.tenure_consensus_hash = block.header.consensus_hash.clone();
+ extend_payload.prev_tenure_consensus_hash = block.header.consensus_hash.clone();
+ extend_payload
+ })
+ .expect_err("Proposal should not validate");
+}
+
+#[test]
+fn check_tenure_extend_read_count() {
+ check_tenure_extend(|view, block| {
+ let mut extend_payload = make_tenure_change_payload();
+ extend_payload.cause = TenureChangeCause::ExtendedReadCount;
+ extend_payload.burn_view_consensus_hash = view.data.consensus_hash.clone();
+ extend_payload.tenure_consensus_hash = block.header.consensus_hash.clone();
+ extend_payload.prev_tenure_consensus_hash = block.header.consensus_hash.clone();
+ extend_payload
+ })
+ .expect("Proposal should validate");
}
#[test]
fn check_proposal_with_extend_during_replay() {
- let (stacks_client, mut signer_db, block_sk, mut block, cur_sortition, _, mut sortitions_view) =
- setup_test_environment(function_name!());
+ let MockServerClient {
+ server,
+ client: stacks_client,
+ config: _,
+ } = MockServerClient::new();
+
+ let rand_int = server.local_addr().unwrap().port();
+
+ let (_, mut signer_db, block_sk, mut block, cur_sortition, _, mut sortitions_view) =
+ setup_test_environment(&format!("{}_{rand_int}", function_name!()));
+
+ let parent_block_header = make_parent_header_meta(&block_sk, &mut block);
+ let response = crate::client::tests::build_get_tenure_tip_response(&parent_block_header);
+
block.header.consensus_hash = cur_sortition.data.consensus_hash.clone();
let mut extend_payload = make_tenure_change_payload();
extend_payload.burn_view_consensus_hash = cur_sortition.data.consensus_hash.clone();
@@ -469,9 +513,14 @@ fn check_proposal_with_extend_during_replay() {
sortitions_view.signer_state.tx_replay_set = replay_set;
- sortitions_view
- .check_proposal(&stacks_client, &mut signer_db, &block)
- .expect("Proposal should validate");
+ let j = std::thread::spawn(move || {
+ sortitions_view
+ .check_proposal(&stacks_client, &mut signer_db, &block)
+ .expect("Proposal should validate");
+ });
+
+ crate::client::tests::write_response(server, response.as_bytes());
+ j.join().unwrap();
}
#[test]
diff --git a/stacks-signer/src/chainstate/v1.rs b/stacks-signer/src/chainstate/v1.rs
index b1d4347fcbe..2383d38fe6c 100644
--- a/stacks-signer/src/chainstate/v1.rs
+++ b/stacks-signer/src/chainstate/v1.rs
@@ -351,8 +351,16 @@ impl SortitionsView {
// (1) if this is the most recent sortition, an extend is allowed if it changes the burnchain view
// (2) if this is the most recent sortition, an extend is allowed if enough time has passed to refresh the block limit
let sortition_consensus_hash = &proposed_by.state().data.consensus_hash;
- let changed_burn_view =
- &tenure_extend.burn_view_consensus_hash != sortition_consensus_hash;
+ let tenure_tip = client.get_tenure_tip(sortition_consensus_hash)
+ .map_err(|e| {
+ warn!("Could not load current tenure tip while evaluating a tenure-extend; cannot approve."; "err" => %e);
+ RejectReason::InvalidTenureExtend
+ })?;
+ let Some(current_burn_view) = tenure_tip.burn_view else {
+ warn!("Tenure-extend attempted in tenure without burn-view.");
+ return Err(RejectReason::InvalidTenureExtend);
+ };
+ let changed_burn_view = tenure_extend.burn_view_consensus_hash != current_burn_view;
let extend_timestamp = signer_db.calculate_full_extend_timestamp(
self.config.tenure_idle_timeout,
block,
@@ -374,6 +382,17 @@ impl SortitionsView {
);
return Err(RejectReason::InvalidTenureExtend);
}
+
+ warn!(
+ "Miner block proposal contains a tenure extend, but the conditions for allowing a tenure extend are not met. Considering proposal invalid.";
+ "proposed_block_consensus_hash" => %block.header.consensus_hash,
+ "signer_signature_hash" => %block.header.signer_signature_hash(),
+ "extend_timestamp" => extend_timestamp,
+ "epoch_time" => epoch_time,
+ "is_in_replay" => is_in_replay,
+ "changed_burn_view" => changed_burn_view,
+ "enough_time_passed" => enough_time_passed,
+ );
}
// is there a read-count tenure extend in this block?
@@ -383,8 +402,16 @@ impl SortitionsView {
{
// burn view changes are not allowed during read-count tenure extends
let sortition_consensus_hash = &proposed_by.state().data.consensus_hash;
- let changed_burn_view =
- &tenure_extend.burn_view_consensus_hash != sortition_consensus_hash;
+ let tenure_tip = client.get_tenure_tip(sortition_consensus_hash)
+ .map_err(|e| {
+ warn!("Could not load current tenure tip while evaluating a tenure-extend; cannot approve."; "err" => %e);
+ RejectReason::InvalidTenureExtend
+ })?;
+ let Some(current_burn_view) = tenure_tip.burn_view else {
+ warn!("Tenure-extend attempted in tenure without burn-view.");
+ return Err(RejectReason::InvalidTenureExtend);
+ };
+ let changed_burn_view = tenure_extend.burn_view_consensus_hash != current_burn_view;
if changed_burn_view {
warn!(
"Miner block proposal contains a read-count extend, but the conditions for allowing a tenure extend are not met. Considering proposal invalid.";
diff --git a/stacks-signer/src/chainstate/v2.rs b/stacks-signer/src/chainstate/v2.rs
index 226b7076359..54063c464a3 100644
--- a/stacks-signer/src/chainstate/v2.rs
+++ b/stacks-signer/src/chainstate/v2.rs
@@ -208,7 +208,16 @@ impl GlobalStateView {
// (1) if this is the most recent sortition, an extend is allowed if it changes the burnchain view
// (2) if this is the most recent sortition, an extend is allowed if enough time has passed to refresh the block limit
// (3) if we are in replay, an extend is allowed
- let changed_burn_view = &tenure_extend.burn_view_consensus_hash != tenure_id;
+ let tenure_tip = client.get_tenure_tip(tenure_id)
+ .map_err(|e| {
+ warn!("Could not load current tenure tip while evaluating a tenure-extend; cannot approve."; "err" => %e);
+ RejectReason::InvalidTenureExtend
+ })?;
+ let Some(current_burn_view) = tenure_tip.burn_view else {
+ warn!("Tenure-extend attempted in tenure without burn-view.");
+ return Err(RejectReason::InvalidTenureExtend);
+ };
+ let changed_burn_view = tenure_extend.burn_view_consensus_hash != current_burn_view;
let extend_timestamp = signer_db.calculate_full_extend_timestamp(
self.config.tenure_idle_timeout,
block,
@@ -238,7 +247,16 @@ impl GlobalStateView {
.filter(|extend| extend.cause.is_read_count_extend())
{
// burn view changes are not allowed during read-count tenure extends
- let changed_burn_view = &tenure_extend.burn_view_consensus_hash != tenure_id;
+ let tenure_tip = client.get_tenure_tip(tenure_id)
+ .map_err(|e| {
+ warn!("Could not load current tenure tip while evaluating a tenure-extend; cannot approve."; "err" => %e);
+ RejectReason::InvalidTenureExtend
+ })?;
+ let Some(current_burn_view) = tenure_tip.burn_view else {
+ warn!("Tenure-extend attempted in tenure without burn-view.");
+ return Err(RejectReason::InvalidTenureExtend);
+ };
+ let changed_burn_view = tenure_extend.burn_view_consensus_hash != current_burn_view;
if changed_burn_view {
warn!(
"Miner block proposal contains a read-count extend, but the conditions for allowing a tenure extend are not met. Considering proposal invalid.";
diff --git a/stacks-signer/src/client/mod.rs b/stacks-signer/src/client/mod.rs
index 9c893237be6..6845bfd8e2a 100644
--- a/stacks-signer/src/client/mod.rs
+++ b/stacks-signer/src/client/mod.rs
@@ -134,9 +134,11 @@ pub(crate) mod tests {
use std::collections::{BTreeMap, HashMap};
use std::io::{Read, Write};
use std::net::{SocketAddr, TcpListener};
+ use std::sync::atomic::{AtomicBool, Ordering};
+ use std::sync::Arc;
use blockstack_lib::chainstate::stacks::boot::POX_4_NAME;
- use blockstack_lib::chainstate::stacks::db::StacksBlockHeaderTypes;
+ use blockstack_lib::net::api::get_tenure_tip_meta::BlockHeaderWithMetadata;
use blockstack_lib::net::api::getinfo::RPCPeerInfoData;
use blockstack_lib::net::api::getpoxinfo::{
RPCPoxCurrentCycleInfo, RPCPoxEpoch, RPCPoxInfoData, RPCPoxNextCycleInfo,
@@ -172,7 +174,18 @@ pub(crate) mod tests {
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();
+ let (server, mock_server_addr) = {
+ let mut iter = 0usize;
+ loop {
+ iter += 1;
+ if let Some(x) = mock_server_random() {
+ break x;
+ }
+ if iter > 10 {
+ panic!("Could not get a port for mock server");
+ }
+ }
+ };
config.node_host = mock_server_addr.to_string();
let client = StacksClient::from(&config);
@@ -196,13 +209,15 @@ pub(crate) mod tests {
}
/// Create a mock server on a random port and return the socket addr
- pub fn mock_server_random() -> (TcpListener, SocketAddr) {
+ pub fn mock_server_random() -> Option<(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();
+ let server = TcpListener::bind(mock_server_addr)
+ .inspect_err(|e| warn!("Failed to bind mock RPC server"; "err" => ?e))
+ .ok()?;
mock_server_addr.set_port(server.local_addr().unwrap().port());
- (server, mock_server_addr)
+ Some((server, mock_server_addr))
}
/// Create a mock server on a same port as in the config
@@ -222,6 +237,35 @@ pub(crate) mod tests {
request_bytes
}
+ /// Continually accept requests and write `bytes` as a response.
+ /// Exits when exit flag is true
+ /// Panics on unexpected errors
+ pub fn write_response_nonblockinig(
+ mock_server: &TcpListener,
+ bytes: &[u8],
+ exit: Arc,
+ ) {
+ mock_server.set_nonblocking(true).unwrap();
+ let mut request_bytes = [0u8; 1024];
+
+ while !exit.load(Ordering::SeqCst) {
+ let mut stream = match mock_server.accept() {
+ Ok((stream, ..)) => stream,
+ Err(e) => {
+ if e.kind() == std::io::ErrorKind::WouldBlock {
+ std::thread::sleep(Duration::from_millis(100));
+ continue;
+ }
+ panic!("Unexpected network error in mock server: {e:?}");
+ }
+ };
+ debug!("Reading request...");
+ let _ = stream.read(&mut request_bytes).unwrap();
+ debug!("Writing a response...");
+ stream.write_all(bytes).unwrap();
+ }
+ }
+
pub fn generate_random_consensus_hash() -> ConsensusHash {
let rng = rand::thread_rng();
let bytes: Vec = rng.sample_iter::(Standard).take(20).collect();
@@ -445,7 +489,7 @@ pub(crate) mod tests {
}
}
- pub fn build_get_tenure_tip_response(header_types: &StacksBlockHeaderTypes) -> String {
+ pub fn build_get_tenure_tip_response(header_types: &BlockHeaderWithMetadata) -> String {
let response_json =
serde_json::to_string(header_types).expect("Failed to serialize tenure tip info");
format!("HTTP/1.1 200 OK\n\n{response_json}")
diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs
index 682e29000b6..1654f6e60be 100644
--- a/stacks-signer/src/client/stacks_client.rs
+++ b/stacks-signer/src/client/stacks_client.rs
@@ -17,9 +17,9 @@ use std::collections::{HashMap, VecDeque};
use blockstack_lib::chainstate::nakamoto::NakamotoBlock;
use blockstack_lib::chainstate::stacks::boot::{NakamotoSignerEntry, SIGNERS_NAME};
-use blockstack_lib::chainstate::stacks::db::StacksBlockHeaderTypes;
use blockstack_lib::chainstate::stacks::{StacksTransaction, TransactionVersion};
use blockstack_lib::net::api::callreadonly::CallReadOnlyResponse;
+use blockstack_lib::net::api::get_tenure_tip_meta::BlockHeaderWithMetadata;
use blockstack_lib::net::api::get_tenures_fork_info::{
TenureForkingInfo, RPC_TENURE_FORKING_INFO_PATH,
};
@@ -161,7 +161,7 @@ impl StacksClient {
pub fn get_tenure_tip(
&self,
tenure_id: &ConsensusHash,
- ) -> Result {
+ ) -> Result {
debug!("StacksClient: Getting tenure tip";
"consensus_hash" => %tenure_id,
);
@@ -721,7 +721,10 @@ impl StacksClient {
}
fn tenure_tip_path(&self, consensus_hash: &ConsensusHash) -> String {
- format!("{}/v3/tenures/tip/{consensus_hash}", self.http_origin)
+ format!(
+ "{}/v3/tenures/tip_metadata/{consensus_hash}",
+ self.http_origin
+ )
}
}
@@ -736,6 +739,7 @@ mod tests {
use blockstack_lib::chainstate::stacks::boot::{
NakamotoSignerEntry, PoxStartCycleInfo, RewardSet,
};
+ use blockstack_lib::chainstate::stacks::db::StacksBlockHeaderTypes;
use clarity::types::chainstate::{StacksBlockId, TrieHash};
use clarity::util::hash::Sha512Trunc256Sum;
use clarity::util::secp256k1::MessageSignature;
@@ -1077,7 +1081,7 @@ mod tests {
fn get_tenure_tip_should_succeed() {
let mock = MockServerClient::new();
let consensus_hash = ConsensusHash([15; 20]);
- let header = StacksBlockHeaderTypes::Nakamoto(NakamotoBlockHeader {
+ let anchored_header = StacksBlockHeaderTypes::Nakamoto(NakamotoBlockHeader {
version: 1,
chain_length: 10,
burn_spent: 10,
@@ -1090,10 +1094,15 @@ mod tests {
signer_signature: vec![],
pox_treatment: BitVec::ones(1).unwrap(),
});
- let response = build_get_tenure_tip_response(&header);
+ let with_metadata = BlockHeaderWithMetadata {
+ anchored_header,
+ burn_view: Some(ConsensusHash([15; 20])),
+ };
+
+ let response = build_get_tenure_tip_response(&with_metadata);
let h = spawn(move || mock.client.get_tenure_tip(&consensus_hash));
write_response(mock.server, response.as_bytes());
- assert_eq!(h.join().unwrap().unwrap(), header);
+ assert_eq!(h.join().unwrap().unwrap(), with_metadata);
}
#[test]
diff --git a/stacks-signer/src/tests/signer_state.rs b/stacks-signer/src/tests/signer_state.rs
index 77c1a8aaa7f..4678c31bddc 100644
--- a/stacks-signer/src/tests/signer_state.rs
+++ b/stacks-signer/src/tests/signer_state.rs
@@ -18,6 +18,7 @@ use std::time::{Duration, SystemTime};
use blockstack_lib::chainstate::nakamoto::NakamotoBlockHeader;
use blockstack_lib::chainstate::stacks::db::StacksBlockHeaderTypes;
+use blockstack_lib::net::api::get_tenure_tip_meta::BlockHeaderWithMetadata;
use blockstack_lib::net::api::get_tenures_fork_info::TenureForkingInfo;
use blockstack_lib::net::api::getsortition::SortitionInfo;
use clarity::types::chainstate::{
@@ -242,11 +243,11 @@ fn check_capitulate_miner_view() {
);
});
- let expected_result = StacksBlockHeaderTypes::Nakamoto(NakamotoBlockHeader {
+ let anchored_header = StacksBlockHeaderTypes::Nakamoto(NakamotoBlockHeader {
version: 1,
chain_length: parent_tenure_last_block_height,
burn_spent: 0,
- consensus_hash: parent_tenure_id,
+ consensus_hash: parent_tenure_id.clone(),
parent_block_id: parent_tenure_last_block,
tx_merkle_root: Sha512Trunc256Sum([0u8; 32]),
state_index_root: TrieHash([0u8; 32]),
@@ -256,6 +257,11 @@ fn check_capitulate_miner_view() {
pox_treatment: BitVec::ones(1).unwrap(),
});
+ let expected_result = BlockHeaderWithMetadata {
+ anchored_header,
+ burn_view: Some(parent_tenure_id),
+ };
+
let to_send = build_get_tenure_tip_response(&expected_result);
for _ in 0..2 {
crate::client::tests::write_response(server, to_send.as_bytes());
@@ -471,9 +477,11 @@ fn check_miner_inactivity_timeout() {
let to_send_2 = format!("HTTP/1.1 200 OK\n\n{json_payload}");
// Then it will grab the tip of the prior sortition
- let expected_result = StacksBlockHeaderTypes::Nakamoto(genesis_block);
- let json_payload = serde_json::to_string(&expected_result).unwrap();
- let to_send_3 = format!("HTTP/1.1 200 OK\n\n{json_payload}");
+ let expected_result = BlockHeaderWithMetadata {
+ burn_view: Some(genesis_block.consensus_hash.clone()),
+ anchored_header: StacksBlockHeaderTypes::Nakamoto(genesis_block),
+ };
+ let to_send_3 = build_get_tenure_tip_response(&expected_result);
let MockServerClient {
mut server,
diff --git a/stacks-signer/src/v0/signer_state.rs b/stacks-signer/src/v0/signer_state.rs
index bc2190d5c89..6a63e472700 100644
--- a/stacks-signer/src/v0/signer_state.rs
+++ b/stacks-signer/src/v0/signer_state.rs
@@ -48,9 +48,9 @@ use crate::client::{ClientError, CurrentAndLastSortition, StackerDB, StacksClien
use crate::signerdb::{BlockValidatedByReplaySet, SignerDb};
/// This is the latest supported protocol version for this signer binary
-pub static SUPPORTED_SIGNER_PROTOCOL_VERSION: u64 = 1;
+pub static SUPPORTED_SIGNER_PROTOCOL_VERSION: u64 = 2;
/// The version at which global signer state activates
-pub static GLOBAL_SIGNER_STATE_ACTIVATION_VERSION: u64 = u64::MAX;
+pub static GLOBAL_SIGNER_STATE_ACTIVATION_VERSION: u64 = 2;
/// Vec of pubkeys that should ignore checking for a bitcoin fork
#[cfg(any(test, feature = "testing"))]
@@ -187,30 +187,23 @@ impl LocalStateMachine {
MinerState::NoValidMiner => StateMachineUpdateMinerState::NoValidMiner,
};
- let content = match state_machine.active_signer_protocol_version {
- 0 => StateMachineUpdateContent::V0 {
- burn_block: state_machine.burn_block.clone(),
- burn_block_height: state_machine.burn_block_height,
- current_miner,
- },
- 1 => StateMachineUpdateContent::V1 {
- burn_block: state_machine.burn_block.clone(),
- burn_block_height: state_machine.burn_block_height,
- current_miner,
- replay_transactions: state_machine.tx_replay_set.clone().unwrap_or_default(),
- },
- 2 => StateMachineUpdateContent::V2 {
- burn_block: state_machine.burn_block.clone(),
- burn_block_height: state_machine.burn_block_height,
- current_miner,
- replay_transactions: state_machine.tx_replay_set.clone().unwrap_or_default(),
- },
- other => {
- return Err(CodecError::DeserializeError(format!(
- "Active signer protocol version is unknown: {other}"
- )))
- }
+ let active_signer_protocol_version = state_machine.active_signer_protocol_version;
+ // Make sure we fall back to our local supported version in the case of the other signers being ahead of us
+ // so we can still participate/inform the global state what our view point is
+ let content_version = if active_signer_protocol_version
+ > local_supported_signer_protocol_version
+ {
+ warn!(
+ "Cannot create state machine update for future protocol version {active_signer_protocol_version}; falling back to supported version {local_supported_signer_protocol_version}"
+ );
+ local_supported_signer_protocol_version
+ } else {
+ active_signer_protocol_version
};
+
+ let content =
+ StateMachineUpdateContent::new(content_version, current_miner, state_machine)?;
+
StateMachineUpdateMessage::new(
state_machine.active_signer_protocol_version,
local_supported_signer_protocol_version,
@@ -386,8 +379,8 @@ impl LocalStateMachine {
.ok()
.map(|header| {
(
- header.height(),
- StacksBlockId::new(parent_tenure_id, &header.block_hash()),
+ header.anchored_header.height(),
+ StacksBlockId::new(parent_tenure_id, &header.anchored_header.block_hash()),
)
});
let signerdb_last_block = SortitionData::get_tenure_last_block_info(
diff --git a/stackslib/src/chainstate/tests/runtime_tests.rs b/stackslib/src/chainstate/tests/runtime_tests.rs
index b11db2a6cfc..7b808e47873 100644
--- a/stackslib/src/chainstate/tests/runtime_tests.rs
+++ b/stackslib/src/chainstate/tests/runtime_tests.rs
@@ -65,113 +65,114 @@ fn variant_coverage_report(variant: RuntimeError) {
_ = match variant {
Arithmetic(_) => Tested(vec![
- arithmetic_sqrti_neg_cdeploy,
- arithmetic_sqrti_neg_ccall,
- arithmetic_log2_neg_cdeploy,
- arithmetic_log2_neg_ccall,
- arithmetic_pow_large_cdeploy,
- arithmetic_pow_large_ccall,
- arithmetic_pow_neg_cdeploy,
- arithmetic_pow_neg_ccall,
- arithmetic_zero_n_log_n_cdeploy,
- arithmetic_zero_n_log_n_ccall,
- ]),
+ arithmetic_sqrti_neg_cdeploy,
+ arithmetic_sqrti_neg_ccall,
+ arithmetic_log2_neg_cdeploy,
+ arithmetic_log2_neg_ccall,
+ arithmetic_pow_large_cdeploy,
+ arithmetic_pow_large_ccall,
+ arithmetic_pow_neg_cdeploy,
+ arithmetic_pow_neg_ccall,
+ arithmetic_zero_n_log_n_cdeploy,
+ arithmetic_zero_n_log_n_ccall,
+ ]),
ArithmeticOverflow => Tested(vec![
- arithmetic_overflow_pow_at_cdeploy,
- arithmetic_overflow_pow_ccall,
- arithmetic_overflow_mul_cdeploy,
- arithmetic_overflow_mul_ccall,
- arithmetic_overflow_add_cdeploy,
- arithmetic_overflow_add_ccall,
- arithmetic_overflow_to_int_cdeploy,
- arithmetic_overflow_to_int_ccall,
- ft_mint_overflow,
- ]),
+ arithmetic_overflow_pow_at_cdeploy,
+ arithmetic_overflow_pow_ccall,
+ arithmetic_overflow_mul_cdeploy,
+ arithmetic_overflow_mul_ccall,
+ arithmetic_overflow_add_cdeploy,
+ arithmetic_overflow_add_ccall,
+ arithmetic_overflow_to_int_cdeploy,
+ arithmetic_overflow_to_int_ccall,
+ ft_mint_overflow,
+ ]),
ArithmeticUnderflow => Tested(vec![
- to_uint_underflow_cdeploy,
- to_uint_underflow_ccall,
- sub_underflow_cdeploy,
- sub_underflow_ccall,
- sub_arg_len_underflow_cdeploy,
- sub_arg_len_underflow_ccall,
- ]),
+ to_uint_underflow_cdeploy,
+ to_uint_underflow_ccall,
+ sub_underflow_cdeploy,
+ sub_underflow_ccall,
+ sub_arg_len_underflow_cdeploy,
+ sub_arg_len_underflow_ccall,
+ ]),
SupplyOverflow(_, _) => Tested(vec![ft_mint_supply_overflow]),
SupplyUnderflow(_, _) => Unreachable_Functionally("
Token supply underflow is prevented by design in Clarity. \
All transfer/mint/burn operations use checked arithmetic and balance \
validation, so negative supply is impossible without manual database corruption."
- ),
+ ),
DivisionByZero => Tested(vec![
- division_by_zero_mod_cdeploy,
- division_by_zero_mod_ccall,
- division_by_zero_cdeploy,
- division_by_zero_ccall,
- ]),
+ division_by_zero_mod_cdeploy,
+ division_by_zero_mod_ccall,
+ division_by_zero_cdeploy,
+ division_by_zero_ccall,
+ ]),
TypeParseFailure(_) => Tested(vec![
- parse_tests::test_invalid_principal_literal,
- principal_wrong_byte_length,
- ]),
+ parse_tests::test_invalid_principal_literal,
+ principal_wrong_byte_length,
+ ]),
ASTError(_) => Unreachable_Functionally(
- "AST errors cannot occur through normal Clarity operations. \
+ "AST errors cannot occur through normal Clarity operations. \
They exist only for CLI and testing functions that bypass AST parsing \
that occurs during a typical contract deploy. These wrapped `ParseError` \
are exhaustively covered by (`parse_tests`)."
- ),
+ ),
MaxStackDepthReached => Tested(vec![
- stack_depth_too_deep_call_chain_ccall,
- stack_depth_too_deep_call_chain_cdeploy
- ]),
+ stack_depth_too_deep_call_chain_ccall,
+ stack_depth_too_deep_call_chain_cdeploy
+ ]),
MaxContextDepthReached => Unreachable_Functionally(
- "The maximum context depth limit cannot be reached through normal Clarity code. \
+ "The maximum context depth limit cannot be reached through normal Clarity code. \
Both the call-stack depth limit and the parser's expression-depth limit \
are significantly lower and will trigger first. Only low-level Rust unit tests \
can construct a context deep enough to hit this error."
- ),
+ ),
BadTypeConstruction => Unreachable_Functionally(
- "BadTypeConstruction is rejected during static analysis at contract-publish time. \
+ "BadTypeConstruction is rejected during static analysis at contract-publish time. \
Any value construction that would produce an ill-formed type fails parsing or \
type-checking before the contract is stored on-chain."
- ),
+ ),
BadBlockHeight(_) => Unreachable_Functionally(
- "All block heights referenced via `at-block` or `get-block-info?` are guaranteed \
+ "All block heights referenced via `at-block` or `get-block-info?` are guaranteed \
to exist in the node's historical database during normal execution. \
This error only surfaces if the chainstate is missing blocks or corrupted."
- ),
+ ),
NoSuchToken => Unreachable_Functionally(
- "NFT operations return `none` when an instance does not exist. \
+ "NFT operations return `none` when an instance does not exist. \
The `NoSuchToken` runtime error is only emitted from internal VM assertions \
and cannot be triggered by regular Clarity code unless storage is manually corrupted."
- ),
+ ),
NotImplemented => Unreachable_Functionally(
- "Indicates use of an unimplemented VM feature. \
+ "Indicates use of an unimplemented VM feature. \
Can only be hit by directly invoking unfinished Rust internals – not reachable from Clarity."
- ),
+ ),
NoCallerInContext => Unreachable_Functionally(
- "Every function call (public, private, or trait) is executed with a valid caller context. \
+ "Every function call (public, private, or trait) is executed with a valid caller context. \
This error only appears when the execution environment is manually constructed incorrectly."
- ),
+ ),
NoSenderInContext => Unreachable_Functionally(
- "Every on-chain transaction and contract-call has a well-defined sender. \
+ "Every on-chain transaction and contract-call has a well-defined sender. \
This error only occurs in malformed test harnesses."
- ),
+ ),
BadNameValue(_, _) => Unreachable_Functionally(
- "Contract, function, trait, and variable names are fully validated during static analysis at publish time. \
+ "Contract, function, trait, and variable names are fully validated during static analysis at publish time. \
The runtime only ever encounters already-validated names. \
Only corrupted state or manual VM manipulation can produce this error."
- ),
+ ),
UnknownBlockHeaderHash(_) => Tested(vec![unknown_block_header_hash_fork]),
BadBlockHash(_) => Tested(vec![bad_block_hash]),
UnwrapFailure => Tested(vec![
- unwrap_err_panic_on_ok_runtime,
- unwrap_panic_on_err_runtime
- ]),
+ unwrap_err_panic_on_ok_runtime,
+ unwrap_panic_on_err_runtime
+ ]),
DefunctPoxContract => Tested(vec![defunct_pox_contracts]),
PoxAlreadyLocked => Ignored(
- "The active PoX contract already returns ERR_STACKING_ALREADY_STACKED for double-locking attempts. \
+ "The active PoX contract already returns ERR_STACKING_ALREADY_STACKED for double-locking attempts. \
The VM-level PoxAlreadyLocked error is only triggerable if locking occurs across PoX boundaries. \
This is better suited for unit testing."
- ),
+ ),
BlockTimeNotAvailable => Tested(vec![block_time_not_available]),
+ BadTokenName(_) => Ignored("Error variant tests should be added"),
}
}
diff --git a/stackslib/src/net/api/get_tenure_tip_meta.rs b/stackslib/src/net/api/get_tenure_tip_meta.rs
new file mode 100644
index 00000000000..547810571ff
--- /dev/null
+++ b/stackslib/src/net/api/get_tenure_tip_meta.rs
@@ -0,0 +1,158 @@
+// Copyright (C) 2026 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 regex::{Captures, Regex};
+use stacks_common::types::chainstate::ConsensusHash;
+
+use crate::chainstate::nakamoto::NakamotoChainState;
+use crate::chainstate::stacks::db::StacksBlockHeaderTypes;
+use crate::net::http::{
+ parse_json, Error, HttpNotFound, HttpRequest, HttpRequestContents, HttpRequestPreamble,
+ HttpResponse, HttpResponseContents, HttpResponsePayload, HttpResponsePreamble, HttpServerError,
+};
+use crate::net::httpcore::{request, RPCRequestHandler, StacksHttpResponse};
+use crate::net::{Error as NetError, StacksNodeState};
+
+#[derive(Clone)]
+pub struct NakamotoTenureTipMetadataRequestHandler {
+ pub(crate) consensus_hash: Option,
+}
+
+#[derive(Serialize, Deserialize, PartialEq, Debug)]
+pub struct BlockHeaderWithMetadata {
+ pub anchored_header: StacksBlockHeaderTypes,
+ pub burn_view: Option,
+}
+
+impl NakamotoTenureTipMetadataRequestHandler {
+ pub fn new() -> Self {
+ Self {
+ consensus_hash: None,
+ }
+ }
+}
+
+/// Decode the HTTP request
+impl HttpRequest for NakamotoTenureTipMetadataRequestHandler {
+ fn verb(&self) -> &'static str {
+ "GET"
+ }
+
+ fn path_regex(&self) -> Regex {
+ Regex::new(r#"^/v3/tenures/tip_metadata/(?P[0-9a-f]{40})$"#).unwrap()
+ }
+
+ fn metrics_identifier(&self) -> &str {
+ "/v3/tenures/tip_metadata/:consensus_hash"
+ }
+
+ /// Try to decode this request.
+ /// There's nothing to load here, so just make sure the request is well-formed.
+ fn try_parse_request(
+ &mut self,
+ preamble: &HttpRequestPreamble,
+ captures: &Captures,
+ query: Option<&str>,
+ _body: &[u8],
+ ) -> Result {
+ if preamble.get_content_length() != 0 {
+ return Err(Error::DecodeError(
+ "Invalid Http request: expected 0-length body".to_string(),
+ ));
+ }
+ let consensus_hash = request::get_consensus_hash(captures, "consensus_hash")?;
+ self.consensus_hash = Some(consensus_hash);
+ Ok(HttpRequestContents::new().query_string(query))
+ }
+}
+
+impl RPCRequestHandler for NakamotoTenureTipMetadataRequestHandler {
+ /// Reset internal state
+ fn restart(&mut self) {
+ self.consensus_hash = None;
+ }
+
+ /// Make the response
+ fn try_handle_request(
+ &mut self,
+ preamble: HttpRequestPreamble,
+ _contents: HttpRequestContents,
+ node: &mut StacksNodeState,
+ ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
+ let consensus_hash = self
+ .consensus_hash
+ .take()
+ .ok_or(NetError::SendError("`consensus_hash` not set".into()))?;
+
+ let tenure_tip_resp =
+ node.with_node_state(|_network, sortdb, chainstate, _mempool, _rpc_args| {
+ let header_info =
+ match NakamotoChainState::find_highest_known_block_header_in_tenure(
+ &chainstate,
+ sortdb,
+ &consensus_hash,
+ ) {
+ Ok(Some(header)) => header,
+ Ok(None) => {
+ let msg = format!("No blocks in tenure {}", &consensus_hash);
+ debug!("{}", &msg);
+ return Err(StacksHttpResponse::new_error(
+ &preamble,
+ &HttpNotFound::new(msg),
+ ));
+ }
+ Err(e) => {
+ let msg = format!(
+ "Failed to query tenure blocks by consensus '{}': {:?}",
+ consensus_hash, &e
+ );
+ error!("{}", &msg);
+ return Err(StacksHttpResponse::new_error(
+ &preamble,
+ &HttpServerError::new(msg),
+ ));
+ }
+ };
+ Ok(header_info)
+ });
+
+ let tenure_tip = match tenure_tip_resp {
+ Ok(tenure_tip) => tenure_tip,
+ Err(response) => {
+ return response.try_into_contents().map_err(NetError::from);
+ }
+ };
+
+ let preamble = HttpResponsePreamble::ok_json(&preamble);
+ let body = HttpResponseContents::try_from_json(&BlockHeaderWithMetadata {
+ anchored_header: tenure_tip.anchored_header,
+ burn_view: tenure_tip.burn_view,
+ })?;
+
+ Ok((preamble, body))
+ }
+}
+
+/// Decode the HTTP response
+impl HttpResponse for NakamotoTenureTipMetadataRequestHandler {
+ fn try_parse_response(
+ &self,
+ preamble: &HttpResponsePreamble,
+ body: &[u8],
+ ) -> Result {
+ let tenure_tip: BlockHeaderWithMetadata = parse_json(preamble, body)?;
+ Ok(HttpResponsePayload::try_from_json(tenure_tip)?)
+ }
+}
diff --git a/stackslib/src/net/api/gettenuretip.rs b/stackslib/src/net/api/gettenuretip.rs
index d150ea240a0..595491c40fe 100644
--- a/stackslib/src/net/api/gettenuretip.rs
+++ b/stackslib/src/net/api/gettenuretip.rs
@@ -166,8 +166,11 @@ impl StacksHttpResponse {
pub fn decode_tenure_tip(self) -> Result {
let contents = self.get_http_payload_ok()?;
let response_json: serde_json::Value = contents.try_into()?;
- let tenure_tip: StacksBlockHeaderTypes = serde_json::from_value(response_json)
- .map_err(|_e| Error::DecodeError("Failed to decode JSON".to_string()))?;
+ let tenure_tip: StacksBlockHeaderTypes =
+ serde_json::from_value(response_json).map_err(|e| {
+ error!("Failed to decode JSON"; "err" => ?e);
+ Error::DecodeError("Failed to decode JSON".to_string())
+ })?;
Ok(tenure_tip)
}
}
diff --git a/stackslib/src/net/api/mod.rs b/stackslib/src/net/api/mod.rs
index a5777a751d9..8e770f563cc 100644
--- a/stackslib/src/net/api/mod.rs
+++ b/stackslib/src/net/api/mod.rs
@@ -20,6 +20,7 @@ use crate::net::Error as NetError;
pub mod blockreplay;
pub mod callreadonly;
pub mod fastcallreadonly;
+pub mod get_tenure_tip_meta;
pub mod get_tenures_fork_info;
pub mod getaccount;
pub mod getattachment;
@@ -126,6 +127,9 @@ impl StacksHttp {
self.register_rpc_endpoint(gettenure::RPCNakamotoTenureRequestHandler::new());
self.register_rpc_endpoint(gettenureinfo::RPCNakamotoTenureInfoRequestHandler::new());
self.register_rpc_endpoint(gettenuretip::RPCNakamotoTenureTipRequestHandler::new());
+ self.register_rpc_endpoint(
+ get_tenure_tip_meta::NakamotoTenureTipMetadataRequestHandler::new(),
+ );
self.register_rpc_endpoint(gettenureblocks::RPCNakamotoTenureBlocksRequestHandler::new());
self.register_rpc_endpoint(
gettenureblocksbyhash::RPCNakamotoTenureBlocksByHashRequestHandler::new(),
diff --git a/stackslib/src/net/api/tests/mod.rs b/stackslib/src/net/api/tests/mod.rs
index aacc86a0fcd..c58d4d1a6ad 100644
--- a/stackslib/src/net/api/tests/mod.rs
+++ b/stackslib/src/net/api/tests/mod.rs
@@ -185,8 +185,8 @@ fn convo_send_recv(sender: &mut ConversationHttp, receiver: &mut ConversationHtt
let all_relays_flushed =
receiver.num_pending_outbound() == 0 && sender.num_pending_outbound() == 0;
- let nw = sender.send(&mut pipe_write).unwrap();
- let nr = receiver.recv(&mut pipe_read).unwrap();
+ let nw = sender.send(&mut pipe_write).expect("Invalid send");
+ let nr = receiver.recv(&mut pipe_read).expect("Invalid receive");
debug!(
"test_rpc: all_relays_flushed = {} ({},{}), nr = {}, nw = {}",
diff --git a/versions.toml b/versions.toml
index 2f541dd0180..bc23c0af1c2 100644
--- a/versions.toml
+++ b/versions.toml
@@ -1,4 +1,4 @@
# Update these values when a new release is created.
# `stacks-common/build.rs` will automatically update `versions.rs` with these values.
-stacks_node_version = "3.3.0.0.2"
-stacks_signer_version = "3.3.0.0.2.0"
+stacks_node_version = "3.3.0.0.4"
+stacks_signer_version = "3.3.0.0.4.0"