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 4541ec0d314..519533012ea 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/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/stacks-node/src/tests/signer/mod.rs b/stacks-node/src/tests/signer/mod.rs
index d8db0e960d0..aa64ea9824e 100644
--- a/stacks-node/src/tests/signer/mod.rs
+++ b/stacks-node/src/tests/signer/mod.rs
@@ -585,7 +585,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 +643,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 +906,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 +987,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());
diff --git a/stacks-node/src/tests/signer/v0.rs b/stacks-node/src/tests/signer/v0.rs
index c3aeaa886a0..53ca77eb87c 100644
--- a/stacks-node/src/tests/signer/v0.rs
+++ b/stacks-node/src/tests/signer/v0.rs
@@ -19566,3 +19566,177 @@ 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();
+}
diff --git a/stacks-signer/CHANGELOG.md b/stacks-signer/CHANGELOG.md
index 632a924a8d7..cd8410c838f 100644
--- a/stacks-signer/CHANGELOG.md
+++ b/stacks-signer/CHANGELOG.md
@@ -5,7 +5,13 @@ 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
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..1d9402eaabc 100644
--- a/stacks-signer/src/v0/signer_state.rs
+++ b/stacks-signer/src/v0/signer_state.rs
@@ -386,8 +386,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"