Skip to content

Commit 982637e

Browse files
committed
Merge remote-tracking branch 'origin/develop' into feat/replacement_tx_id_fork
2 parents fec6a68 + c900752 commit 982637e

File tree

21 files changed

+796
-911
lines changed

21 files changed

+796
-911
lines changed

.github/workflows/bitcoin-tests.yml

+1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ jobs:
133133
- tests::signer::v0::block_validation_response_timeout
134134
- tests::signer::v0::tenure_extend_after_bad_commit
135135
- tests::signer::v0::block_proposal_max_age_rejections
136+
- tests::signer::v0::global_acceptance_depends_on_block_announcement
136137
- tests::nakamoto_integrations::burn_ops_integration_test
137138
- tests::nakamoto_integrations::check_block_heights
138139
- tests::nakamoto_integrations::clarity_burn_state

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
88
## [Unreleased]
99

1010
### Added
11+
1112
- Add `tenure_timeout_secs` to the miner for determining when a time-based tenure extend should be attempted.
1213

1314
### Changed
1415

1516
- When a transaction is dropped due to replace-by-fee, the `/drop_mempool_tx` event observer payload now includes `new_txid`, which is the transaction that replaced this dropped transaction. When a transaction is dropped for other reasons, `new_txid` is `null`. [#5381](https://github.com/stacks-network/stacks-core/pull/5381)
17+
- Nodes will assume that all PoX anchor blocks exist by default, and stall initial block download indefinitely to await their arrival (#5502)
1618

1719
## [3.1.0.0.1]
1820

libsigner/src/events.rs

+82-102
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,13 @@ pub enum SignerEvent<T: SignerEventTrait> {
114114
/// the time at which this event was received by the signer's event processor
115115
received_time: SystemTime,
116116
},
117+
/// A new processed Stacks block was received from the node with the given block hash
118+
NewBlock {
119+
/// The block header hash for the newly processed stacks block
120+
block_hash: Sha512Trunc256Sum,
121+
/// The block height for the newly processed stacks block
122+
block_height: u64,
123+
},
117124
}
118125

119126
/// Trait to implement a stop-signaler for the event receiver thread.
@@ -298,29 +305,25 @@ impl<T: SignerEventTrait> EventReceiver<T> for SignerEventReceiver<T> {
298305
&request.method(),
299306
)));
300307
}
308+
debug!("Processing {} event", request.url());
301309
if request.url() == "/stackerdb_chunks" {
302-
process_stackerdb_event(event_receiver.local_addr, request)
303-
.map_err(|e| {
304-
error!("Error processing stackerdb_chunks message"; "err" => ?e);
305-
e
306-
})
310+
process_event::<T, StackerDBChunksEvent>(request)
307311
} else if request.url() == "/proposal_response" {
308-
process_proposal_response(request)
312+
process_event::<T, BlockValidateResponse>(request)
309313
} else if request.url() == "/new_burn_block" {
310-
process_new_burn_block_event(request)
314+
process_event::<T, BurnBlockEvent>(request)
311315
} else if request.url() == "/shutdown" {
312316
event_receiver.stop_signal.store(true, Ordering::SeqCst);
313-
return Err(EventError::Terminated);
317+
Err(EventError::Terminated)
318+
} else if request.url() == "/new_block" {
319+
process_event::<T, BlockEvent>(request)
314320
} else {
315321
let url = request.url().to_string();
316-
// `/new_block` is expected, but not specifically handled. do not log.
317-
if &url != "/new_block" {
318-
debug!(
319-
"[{:?}] next_event got request with unexpected url {}, return OK so other side doesn't keep sending this",
320-
event_receiver.local_addr,
321-
url
322-
);
323-
}
322+
debug!(
323+
"[{:?}] next_event got request with unexpected url {}, return OK so other side doesn't keep sending this",
324+
event_receiver.local_addr,
325+
url
326+
);
324327
ack_dispatcher(request);
325328
Err(EventError::UnrecognizedEvent(url))
326329
}
@@ -385,12 +388,13 @@ fn ack_dispatcher(request: HttpRequest) {
385388

386389
// TODO: add tests from mutation testing results #4835
387390
#[cfg_attr(test, mutants::skip)]
388-
/// Process a stackerdb event from the node
389-
fn process_stackerdb_event<T: SignerEventTrait>(
390-
local_addr: Option<SocketAddr>,
391-
mut request: HttpRequest,
392-
) -> Result<SignerEvent<T>, EventError> {
391+
fn process_event<T, E>(mut request: HttpRequest) -> Result<SignerEvent<T>, EventError>
392+
where
393+
T: SignerEventTrait,
394+
E: serde::de::DeserializeOwned + TryInto<SignerEvent<T>, Error = EventError>,
395+
{
393396
let mut body = String::new();
397+
394398
if let Err(e) = request.as_reader().read_to_string(&mut body) {
395399
error!("Failed to read body: {:?}", &e);
396400
ack_dispatcher(request);
@@ -399,27 +403,12 @@ fn process_stackerdb_event<T: SignerEventTrait>(
399403
&e
400404
)));
401405
}
402-
403-
debug!("Got stackerdb_chunks event"; "chunks_event_body" => %body);
404-
let event: StackerDBChunksEvent = serde_json::from_slice(body.as_bytes())
406+
// Regardless of whether we successfully deserialize, we should ack the dispatcher so they don't keep resending it
407+
ack_dispatcher(request);
408+
let json_event: E = serde_json::from_slice(body.as_bytes())
405409
.map_err(|e| EventError::Deserialize(format!("Could not decode body to JSON: {:?}", &e)))?;
406410

407-
let event_contract_id = event.contract_id.clone();
408-
409-
let signer_event = match SignerEvent::try_from(event) {
410-
Err(e) => {
411-
info!(
412-
"[{:?}] next_event got event from an unexpected contract id {}, return OK so other side doesn't keep sending this",
413-
local_addr,
414-
event_contract_id
415-
);
416-
ack_dispatcher(request);
417-
return Err(e);
418-
}
419-
Ok(x) => x,
420-
};
421-
422-
ack_dispatcher(request);
411+
let signer_event: SignerEvent<T> = json_event.try_into()?;
423412

424413
Ok(signer_event)
425414
}
@@ -466,78 +455,69 @@ impl<T: SignerEventTrait> TryFrom<StackerDBChunksEvent> for SignerEvent<T> {
466455
}
467456
}
468457

469-
/// Process a proposal response from the node
470-
fn process_proposal_response<T: SignerEventTrait>(
471-
mut request: HttpRequest,
472-
) -> Result<SignerEvent<T>, EventError> {
473-
debug!("Got proposal_response event");
474-
let mut body = String::new();
475-
if let Err(e) = request.as_reader().read_to_string(&mut body) {
476-
error!("Failed to read body: {:?}", &e);
458+
impl<T: SignerEventTrait> TryFrom<BlockValidateResponse> for SignerEvent<T> {
459+
type Error = EventError;
477460

478-
if let Err(e) = request.respond(HttpResponse::empty(200u16)) {
479-
error!("Failed to respond to request: {:?}", &e);
480-
}
481-
return Err(EventError::MalformedRequest(format!(
482-
"Failed to read body: {:?}",
483-
&e
484-
)));
461+
fn try_from(block_validate_response: BlockValidateResponse) -> Result<Self, Self::Error> {
462+
Ok(SignerEvent::BlockValidationResponse(
463+
block_validate_response,
464+
))
485465
}
466+
}
486467

487-
let event: BlockValidateResponse = serde_json::from_slice(body.as_bytes())
488-
.map_err(|e| EventError::Deserialize(format!("Could not decode body to JSON: {:?}", &e)))?;
468+
#[derive(Debug, Deserialize)]
469+
struct BurnBlockEvent {
470+
burn_block_hash: String,
471+
burn_block_height: u64,
472+
reward_recipients: Vec<serde_json::Value>,
473+
reward_slot_holders: Vec<String>,
474+
burn_amount: u64,
475+
}
489476

490-
if let Err(e) = request.respond(HttpResponse::empty(200u16)) {
491-
error!("Failed to respond to request: {:?}", &e);
477+
impl<T: SignerEventTrait> TryFrom<BurnBlockEvent> for SignerEvent<T> {
478+
type Error = EventError;
479+
480+
fn try_from(burn_block_event: BurnBlockEvent) -> Result<Self, Self::Error> {
481+
let burn_header_hash = burn_block_event
482+
.burn_block_hash
483+
.get(2..)
484+
.ok_or_else(|| EventError::Deserialize("Hex string should be 0x prefixed".into()))
485+
.and_then(|hex| {
486+
BurnchainHeaderHash::from_hex(hex)
487+
.map_err(|e| EventError::Deserialize(format!("Invalid hex string: {e}")))
488+
})?;
489+
490+
Ok(SignerEvent::NewBurnBlock {
491+
burn_height: burn_block_event.burn_block_height,
492+
received_time: SystemTime::now(),
493+
burn_header_hash,
494+
})
492495
}
496+
}
493497

494-
Ok(SignerEvent::BlockValidationResponse(event))
498+
#[derive(Debug, Deserialize)]
499+
struct BlockEvent {
500+
block_hash: String,
501+
block_height: u64,
495502
}
496503

497-
/// Process a new burn block event from the node
498-
fn process_new_burn_block_event<T: SignerEventTrait>(
499-
mut request: HttpRequest,
500-
) -> Result<SignerEvent<T>, EventError> {
501-
debug!("Got burn_block event");
502-
let mut body = String::new();
503-
if let Err(e) = request.as_reader().read_to_string(&mut body) {
504-
error!("Failed to read body: {:?}", &e);
504+
impl<T: SignerEventTrait> TryFrom<BlockEvent> for SignerEvent<T> {
505+
type Error = EventError;
505506

506-
if let Err(e) = request.respond(HttpResponse::empty(200u16)) {
507-
error!("Failed to respond to request: {:?}", &e);
508-
}
509-
return Err(EventError::MalformedRequest(format!(
510-
"Failed to read body: {:?}",
511-
&e
512-
)));
513-
}
514-
#[derive(Debug, Deserialize)]
515-
struct TempBurnBlockEvent {
516-
burn_block_hash: String,
517-
burn_block_height: u64,
518-
reward_recipients: Vec<serde_json::Value>,
519-
reward_slot_holders: Vec<String>,
520-
burn_amount: u64,
521-
}
522-
let temp: TempBurnBlockEvent = serde_json::from_slice(body.as_bytes())
523-
.map_err(|e| EventError::Deserialize(format!("Could not decode body to JSON: {:?}", &e)))?;
524-
let burn_header_hash = temp
525-
.burn_block_hash
526-
.get(2..)
527-
.ok_or_else(|| EventError::Deserialize("Hex string should be 0x prefixed".into()))
528-
.and_then(|hex| {
529-
BurnchainHeaderHash::from_hex(hex)
530-
.map_err(|e| EventError::Deserialize(format!("Invalid hex string: {e}")))
531-
})?;
532-
let event = SignerEvent::NewBurnBlock {
533-
burn_height: temp.burn_block_height,
534-
received_time: SystemTime::now(),
535-
burn_header_hash,
536-
};
537-
if let Err(e) = request.respond(HttpResponse::empty(200u16)) {
538-
error!("Failed to respond to request: {:?}", &e);
507+
fn try_from(block_event: BlockEvent) -> Result<Self, Self::Error> {
508+
let block_hash: Sha512Trunc256Sum = block_event
509+
.block_hash
510+
.get(2..)
511+
.ok_or_else(|| EventError::Deserialize("Hex string should be 0x prefixed".into()))
512+
.and_then(|hex| {
513+
Sha512Trunc256Sum::from_hex(hex)
514+
.map_err(|e| EventError::Deserialize(format!("Invalid hex string: {e}")))
515+
})?;
516+
Ok(SignerEvent::NewBlock {
517+
block_hash,
518+
block_height: block_event.block_height,
519+
})
539520
}
540-
Ok(event)
541521
}
542522

543523
pub fn get_signers_db_signer_set_message_id(name: &str) -> Option<(u32, u32)> {

stacks-common/src/util/mod.rs

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ use std::path::Path;
3535
use std::time::{SystemTime, UNIX_EPOCH};
3636
use std::{error, fmt, thread, time};
3737

38+
#[cfg(any(test, feature = "testing"))]
39+
pub mod tests;
40+
3841
pub fn get_epoch_time_secs() -> u64 {
3942
let start = SystemTime::now();
4043
let since_the_epoch = start

stacks-common/src/util/tests.rs

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright (C) 2020-2024 Stacks Open Internet Foundation
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
use std::sync::{Arc, Mutex};
17+
/// `TestFlag` is a thread-safe utility designed for managing shared state in testing scenarios. It wraps
18+
/// a value of type `T` inside an `Arc<Mutex<Option<T>>>`, allowing you to set and retrieve a value
19+
/// across different parts of your codebase while ensuring thread safety.
20+
///
21+
/// This structure is particularly useful when:
22+
/// - You need a global or static variable in tests.
23+
/// - You want to control the execution of custom test code paths by setting and checking a shared value.
24+
///
25+
/// # Type Parameter
26+
/// - `T`: The type of the value managed by the `TestFlag`. It must implement the `Default` and `Clone` traits.
27+
///
28+
/// # Examples
29+
///
30+
/// ```rust
31+
/// use stacks_common::util::tests::TestFlag;
32+
/// use std::sync::{Arc, Mutex};
33+
///
34+
/// // Create a TestFlag instance
35+
/// let test_flag = TestFlag::default();
36+
///
37+
/// // Set a value in the test flag
38+
/// test_flag.set("test_value".to_string());
39+
///
40+
/// // Retrieve the value
41+
/// assert_eq!(test_flag.get(), "test_value".to_string());
42+
///
43+
/// // Reset the value to default
44+
/// test_flag.set("".to_string());
45+
/// assert_eq!(test_flag.get(), "".to_string());
46+
/// ```
47+
#[derive(Clone)]
48+
pub struct TestFlag<T>(pub Arc<Mutex<Option<T>>>);
49+
50+
impl<T: Default + Clone> Default for TestFlag<T> {
51+
fn default() -> Self {
52+
Self(Arc::new(Mutex::new(None)))
53+
}
54+
}
55+
56+
impl<T: Default + Clone> TestFlag<T> {
57+
/// Sets the value of the test flag.
58+
///
59+
/// This method updates the value stored inside the `TestFlag`, replacing any existing value.
60+
///
61+
/// # Arguments
62+
/// - `value`: The new value to set for the `TestFlag`.
63+
///
64+
/// # Examples
65+
///
66+
/// ```rust
67+
/// let test_flag = TestFlag::default();
68+
/// test_flag.set(42);
69+
/// assert_eq!(test_flag.get(), 42);
70+
/// ```
71+
pub fn set(&self, value: T) {
72+
*self.0.lock().unwrap() = Some(value);
73+
}
74+
75+
/// Retrieves the current value of the test flag.
76+
///
77+
/// If no value has been set, this method returns the default value for the type `T`.
78+
///
79+
/// # Returns
80+
/// - The current value of the test flag, or the default value of `T` if none has been set.
81+
///
82+
/// # Examples
83+
///
84+
/// ```rust
85+
/// let test_flag = TestFlag::default();
86+
///
87+
/// // Get the default value
88+
/// assert_eq!(test_flag.get(), 0); // For T = i32, default is 0
89+
///
90+
/// // Set a value
91+
/// test_flag.set(123);
92+
///
93+
/// // Get the updated value
94+
/// assert_eq!(test_flag.get(), 123);
95+
/// ```
96+
pub fn get(&self) -> T {
97+
self.0.lock().unwrap().clone().unwrap_or_default().clone()
98+
}
99+
}

stacks-signer/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
1313

1414
## Changed
1515
- Improvements to the stale signer cleanup logic: deletes the prior signer if it has no remaining unprocessed blocks in its database
16+
- Signers now listen to new block events from the stacks node to determine whether a block has been successfully appended to the chain tip
1617

1718
## [3.1.0.0.1.0]
1819

stacks-signer/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ serde_stacker = "0.1"
3636
slog = { version = "2.5.2", features = [ "max_level_trace" ] }
3737
slog-json = { version = "2.3.0", optional = true }
3838
slog-term = "2.6.0"
39-
stacks-common = { path = "../stacks-common" }
39+
stacks-common = { path = "../stacks-common", features = ["testing"] }
4040
stackslib = { path = "../stackslib" }
4141
thiserror = { workspace = true }
4242
tiny_http = { version = "0.12", optional = true }

0 commit comments

Comments
 (0)