Repository: stacks-network/stacks-core (language: python)

Issue:
feat: add chain_id to signer config
- Closes https://github.com/stacks-network/stacks-core/issues/5313

This adds an optional `chain_id` field to the signer's config. If present, it is used in the `/v3/block_proposal` endpoint.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f5c84db9a6..5ccb9b5cac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
     - `get-tenure-info?` added
     - `get-block-info?` removed
 - Added `/v3/signer/{signer_pubkey}/{reward_cycle}` endpoint
+- Added optional `timeout_ms` to `events_observer` configuration
 
 ## [2.5.0.0.7]
 
diff --git a/stacks-common/src/libcommon.rs b/stacks-common/src/libcommon.rs
index c42c97caf2..53d4878af4 100644
--- a/stacks-common/src/libcommon.rs
+++ b/stacks-common/src/libcommon.rs
@@ -86,7 +86,7 @@ pub mod consts {
 
     /// this should be updated to the latest network epoch version supported by
     ///  this node. this will be checked by the `validate_epochs()` method.
-    pub const PEER_NETWORK_EPOCH: u32 = PEER_VERSION_EPOCH_2_5 as u32;
+    pub const PEER_NETWORK_EPOCH: u32 = PEER_VERSION_EPOCH_3_0 as u32;
 
     /// set the fourth byte of the peer version
     pub const PEER_VERSION_MAINNET: u32 = PEER_VERSION_MAINNET_MAJOR | PEER_NETWORK_EPOCH;
diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs
index b3f6528232..0fc43350db 100644
--- a/stacks-signer/src/client/stackerdb.rs
+++ b/stacks-signer/src/client/stackerdb.rs
@@ -255,6 +255,7 @@ mod tests {
             Some(100_000),
             None,
             Some(9000),
+            None,
         );
         let config = GlobalConfig::load_from_str(&signer_config[0]).unwrap();
         let signer_config = generate_signer_config(&config, 5);
diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs
index 5caf9d3f42..cae6a210b7 100644
--- a/stacks-signer/src/client/stacks_client.rs
+++ b/stacks-signer/src/client/stacks_client.rs
@@ -45,7 +45,7 @@ use serde::Deserialize;
 use serde_json::json;
 use slog::{slog_debug, slog_warn};
 use stacks_common::codec::StacksMessageCodec;
-use stacks_common::consts::{CHAIN_ID_MAINNET, CHAIN_ID_TESTNET};
+use stacks_common::consts::CHAIN_ID_MAINNET;
 use stacks_common::types::chainstate::{
     ConsensusHash, StacksAddress, StacksPrivateKey, StacksPublicKey,
 };
@@ -99,7 +99,7 @@ impl From<&GlobalConfig> for StacksClient {
             stacks_address: config.stacks_address,
             http_origin: format!("http://{}", config.node_host),
             tx_version: config.network.to_transaction_version(),
-            chain_id: config.network.to_chain_id(),
+            chain_id: config.to_chain_id(),
             stacks_node_client: reqwest::blocking::Client::new(),
             mainnet: config.network.is_mainnet(),
             auth_password: config.auth_password.clone(),
@@ -114,6 +114,7 @@ impl StacksClient {
         node_host: String,
         auth_password: String,
         mainnet: bool,
+        chain_id: u32,
     ) -> Self {
         let pubkey = StacksPublicKey::from_private(&stacks_private_key);
         let tx_version = if mainnet {
@@ -121,11 +122,6 @@ impl StacksClient {
         } else {
             TransactionVersion::Testnet
         };
-        let chain_id = if mainnet {
-            CHAIN_ID_MAINNET
-        } else {
-            CHAIN_ID_TESTNET
-        };
         let stacks_address = StacksAddress::p2pkh(mainnet, &pubkey);
         Self {
             stacks_private_key,
@@ -145,7 +141,13 @@ impl StacksClient {
         node_host: String,
         auth_password: String,
     ) -> Result<Self, ClientError> {
-        let mut stacks_client = Self::new(stacks_private_key, node_host, auth_password, true);
+        let mut stacks_client = Self::new(
+            stacks_private_key,
+            node_host,
+            auth_password,
+            true,
+            CHAIN_ID_MAINNET,
+        );
         let pubkey = StacksPublicKey::from_private(&stacks_private_key);
         let info = stacks_client.get_peer_info()?;
         if info.network_id == CHAIN_ID_MAINNET {
@@ -154,7 +156,7 @@ impl StacksClient {
             stacks_client.tx_version = TransactionVersion::Mainnet;
         } else {
             stacks_client.mainnet = false;
-            stacks_client.chain_id = CHAIN_ID_TESTNET;
+            stacks_client.chain_id = info.network_id;
             stacks_client.tx_version = TransactionVersion::Testnet;
         }
         stacks_client.stacks_address = StacksAddress::p2pkh(stacks_client.mainnet, &pubkey);
@@ -1219,4 +1221,12 @@ mod tests {
         write_response(mock.server, response.as_bytes());
         assert_eq!(h.join().unwrap().unwrap(), reward_cycle as u128);
     }
+
+    #[test]
+    fn get_chain_id_from_config() {
+        let mock = MockServerClient::from_config(
+            GlobalConfig::load_from_file("./src/tests/conf/signer-custom-chain-id.toml").unwrap(),
+        );
+        assert_eq!(mock.client.chain_id, 0x80000100);
+    }
 }
diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs
index 9f72e171e5..375ed1a171 100644
--- a/stacks-signer/src/config.rs
+++ b/stacks-signer/src/config.rs
@@ -76,14 +76,6 @@ impl std::fmt::Display for Network {
 }
 
 impl Network {
-    /// Converts a Network enum variant to a corresponding chain id
-    pub const fn to_chain_id(&self) -> u32 {
-        match self {
-            Self::Mainnet => CHAIN_ID_MAINNET,
-            Self::Testnet | Self::Mocknet => CHAIN_ID_TESTNET,
-        }
-    }
-
     /// Convert a Network enum variant to a corresponding address version
     pub const fn to_address_version(&self) -> u8 {
         match self {
@@ -163,6 +155,8 @@ pub struct GlobalConfig {
     pub first_proposal_burn_block_timing: Duration,
     /// How much time to wait for a miner to propose a block following a sortition
     pub block_proposal_timeout: Duration,
+    /// An optional custom Chain ID
+    chain_id: Option<u32>,
 }
 
 /// Internal struct for loading up the config file
@@ -190,6 +184,8 @@ struct RawConfigFile {
     pub first_proposal_burn_block_timing_secs: Option<u64>,
     /// How much time to wait for a miner to propose a block following a sortition in milliseconds
     pub block_proposal_timeout_ms: Option<u64>,
+    /// An optional custom Chain ID
+    pub chain_id: Option<u32>,
 }
 
 impl RawConfigFile {
@@ -278,6 +274,7 @@ impl TryFrom<RawConfigFile> for GlobalConfig {
             metrics_endpoint,
             first_proposal_burn_block_timing,
             block_proposal_timeout,
+            chain_id: raw_data.chain_id,
         })
     }
 }
@@ -308,6 +305,7 @@ impl GlobalConfig {
             Some(endpoint) => endpoint.to_string(),
             None => "None".to_string(),
         };
+        let chain_id = format!("{:x}", self.to_chain_id());
         format!(
             r#"
 Stacks node host: {node_host}
@@ -315,6 +313,7 @@ Signer endpoint: {endpoint}
 Stacks address: {stacks_address}
 Public key: {public_key}
 Network: {network}
+Chain ID: 0x{chain_id}
 Database path: {db_path}
 Metrics endpoint: {metrics_endpoint}
 "#,
@@ -329,6 +328,14 @@ Metrics endpoint: {metrics_endpoint}
             metrics_endpoint = metrics_endpoint,
         )
     }
+
+    /// Get the chain ID for the network
+    pub fn to_chain_id(&self) -> u32 {
+        self.chain_id.unwrap_or_else(|| match self.network {
+            Network::Mainnet => CHAIN_ID_MAINNET,
+            Network::Testnet | Network::Mocknet => CHAIN_ID_TESTNET,
+        })
+    }
 }
 
 impl Display for GlobalConfig {
@@ -356,6 +363,7 @@ pub fn build_signer_config_tomls(
     max_tx_fee_ustx: Option<u64>,
     tx_fee_ustx: Option<u64>,
     mut metrics_port_start: Option<usize>,
+    chain_id: Option<u32>,
 ) -> Vec<String> {
     let mut signer_config_tomls = vec![];
 
@@ -421,6 +429,15 @@ metrics_endpoint = "{metrics_endpoint}"
             metrics_port_start = Some(metrics_port + 1);
         }
 
+        if let Some(chain_id) = chain_id {
+            signer_config_toml = format!(
+                r#"
+{signer_config_toml}
+chain_id = {chain_id}
+"#
+            )
+        }
+
         signer_config_tomls.push(signer_config_toml);
     }
 
@@ -453,6 +470,7 @@ mod tests {
             None,
             None,
             Some(4000),
+            None,
         );
 
         let config =
@@ -460,6 +478,8 @@ mod tests {
 
         assert_eq!(config.auth_password, "melon");
         assert_eq!(config.metrics_endpoint, Some("localhost:4000".to_string()));
+        let global_config = GlobalConfig::try_from(config).unwrap();
+        assert_eq!(global_config.to_chain_id(), CHAIN_ID_TESTNET);
     }
 
     #[test]
@@ -473,8 +493,10 @@ Signer endpoint: 127.0.0.1:30000
 Stacks address: ST3FPN8KBZ3YPBP0ZJGAAHTVFMQDTJCR5QPS7VTNJ
 Public key: 03bc489f27da3701d9f9e577c88de5567cf4023111b7577042d55cde4d823a3505
 Network: testnet
+Chain ID: 0x80000000
 Database path: :memory:
 Metrics endpoint: 0.0.0.0:9090
+Chain ID: 2147483648
 "#;
 
         let expected_str_v6 = r#"
@@ -483,6 +505,7 @@ Signer endpoint: [::1]:30000
 Stacks address: ST3FPN8KBZ3YPBP0ZJGAAHTVFMQDTJCR5QPS7VTNJ
 Public key: 03bc489f27da3701d9f9e577c88de5567cf4023111b7577042d55cde4d823a3505
 Network: testnet
+Chain ID: 0x80000000
 Database path: :memory:
 Metrics endpoint: 0.0.0.0:9090
 "#;
@@ -531,5 +554,37 @@ db_path = ":memory:"
         );
         let config = GlobalConfig::load_from_str(&config_toml).unwrap();
         assert_eq!(config.stacks_address.to_string(), expected_addr);
+        assert_eq!(config.to_chain_id(), CHAIN_ID_MAINNET);
+    }
+
+    #[test]
+    fn test_custom_chain_id() {
+        let pk = StacksPrivateKey::from_hex(
+            "eb05c83546fdd2c79f10f5ad5434a90dd28f7e3acb7c092157aa1bc3656b012c01",
+        )
+        .unwrap();
+
+        let node_host = "localhost";
+        let network = Network::Testnet;
+        let password = "melon";
+        let config_tomls = build_signer_config_tomls(
+            &[pk],
+            node_host,
+            None,
+            &network,
+            password,
+            rand::random(),
+            3000,
+            None,
+            None,
+            Some(4000),
+            Some(0x80000100),
+        );
+
+        let config =
+            RawConfigFile::load_from_str(&config_tomls[0]).expect("Failed to parse config file");
+        assert_eq!(config.chain_id, Some(0x80000100));
+        let global_config = GlobalConfig::try_from(config).unwrap();
+        assert_eq!(global_config.to_chain_id(), 0x80000100);
     }
 }
diff --git a/stacks-signer/src/main.rs b/stacks-signer/src/main.rs
index 520d455258..bb680aae0b 100644
--- a/stacks-signer/src/main.rs
+++ b/stacks-signer/src/main.rs
@@ -121,7 +121,7 @@ fn handle_generate_stacking_signature(
         &private_key, //
         args.reward_cycle.into(),
         args.method.topic(),
-        config.network.to_chain_id(),
+        config.to_chain_id(),
         args.period.into(),
         args.max_amount,
         args.auth_id,
diff --git a/stackslib/src/chainstate/nakamoto/mod.rs b/stackslib/src/chainstate/nakamoto/mod.rs
index 928bb62a64..b0267a6ea9 100644
--- a/stackslib/src/chainstate/nakamoto/mod.rs
+++ b/stackslib/src/chainstate/nakamoto/mod.rs
@@ -3810,6 +3810,15 @@ impl NakamotoChainState {
         active_reward_set: &RewardSet,
     ) -> Result<(), ChainstateError> {
         if !tenure_block_commit.treatment.is_empty() {
+            let address_to_indeces: HashMap<_, Vec<_>> = active_reward_set
+                .rewarded_addresses
+                .iter()
+                .enumerate()
+                .fold(HashMap::new(), |mut map, (ix, addr)| {
+                    map.entry(addr).or_insert_with(Vec::new).push(ix);
+                    map
+                });
+
             // our block commit issued a punishment, check the reward set and bitvector
             //  to ensure that this was valid.
             for treated_addr in tenure_block_commit.treatment.iter() {
@@ -3820,24 +3829,19 @@ impl NakamotoChainState {
                 }
                 // otherwise, we need to find the indices in the rewarded_addresses
                 //  corresponding to this address.
-                let address_indices = active_reward_set
-                    .rewarded_addresses
-                    .iter()
-                    .enumerate()
-                    .filter_map(|(ix, addr)| {
-                        if addr == treated_addr.deref() {
-                            Some(ix)
-                        } else {
-                            None
-                        }
-                    });
+                let empty_vec = vec![];
+                let address_indices = address_to_indeces
+                    .get(treated_addr.deref())
+                    .unwrap_or(&empty_vec);
+
                 // if any of them are 0, punishment is okay.
                 // if all of them are 1, punishment is not okay.
                 // if all of them are 0, *must* have punished
                 let bitvec_values: Result<Vec<_>, ChainstateError> = address_indices
+                    .iter()
                     .map(
                         |ix| {
-                            let ix = u16::try_from(ix)
+                            let ix = u16::try_from(*ix)
                                 .map_err(|_| ChainstateError::InvalidStacksBlock("Reward set index outside of u16".into()))?;
                             let bitvec_value = block_bitvec.get(ix)
                                 .unwrap_or_else(|| {
diff --git a/stackslib/src/net/download/nakamoto/tenure.rs b/stackslib/src/net/download/nakamoto/tenure.rs
index 98f102969a..53f9105156 100644
--- a/stackslib/src/net/download/nakamoto/tenure.rs
+++ b/stackslib/src/net/download/nakamoto/tenure.rs
@@ -325,15 +325,12 @@ impl TenureStartEnd {
                 wt_start.winning_block_id.clone(),
                 wt_end.winning_block_id.clone(),
                 rc,
-                downloader_block_height_to_reward_cycle(
-                    pox_constants,
-                    first_burn_height,
-                    wt_start.burn_height,
-                )
-                .expect(&format!(
-                    "FATAL: tenure from before system start ({} <= {})",
-                    wt_start.burn_height, first_burn_height
-                )),
+                pox_constants
+                    .block_height_to_reward_cycle(first_burn_height, wt_start.burn_height)
+                    .expect(&format!(
+                        "FATAL: tenure from before system start ({} <= {})",
+                        wt_start.burn_height, first_burn_height
+                    )),
                 wt.processed,
             );
             tenure_start_end.fetch_end_block = true;
diff --git a/stackslib/src/net/download/nakamoto/tenure_downloader_set.rs b/stackslib/src/net/download/nakamoto/tenure_downloader_set.rs
index 160bad309e..49b32c2634 100644
--- a/stackslib/src/net/download/nakamoto/tenure_downloader_set.rs
+++ b/stackslib/src/net/download/nakamoto/tenure_downloader_set.rs
@@ -407,6 +407,12 @@ impl NakamotoTenureDownloaderSet {
                 continue;
             };
 
+            info!("Download tenure {}", &ch;
+                "tenure_start_block" => %tenure_info.start_block_id,
+                "tenure_end_block" => %tenure_info.end_block_id,
+                "tenure_start_reward_cycle" => tenure_info.start_reward_cycle,
+                "tenure_end_reward_cycle" => tenure_info.end_reward_cycle);
+
             debug!(
                 "Download tenure {} (start={}, end={}) (rc {},{})",
                 &ch,
diff --git a/stackslib/src/net/inv/nakamoto.rs b/stackslib/src/net/inv/nakamoto.rs
index 8971a8230f..3f4fcb6165 100644
--- a/stackslib/src/net/inv/nakamoto.rs
+++ b/stackslib/src/net/inv/nakamoto.rs
@@ -679,6 +679,7 @@ impl NakamotoTenureInv {
             }
             StacksMessageType::Nack(nack_data) => {
                 info!("{:?}: remote peer NACKed our GetNakamotoInv", network.get_local_peer();
+                      "remote_peer" => %self.neighbor_address,
                       "error_code" => nack_data.error_code);
 
                 if nack_data.error_code != NackErrorCodes::NoSuchBurnchainBlock {
diff --git a/stackslib/src/net/p2p.rs b/stackslib/src/net/p2p.rs
index d7ea9684f3..eb224f3e80 100644
--- a/stackslib/src/net/p2p.rs
+++ b/stackslib/src/net/p2p.rs
@@ -4998,7 +4998,7 @@ impl PeerNetwork {
     /// Log our neighbors.
     /// Used for testing and debuggin
     fn log_neighbors(&mut self) {
-        if self.get_connection_opts().log_neighbors_freq == 0 {
+        if !cfg!(test) && self.get_connection_opts().log_neighbors_freq == 0 {
             return;
         }
 
diff --git a/stackslib/src/net/poll.rs b/stackslib/src/net/poll.rs
index bdda12e6d4..0362745f90 100644
--- a/stackslib/src/net/poll.rs
+++ b/stackslib/src/net/poll.rs
@@ -89,7 +89,7 @@ impl NetworkState {
     }
 
     fn bind_address(addr: &SocketAddr) -> Result<mio_net::TcpListener, net_error> {
-        if !cfg!(test) {
+        if !cfg!(test) && !cfg!(feature = "testing") {
             mio_net::TcpListener::bind(addr).map_err(|e| {
                 error!("Failed to bind to {:?}: {:?}", addr, e);
                 net_error::BindError
diff --git a/stackslib/src/net/stackerdb/mod.rs b/stackslib/src/net/stackerdb/mod.rs
index 40fbc7711a..57d1a427dc 100644
--- a/stackslib/src/net/stackerdb/mod.rs
+++ b/stackslib/src/net/stackerdb/mod.rs
@@ -443,6 +443,8 @@ pub struct StackerDBSync<NC: NeighborComms> {
     rounds: u128,
     /// Round when we last pushed
     push_round: u128,
+    /// time we last deliberately evicted a peer
+    last_eviction_time: u64,
 }
 
 impl StackerDBSyncResult {
diff --git a/stackslib/src/net/stackerdb/sync.rs b/stackslib/src/net/stackerdb/sync.rs
index 08e6e978ea..237f582d26 100644
--- a/stackslib/src/net/stackerdb/sync.rs
+++ b/stackslib/src/net/stackerdb/sync.rs
@@ -79,6 +79,7 @@ impl<NC: NeighborComms> StackerDBSync<NC> {
             num_attempted_connections: 0,
             rounds: 0,
             push_round: 0,
+            last_eviction_time: get_epoch_time_secs(),
         };
         dbsync.reset(None, config);
         dbsync
@@ -217,9 +218,36 @@ impl<NC: NeighborComms> StackerDBSync<NC> {
         self.expected_versions.clear();
         self.downloaded_chunks.clear();
 
-        // reset comms, but keep all connected replicas pinned
+        // reset comms, but keep all connected replicas pinned.
+        // Randomly evict one every so often.
         self.comms.reset();
         if let Some(network) = network {
+            let mut eviction_index = None;
+            if self.last_eviction_time + 60 < get_epoch_time_secs() {
+                self.last_eviction_time = get_epoch_time_secs();
+                if self.replicas.len() > 0 {
+                    eviction_index = Some(thread_rng().gen_range(0..self.replicas.len()));
+                }
+            }
+
+            let remove_naddr = eviction_index.and_then(|idx| {
+                let removed = self.replicas.iter().nth(idx).cloned();
+                if let Some(naddr) = removed.as_ref() {
+                    debug!(
+                        "{:?}: {}: don't reuse connection for replica {:?}",
+                        network.get_local_peer(),
+                        &self.smart_contract_id,
+                        &naddr,
+                    );
+                }
+                removed
+            });
+
+            if let Some(naddr) = remove_naddr {
+                self.replicas.remove(&naddr);
+            }
+
+            // retain the remaining replica connections
             for naddr in self.replicas.iter() {
                 if let Some(event_id) = network.get_event_id(&naddr.to_neighbor_key(network)) {
                     self.comms.pin_connection(event_id);
@@ -668,7 +696,8 @@ impl<NC: NeighborComms> StackerDBSync<NC> {
     /// We might not be connected to any yet.
     /// Clears self.replicas, and fills in self.connected_replicas with already-connected neighbors
     /// Returns Ok(true) if we can proceed to sync
-    /// Returns Ok(false) if we have no known peers
+    /// Returns Ok(false) if we should try this again
+    /// Returns Err(NoSuchNeighbor) if we don't have anyone to talk to
     /// Returns Err(..) on DB query error
     pub fn connect_begin(&mut self, network: &mut PeerNetwork) -> Result<bool, net_error> {
         if self.replicas.len() == 0 {
@@ -686,7 +715,7 @@ impl<NC: NeighborComms> StackerDBSync<NC> {
         );
         if self.replicas.len() == 0 {
             // nothing to do
-            return Ok(false);
+            return Err(net_error::NoSuchNeighbor);
         }
 
         let naddrs = mem::replace(&mut self.replicas, HashSet::new());
@@ -729,11 +758,12 @@ impl<NC: NeighborComms> StackerDBSync<NC> {
                     );
                     self.num_attempted_connections += 1;
                     self.num_connections += 1;
+                    self.connected_replicas.insert(naddr);
                 }
                 Ok(false) => {
                     // need to retry
-                    self.replicas.insert(naddr);
                     self.num_attempted_connections += 1;
+                    self.replicas.insert(naddr);
                 }
                 Err(_e) => {
                     debug!(
@@ -746,7 +776,7 @@ impl<NC: NeighborComms> StackerDBSync<NC> {
                 }
             }
         }
-        Ok(self.replicas.len() == 0)
+        Ok(self.connected_replicas.len() > 0)
     }
 
     /// Finish up connecting to our replicas.
@@ -1154,7 +1184,8 @@ impl<NC: NeighborComms> StackerDBSync<NC> {
         );
 
         // fill up our comms with $capacity requests
-        for _i in 0..self.request_capacity {
+        let mut num_sent = 0;
+        for _i in 0..self.chunk_push_priorities.len() {
             if self.comms.count_inflight() >= self.request_capacity {
                 break;
             }
@@ -1173,6 +1204,9 @@ impl<NC: NeighborComms> StackerDBSync<NC> {
                     chunk_push.chunk_data.slot_id,
                     chunk_push.chunk_data.slot_version,
                 );
+
+                // next-prioritized chunk
+                cur_priority = (cur_priority + 1) % self.chunk_push_priorities.len();
                 continue;
             };
 
@@ -1213,6 +1247,11 @@ impl<NC: NeighborComms> StackerDBSync<NC> {
 
             // next-prioritized chunk
             cur_priority = (cur_priority + 1) % self.chunk_push_priorities.len();
+
+            num_sent += 1;
+            if num_sent > self.request_capacity {
+                break;
+            }
         }
         self.next_chunk_push_priority = cur_priority;
         Ok(self
@@ -1370,14 +1409,22 @@ impl<NC: NeighborComms> StackerDBSync<NC> {
             let mut blocked = true;
             match self.state {
                 StackerDBSyncState::ConnectBegin => {
-                    let done = self.connect_begin(network)?;
+                    let done = match self.connect_begin(network) {
+                        Ok(done) => done,
+                        Err(net_error::NoSuchNeighbor) => {
+                            // nothing to do
+                            self.state = StackerDBSyncState::Finished;
+                            blocked = false;
+                            false
+                        }
+                        Err(e) => {
+                            return Err(e);
+                        }
+                    };
                     if done {
                         self.state = StackerDBSyncState::ConnectFinish;
-                    } else {
-                        // no replicas; try again
-                        self.state = StackerDBSyncState::Finished;
+                        blocked = false;
                     }
-                    blocked = false;
                 }
                 StackerDBSyncState::ConnectFinish => {
                     let done = self.connect_try_finish(network)?;


Test patch:
diff --git a/stacks-signer/src/tests/chainstate.rs b/stacks-signer/src/tests/chainstate.rs
index 432325daf2..886480f063 100644
--- a/stacks-signer/src/tests/chainstate.rs
+++ b/stacks-signer/src/tests/chainstate.rs
@@ -32,6 +32,7 @@ use clarity::util::vrf::VRFProof;
 use libsigner::BlockProposal;
 use slog::slog_info;
 use stacks_common::bitvec::BitVec;
+use stacks_common::consts::CHAIN_ID_TESTNET;
 use stacks_common::info;
 use stacks_common::types::chainstate::{
     ConsensusHash, StacksBlockId, StacksPrivateKey, StacksPublicKey, TrieHash,
@@ -96,6 +97,7 @@ fn setup_test_environment(
         SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 10000).to_string(),
         "FOO".into(),
         false,
+        CHAIN_ID_TESTNET,
     );
 
     let signer_db_dir = "/tmp/stacks-node-tests/signer-units/";
diff --git a/stacks-signer/src/tests/conf/signer-custom-chain-id.toml b/stacks-signer/src/tests/conf/signer-custom-chain-id.toml
new file mode 100644
index 0000000000..1d1de36f1f
--- /dev/null
+++ b/stacks-signer/src/tests/conf/signer-custom-chain-id.toml
@@ -0,0 +1,7 @@
+stacks_private_key = "126e916e77359ccf521e168feea1fcb9626c59dc375cae00c7464303381c7dff01"
+node_host = "127.0.0.1:20444"
+endpoint = "localhost:30001"
+network = "testnet"
+auth_password = "12345"
+db_path = ":memory:"
+chain_id = 0x80000100
diff --git a/stackslib/src/net/stackerdb/tests/sync.rs b/stackslib/src/net/stackerdb/tests/sync.rs
index 565a97f422..5f6e8a7bed 100644
--- a/stackslib/src/net/stackerdb/tests/sync.rs
+++ b/stackslib/src/net/stackerdb/tests/sync.rs
@@ -183,7 +183,12 @@ fn load_stackerdb(peer: &TestPeer, idx: usize) -> Vec<(SlotMetadata, Vec<u8>)> {
 
 fn check_sync_results(network_sync: &NetworkResult) {
     for res in network_sync.stacker_db_sync_results.iter() {
-        assert!(res.num_connections >= res.num_attempted_connections);
+        assert!(
+            res.num_connections <= res.num_attempted_connections,
+            "{} < {}",
+            res.num_connections,
+            res.num_attempted_connections
+        );
     }
 }
 
@@ -194,7 +199,14 @@ fn test_reconnect(network: &mut PeerNetwork) {
         .expect("FATAL: did not replace stacker dbs");
 
     for (_sc, stacker_db_sync) in stacker_db_syncs.iter_mut() {
-        stacker_db_sync.connect_begin(network).unwrap();
+        match stacker_db_sync.connect_begin(network) {
+            Ok(x) => {}
+            Err(net_error::PeerNotConnected) => {}
+            Err(net_error::NoSuchNeighbor) => {}
+            Err(e) => {
+                panic!("Failed to connect_begin: {:?}", &e);
+            }
+        }
     }
 
     network.stacker_db_syncs = Some(stacker_db_syncs);
diff --git a/testnet/stacks-node/conf/local-follower-conf.toml b/testnet/stacks-node/conf/local-follower-conf.toml
index c828c18373..8186b57f54 100644
--- a/testnet/stacks-node/conf/local-follower-conf.toml
+++ b/testnet/stacks-node/conf/local-follower-conf.toml
@@ -15,7 +15,6 @@ peer_port = 18444
 # Used for sending events to a local stacks-blockchain-api service
 # [[events_observer]]
 # endpoint = "localhost:3700"
-# retry_count = 255
 # events_keys = ["*"]
 
 [[ustx_balance]]
diff --git a/testnet/stacks-node/conf/mainnet-follower-conf.toml b/testnet/stacks-node/conf/mainnet-follower-conf.toml
index 2ecbc80686..6f6bab70d8 100644
--- a/testnet/stacks-node/conf/mainnet-follower-conf.toml
+++ b/testnet/stacks-node/conf/mainnet-follower-conf.toml
@@ -16,5 +16,4 @@ peer_port = 8333
 # Used for sending events to a local stacks-blockchain-api service
 # [[events_observer]]
 # endpoint = "localhost:3700"
-# retry_count = 255
 # events_keys = ["*"]
diff --git a/testnet/stacks-node/conf/mocknet-follower-conf.toml b/testnet/stacks-node/conf/mocknet-follower-conf.toml
index 3cb9beb5d7..e9a0e7a643 100644
--- a/testnet/stacks-node/conf/mocknet-follower-conf.toml
+++ b/testnet/stacks-node/conf/mocknet-follower-conf.toml
@@ -13,7 +13,6 @@ mode = "mocknet"
 # Used for sending events to a local stacks-blockchain-api service
 # [[events_observer]]
 # endpoint = "localhost:3700"
-# retry_count = 255
 # events_keys = ["*"]
 
 [[ustx_balance]]
diff --git a/testnet/stacks-node/conf/regtest-follower-conf.toml b/testnet/stacks-node/conf/regtest-follower-conf.toml
index a2a71c8acb..151446fbaf 100644
--- a/testnet/stacks-node/conf/regtest-follower-conf.toml
+++ b/testnet/stacks-node/conf/regtest-follower-conf.toml
@@ -17,7 +17,6 @@ peer_port = 18444
 # Used for sending events to a local stacks-blockchain-api service
 # [[events_observer]]
 # endpoint = "localhost:3700"
-# retry_count = 255
 # events_keys = ["*"]
 
 [[ustx_balance]]
diff --git a/testnet/stacks-node/conf/testnet-follower-conf.toml b/testnet/stacks-node/conf/testnet-follower-conf.toml
index cb23477b27..5fe717bfb1 100644
--- a/testnet/stacks-node/conf/testnet-follower-conf.toml
+++ b/testnet/stacks-node/conf/testnet-follower-conf.toml
@@ -17,7 +17,6 @@ peer_port = 18333
 # Used for sending events to a local stacks-blockchain-api service
 # [[events_observer]]
 # endpoint = "localhost:3700"
-# retry_count = 255
 # events_keys = ["*"]
 
 [[ustx_balance]]
diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs
index 126cacad13..004b30fc94 100644
--- a/testnet/stacks-node/src/config.rs
+++ b/testnet/stacks-node/src/config.rs
@@ -89,11 +89,14 @@ const INV_REWARD_CYCLES_TESTNET: u64 = 6;
 const DEFAULT_MIN_TIME_BETWEEN_BLOCKS_MS: u64 = 1000;
 
 #[derive(Clone, Deserialize, Default, Debug)]
+#[serde(deny_unknown_fields)]
 pub struct ConfigFile {
     pub __path: Option<String>, // Only used for config file reloads
     pub burnchain: Option<BurnchainConfigFile>,
     pub node: Option<NodeConfigFile>,
     pub ustx_balance: Option<Vec<InitialBalanceFile>>,
+    /// Deprecated: use `ustx_balance` instead
+    pub mstx_balance: Option<Vec<InitialBalanceFile>>,
     pub events_observer: Option<HashSet<EventObserverConfigFile>>,
     pub connection_options: Option<ConnectionOptionsFile>,
     pub fee_estimation: Option<FeeEstimationConfigFile>,
@@ -101,245 +104,6 @@ pub struct ConfigFile {
     pub atlas: Option<AtlasConfigFile>,
 }
 
-#[derive(Clone, Deserialize, Default)]
-pub struct LegacyMstxConfigFile {
-    pub mstx_balance: Option<Vec<InitialBalanceFile>>,
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_config_file() {
-        assert_eq!(
-            format!("Invalid path: No such file or directory (os error 2)"),
-            ConfigFile::from_path("some_path").unwrap_err()
-        );
-        assert_eq!(
-            format!("Invalid toml: unexpected character found: `/` at line 1 column 1"),
-            ConfigFile::from_str("//[node]").unwrap_err()
-        );
-        assert!(ConfigFile::from_str("").is_ok());
-    }
-
-    #[test]
-    fn test_config() {
-        assert_eq!(
-            format!("node.seed should be a hex encoded string"),
-            Config::from_config_file(
-                ConfigFile::from_str(
-                    r#"
-                    [node]
-                    seed = "invalid-hex-value"
-                    "#,
-                )
-                .unwrap(),
-                false
-            )
-            .unwrap_err()
-        );
-
-        assert_eq!(
-            format!("node.local_peer_seed should be a hex encoded string"),
-            Config::from_config_file(
-                ConfigFile::from_str(
-                    r#"
-                    [node]
-                    local_peer_seed = "invalid-hex-value"
-                    "#,
-                )
-                .unwrap(),
-                false
-            )
-            .unwrap_err()
-        );
-
-        let expected_err_prefix =
-            "Invalid burnchain.peer_host: failed to lookup address information:";
-        let actual_err_msg = Config::from_config_file(
-            ConfigFile::from_str(
-                r#"
-                [burnchain]
-                peer_host = "bitcoin2.blockstack.com"
-                "#,
-            )
-            .unwrap(),
-            false,
-        )
-        .unwrap_err();
-        assert_eq!(
-            expected_err_prefix,
-            &actual_err_msg[..expected_err_prefix.len()]
-        );
-
-        assert!(Config::from_config_file(ConfigFile::from_str("").unwrap(), false).is_ok());
-    }
-
-    #[test]
-    fn should_load_legacy_mstx_balances_toml() {
-        let config = ConfigFile::from_str(
-            r#"
-            [[ustx_balance]]
-            address = "ST2QKZ4FKHAH1NQKYKYAYZPY440FEPK7GZ1R5HBP2"
-            amount = 10000000000000000
-
-            [[ustx_balance]]
-            address = "ST319CF5WV77KYR1H3GT0GZ7B8Q4AQPY42ETP1VPF"
-            amount = 10000000000000000
-
-            [[mstx_balance]] # legacy property name
-            address = "ST221Z6TDTC5E0BYR2V624Q2ST6R0Q71T78WTAX6H"
-            amount = 10000000000000000
-
-            [[mstx_balance]] # legacy property name
-            address = "ST2TFVBMRPS5SSNP98DQKQ5JNB2B6NZM91C4K3P7B"
-            amount = 10000000000000000
-            "#,
-        );
-        let config = config.unwrap();
-        assert!(config.ustx_balance.is_some());
-        let balances = config
-            .ustx_balance
-            .expect("Failed to parse stx balances from toml");
-        assert_eq!(balances.len(), 4);
-        assert_eq!(
-            balances[0].address,
-            "ST2QKZ4FKHAH1NQKYKYAYZPY440FEPK7GZ1R5HBP2"
-        );
-        assert_eq!(
-            balances[1].address,
-            "ST319CF5WV77KYR1H3GT0GZ7B8Q4AQPY42ETP1VPF"
-        );
-        assert_eq!(
-            balances[2].address,
-            "ST221Z6TDTC5E0BYR2V624Q2ST6R0Q71T78WTAX6H"
-        );
-        assert_eq!(
-            balances[3].address,
-            "ST2TFVBMRPS5SSNP98DQKQ5JNB2B6NZM91C4K3P7B"
-        );
-    }
-
-    #[test]
-    fn should_load_auth_token() {
-        let config = Config::from_config_file(
-            ConfigFile::from_str(
-                r#"
-                [connection_options]
-                auth_token = "password"
-                "#,
-            )
-            .unwrap(),
-            false,
-        )
-        .expect("Expected to be able to parse block proposal token from file");
-
-        assert_eq!(
-            config.connection_options.auth_token,
-            Some("password".to_string())
-        );
-    }
-
-    #[test]
-    fn should_load_affirmation_map() {
-        let affirmation_string = "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnnpppppnnnnnnnnnnnnnnnnnnnnnnnpppppppppppppppnnnnnnnnnnnnnnnnnnnnnnnppppppppppnnnnnnnnnnnnnnnnnnnppppnnnnnnnnnnnnnnnnnnnnnnnppppppppnnnnnnnnnnnnnnnnnnnnnnnppnppnnnnnnnnnnnnnnnnnnnnnnnppppnnnnnnnnnnnnnnnnnnnnnnnnnppppppnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnnnpppppppnnnnnnnnnnnnnnnnnnnnnnnnnnpnnnnnnnnnnnnnnnnnnnnnnnnnpppnppppppppppppppnnppppnpa";
-        let affirmation =
-            AffirmationMap::decode(affirmation_string).expect("Failed to decode affirmation map");
-        let config = Config::from_config_file(
-            ConfigFile::from_str(&format!(
-                r#"
-                    [[burnchain.affirmation_overrides]]
-                    reward_cycle = 413
-                    affirmation = "{affirmation_string}"
-                "#
-            ))
-            .expect("Expected to be able to parse config file from string"),
-            false,
-        )
-        .expect("Expected to be able to parse affirmation map from file");
-
-        assert_eq!(config.burnchain.affirmation_overrides.len(), 1);
-        assert_eq!(config.burnchain.affirmation_overrides.get(&0), None);
-        assert_eq!(
-            config.burnchain.affirmation_overrides.get(&413),
-            Some(&affirmation)
-        );
-    }
-
-    #[test]
-    fn should_fail_to_load_invalid_affirmation_map() {
-        let bad_affirmation_string = "bad_map";
-        let file = ConfigFile::from_str(&format!(
-            r#"
-                    [[burnchain.affirmation_overrides]]
-                    reward_cycle = 1
-                    affirmation = "{bad_affirmation_string}"
-                "#
-        ))
-        .expect("Expected to be able to parse config file from string");
-
-        assert!(Config::from_config_file(file, false).is_err());
-    }
-
-    #[test]
-    fn should_load_empty_affirmation_map() {
-        let config = Config::from_config_file(
-            ConfigFile::from_str(r#""#)
-                .expect("Expected to be able to parse config file from string"),
-            false,
-        )
-        .expect("Expected to be able to parse affirmation map from file");
-
-        assert!(config.burnchain.affirmation_overrides.is_empty());
-    }
-
-    #[test]
-    fn should_include_xenon_default_affirmation_overrides() {
-        let config = Config::from_config_file(
-            ConfigFile::from_str(
-                r#"
-                [burnchain]
-                chain = "bitcoin"
-                mode = "xenon"
-                "#,
-            )
-            .expect("Expected to be able to parse config file from string"),
-            false,
-        )
-        .expect("Expected to be able to parse affirmation map from file");
-        // Should default add xenon affirmation overrides
-        assert_eq!(config.burnchain.affirmation_overrides.len(), 5);
-    }
-
-    #[test]
-    fn should_override_xenon_default_affirmation_overrides() {
-        let affirmation_string = "aaapnnnnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnnpppppnnnnnnnnnnnnnnnnnnnnnnnpppppppppppppppnnnnnnnnnnnnnnnnnnnnnnnppppppppppnnnnnnnnnnnnnnnnnnnppppnnnnnnnnnnnnnnnnnnnnnnnppppppppnnnnnnnnnnnnnnnnnnnnnnnppnppnnnnnnnnnnnnnnnnnnnnnnnppppnnnnnnnnnnnnnnnnnnnnnnnnnppppppnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnnnpppppppnnnnnnnnnnnnnnnnnnnnnnnnnnpnnnnnnnnnnnnnnnnnnnnnnnnnpppnppppppppppppppnnppppnpa";
-        let affirmation =
-            AffirmationMap::decode(affirmation_string).expect("Failed to decode affirmation map");
-
-        let config = Config::from_config_file(
-            ConfigFile::from_str(&format!(
-                r#"
-                [burnchain]
-                chain = "bitcoin"
-                mode = "xenon"
-
-                [[burnchain.affirmation_overrides]]
-                reward_cycle = 413
-                affirmation = "{affirmation_string}"
-                "#,
-            ))
-            .expect("Expected to be able to parse config file from string"),
-            false,
-        )
-        .expect("Expected to be able to parse affirmation map from file");
-        // Should default add xenon affirmation overrides, but overwrite with the configured one above
-        assert_eq!(config.burnchain.affirmation_overrides.len(), 5);
-        assert_eq!(config.burnchain.affirmation_overrides[&413], affirmation);
-    }
-}
-
 impl ConfigFile {
     pub fn from_path(path: &str) -> Result<ConfigFile, String> {
         let content = fs::read_to_string(path).map_err(|e| format!("Invalid path: {}", &e))?;
@@ -351,13 +115,16 @@ impl ConfigFile {
     pub fn from_str(content: &str) -> Result<ConfigFile, String> {
         let mut config: ConfigFile =
             toml::from_str(content).map_err(|e| format!("Invalid toml: {}", e))?;
-        let legacy_config: LegacyMstxConfigFile = toml::from_str(content).unwrap();
-        if let Some(mstx_balance) = legacy_config.mstx_balance {
-            warn!("'mstx_balance' inside toml config is deprecated, replace with 'ustx_balance'");
-            config.ustx_balance = match config.ustx_balance {
-                Some(balance) => Some([balance, mstx_balance].concat()),
-                None => Some(mstx_balance),
-            };
+        if let Some(mstx_balance) = config.mstx_balance.take() {
+            warn!("'mstx_balance' in the config is deprecated; please use 'ustx_balance' instead.");
+            match config.ustx_balance {
+                Some(ref mut ustx_balance) => {
+                    ustx_balance.extend(mstx_balance);
+                }
+                None => {
+                    config.ustx_balance = Some(mstx_balance);
+                }
+            }
         }
         Ok(config)
     }
@@ -1128,11 +895,10 @@ impl Config {
                         .map(|e| EventKeyType::from_string(e).unwrap())
                         .collect();
 
-                    let endpoint = format!("{}", observer.endpoint);
-
                     observers.insert(EventObserverConfig {
-                        endpoint,
+                        endpoint: observer.endpoint,
                         events_keys,
+                        timeout_ms: observer.timeout_ms.unwrap_or(1_000),
                     });
                 }
                 observers
@@ -1146,6 +912,7 @@ impl Config {
                 events_observers.insert(EventObserverConfig {
                     endpoint: val,
                     events_keys: vec![EventKeyType::AnyEvent],
+                    timeout_ms: 1_000,
                 });
                 ()
             }
@@ -1552,6 +1319,7 @@ pub struct AffirmationOverride {
 }
 
 #[derive(Clone, Deserialize, Default, Debug)]
+#[serde(deny_unknown_fields)]
 pub struct BurnchainConfigFile {
     pub chain: Option<String>,
     pub chain_id: Option<u32>,
@@ -1703,10 +1471,22 @@ impl BurnchainConfigFile {
 
         let mut config = BurnchainConfig {
             chain: self.chain.unwrap_or(default_burnchain_config.chain),
-            chain_id: if is_mainnet {
-                CHAIN_ID_MAINNET
-            } else {
-                CHAIN_ID_TESTNET
+            chain_id: match self.chain_id {
+                Some(chain_id) => {
+                    if is_mainnet && chain_id != CHAIN_ID_MAINNET {
+                        return Err(format!(
+                            "Attempted to run mainnet node with chain_id {chain_id}",
+                        ));
+                    }
+                    chain_id
+                }
+                None => {
+                    if is_mainnet {
+                        CHAIN_ID_MAINNET
+                    } else {
+                        CHAIN_ID_TESTNET
+                    }
+                }
             },
             peer_version: if is_mainnet {
                 PEER_VERSION_MAINNET
@@ -1842,6 +1622,7 @@ impl BurnchainConfigFile {
         Ok(config)
     }
 }
+
 #[derive(Clone, Debug)]
 pub struct NodeConfig {
     pub name: String,
@@ -2421,6 +2202,7 @@ impl Default for MinerConfig {
 }
 
 #[derive(Clone, Default, Deserialize, Debug)]
+#[serde(deny_unknown_fields)]
 pub struct ConnectionOptionsFile {
     pub inbox_maxlen: Option<usize>,
     pub outbox_maxlen: Option<usize>,
@@ -2604,6 +2386,7 @@ impl ConnectionOptionsFile {
 }
 
 #[derive(Clone, Deserialize, Default, Debug)]
+#[serde(deny_unknown_fields)]
 pub struct NodeConfigFile {
     pub name: Option<String>,
     pub seed: Option<String>,
@@ -2738,6 +2521,7 @@ impl NodeConfigFile {
 }
 
 #[derive(Clone, Deserialize, Default, Debug)]
+#[serde(deny_unknown_fields)]
 pub struct FeeEstimationConfigFile {
     pub cost_estimator: Option<String>,
     pub fee_estimator: Option<String>,
@@ -2749,6 +2533,7 @@ pub struct FeeEstimationConfigFile {
 }
 
 #[derive(Clone, Deserialize, Default, Debug)]
+#[serde(deny_unknown_fields)]
 pub struct MinerConfigFile {
     pub first_attempt_time_ms: Option<u64>,
     pub subsequent_attempt_time_ms: Option<u64>,
@@ -2891,6 +2676,7 @@ impl MinerConfigFile {
     }
 }
 #[derive(Clone, Deserialize, Default, Debug)]
+#[serde(deny_unknown_fields)]
 pub struct AtlasConfigFile {
     pub attachments_max_size: Option<u32>,
     pub max_uninstantiated_attachments: Option<u32>,
@@ -2919,15 +2705,18 @@ impl AtlasConfigFile {
 }
 
 #[derive(Clone, Deserialize, Default, Debug, Hash, PartialEq, Eq, PartialOrd)]
+#[serde(deny_unknown_fields)]
 pub struct EventObserverConfigFile {
     pub endpoint: String,
     pub events_keys: Vec<String>,
+    pub timeout_ms: Option<u64>,
 }
 
 #[derive(Clone, Default, Debug, Hash, PartialEq, Eq, PartialOrd)]
 pub struct EventObserverConfig {
     pub endpoint: String,
     pub events_keys: Vec<EventKeyType>,
+    pub timeout_ms: u64,
 }
 
 #[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd)]
@@ -3019,7 +2808,449 @@ pub struct InitialBalance {
 }
 
 #[derive(Clone, Deserialize, Default, Debug)]
+#[serde(deny_unknown_fields)]
 pub struct InitialBalanceFile {
     pub address: String,
     pub amount: u64,
 }
+
+#[cfg(test)]
+mod tests {
+    use std::path::Path;
+
+    use super::*;
+
+    #[test]
+    fn test_config_file() {
+        assert_eq!(
+            format!("Invalid path: No such file or directory (os error 2)"),
+            ConfigFile::from_path("some_path").unwrap_err()
+        );
+        assert_eq!(
+            format!("Invalid toml: unexpected character found: `/` at line 1 column 1"),
+            ConfigFile::from_str("//[node]").unwrap_err()
+        );
+        assert!(ConfigFile::from_str("").is_ok());
+    }
+
+    #[test]
+    fn test_config() {
+        assert_eq!(
+            format!("node.seed should be a hex encoded string"),
+            Config::from_config_file(
+                ConfigFile::from_str(
+                    r#"
+                    [node]
+                    seed = "invalid-hex-value"
+                    "#,
+                )
+                .unwrap(),
+                false
+            )
+            .unwrap_err()
+        );
+
+        assert_eq!(
+            format!("node.local_peer_seed should be a hex encoded string"),
+            Config::from_config_file(
+                ConfigFile::from_str(
+                    r#"
+                    [node]
+                    local_peer_seed = "invalid-hex-value"
+                    "#,
+                )
+                .unwrap(),
+                false
+            )
+            .unwrap_err()
+        );
+
+        let expected_err_prefix =
+            "Invalid burnchain.peer_host: failed to lookup address information:";
+        let actual_err_msg = Config::from_config_file(
+            ConfigFile::from_str(
+                r#"
+                [burnchain]
+                peer_host = "bitcoin2.blockstack.com"
+                "#,
+            )
+            .unwrap(),
+            false,
+        )
+        .unwrap_err();
+        assert_eq!(
+            expected_err_prefix,
+            &actual_err_msg[..expected_err_prefix.len()]
+        );
+
+        assert!(Config::from_config_file(ConfigFile::from_str("").unwrap(), false).is_ok());
+    }
+
+    #[test]
+    fn test_deny_unknown_fields() {
+        {
+            let err = ConfigFile::from_str(
+                r#"
+            [node]
+            name = "test"
+            unknown_field = "test"
+            "#,
+            )
+            .unwrap_err();
+            assert!(err.starts_with("Invalid toml: unknown field `unknown_field`"));
+        }
+
+        {
+            let err = ConfigFile::from_str(
+                r#"
+            [burnchain]
+            chain_id = 0x00000500
+            unknown_field = "test"
+            chain = "bitcoin"
+            "#,
+            )
+            .unwrap_err();
+            assert!(err.starts_with("Invalid toml: unknown field `unknown_field`"));
+        }
+
+        {
+            let err = ConfigFile::from_str(
+                r#"
+            [node]
+            rpc_bind = "0.0.0.0:20443"
+            unknown_field = "test"
+            p2p_bind = "0.0.0.0:20444"
+            "#,
+            )
+            .unwrap_err();
+            assert!(err.starts_with("Invalid toml: unknown field `unknown_field`"));
+        }
+
+        {
+            let err = ConfigFile::from_str(
+                r#"
+            [[ustx_balance]]
+            address = "ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0"
+            amount = 10000000000000000
+            unknown_field = "test"
+            "#,
+            )
+            .unwrap_err();
+            assert!(err.starts_with("Invalid toml: unknown field `unknown_field`"));
+        }
+
+        {
+            let err = ConfigFile::from_str(
+                r#"
+            [[events_observer]]
+            endpoint = "localhost:30000"
+            unknown_field = "test"
+            events_keys = ["stackerdb", "block_proposal", "burn_blocks"]
+            "#,
+            )
+            .unwrap_err();
+            assert!(err.starts_with("Invalid toml: unknown field `unknown_field`"));
+        }
+
+        {
+            let err = ConfigFile::from_str(
+                r#"
+            [connection_options]
+            inbox_maxlen = 100
+            outbox_maxlen = 200
+            unknown_field = "test"
+            "#,
+            )
+            .unwrap_err();
+            assert!(err.starts_with("Invalid toml: unknown field `unknown_field`"));
+        }
+
+        {
+            let err = ConfigFile::from_str(
+                r#"
+            [fee_estimation]
+            cost_estimator = "foo"
+            unknown_field = "test"
+            "#,
+            )
+            .unwrap_err();
+            assert!(err.starts_with("Invalid toml: unknown field `unknown_field`"));
+        }
+
+        {
+            let err = ConfigFile::from_str(
+                r#"
+            [miner]
+            first_attempt_time_ms = 180_000
+            unknown_field = "test"
+            subsequent_attempt_time_ms = 360_000
+            "#,
+            )
+            .unwrap_err();
+            println!("{}", err);
+            assert!(err.starts_with("Invalid toml: unknown field `unknown_field`"));
+        }
+
+        {
+            let err = ConfigFile::from_str(
+                r#"
+                [atlas]
+                attachments_max_size = 100
+                unknown_field = "test"
+                "#,
+            )
+            .unwrap_err();
+            assert!(err.starts_with("Invalid toml: unknown field `unknown_field`"));
+        }
+    }
+
+    #[test]
+    fn test_example_confs() {
+        // For each config file in the ../conf/ directory, we should be able to parse it
+        let conf_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("conf");
+        println!("Reading config files from: {:?}", conf_dir);
+        let conf_files = fs::read_dir(conf_dir).unwrap();
+
+        for entry in conf_files {
+            let entry = entry.unwrap();
+            let path = entry.path();
+            if path.is_file() {
+                let file_name = path.file_name().unwrap().to_str().unwrap();
+                if file_name.ends_with(".toml") {
+                    let _config = ConfigFile::from_path(path.to_str().unwrap()).unwrap();
+                    debug!("Parsed config file: {}", file_name);
+                }
+            }
+        }
+    }
+
+    #[test]
+    fn should_load_legacy_mstx_balances_toml() {
+        let config = ConfigFile::from_str(
+            r#"
+            [[ustx_balance]]
+            address = "ST2QKZ4FKHAH1NQKYKYAYZPY440FEPK7GZ1R5HBP2"
+            amount = 10000000000000000
+
+            [[ustx_balance]]
+            address = "ST319CF5WV77KYR1H3GT0GZ7B8Q4AQPY42ETP1VPF"
+            amount = 10000000000000000
+
+            [[mstx_balance]] # legacy property name
+            address = "ST221Z6TDTC5E0BYR2V624Q2ST6R0Q71T78WTAX6H"
+            amount = 10000000000000000
+
+            [[mstx_balance]] # legacy property name
+            address = "ST2TFVBMRPS5SSNP98DQKQ5JNB2B6NZM91C4K3P7B"
+            amount = 10000000000000000
+            "#,
+        );
+        let config = config.unwrap();
+        assert!(config.ustx_balance.is_some());
+        let balances = config
+            .ustx_balance
+            .expect("Failed to parse stx balances from toml");
+        assert_eq!(balances.len(), 4);
+        assert_eq!(
+            balances[0].address,
+            "ST2QKZ4FKHAH1NQKYKYAYZPY440FEPK7GZ1R5HBP2"
+        );
+        assert_eq!(
+            balances[1].address,
+            "ST319CF5WV77KYR1H3GT0GZ7B8Q4AQPY42ETP1VPF"
+        );
+        assert_eq!(
+            balances[2].address,
+            "ST221Z6TDTC5E0BYR2V624Q2ST6R0Q71T78WTAX6H"
+        );
+        assert_eq!(
+            balances[3].address,
+            "ST2TFVBMRPS5SSNP98DQKQ5JNB2B6NZM91C4K3P7B"
+        );
+    }
+
+    #[test]
+    fn should_load_auth_token() {
+        let config = Config::from_config_file(
+            ConfigFile::from_str(
+                r#"
+                [connection_options]
+                auth_token = "password"
+                "#,
+            )
+            .unwrap(),
+            false,
+        )
+        .expect("Expected to be able to parse block proposal token from file");
+
+        assert_eq!(
+            config.connection_options.auth_token,
+            Some("password".to_string())
+        );
+    }
+
+    #[test]
+    fn should_load_affirmation_map() {
+        let affirmation_string = "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnnpppppnnnnnnnnnnnnnnnnnnnnnnnpppppppppppppppnnnnnnnnnnnnnnnnnnnnnnnppppppppppnnnnnnnnnnnnnnnnnnnppppnnnnnnnnnnnnnnnnnnnnnnnppppppppnnnnnnnnnnnnnnnnnnnnnnnppnppnnnnnnnnnnnnnnnnnnnnnnnppppnnnnnnnnnnnnnnnnnnnnnnnnnppppppnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnnnpppppppnnnnnnnnnnnnnnnnnnnnnnnnnnpnnnnnnnnnnnnnnnnnnnnnnnnnpppnppppppppppppppnnppppnpa";
+        let affirmation =
+            AffirmationMap::decode(affirmation_string).expect("Failed to decode affirmation map");
+        let config = Config::from_config_file(
+            ConfigFile::from_str(&format!(
+                r#"
+                    [[burnchain.affirmation_overrides]]
+                    reward_cycle = 413
+                    affirmation = "{affirmation_string}"
+                "#
+            ))
+            .expect("Expected to be able to parse config file from string"),
+            false,
+        )
+        .expect("Expected to be able to parse affirmation map from file");
+
+        assert_eq!(config.burnchain.affirmation_overrides.len(), 1);
+        assert_eq!(config.burnchain.affirmation_overrides.get(&0), None);
+        assert_eq!(
+            config.burnchain.affirmation_overrides.get(&413),
+            Some(&affirmation)
+        );
+    }
+
+    #[test]
+    fn should_fail_to_load_invalid_affirmation_map() {
+        let bad_affirmation_string = "bad_map";
+        let file = ConfigFile::from_str(&format!(
+            r#"
+                    [[burnchain.affirmation_overrides]]
+                    reward_cycle = 1
+                    affirmation = "{bad_affirmation_string}"
+                "#
+        ))
+        .expect("Expected to be able to parse config file from string");
+
+        assert!(Config::from_config_file(file, false).is_err());
+    }
+
+    #[test]
+    fn should_load_empty_affirmation_map() {
+        let config = Config::from_config_file(
+            ConfigFile::from_str(r#""#)
+                .expect("Expected to be able to parse config file from string"),
+            false,
+        )
+        .expect("Expected to be able to parse affirmation map from file");
+
+        assert!(config.burnchain.affirmation_overrides.is_empty());
+    }
+
+    #[test]
+    fn should_include_xenon_default_affirmation_overrides() {
+        let config = Config::from_config_file(
+            ConfigFile::from_str(
+                r#"
+                [burnchain]
+                chain = "bitcoin"
+                mode = "xenon"
+                "#,
+            )
+            .expect("Expected to be able to parse config file from string"),
+            false,
+        )
+        .expect("Expected to be able to parse affirmation map from file");
+        // Should default add xenon affirmation overrides
+        assert_eq!(config.burnchain.affirmation_overrides.len(), 5);
+    }
+
+    #[test]
+    fn should_override_xenon_default_affirmation_overrides() {
+        let affirmation_string = "aaapnnnnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnnpppppnnnnnnnnnnnnnnnnnnnnnnnpppppppppppppppnnnnnnnnnnnnnnnnnnnnnnnppppppppppnnnnnnnnnnnnnnnnnnnppppnnnnnnnnnnnnnnnnnnnnnnnppppppppnnnnnnnnnnnnnnnnnnnnnnnppnppnnnnnnnnnnnnnnnnnnnnnnnppppnnnnnnnnnnnnnnnnnnnnnnnnnppppppnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnnnpppppppnnnnnnnnnnnnnnnnnnnnnnnnnnpnnnnnnnnnnnnnnnnnnnnnnnnnpppnppppppppppppppnnppppnpa";
+        let affirmation =
+            AffirmationMap::decode(affirmation_string).expect("Failed to decode affirmation map");
+
+        let config = Config::from_config_file(
+            ConfigFile::from_str(&format!(
+                r#"
+                [burnchain]
+                chain = "bitcoin"
+                mode = "xenon"
+
+                [[burnchain.affirmation_overrides]]
+                reward_cycle = 413
+                affirmation = "{affirmation_string}"
+                "#,
+            ))
+            .expect("Expected to be able to parse config file from string"),
+            false,
+        )
+        .expect("Expected to be able to parse affirmation map from file");
+        // Should default add xenon affirmation overrides, but overwrite with the configured one above
+        assert_eq!(config.burnchain.affirmation_overrides.len(), 5);
+        assert_eq!(config.burnchain.affirmation_overrides[&413], affirmation);
+    }
+
+    #[test]
+    fn test_into_config_default_chain_id() {
+        // Helper function to create BurnchainConfigFile with mode and optional chain_id
+        fn make_burnchain_config_file(mainnet: bool, chain_id: Option<u32>) -> BurnchainConfigFile {
+            let mut config = BurnchainConfigFile::default();
+            if mainnet {
+                config.mode = Some("mainnet".to_string());
+            }
+            config.chain_id = chain_id;
+            config
+        }
+        let default_burnchain_config = BurnchainConfig::default();
+
+        // **Case 1a:** Should panic when `is_mainnet` is true and `chain_id` != `CHAIN_ID_MAINNET`
+        {
+            let config_file = make_burnchain_config_file(true, Some(CHAIN_ID_TESTNET));
+
+            let result = config_file.into_config_default(default_burnchain_config.clone());
+
+            assert!(
+                result.is_err(),
+                "Expected error when chain_id != CHAIN_ID_MAINNET on mainnet"
+            );
+        }
+
+        // **Case 1b:** Should not panic when `is_mainnet` is true and `chain_id` == `CHAIN_ID_MAINNET`
+        {
+            let config_file = make_burnchain_config_file(true, Some(CHAIN_ID_MAINNET));
+
+            let config = config_file
+                .into_config_default(default_burnchain_config.clone())
+                .expect("Should not panic");
+            assert_eq!(config.chain_id, CHAIN_ID_MAINNET);
+        }
+
+        // **Case 1c:** Should not panic when `is_mainnet` is false; chain_id should be as provided
+        {
+            let chain_id = 123456;
+            let config_file = make_burnchain_config_file(false, Some(chain_id));
+
+            let config = config_file
+                .into_config_default(default_burnchain_config.clone())
+                .expect("Should not panic");
+            assert_eq!(config.chain_id, chain_id);
+        }
+
+        // **Case 2a:** Should not panic when `chain_id` is None and `is_mainnet` is true
+        {
+            let config_file = make_burnchain_config_file(true, None);
+
+            let config = config_file
+                .into_config_default(default_burnchain_config.clone())
+                .expect("Should not panic");
+            assert_eq!(config.chain_id, CHAIN_ID_MAINNET);
+        }
+
+        // **Case 2b:** Should not panic when `chain_id` is None and `is_mainnet` is false
+        {
+            let config_file = make_burnchain_config_file(false, None);
+
+            let config = config_file
+                .into_config_default(default_burnchain_config.clone())
+                .expect("Should not panic");
+            assert_eq!(config.chain_id, CHAIN_ID_TESTNET);
+        }
+    }
+}
diff --git a/testnet/stacks-node/src/event_dispatcher.rs b/testnet/stacks-node/src/event_dispatcher.rs
index 864a964ee6..faf437c444 100644
--- a/testnet/stacks-node/src/event_dispatcher.rs
+++ b/testnet/stacks-node/src/event_dispatcher.rs
@@ -69,6 +69,7 @@ use super::config::{EventKeyType, EventObserverConfig};
 #[derive(Debug, Clone)]
 struct EventObserver {
     endpoint: String,
+    timeout: Duration,
 }
 
 struct ReceiptPayloadInfo<'a> {
@@ -335,8 +336,7 @@ impl EventObserver {
             .parse()
             .unwrap_or(PeerHost::DNS(host.to_string(), port));
 
-        let backoff = Duration::from_millis(1000); // 1 second
-
+        let mut backoff = Duration::from_millis(100);
         loop {
             let mut request = StacksHttpRequest::new_for_peer(
                 peerhost.clone(),
@@ -347,7 +347,7 @@ impl EventObserver {
             .unwrap_or_else(|_| panic!("FATAL: failed to encode infallible data as HTTP request"));
             request.add_header("Connection".into(), "close".into());
 
-            match send_http_request(host, port, request, backoff) {
+            match send_http_request(host, port, request, self.timeout) {
                 Ok(response) => {
                     if response.preamble().status_code == 200 {
                         debug!(
@@ -368,6 +368,7 @@ impl EventObserver {
                 }
             }
             sleep(backoff);
+            backoff *= 2;
         }
     }
 
@@ -1406,6 +1407,7 @@ impl EventDispatcher {
         info!("Registering event observer at: {}", conf.endpoint);
         let event_observer = EventObserver {
             endpoint: conf.endpoint.clone(),
+            timeout: Duration::from_millis(conf.timeout_ms),
         };
 
         let observer_index = self.registered_observers.len() as u16;
@@ -1498,6 +1500,7 @@ mod test {
     fn build_block_processed_event() {
         let observer = EventObserver {
             endpoint: "nowhere".to_string(),
+            timeout: Duration::from_secs(3),
         };
 
         let filtered_events = vec![];
@@ -1558,6 +1561,7 @@ mod test {
     fn test_block_processed_event_nakamoto() {
         let observer = EventObserver {
             endpoint: "nowhere".to_string(),
+            timeout: Duration::from_secs(3),
         };
 
         let filtered_events = vec![];
@@ -1699,6 +1703,7 @@ mod test {
 
         let observer = EventObserver {
             endpoint: format!("127.0.0.1:{}", port),
+            timeout: Duration::from_secs(3),
         };
 
         let payload = json!({"key": "value"});
@@ -1749,6 +1754,7 @@ mod test {
 
         let observer = EventObserver {
             endpoint: format!("127.0.0.1:{}", port),
+            timeout: Duration::from_secs(3),
         };
 
         let payload = json!({"key": "value"});
@@ -1759,4 +1765,69 @@ mod test {
         rx.recv_timeout(Duration::from_secs(5))
             .expect("Server did not receive request in time");
     }
+
+    #[test]
+    fn test_send_payload_timeout() {
+        let port = get_random_port();
+        let timeout = Duration::from_secs(3);
+
+        // Set up a channel to notify when the server has processed the request
+        let (tx, rx) = channel();
+
+        // Start a mock server in a separate thread
+        let server = Server::http(format!("127.0.0.1:{}", port)).unwrap();
+        thread::spawn(move || {
+            let mut attempt = 0;
+            let mut _request_holder = None;
+            while let Ok(request) = server.recv() {
+                attempt += 1;
+                if attempt == 1 {
+                    debug!("Mock server received request attempt 1");
+                    // Do not reply, forcing the sender to timeout and retry,
+                    // but don't drop the request or it will receive a 500 error,
+                    _request_holder = Some(request);
+                } else {
+                    debug!("Mock server received request attempt 2");
+                    // Simulate a successful response on the second attempt
+                    let response = Response::from_string("HTTP/1.1 200 OK");
+                    request.respond(response).unwrap();
+
+                    // Notify the test that the request was processed successfully
+                    tx.send(()).unwrap();
+                    break;
+                }
+            }
+        });
+
+        let observer = EventObserver {
+            endpoint: format!("127.0.0.1:{}", port),
+            timeout,
+        };
+
+        let payload = json!({"key": "value"});
+
+        // Record the time before sending the payload
+        let start_time = Instant::now();
+
+        // Call the function being tested
+        observer.send_payload(&payload, "/test");
+
+        // Record the time after the function returns
+        let elapsed_time = start_time.elapsed();
+
+        println!("Elapsed time: {:?}", elapsed_time);
+        assert!(
+            elapsed_time >= timeout,
+            "Expected a timeout, but the function returned too quickly"
+        );
+
+        assert!(
+            elapsed_time < timeout + Duration::from_secs(1),
+            "Expected a timeout, but the function took too long"
+        );
+
+        // Wait for the server to process the request
+        rx.recv_timeout(Duration::from_secs(5))
+            .expect("Server did not receive request in time");
+    }
 }
diff --git a/testnet/stacks-node/src/nakamoto_node/relayer.rs b/testnet/stacks-node/src/nakamoto_node/relayer.rs
index dd6fd02a71..ef01f67f4b 100644
--- a/testnet/stacks-node/src/nakamoto_node/relayer.rs
+++ b/testnet/stacks-node/src/nakamoto_node/relayer.rs
@@ -1066,20 +1066,23 @@ impl RelayerThread {
 
         // sign and broadcast
         let mut op_signer = self.keychain.generate_op_signer();
-        let txid = self
-            .bitcoin_controller
-            .submit_operation(
-                last_committed.get_epoch_id().clone(),
-                BlockstackOperationType::LeaderBlockCommit(
-                    last_committed.get_block_commit().clone(),
-                ),
-                &mut op_signer,
-                1,
-            )
-            .map_err(|e| {
+        let res = self.bitcoin_controller.submit_operation(
+            last_committed.get_epoch_id().clone(),
+            BlockstackOperationType::LeaderBlockCommit(last_committed.get_block_commit().clone()),
+            &mut op_signer,
+            1,
+        );
+        let txid = match res {
+            Ok(txid) => txid,
+            Err(e) => {
+                if self.config.node.mock_mining {
+                    debug!("Relayer: Mock-mining enabled; not sending Bitcoin transaction");
+                    return Ok(());
+                }
                 warn!("Failed to submit block-commit bitcoin transaction: {e}");
-                NakamotoNodeError::BurnchainSubmissionFailed(e)
-            })?;
+                return Err(NakamotoNodeError::BurnchainSubmissionFailed(e));
+            }
+        };
 
         info!(
             "Relayer: Submitted block-commit";
diff --git a/testnet/stacks-node/src/tests/epoch_205.rs b/testnet/stacks-node/src/tests/epoch_205.rs
index 2006abb05e..076a5f61f3 100644
--- a/testnet/stacks-node/src/tests/epoch_205.rs
+++ b/testnet/stacks-node/src/tests/epoch_205.rs
@@ -25,7 +25,7 @@ use stacks_common::types::chainstate::{
 use stacks_common::util::hash::hex_bytes;
 use stacks_common::util::sleep_ms;
 
-use crate::config::{EventKeyType, EventObserverConfig, InitialBalance};
+use crate::config::{EventKeyType, InitialBalance};
 use crate::tests::bitcoin_regtest::BitcoinCoreController;
 use crate::tests::neon_integrations::*;
 use crate::tests::{
@@ -78,8 +78,14 @@ fn test_exact_block_costs() {
                (ok 1)))
     ";
 
-    let contract_publish_tx =
-        make_contract_publish(&spender_sk, 0, 210_000, contract_name, contract_content);
+    let contract_publish_tx = make_contract_publish(
+        &spender_sk,
+        0,
+        210_000,
+        conf.burnchain.chain_id,
+        contract_name,
+        contract_content,
+    );
 
     // make txs that alternate between
     let txs: Vec<_> = (1..transactions_to_broadcast + 1)
@@ -89,6 +95,7 @@ fn test_exact_block_costs() {
                     &spender_sk,
                     nonce,
                     200_000,
+                    conf.burnchain.chain_id,
                     &spender_addr_c32,
                     contract_name,
                     "db-get2",
@@ -99,6 +106,7 @@ fn test_exact_block_costs() {
                     &spender_sk,
                     nonce,
                     200_000,
+                    conf.burnchain.chain_id,
                     &spender_addr_c32,
                     contract_name,
                     "db-get2",
@@ -109,10 +117,10 @@ fn test_exact_block_costs() {
         .collect();
 
     test_observer::spawn();
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent, EventKeyType::MinedBlocks],
-    });
+    test_observer::register(
+        &mut conf,
+        &[EventKeyType::AnyEvent, EventKeyType::MinedBlocks],
+    );
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -307,14 +315,22 @@ fn test_dynamic_db_method_costs() {
         amount: 200_000_000,
     });
 
-    let contract_publish_tx =
-        make_contract_publish(&spender_sk, 0, 210_000, contract_name, contract_content);
+    let contract_publish_tx = make_contract_publish(
+        &spender_sk,
+        0,
+        210_000,
+        conf.burnchain.chain_id,
+        contract_name,
+        contract_content,
+    );
 
+    let chain_id = conf.burnchain.chain_id;
     let make_db_get1_call = |nonce| {
         make_contract_call(
             &spender_sk,
             nonce,
             200_000,
+            chain_id,
             &spender_addr_c32,
             contract_name,
             "db-get1",
@@ -327,6 +343,7 @@ fn test_dynamic_db_method_costs() {
             &spender_sk,
             nonce,
             200_000,
+            chain_id,
             &spender_addr_c32,
             contract_name,
             "db-get2",
@@ -335,10 +352,7 @@ fn test_dynamic_db_method_costs() {
     };
 
     test_observer::spawn();
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -772,10 +786,7 @@ fn test_cost_limit_switch_version205() {
     });
 
     test_observer::spawn();
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -806,6 +817,7 @@ fn test_cost_limit_switch_version205() {
             &creator_sk,
             0,
             1100000,
+            conf.burnchain.chain_id,
             "increment-contract",
             &giant_contract,
         ),
@@ -834,6 +846,7 @@ fn test_cost_limit_switch_version205() {
             &alice_sk,
             0,
             1000,
+            conf.burnchain.chain_id,
             &creator_addr.into(),
             "increment-contract",
             "increment-many",
@@ -868,6 +881,7 @@ fn test_cost_limit_switch_version205() {
             &bob_sk,
             0,
             1000,
+            conf.burnchain.chain_id,
             &creator_addr.into(),
             "increment-contract",
             "increment-many",
@@ -908,65 +922,6 @@ fn bigger_microblock_streams_in_2_05() {
         .collect();
     let spender_addrs: Vec<PrincipalData> = spender_sks.iter().map(|x| to_addr(x).into()).collect();
 
-    let txs: Vec<Vec<_>> = spender_sks
-        .iter()
-        .enumerate()
-        .map(|(ix, spender_sk)| {
-            // almost fills a whole block
-            make_contract_publish_microblock_only(
-                spender_sk,
-                0,
-                1049230,
-                &format!("large-{}", ix),
-                &format!("
-                    ;; a single one of these transactions consumes over half the runtime budget
-                    (define-constant BUFF_TO_BYTE (list
-                       0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f
-                       0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17 0x18 0x19 0x1a 0x1b 0x1c 0x1d 0x1e 0x1f
-                       0x20 0x21 0x22 0x23 0x24 0x25 0x26 0x27 0x28 0x29 0x2a 0x2b 0x2c 0x2d 0x2e 0x2f
-                       0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x3a 0x3b 0x3c 0x3d 0x3e 0x3f
-                       0x40 0x41 0x42 0x43 0x44 0x45 0x46 0x47 0x48 0x49 0x4a 0x4b 0x4c 0x4d 0x4e 0x4f
-                       0x50 0x51 0x52 0x53 0x54 0x55 0x56 0x57 0x58 0x59 0x5a 0x5b 0x5c 0x5d 0x5e 0x5f
-                       0x60 0x61 0x62 0x63 0x64 0x65 0x66 0x67 0x68 0x69 0x6a 0x6b 0x6c 0x6d 0x6e 0x6f
-                       0x70 0x71 0x72 0x73 0x74 0x75 0x76 0x77 0x78 0x79 0x7a 0x7b 0x7c 0x7d 0x7e 0x7f
-                       0x80 0x81 0x82 0x83 0x84 0x85 0x86 0x87 0x88 0x89 0x8a 0x8b 0x8c 0x8d 0x8e 0x8f
-                       0x90 0x91 0x92 0x93 0x94 0x95 0x96 0x97 0x98 0x99 0x9a 0x9b 0x9c 0x9d 0x9e 0x9f
-                       0xa0 0xa1 0xa2 0xa3 0xa4 0xa5 0xa6 0xa7 0xa8 0xa9 0xaa 0xab 0xac 0xad 0xae 0xaf
-                       0xb0 0xb1 0xb2 0xb3 0xb4 0xb5 0xb6 0xb7 0xb8 0xb9 0xba 0xbb 0xbc 0xbd 0xbe 0xbf
-                       0xc0 0xc1 0xc2 0xc3 0xc4 0xc5 0xc6 0xc7 0xc8 0xc9 0xca 0xcb 0xcc 0xcd 0xce 0xcf
-                       0xd0 0xd1 0xd2 0xd3 0xd4 0xd5 0xd6 0xd7 0xd8 0xd9 0xda 0xdb 0xdc 0xdd 0xde 0xdf
-                       0xe0 0xe1 0xe2 0xe3 0xe4 0xe5 0xe6 0xe7 0xe8 0xe9 0xea 0xeb 0xec 0xed 0xee 0xef
-                       0xf0 0xf1 0xf2 0xf3 0xf4 0xf5 0xf6 0xf7 0xf8 0xf9 0xfa 0xfb 0xfc 0xfd 0xfe 0xff
-                    ))
-                    (define-private (crash-me-folder (input (buff 1)) (ctr uint))
-                        (begin
-                            (unwrap-panic (index-of BUFF_TO_BYTE input))
-                            (unwrap-panic (index-of BUFF_TO_BYTE input))
-                            (unwrap-panic (index-of BUFF_TO_BYTE input))
-                            (unwrap-panic (index-of BUFF_TO_BYTE input))
-                            (unwrap-panic (index-of BUFF_TO_BYTE input))
-                            (unwrap-panic (index-of BUFF_TO_BYTE input))
-                            (unwrap-panic (index-of BUFF_TO_BYTE input))
-                            (unwrap-panic (index-of BUFF_TO_BYTE input))
-                            (+ u1 ctr)
-                        )
-                    )
-                    (define-public (crash-me (name (string-ascii 128)))
-                        (begin
-                            (fold crash-me-folder BUFF_TO_BYTE u0)
-                            (print name)
-                            (ok u0)
-                        )
-                    )
-                    (begin
-                        (crash-me \"{}\"))
-                    ",
-                    &format!("large-contract-{}", &ix)
-                )
-            )
-        })
-        .collect();
-
     let (mut conf, miner_account) = neon_integration_test_conf();
 
     for spender_addr in spender_addrs.iter() {
@@ -1028,11 +983,68 @@ fn bigger_microblock_streams_in_2_05() {
     ]);
     conf.burnchain.pox_2_activation = Some(10_003);
 
+    let txs: Vec<Vec<_>> = spender_sks
+        .iter()
+        .enumerate()
+        .map(|(ix, spender_sk)| {
+            // almost fills a whole block
+            make_contract_publish_microblock_only(
+                spender_sk,
+                0,
+                1049230,
+                conf.burnchain.chain_id,
+                &format!("large-{}", ix),
+                &format!("
+                    ;; a single one of these transactions consumes over half the runtime budget
+                    (define-constant BUFF_TO_BYTE (list
+                       0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f
+                       0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17 0x18 0x19 0x1a 0x1b 0x1c 0x1d 0x1e 0x1f
+                       0x20 0x21 0x22 0x23 0x24 0x25 0x26 0x27 0x28 0x29 0x2a 0x2b 0x2c 0x2d 0x2e 0x2f
+                       0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x3a 0x3b 0x3c 0x3d 0x3e 0x3f
+                       0x40 0x41 0x42 0x43 0x44 0x45 0x46 0x47 0x48 0x49 0x4a 0x4b 0x4c 0x4d 0x4e 0x4f
+                       0x50 0x51 0x52 0x53 0x54 0x55 0x56 0x57 0x58 0x59 0x5a 0x5b 0x5c 0x5d 0x5e 0x5f
+                       0x60 0x61 0x62 0x63 0x64 0x65 0x66 0x67 0x68 0x69 0x6a 0x6b 0x6c 0x6d 0x6e 0x6f
+                       0x70 0x71 0x72 0x73 0x74 0x75 0x76 0x77 0x78 0x79 0x7a 0x7b 0x7c 0x7d 0x7e 0x7f
+                       0x80 0x81 0x82 0x83 0x84 0x85 0x86 0x87 0x88 0x89 0x8a 0x8b 0x8c 0x8d 0x8e 0x8f
+                       0x90 0x91 0x92 0x93 0x94 0x95 0x96 0x97 0x98 0x99 0x9a 0x9b 0x9c 0x9d 0x9e 0x9f
+                       0xa0 0xa1 0xa2 0xa3 0xa4 0xa5 0xa6 0xa7 0xa8 0xa9 0xaa 0xab 0xac 0xad 0xae 0xaf
+                       0xb0 0xb1 0xb2 0xb3 0xb4 0xb5 0xb6 0xb7 0xb8 0xb9 0xba 0xbb 0xbc 0xbd 0xbe 0xbf
+                       0xc0 0xc1 0xc2 0xc3 0xc4 0xc5 0xc6 0xc7 0xc8 0xc9 0xca 0xcb 0xcc 0xcd 0xce 0xcf
+                       0xd0 0xd1 0xd2 0xd3 0xd4 0xd5 0xd6 0xd7 0xd8 0xd9 0xda 0xdb 0xdc 0xdd 0xde 0xdf
+                       0xe0 0xe1 0xe2 0xe3 0xe4 0xe5 0xe6 0xe7 0xe8 0xe9 0xea 0xeb 0xec 0xed 0xee 0xef
+                       0xf0 0xf1 0xf2 0xf3 0xf4 0xf5 0xf6 0xf7 0xf8 0xf9 0xfa 0xfb 0xfc 0xfd 0xfe 0xff
+                    ))
+                    (define-private (crash-me-folder (input (buff 1)) (ctr uint))
+                        (begin
+                            (unwrap-panic (index-of BUFF_TO_BYTE input))
+                            (unwrap-panic (index-of BUFF_TO_BYTE input))
+                            (unwrap-panic (index-of BUFF_TO_BYTE input))
+                            (unwrap-panic (index-of BUFF_TO_BYTE input))
+                            (unwrap-panic (index-of BUFF_TO_BYTE input))
+                            (unwrap-panic (index-of BUFF_TO_BYTE input))
+                            (unwrap-panic (index-of BUFF_TO_BYTE input))
+                            (unwrap-panic (index-of BUFF_TO_BYTE input))
+                            (+ u1 ctr)
+                        )
+                    )
+                    (define-public (crash-me (name (string-ascii 128)))
+                        (begin
+                            (fold crash-me-folder BUFF_TO_BYTE u0)
+                            (print name)
+                            (ok u0)
+                        )
+                    )
+                    (begin
+                        (crash-me \"{}\"))
+                    ",
+                    &format!("large-contract-{}", &ix)
+                )
+            )
+        })
+        .collect();
+
     test_observer::spawn();
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
diff --git a/testnet/stacks-node/src/tests/epoch_21.rs b/testnet/stacks-node/src/tests/epoch_21.rs
index 2f74ffa770..8f6c466318 100644
--- a/testnet/stacks-node/src/tests/epoch_21.rs
+++ b/testnet/stacks-node/src/tests/epoch_21.rs
@@ -35,7 +35,7 @@ use stacks_common::util::secp256k1::{Secp256k1PrivateKey, Secp256k1PublicKey};
 use stacks_common::util::sleep_ms;
 
 use crate::burnchains::bitcoin_regtest_controller::UTXO;
-use crate::config::{Config, EventKeyType, EventObserverConfig, InitialBalance};
+use crate::config::{Config, InitialBalance};
 use crate::neon::RunLoopCounter;
 use crate::operations::BurnchainOpSigner;
 use crate::stacks_common::address::AddressHashMode;
@@ -71,11 +71,7 @@ fn advance_to_2_1(
     conf.burnchain.peer_host = "localhost".to_string();
     conf.initial_balances.append(&mut initial_balances);
     conf.miner.block_reward_recipient = block_reward_recipient;
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut epochs = core::STACKS_EPOCHS_REGTEST.to_vec();
     epochs[1].end_height = epoch_2_05;
@@ -364,6 +360,7 @@ fn transition_adds_burn_block_height() {
         &spender_sk,
         0,
         (2 * contract.len()) as u64,
+        conf.burnchain.chain_id,
         "test-burn-headers",
         contract,
     );
@@ -377,6 +374,7 @@ fn transition_adds_burn_block_height() {
         &spender_sk,
         1,
         (2 * contract.len()) as u64,
+        conf.burnchain.chain_id,
         &spender_addr_c32,
         "test-burn-headers",
         "test-burn-headers",
@@ -576,10 +574,7 @@ fn transition_fixes_bitcoin_rigidity() {
     ];
 
     conf.initial_balances.append(&mut initial_balances);
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut epochs = core::STACKS_EPOCHS_REGTEST.to_vec();
     epochs[1].end_height = epoch_2_05;
@@ -1114,6 +1109,7 @@ fn transition_adds_get_pox_addr_recipients() {
             &spender_sk,
             0,
             300,
+            conf.burnchain.chain_id,
             &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
             "pox-2",
             "stack-stx",
@@ -1154,6 +1150,7 @@ fn transition_adds_get_pox_addr_recipients() {
             &spender_sk,
             0,
             300,
+            conf.burnchain.chain_id,
             &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
             "pox-2",
             "stack-stx",
@@ -1191,6 +1188,7 @@ fn transition_adds_get_pox_addr_recipients() {
         &spender_sks[0],
         1,
         (2 * contract.len()) as u64,
+        conf.burnchain.chain_id,
         "test-get-pox-addrs",
         contract,
     );
@@ -1216,6 +1214,7 @@ fn transition_adds_get_pox_addr_recipients() {
         &spender_sks[0],
         2,
         (2 * contract.len()) as u64,
+        conf.burnchain.chain_id,
         &spender_addr_c32,
         "test-get-pox-addrs",
         "test-get-pox-addrs",
@@ -1472,11 +1471,7 @@ fn transition_removes_pox_sunset() {
     let (mut conf, miner_account) = neon_integration_test_conf();
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     conf.initial_balances.push(InitialBalance {
         address: spender_addr.clone(),
@@ -1589,6 +1584,7 @@ fn transition_removes_pox_sunset() {
         &spender_sk,
         0,
         260,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox",
         "stack-stx",
@@ -1651,6 +1647,7 @@ fn transition_removes_pox_sunset() {
         &spender_sk,
         1,
         260 * 2,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-2",
         "stack-stx",
@@ -1787,11 +1784,7 @@ fn transition_empty_blocks() {
     conf.burnchain.epochs = Some(epochs);
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let keychain = Keychain::default(conf.node.seed.clone());
     let http_origin = format!("http://{}", &conf.node.rpc_bind);
@@ -2288,6 +2281,7 @@ fn test_pox_reorgs_three_flaps() {
                 pk,
                 0,
                 1360,
+                conf_template.burnchain.chain_id,
                 &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
                 "pox-2",
                 "stack-stx",
@@ -2311,7 +2305,9 @@ fn test_pox_reorgs_three_flaps() {
     let all_txs: Vec<_> = privks
         .iter()
         .enumerate()
-        .map(|(i, pk)| make_random_tx_chain(pk, (25 * i) as u64, false))
+        .map(|(i, pk)| {
+            make_random_tx_chain(pk, (25 * i) as u64, conf_template.burnchain.chain_id, false)
+        })
         .collect();
 
     // everyone locks up
@@ -2825,6 +2821,7 @@ fn test_pox_reorg_one_flap() {
                 pk,
                 0,
                 1360,
+                conf_template.burnchain.chain_id,
                 &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
                 "pox-2",
                 "stack-stx",
@@ -2848,7 +2845,9 @@ fn test_pox_reorg_one_flap() {
     let all_txs: Vec<_> = privks
         .iter()
         .enumerate()
-        .map(|(i, pk)| make_random_tx_chain(pk, (25 * i) as u64, false))
+        .map(|(i, pk)| {
+            make_random_tx_chain(pk, (25 * i) as u64, conf_template.burnchain.chain_id, false)
+        })
         .collect();
 
     // everyone locks up
@@ -3250,6 +3249,7 @@ fn test_pox_reorg_flap_duel() {
                 pk,
                 0,
                 1360,
+                conf_template.burnchain.chain_id,
                 &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
                 "pox-2",
                 "stack-stx",
@@ -3273,7 +3273,9 @@ fn test_pox_reorg_flap_duel() {
     let all_txs: Vec<_> = privks
         .iter()
         .enumerate()
-        .map(|(i, pk)| make_random_tx_chain(pk, (25 * i) as u64, false))
+        .map(|(i, pk)| {
+            make_random_tx_chain(pk, (25 * i) as u64, conf_template.burnchain.chain_id, false)
+        })
         .collect();
 
     // everyone locks up
@@ -3685,6 +3687,7 @@ fn test_pox_reorg_flap_reward_cycles() {
                 pk,
                 0,
                 1360,
+                conf_template.burnchain.chain_id,
                 &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
                 "pox-2",
                 "stack-stx",
@@ -3708,7 +3711,9 @@ fn test_pox_reorg_flap_reward_cycles() {
     let all_txs: Vec<_> = privks
         .iter()
         .enumerate()
-        .map(|(i, pk)| make_random_tx_chain(pk, (25 * i) as u64, false))
+        .map(|(i, pk)| {
+            make_random_tx_chain(pk, (25 * i) as u64, conf_template.burnchain.chain_id, false)
+        })
         .collect();
 
     // everyone locks up
@@ -4114,6 +4119,7 @@ fn test_pox_missing_five_anchor_blocks() {
                 pk,
                 0,
                 1360,
+                conf_template.burnchain.chain_id,
                 &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
                 "pox-2",
                 "stack-stx",
@@ -4137,7 +4143,9 @@ fn test_pox_missing_five_anchor_blocks() {
     let all_txs: Vec<_> = privks
         .iter()
         .enumerate()
-        .map(|(i, pk)| make_random_tx_chain(pk, (25 * i) as u64, false))
+        .map(|(i, pk)| {
+            make_random_tx_chain(pk, (25 * i) as u64, conf_template.burnchain.chain_id, false)
+        })
         .collect();
 
     // everyone locks up
@@ -4515,6 +4523,7 @@ fn test_sortition_divergence_pre_21() {
                 pk,
                 0,
                 1360,
+                conf_template.burnchain.chain_id,
                 &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
                 "pox",
                 "stack-stx",
@@ -4538,7 +4547,9 @@ fn test_sortition_divergence_pre_21() {
     let all_txs: Vec<_> = privks
         .iter()
         .enumerate()
-        .map(|(i, pk)| make_random_tx_chain(pk, (25 * i) as u64, false))
+        .map(|(i, pk)| {
+            make_random_tx_chain(pk, (25 * i) as u64, conf_template.burnchain.chain_id, false)
+        })
         .collect();
 
     // everyone locks up
@@ -4737,10 +4748,7 @@ fn trait_invocation_cross_epoch() {
         amount: 200_000_000,
     }];
     conf.initial_balances.append(&mut initial_balances);
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
     let mut epochs = core::STACKS_EPOCHS_REGTEST.to_vec();
     epochs[1].end_height = epoch_2_05;
     epochs[2].start_height = epoch_2_05;
@@ -4816,13 +4824,34 @@ fn trait_invocation_cross_epoch() {
     let tip_info = get_chain_info(&conf);
     assert_eq!(tip_info.burn_block_height, epoch_2_05 + 1);
 
-    let tx = make_contract_publish(&spender_sk, 0, 10_000, "simple-trait", trait_contract);
+    let tx = make_contract_publish(
+        &spender_sk,
+        0,
+        10_000,
+        conf.burnchain.chain_id,
+        "simple-trait",
+        trait_contract,
+    );
     let trait_txid = submit_tx(&http_origin, &tx);
 
-    let tx = make_contract_publish(&spender_sk, 1, 10_000, "impl-simple", impl_contract);
+    let tx = make_contract_publish(
+        &spender_sk,
+        1,
+        10_000,
+        conf.burnchain.chain_id,
+        "impl-simple",
+        impl_contract,
+    );
     let impl_txid = submit_tx(&http_origin, &tx);
 
-    let tx = make_contract_publish(&spender_sk, 2, 10_000, "use-simple", use_contract);
+    let tx = make_contract_publish(
+        &spender_sk,
+        2,
+        10_000,
+        conf.burnchain.chain_id,
+        "use-simple",
+        use_contract,
+    );
     let use_txid = submit_tx(&http_origin, &tx);
 
     // mine the transactions and advance to epoch 2.1
@@ -4833,7 +4862,14 @@ fn trait_invocation_cross_epoch() {
     let tip_info = get_chain_info(&conf);
     assert_eq!(tip_info.burn_block_height, epoch_2_1 + 1);
 
-    let tx = make_contract_publish(&spender_sk, 3, 10_000, "invoke-simple", invoke_contract);
+    let tx = make_contract_publish(
+        &spender_sk,
+        3,
+        10_000,
+        conf.burnchain.chain_id,
+        "invoke-simple",
+        invoke_contract,
+    );
     let invoke_txid = submit_tx(&http_origin, &tx);
 
     for _ in 0..2 {
@@ -4844,6 +4880,7 @@ fn trait_invocation_cross_epoch() {
         &spender_sk,
         4,
         10_000,
+        conf.burnchain.chain_id,
         &spender_addr_c32,
         "invoke-simple",
         "invocation-1",
@@ -4855,6 +4892,7 @@ fn trait_invocation_cross_epoch() {
         &spender_sk,
         5,
         10_000,
+        conf.burnchain.chain_id,
         &spender_addr_c32,
         "invoke-simple",
         "invocation-2",
@@ -4982,11 +5020,7 @@ fn test_v1_unlock_height_with_current_stackers() {
     conf.miner.subsequent_attempt_time_ms = i64::MAX as u64;
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
     conf.initial_balances.append(&mut initial_balances);
 
     let mut epochs = core::STACKS_EPOCHS_REGTEST.to_vec();
@@ -5064,6 +5098,7 @@ fn test_v1_unlock_height_with_current_stackers() {
         &spender_sk,
         0,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox",
         "stack-stx",
@@ -5100,6 +5135,7 @@ fn test_v1_unlock_height_with_current_stackers() {
         &spender_sk,
         1,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-2",
         "stack-stx",
@@ -5247,11 +5283,7 @@ fn test_v1_unlock_height_with_delay_and_current_stackers() {
     conf.miner.subsequent_attempt_time_ms = i64::MAX as u64;
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
     conf.initial_balances.append(&mut initial_balances);
 
     let mut epochs = core::STACKS_EPOCHS_REGTEST.to_vec();
@@ -5332,6 +5364,7 @@ fn test_v1_unlock_height_with_delay_and_current_stackers() {
         &spender_sk,
         0,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox",
         "stack-stx",
@@ -5380,6 +5413,7 @@ fn test_v1_unlock_height_with_delay_and_current_stackers() {
         &spender_sk,
         1,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-2",
         "stack-stx",
diff --git a/testnet/stacks-node/src/tests/epoch_22.rs b/testnet/stacks-node/src/tests/epoch_22.rs
index 289d09be64..9bffca7c8a 100644
--- a/testnet/stacks-node/src/tests/epoch_22.rs
+++ b/testnet/stacks-node/src/tests/epoch_22.rs
@@ -134,11 +134,7 @@ fn disable_pox() {
     conf.miner.subsequent_attempt_time_ms = i64::MAX as u64;
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
     conf.initial_balances.append(&mut initial_balances);
 
     let mut epochs = core::STACKS_EPOCHS_REGTEST.to_vec();
@@ -231,6 +227,7 @@ fn disable_pox() {
         &spender_sk,
         0,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox",
         "stack-stx",
@@ -279,6 +276,7 @@ fn disable_pox() {
         &spender_sk,
         1,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-2",
         "stack-stx",
@@ -297,6 +295,7 @@ fn disable_pox() {
         &spender_2_sk,
         0,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-2",
         "stack-stx",
@@ -328,6 +327,7 @@ fn disable_pox() {
         &spender_sk,
         2,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-2",
         "stack-increase",
@@ -354,6 +354,7 @@ fn disable_pox() {
         &spender_sk,
         aborted_increase_nonce,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-2",
         "stack-increase",
@@ -671,6 +672,7 @@ fn pox_2_unlock_all() {
     conf.events_observers.insert(EventObserverConfig {
         endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
         events_keys: vec![EventKeyType::AnyEvent],
+        timeout_ms: 1000,
     });
     conf.initial_balances.append(&mut initial_balances);
 
@@ -764,6 +766,7 @@ fn pox_2_unlock_all() {
         &spender_sk,
         0,
         tx_fee,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox",
         "stack-stx",
@@ -814,6 +817,7 @@ fn pox_2_unlock_all() {
         &spender_sk,
         1,
         tx_fee,
+        conf.burnchain.chain_id,
         "unlock-height",
         "(define-public (unlock-height (x principal)) (ok (get unlock-height (stx-account x))))",
     );
@@ -823,6 +827,7 @@ fn pox_2_unlock_all() {
         &spender_sk,
         2,
         tx_fee,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-2",
         "stack-stx",
@@ -842,6 +847,7 @@ fn pox_2_unlock_all() {
         &spender_2_sk,
         0,
         tx_fee,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-2",
         "stack-stx",
@@ -872,6 +878,7 @@ fn pox_2_unlock_all() {
         &spender_sk,
         3,
         tx_fee,
+        conf.burnchain.chain_id,
         &to_addr(&spender_sk),
         "unlock-height",
         "unlock-height",
@@ -891,6 +898,7 @@ fn pox_2_unlock_all() {
         &spender_sk,
         4,
         tx_fee,
+        conf.burnchain.chain_id,
         &to_addr(&spender_sk),
         "unlock-height",
         "unlock-height",
@@ -980,7 +988,14 @@ fn pox_2_unlock_all() {
     );
 
     // perform a transfer
-    let tx = make_stacks_transfer(&spender_sk, 5, tx_fee, &spender_3_addr, 1_000_000);
+    let tx = make_stacks_transfer(
+        &spender_sk,
+        5,
+        tx_fee,
+        conf.burnchain.chain_id,
+        &spender_3_addr,
+        1_000_000,
+    );
 
     info!("Submit stack transfer tx to {:?}", &http_origin);
     submit_tx(&http_origin, &tx);
@@ -1511,6 +1526,7 @@ fn test_pox_reorg_one_flap() {
                 pk,
                 0,
                 1360,
+                conf_template.burnchain.chain_id,
                 &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
                 "pox-2",
                 "stack-stx",
@@ -1534,7 +1550,9 @@ fn test_pox_reorg_one_flap() {
     let all_txs: Vec<_> = privks
         .iter()
         .enumerate()
-        .map(|(i, pk)| make_random_tx_chain(pk, (25 * i) as u64, false))
+        .map(|(i, pk)| {
+            make_random_tx_chain(pk, (25 * i) as u64, conf_template.burnchain.chain_id, false)
+        })
         .collect();
 
     // everyone locks up
diff --git a/testnet/stacks-node/src/tests/epoch_23.rs b/testnet/stacks-node/src/tests/epoch_23.rs
index 0452be8476..2355f7521d 100644
--- a/testnet/stacks-node/src/tests/epoch_23.rs
+++ b/testnet/stacks-node/src/tests/epoch_23.rs
@@ -22,7 +22,7 @@ use stacks::core;
 use stacks::core::STACKS_EPOCH_MAX;
 use stacks_common::util::sleep_ms;
 
-use crate::config::{EventKeyType, EventObserverConfig, InitialBalance};
+use crate::config::InitialBalance;
 use crate::tests::bitcoin_regtest::BitcoinCoreController;
 use crate::tests::neon_integrations::*;
 use crate::tests::*;
@@ -100,11 +100,7 @@ fn trait_invocation_behavior() {
     conf.miner.subsequent_attempt_time_ms = i64::MAX as u64;
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
     conf.initial_balances.append(&mut initial_balances);
 
     let mut epochs = core::STACKS_EPOCHS_REGTEST.to_vec();
@@ -184,6 +180,7 @@ fn trait_invocation_behavior() {
         &spender_sk,
         spender_nonce,
         fee_amount,
+        conf.burnchain.chain_id,
         "simple-trait",
         trait_contract,
     );
@@ -194,6 +191,7 @@ fn trait_invocation_behavior() {
         &spender_sk,
         spender_nonce,
         fee_amount,
+        conf.burnchain.chain_id,
         "impl-simple",
         impl_contract,
     );
@@ -204,6 +202,7 @@ fn trait_invocation_behavior() {
         &spender_sk,
         spender_nonce,
         fee_amount,
+        conf.burnchain.chain_id,
         "use-simple",
         use_contract,
     );
@@ -214,6 +213,7 @@ fn trait_invocation_behavior() {
         &spender_sk,
         spender_nonce,
         fee_amount,
+        conf.burnchain.chain_id,
         "invoke-simple",
         invoke_contract,
     );
@@ -245,6 +245,7 @@ fn trait_invocation_behavior() {
         &spender_sk,
         spender_nonce,
         fee_amount,
+        conf.burnchain.chain_id,
         &contract_addr,
         "invoke-simple",
         "invocation-1",
@@ -257,6 +258,7 @@ fn trait_invocation_behavior() {
         &spender_sk,
         spender_nonce,
         fee_amount,
+        conf.burnchain.chain_id,
         &contract_addr,
         "invoke-simple",
         "invocation-2",
@@ -278,6 +280,7 @@ fn trait_invocation_behavior() {
         &spender_sk,
         spender_nonce,
         fee_amount,
+        conf.burnchain.chain_id,
         &contract_addr,
         "invoke-simple",
         "invocation-1",
@@ -290,6 +293,7 @@ fn trait_invocation_behavior() {
         &spender_sk,
         spender_nonce,
         fee_amount,
+        conf.burnchain.chain_id,
         &contract_addr,
         "invoke-simple",
         "invocation-2",
@@ -316,6 +320,7 @@ fn trait_invocation_behavior() {
         &spender_sk,
         spender_nonce,
         fee_amount,
+        conf.burnchain.chain_id,
         &contract_addr,
         "invoke-simple",
         "invocation-1",
@@ -328,6 +333,7 @@ fn trait_invocation_behavior() {
         &spender_sk,
         spender_nonce,
         fee_amount,
+        conf.burnchain.chain_id,
         &contract_addr,
         "invoke-simple",
         "invocation-2",
@@ -348,6 +354,7 @@ fn trait_invocation_behavior() {
         &spender_sk,
         spender_nonce,
         fee_amount,
+        conf.burnchain.chain_id,
         "wrap-simple",
         wrapper_contract,
     );
@@ -363,6 +370,7 @@ fn trait_invocation_behavior() {
         &spender_sk,
         spender_nonce,
         fee_amount,
+        conf.burnchain.chain_id,
         &contract_addr,
         "wrap-simple",
         "invocation-1",
@@ -375,6 +383,7 @@ fn trait_invocation_behavior() {
         &spender_sk,
         spender_nonce,
         fee_amount,
+        conf.burnchain.chain_id,
         &contract_addr,
         "wrap-simple",
         "invocation-2",
@@ -401,6 +410,7 @@ fn trait_invocation_behavior() {
         &spender_sk,
         spender_nonce,
         fee_amount,
+        conf.burnchain.chain_id,
         &contract_addr,
         "wrap-simple",
         "invocation-1",
@@ -413,6 +423,7 @@ fn trait_invocation_behavior() {
         &spender_sk,
         spender_nonce,
         fee_amount,
+        conf.burnchain.chain_id,
         &contract_addr,
         "wrap-simple",
         "invocation-2",
@@ -435,6 +446,7 @@ fn trait_invocation_behavior() {
         &spender_sk,
         spender_nonce,
         fee_amount,
+        conf.burnchain.chain_id,
         &contract_addr,
         "wrap-simple",
         "invocation-1",
@@ -447,6 +459,7 @@ fn trait_invocation_behavior() {
         &spender_sk,
         spender_nonce,
         fee_amount,
+        conf.burnchain.chain_id,
         &contract_addr,
         "wrap-simple",
         "invocation-2",
@@ -468,6 +481,7 @@ fn trait_invocation_behavior() {
         &spender_sk,
         spender_nonce,
         fee_amount,
+        conf.burnchain.chain_id,
         &contract_addr,
         "invoke-simple",
         "invocation-1",
@@ -480,6 +494,7 @@ fn trait_invocation_behavior() {
         &spender_sk,
         spender_nonce,
         fee_amount,
+        conf.burnchain.chain_id,
         &contract_addr,
         "invoke-simple",
         "invocation-2",
diff --git a/testnet/stacks-node/src/tests/epoch_24.rs b/testnet/stacks-node/src/tests/epoch_24.rs
index 3fc3b3d590..26ad007ca7 100644
--- a/testnet/stacks-node/src/tests/epoch_24.rs
+++ b/testnet/stacks-node/src/tests/epoch_24.rs
@@ -35,7 +35,7 @@ use stacks_common::util::hash::{bytes_to_hex, hex_bytes, Hash160};
 use stacks_common::util::secp256k1::Secp256k1PublicKey;
 use stacks_common::util::sleep_ms;
 
-use crate::config::{EventKeyType, EventObserverConfig, InitialBalance};
+use crate::config::InitialBalance;
 use crate::stacks_common::codec::StacksMessageCodec;
 use crate::tests::bitcoin_regtest::BitcoinCoreController;
 use crate::tests::neon_integrations::{
@@ -152,11 +152,7 @@ fn fix_to_pox_contract() {
     conf.miner.subsequent_attempt_time_ms = i64::MAX as u64;
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
     conf.initial_balances.append(&mut initial_balances);
 
     let mut epochs = core::STACKS_EPOCHS_REGTEST.to_vec();
@@ -253,6 +249,7 @@ fn fix_to_pox_contract() {
         &spender_sk,
         0,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox",
         "stack-stx",
@@ -302,6 +299,7 @@ fn fix_to_pox_contract() {
         &spender_sk,
         1,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-2",
         "stack-stx",
@@ -336,6 +334,7 @@ fn fix_to_pox_contract() {
         &spender_sk,
         aborted_increase_nonce_2_2,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-2",
         "stack-increase",
@@ -361,6 +360,7 @@ fn fix_to_pox_contract() {
         &spender_sk,
         aborted_increase_nonce_2_3,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-2",
         "stack-increase",
@@ -399,6 +399,7 @@ fn fix_to_pox_contract() {
         &spender_sk,
         4,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-3",
         "stack-stx",
@@ -418,6 +419,7 @@ fn fix_to_pox_contract() {
         &spender_2_sk,
         0,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-3",
         "stack-stx",
@@ -449,6 +451,7 @@ fn fix_to_pox_contract() {
         &spender_sk,
         5,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-3",
         "stack-increase",
@@ -791,11 +794,7 @@ fn verify_auto_unlock_behavior() {
     conf.miner.subsequent_attempt_time_ms = i64::MAX as u64;
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
     conf.initial_balances.append(&mut initial_balances);
 
     let mut epochs = core::STACKS_EPOCHS_REGTEST.to_vec();
@@ -897,6 +896,7 @@ fn verify_auto_unlock_behavior() {
         &spender_sk,
         0,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox",
         "stack-stx",
@@ -946,6 +946,7 @@ fn verify_auto_unlock_behavior() {
         &spender_sk,
         1,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-2",
         "stack-stx",
@@ -1031,6 +1032,7 @@ fn verify_auto_unlock_behavior() {
         &spender_sk,
         2,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-3",
         "stack-stx",
@@ -1050,6 +1052,7 @@ fn verify_auto_unlock_behavior() {
         &spender_2_sk,
         0,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-3",
         "stack-stx",
@@ -1131,6 +1134,7 @@ fn verify_auto_unlock_behavior() {
         &spender_sk,
         3,
         3000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-3",
         "stack-increase",
diff --git a/testnet/stacks-node/src/tests/epoch_25.rs b/testnet/stacks-node/src/tests/epoch_25.rs
index 5a45b35e86..6af1bee626 100644
--- a/testnet/stacks-node/src/tests/epoch_25.rs
+++ b/testnet/stacks-node/src/tests/epoch_25.rs
@@ -21,7 +21,7 @@ use stacks::core;
 use stacks_common::consts::STACKS_EPOCH_MAX;
 use stacks_common::types::chainstate::StacksPrivateKey;
 
-use crate::config::{EventKeyType, EventObserverConfig, InitialBalance};
+use crate::config::InitialBalance;
 use crate::tests::bitcoin_regtest::BitcoinCoreController;
 use crate::tests::nakamoto_integrations::wait_for;
 use crate::tests::neon_integrations::{
@@ -83,11 +83,7 @@ fn microblocks_disabled() {
     conf.miner.subsequent_attempt_time_ms = i64::max_value() as u64;
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
     conf.initial_balances.append(&mut initial_balances);
 
     let mut epochs = core::STACKS_EPOCHS_REGTEST.to_vec();
@@ -166,7 +162,14 @@ fn microblocks_disabled() {
     // push us to block 205
     next_block_and_wait(&mut btc_regtest_controller, &blocks_processed);
 
-    let tx = make_stacks_transfer_mblock_only(&spender_1_sk, 0, 500, &spender_2_addr, 500);
+    let tx = make_stacks_transfer_mblock_only(
+        &spender_1_sk,
+        0,
+        500,
+        conf.burnchain.chain_id,
+        &spender_2_addr,
+        500,
+    );
     submit_tx(&http_origin, &tx);
 
     // wait until just before epoch 2.5
@@ -198,7 +201,14 @@ fn microblocks_disabled() {
     );
     assert_eq!(account.nonce, 1);
 
-    let tx = make_stacks_transfer_mblock_only(&spender_1_sk, 1, 500, &spender_2_addr, 500);
+    let tx = make_stacks_transfer_mblock_only(
+        &spender_1_sk,
+        1,
+        500,
+        conf.burnchain.chain_id,
+        &spender_2_addr,
+        500,
+    );
     submit_tx(&http_origin, &tx);
 
     let mut last_block_height = get_chain_info(&conf).burn_block_height;
diff --git a/testnet/stacks-node/src/tests/integrations.rs b/testnet/stacks-node/src/tests/integrations.rs
index dca0c7e250..4c573340d5 100644
--- a/testnet/stacks-node/src/tests/integrations.rs
+++ b/testnet/stacks-node/src/tests/integrations.rs
@@ -25,7 +25,7 @@ use stacks::clarity_vm::clarity::ClarityConnection;
 use stacks::codec::StacksMessageCodec;
 use stacks::core::mempool::MAXIMUM_MEMPOOL_TX_CHAINING;
 use stacks::core::{
-    StacksEpoch, StacksEpochId, PEER_VERSION_EPOCH_2_0, PEER_VERSION_EPOCH_2_05,
+    StacksEpoch, StacksEpochId, CHAIN_ID_TESTNET, PEER_VERSION_EPOCH_2_0, PEER_VERSION_EPOCH_2_05,
     PEER_VERSION_EPOCH_2_1,
 };
 use stacks::net::api::callreadonly::CallReadOnlyRequestBody;
@@ -211,8 +211,14 @@ fn integration_test_get_info() {
             if round == 1 {
                 // block-height = 2
                 eprintln!("Tenure in 1 started!");
-                let publish_tx =
-                    make_contract_publish(&contract_sk, 0, 10, "get-info", GET_INFO_CONTRACT);
+                let publish_tx = make_contract_publish(
+                    &contract_sk,
+                    0,
+                    10,
+                    CHAIN_ID_TESTNET,
+                    "get-info",
+                    GET_INFO_CONTRACT,
+                );
                 tenure
                     .mem_pool
                     .submit_raw(
@@ -225,8 +231,14 @@ fn integration_test_get_info() {
                         &StacksEpochId::Epoch21,
                     )
                     .unwrap();
-                let publish_tx =
-                    make_contract_publish(&contract_sk, 1, 10, "other", OTHER_CONTRACT);
+                let publish_tx = make_contract_publish(
+                    &contract_sk,
+                    1,
+                    10,
+                    CHAIN_ID_TESTNET,
+                    "other",
+                    OTHER_CONTRACT,
+                );
                 tenure
                     .mem_pool
                     .submit_raw(
@@ -239,8 +251,14 @@ fn integration_test_get_info() {
                         &StacksEpochId::Epoch21,
                     )
                     .unwrap();
-                let publish_tx =
-                    make_contract_publish(&contract_sk, 2, 10, "main", CALL_READ_CONTRACT);
+                let publish_tx = make_contract_publish(
+                    &contract_sk,
+                    2,
+                    10,
+                    CHAIN_ID_TESTNET,
+                    "main",
+                    CALL_READ_CONTRACT,
+                );
                 tenure
                     .mem_pool
                     .submit_raw(
@@ -272,6 +290,7 @@ fn integration_test_get_info() {
                     &contract_sk,
                     3,
                     10,
+                    CHAIN_ID_TESTNET,
                     "impl-trait-contract",
                     IMPL_TRAIT_CONTRACT,
                 );
@@ -294,6 +313,7 @@ fn integration_test_get_info() {
                     &principal_sk,
                     (round - 3).into(),
                     10,
+                    CHAIN_ID_TESTNET,
                     &to_addr(&contract_sk),
                     "get-info",
                     "update-info",
@@ -319,6 +339,7 @@ fn integration_test_get_info() {
                     &spender_sk,
                     (round - 1).into(),
                     10,
+                    CHAIN_ID_TESTNET,
                     &StacksAddress::from_string(ADDR_4).unwrap().into(),
                     100,
                 );
@@ -797,8 +818,13 @@ fn integration_test_get_info() {
                 eprintln!("Test: POST {} (valid)", path);
 
                 // tx_xfer is 180 bytes long
-                let tx_xfer = make_stacks_transfer(&spender_sk, round.into(), 200,
-                                                   &StacksAddress::from_string(ADDR_4).unwrap().into(), 123);
+                let tx_xfer = make_stacks_transfer(
+                    &spender_sk,
+                    round.into(),
+                    200,
+                    CHAIN_ID_TESTNET,
+                    &StacksAddress::from_string(ADDR_4).unwrap().into(),
+                    123);
 
                 let res: String = client.post(&path)
                     .header("Content-Type", "application/octet-stream")
@@ -829,7 +855,8 @@ fn integration_test_get_info() {
                 eprintln!("Test: POST {} (invalid)", path);
 
                 // tx_xfer_invalid is 180 bytes long
-                let tx_xfer_invalid = make_stacks_transfer(&spender_sk, (round + 30).into(), 200,     // bad nonce
+                // bad nonce
+                let tx_xfer_invalid = make_stacks_transfer(&spender_sk, (round + 30).into(), 200, CHAIN_ID_TESTNET,
                                                            &StacksAddress::from_string(ADDR_4).unwrap().into(), 456);
 
                 let tx_xfer_invalid_tx = StacksTransaction::consensus_deserialize(&mut &tx_xfer_invalid[..]).unwrap();
@@ -1114,8 +1141,14 @@ fn contract_stx_transfer() {
 
             if round == 1 {
                 // block-height = 2
-                let xfer_to_contract =
-                    make_stacks_transfer(&sk_3, 0, 10, &contract_identifier.into(), 1000);
+                let xfer_to_contract = make_stacks_transfer(
+                    &sk_3,
+                    0,
+                    10,
+                    CHAIN_ID_TESTNET,
+                    &contract_identifier.into(),
+                    1000,
+                );
                 tenure
                     .mem_pool
                     .submit_raw(
@@ -1130,8 +1163,14 @@ fn contract_stx_transfer() {
                     .unwrap();
             } else if round == 2 {
                 // block-height > 2
-                let publish_tx =
-                    make_contract_publish(&contract_sk, 0, 10, "faucet", FAUCET_CONTRACT);
+                let publish_tx = make_contract_publish(
+                    &contract_sk,
+                    0,
+                    10,
+                    CHAIN_ID_TESTNET,
+                    "faucet",
+                    FAUCET_CONTRACT,
+                );
                 tenure
                     .mem_pool
                     .submit_raw(
@@ -1146,8 +1185,14 @@ fn contract_stx_transfer() {
                     .unwrap();
             } else if round == 3 {
                 // try to publish again
-                let publish_tx =
-                    make_contract_publish(&contract_sk, 1, 10, "faucet", FAUCET_CONTRACT);
+                let publish_tx = make_contract_publish(
+                    &contract_sk,
+                    1,
+                    10,
+                    CHAIN_ID_TESTNET,
+                    "faucet",
+                    FAUCET_CONTRACT,
+                );
 
                 let (consensus_hash, block_hash) = (
                     &tenure.parent_block.metadata.consensus_hash,
@@ -1170,6 +1215,7 @@ fn contract_stx_transfer() {
                     &sk_2,
                     0,
                     10,
+                    CHAIN_ID_TESTNET,
                     &to_addr(&contract_sk),
                     "faucet",
                     "spout",
@@ -1194,6 +1240,7 @@ fn contract_stx_transfer() {
                         &sk_3,
                         1 + i,
                         200,
+                        CHAIN_ID_TESTNET,
                         &contract_identifier.clone().into(),
                         1000,
                     );
@@ -1215,8 +1262,14 @@ fn contract_stx_transfer() {
                         .unwrap();
                 }
                 // this one should fail because the nonce is already in the mempool
-                let xfer_to_contract =
-                    make_stacks_transfer(&sk_3, 3, 190, &contract_identifier.clone().into(), 1000);
+                let xfer_to_contract = make_stacks_transfer(
+                    &sk_3,
+                    3,
+                    190,
+                    CHAIN_ID_TESTNET,
+                    &contract_identifier.clone().into(),
+                    1000,
+                );
                 let xfer_to_contract =
                     StacksTransaction::consensus_deserialize(&mut &xfer_to_contract[..]).unwrap();
                 match tenure
@@ -1446,8 +1499,14 @@ fn mine_transactions_out_of_order() {
 
             if round == 1 {
                 // block-height = 2
-                let xfer_to_contract =
-                    make_stacks_transfer(&sk, 1, 10, &contract_identifier.into(), 1000);
+                let xfer_to_contract = make_stacks_transfer(
+                    &sk,
+                    1,
+                    10,
+                    CHAIN_ID_TESTNET,
+                    &contract_identifier.into(),
+                    1000,
+                );
                 tenure
                     .mem_pool
                     .submit_raw(
@@ -1462,7 +1521,8 @@ fn mine_transactions_out_of_order() {
                     .unwrap();
             } else if round == 2 {
                 // block-height > 2
-                let publish_tx = make_contract_publish(&sk, 2, 10, "faucet", FAUCET_CONTRACT);
+                let publish_tx =
+                    make_contract_publish(&sk, 2, 10, CHAIN_ID_TESTNET, "faucet", FAUCET_CONTRACT);
                 tenure
                     .mem_pool
                     .submit_raw(
@@ -1476,8 +1536,14 @@ fn mine_transactions_out_of_order() {
                     )
                     .unwrap();
             } else if round == 3 {
-                let xfer_to_contract =
-                    make_stacks_transfer(&sk, 3, 10, &contract_identifier.into(), 1000);
+                let xfer_to_contract = make_stacks_transfer(
+                    &sk,
+                    3,
+                    10,
+                    CHAIN_ID_TESTNET,
+                    &contract_identifier.into(),
+                    1000,
+                );
                 tenure
                     .mem_pool
                     .submit_raw(
@@ -1491,8 +1557,14 @@ fn mine_transactions_out_of_order() {
                     )
                     .unwrap();
             } else if round == 4 {
-                let xfer_to_contract =
-                    make_stacks_transfer(&sk, 0, 10, &contract_identifier.into(), 1000);
+                let xfer_to_contract = make_stacks_transfer(
+                    &sk,
+                    0,
+                    10,
+                    CHAIN_ID_TESTNET,
+                    &contract_identifier.into(),
+                    1000,
+                );
                 tenure
                     .mem_pool
                     .submit_raw(
@@ -1593,8 +1665,14 @@ fn mine_contract_twice() {
 
             if round == 1 {
                 // block-height = 2
-                let publish_tx =
-                    make_contract_publish(&contract_sk, 0, 10, "faucet", FAUCET_CONTRACT);
+                let publish_tx = make_contract_publish(
+                    &contract_sk,
+                    0,
+                    10,
+                    CHAIN_ID_TESTNET,
+                    "faucet",
+                    FAUCET_CONTRACT,
+                );
                 let (consensus_hash, block_hash) = (
                     &tenure.parent_block.metadata.consensus_hash,
                     &tenure.parent_block.metadata.anchored_header.block_hash(),
@@ -1691,8 +1769,14 @@ fn bad_contract_tx_rollback() {
 
             if round == 1 {
                 // block-height = 2
-                let xfer_to_contract =
-                    make_stacks_transfer(&sk_3, 0, 10, &contract_identifier.into(), 1000);
+                let xfer_to_contract = make_stacks_transfer(
+                    &sk_3,
+                    0,
+                    10,
+                    CHAIN_ID_TESTNET,
+                    &contract_identifier.into(),
+                    1000,
+                );
                 let (consensus_hash, block_hash) = (
                     &tenure.parent_block.metadata.consensus_hash,
                     &tenure.parent_block.metadata.anchored_header.block_hash(),
@@ -1711,7 +1795,8 @@ fn bad_contract_tx_rollback() {
                     .unwrap();
             } else if round == 2 {
                 // block-height = 3
-                let xfer_to_contract = make_stacks_transfer(&sk_3, 1, 10, &addr_2.into(), 1000);
+                let xfer_to_contract =
+                    make_stacks_transfer(&sk_3, 1, 10, CHAIN_ID_TESTNET, &addr_2.into(), 1000);
                 let (consensus_hash, block_hash) = (
                     &tenure.parent_block.metadata.consensus_hash,
                     &tenure.parent_block.metadata.anchored_header.block_hash(),
@@ -1730,7 +1815,8 @@ fn bad_contract_tx_rollback() {
                     .unwrap();
 
                 // doesn't consistently get mined by the StacksBlockBuilder, because order matters!
-                let xfer_to_contract = make_stacks_transfer(&sk_3, 2, 10, &addr_2.into(), 3000);
+                let xfer_to_contract =
+                    make_stacks_transfer(&sk_3, 2, 10, CHAIN_ID_TESTNET, &addr_2.into(), 3000);
                 tenure
                     .mem_pool
                     .submit_raw(
@@ -1744,8 +1830,14 @@ fn bad_contract_tx_rollback() {
                     )
                     .unwrap();
 
-                let publish_tx =
-                    make_contract_publish(&contract_sk, 0, 10, "faucet", FAUCET_CONTRACT);
+                let publish_tx = make_contract_publish(
+                    &contract_sk,
+                    0,
+                    10,
+                    CHAIN_ID_TESTNET,
+                    "faucet",
+                    FAUCET_CONTRACT,
+                );
                 tenure
                     .mem_pool
                     .submit_raw(
@@ -1759,8 +1851,14 @@ fn bad_contract_tx_rollback() {
                     )
                     .unwrap();
 
-                let publish_tx =
-                    make_contract_publish(&contract_sk, 1, 10, "faucet", FAUCET_CONTRACT);
+                let publish_tx = make_contract_publish(
+                    &contract_sk,
+                    1,
+                    10,
+                    CHAIN_ID_TESTNET,
+                    "faucet",
+                    FAUCET_CONTRACT,
+                );
                 tenure
                     .mem_pool
                     .submit_raw(
@@ -2014,6 +2112,7 @@ fn block_limit_runtime_test() {
                     &contract_sk,
                     0,
                     10,
+                    CHAIN_ID_TESTNET,
                     "hello-contract",
                     EXPENSIVE_CONTRACT.as_str(),
                 );
@@ -2042,6 +2141,7 @@ fn block_limit_runtime_test() {
                         sk,
                         0,
                         10,
+                        CHAIN_ID_TESTNET,
                         &to_addr(&contract_sk),
                         "hello-contract",
                         "do-it",
@@ -2132,8 +2232,14 @@ fn mempool_errors() {
 
             if round == 1 {
                 // block-height = 2
-                let publish_tx =
-                    make_contract_publish(&contract_sk, 0, 10, "get-info", GET_INFO_CONTRACT);
+                let publish_tx = make_contract_publish(
+                    &contract_sk,
+                    0,
+                    10,
+                    CHAIN_ID_TESTNET,
+                    "get-info",
+                    GET_INFO_CONTRACT,
+                );
                 eprintln!("Tenure in 1 started!");
                 tenure
                     .mem_pool
@@ -2176,6 +2282,7 @@ fn mempool_errors() {
                     &spender_sk,
                     30, // bad nonce -- too much chaining
                     200,
+                    CHAIN_ID_TESTNET,
                     &send_to,
                     456,
                 );
@@ -2217,6 +2324,7 @@ fn mempool_errors() {
                     &spender_sk,
                     0,
                     1, // bad fee
+                    CHAIN_ID_TESTNET,
                     &send_to,
                     456,
                 );
@@ -2250,6 +2358,7 @@ fn mempool_errors() {
                     &contract_sk,
                     1,
                     2000, // not enough funds!
+                    CHAIN_ID_TESTNET,
                     &send_to,
                     456,
                 );
@@ -2294,6 +2403,7 @@ fn mempool_errors() {
                     1 + MAXIMUM_MEMPOOL_TX_CHAINING,
                     1,
                     2000,
+                    CHAIN_ID_TESTNET,
                     &send_to,
                     1000,
                 );
diff --git a/testnet/stacks-node/src/tests/mempool.rs b/testnet/stacks-node/src/tests/mempool.rs
index 6221c6cf11..b701e70a15 100644
--- a/testnet/stacks-node/src/tests/mempool.rs
+++ b/testnet/stacks-node/src/tests/mempool.rs
@@ -112,8 +112,14 @@ fn mempool_setup_chainstate() {
             if round == 1 {
                 eprintln!("Tenure in 1 started!");
 
-                let publish_tx1 =
-                    make_contract_publish(&contract_sk, 0, 100, "foo_contract", FOO_CONTRACT);
+                let publish_tx1 = make_contract_publish(
+                    &contract_sk,
+                    0,
+                    100,
+                    CHAIN_ID_TESTNET,
+                    "foo_contract",
+                    FOO_CONTRACT,
+                );
                 tenure
                     .mem_pool
                     .submit_raw(
@@ -127,8 +133,14 @@ fn mempool_setup_chainstate() {
                     )
                     .unwrap();
 
-                let publish_tx2 =
-                    make_contract_publish(&contract_sk, 1, 100, "trait-contract", TRAIT_CONTRACT);
+                let publish_tx2 = make_contract_publish(
+                    &contract_sk,
+                    1,
+                    100,
+                    CHAIN_ID_TESTNET,
+                    "trait-contract",
+                    TRAIT_CONTRACT,
+                );
                 tenure
                     .mem_pool
                     .submit_raw(
@@ -146,6 +158,7 @@ fn mempool_setup_chainstate() {
                     &contract_sk,
                     2,
                     100,
+                    CHAIN_ID_TESTNET,
                     "use-trait-contract",
                     USE_TRAIT_CONTRACT,
                 );
@@ -166,6 +179,7 @@ fn mempool_setup_chainstate() {
                     &contract_sk,
                     3,
                     100,
+                    CHAIN_ID_TESTNET,
                     "implement-trait-contract",
                     IMPLEMENT_TRAIT_CONTRACT,
                 );
@@ -186,6 +200,7 @@ fn mempool_setup_chainstate() {
                     &contract_sk,
                     4,
                     100,
+                    CHAIN_ID_TESTNET,
                     "bad-trait-contract",
                     BAD_TRAIT_CONTRACT,
                 );
@@ -234,8 +249,14 @@ fn mempool_setup_chainstate() {
 
                 // let's throw some transactions at it.
                 // first a couple valid ones:
-                let tx_bytes =
-                    make_contract_publish(&contract_sk, 5, 1000, "bar_contract", FOO_CONTRACT);
+                let tx_bytes = make_contract_publish(
+                    &contract_sk,
+                    5,
+                    1000,
+                    CHAIN_ID_TESTNET,
+                    "bar_contract",
+                    FOO_CONTRACT,
+                );
                 let tx =
                     StacksTransaction::consensus_deserialize(&mut tx_bytes.as_slice()).unwrap();
                 chain_state
@@ -252,6 +273,7 @@ fn mempool_setup_chainstate() {
                     &contract_sk,
                     5,
                     200,
+                    CHAIN_ID_TESTNET,
                     &contract_addr,
                     "foo_contract",
                     "bar",
@@ -269,7 +291,8 @@ fn mempool_setup_chainstate() {
                     )
                     .unwrap();
 
-                let tx_bytes = make_stacks_transfer(&contract_sk, 5, 200, &other_addr, 1000);
+                let tx_bytes =
+                    make_stacks_transfer(&contract_sk, 5, 200, CHAIN_ID_TESTNET, &other_addr, 1000);
                 let tx =
                     StacksTransaction::consensus_deserialize(&mut tx_bytes.as_slice()).unwrap();
                 chain_state
@@ -321,6 +344,7 @@ fn mempool_setup_chainstate() {
                     &contract_sk,
                     5,
                     200,
+                    CHAIN_ID_TESTNET,
                     &bad_addr,
                     "foo_contract",
                     "bar",
@@ -354,7 +378,8 @@ fn mempool_setup_chainstate() {
                 .unwrap()
                 .into();
 
-                let tx_bytes = make_stacks_transfer(&contract_sk, 5, 200, &bad_addr, 1000);
+                let tx_bytes =
+                    make_stacks_transfer(&contract_sk, 5, 200, CHAIN_ID_TESTNET, &bad_addr, 1000);
                 let tx =
                     StacksTransaction::consensus_deserialize(&mut tx_bytes.as_slice()).unwrap();
                 let e = chain_state
@@ -373,7 +398,8 @@ fn mempool_setup_chainstate() {
                 });
 
                 // bad fees
-                let tx_bytes = make_stacks_transfer(&contract_sk, 5, 0, &other_addr, 1000);
+                let tx_bytes =
+                    make_stacks_transfer(&contract_sk, 5, 0, CHAIN_ID_TESTNET, &other_addr, 1000);
                 let tx =
                     StacksTransaction::consensus_deserialize(&mut tx_bytes.as_slice()).unwrap();
                 let e = chain_state
@@ -393,7 +419,8 @@ fn mempool_setup_chainstate() {
                 });
 
                 // bad nonce
-                let tx_bytes = make_stacks_transfer(&contract_sk, 0, 200, &other_addr, 1000);
+                let tx_bytes =
+                    make_stacks_transfer(&contract_sk, 0, 200, CHAIN_ID_TESTNET, &other_addr, 1000);
                 let tx =
                     StacksTransaction::consensus_deserialize(&mut tx_bytes.as_slice()).unwrap();
                 let e = chain_state
@@ -413,7 +440,14 @@ fn mempool_setup_chainstate() {
                 });
 
                 // not enough funds
-                let tx_bytes = make_stacks_transfer(&contract_sk, 5, 110000, &other_addr, 1000);
+                let tx_bytes = make_stacks_transfer(
+                    &contract_sk,
+                    5,
+                    110000,
+                    CHAIN_ID_TESTNET,
+                    &other_addr,
+                    1000,
+                );
                 let tx =
                     StacksTransaction::consensus_deserialize(&mut tx_bytes.as_slice()).unwrap();
                 let e = chain_state
@@ -434,7 +468,14 @@ fn mempool_setup_chainstate() {
 
                 // sender == recipient
                 let contract_princ = PrincipalData::from(contract_addr.clone());
-                let tx_bytes = make_stacks_transfer(&contract_sk, 5, 300, &contract_princ, 1000);
+                let tx_bytes = make_stacks_transfer(
+                    &contract_sk,
+                    5,
+                    300,
+                    CHAIN_ID_TESTNET,
+                    &contract_princ,
+                    1000,
+                );
                 let tx =
                     StacksTransaction::consensus_deserialize(&mut tx_bytes.as_slice()).unwrap();
                 let e = chain_state
@@ -457,7 +498,14 @@ fn mempool_setup_chainstate() {
                 let mut mainnet_recipient = to_addr(&other_sk);
                 mainnet_recipient.version = C32_ADDRESS_VERSION_MAINNET_SINGLESIG;
                 let mainnet_princ = mainnet_recipient.into();
-                let tx_bytes = make_stacks_transfer(&contract_sk, 5, 300, &mainnet_princ, 1000);
+                let tx_bytes = make_stacks_transfer(
+                    &contract_sk,
+                    5,
+                    300,
+                    CHAIN_ID_TESTNET,
+                    &mainnet_princ,
+                    1000,
+                );
                 let tx =
                     StacksTransaction::consensus_deserialize(&mut tx_bytes.as_slice()).unwrap();
                 let e = chain_state
@@ -488,6 +536,7 @@ fn mempool_setup_chainstate() {
                     &contract_sk,
                     5,
                     300,
+                    CHAIN_ID_TESTNET,
                     TransactionAnchorMode::OnChainOnly,
                     TransactionVersion::Mainnet,
                 );
@@ -510,7 +559,8 @@ fn mempool_setup_chainstate() {
                 });
 
                 // send amount must be positive
-                let tx_bytes = make_stacks_transfer(&contract_sk, 5, 300, &other_addr, 0);
+                let tx_bytes =
+                    make_stacks_transfer(&contract_sk, 5, 300, CHAIN_ID_TESTNET, &other_addr, 0);
                 let tx =
                     StacksTransaction::consensus_deserialize(&mut tx_bytes.as_slice()).unwrap();
                 let e = chain_state
@@ -530,7 +580,14 @@ fn mempool_setup_chainstate() {
                 });
 
                 // not enough funds
-                let tx_bytes = make_stacks_transfer(&contract_sk, 5, 110000, &other_addr, 1000);
+                let tx_bytes = make_stacks_transfer(
+                    &contract_sk,
+                    5,
+                    110000,
+                    CHAIN_ID_TESTNET,
+                    &other_addr,
+                    1000,
+                );
                 let tx =
                     StacksTransaction::consensus_deserialize(&mut tx_bytes.as_slice()).unwrap();
                 let e = chain_state
@@ -549,7 +606,14 @@ fn mempool_setup_chainstate() {
                     false
                 });
 
-                let tx_bytes = make_stacks_transfer(&contract_sk, 5, 99700, &other_addr, 1000);
+                let tx_bytes = make_stacks_transfer(
+                    &contract_sk,
+                    5,
+                    99700,
+                    CHAIN_ID_TESTNET,
+                    &other_addr,
+                    1000,
+                );
                 let tx =
                     StacksTransaction::consensus_deserialize(&mut tx_bytes.as_slice()).unwrap();
                 let e = chain_state
@@ -572,6 +636,7 @@ fn mempool_setup_chainstate() {
                     &contract_sk,
                     5,
                     200,
+                    CHAIN_ID_TESTNET,
                     &contract_addr,
                     "bar_contract",
                     "bar",
@@ -599,6 +664,7 @@ fn mempool_setup_chainstate() {
                     &contract_sk,
                     5,
                     200,
+                    CHAIN_ID_TESTNET,
                     &contract_addr,
                     "foo_contract",
                     "foobar",
@@ -626,6 +692,7 @@ fn mempool_setup_chainstate() {
                     &contract_sk,
                     5,
                     200,
+                    CHAIN_ID_TESTNET,
                     &contract_addr,
                     "foo_contract",
                     "bar",
@@ -649,8 +716,14 @@ fn mempool_setup_chainstate() {
                     false
                 });
 
-                let tx_bytes =
-                    make_contract_publish(&contract_sk, 5, 1000, "foo_contract", FOO_CONTRACT);
+                let tx_bytes = make_contract_publish(
+                    &contract_sk,
+                    5,
+                    1000,
+                    CHAIN_ID_TESTNET,
+                    "foo_contract",
+                    FOO_CONTRACT,
+                );
                 let tx =
                     StacksTransaction::consensus_deserialize(&mut tx_bytes.as_slice()).unwrap();
                 let e = chain_state
@@ -685,7 +758,14 @@ fn mempool_setup_chainstate() {
                     signature: MessageSignature([1; 65]),
                 };
 
-                let tx_bytes = make_poison(&contract_sk, 5, 1000, microblock_1, microblock_2);
+                let tx_bytes = make_poison(
+                    &contract_sk,
+                    5,
+                    1000,
+                    CHAIN_ID_TESTNET,
+                    microblock_1,
+                    microblock_2,
+                );
                 let tx =
                     StacksTransaction::consensus_deserialize(&mut tx_bytes.as_slice()).unwrap();
                 let e = chain_state
@@ -716,7 +796,14 @@ fn mempool_setup_chainstate() {
                     signature: MessageSignature([0; 65]),
                 };
 
-                let tx_bytes = make_poison(&contract_sk, 5, 1000, microblock_1, microblock_2);
+                let tx_bytes = make_poison(
+                    &contract_sk,
+                    5,
+                    1000,
+                    CHAIN_ID_TESTNET,
+                    microblock_1,
+                    microblock_2,
+                );
                 let tx =
                     StacksTransaction::consensus_deserialize(&mut tx_bytes.as_slice()).unwrap();
                 let e = chain_state
@@ -750,7 +837,14 @@ fn mempool_setup_chainstate() {
                 microblock_1.sign(&other_sk).unwrap();
                 microblock_2.sign(&other_sk).unwrap();
 
-                let tx_bytes = make_poison(&contract_sk, 5, 1000, microblock_1, microblock_2);
+                let tx_bytes = make_poison(
+                    &contract_sk,
+                    5,
+                    1000,
+                    CHAIN_ID_TESTNET,
+                    microblock_1,
+                    microblock_2,
+                );
                 let tx =
                     StacksTransaction::consensus_deserialize(&mut tx_bytes.as_slice()).unwrap();
                 let e = chain_state
@@ -765,7 +859,7 @@ fn mempool_setup_chainstate() {
                 eprintln!("Err: {:?}", e);
                 assert!(matches!(e, MemPoolRejection::Other(_)));
 
-                let tx_bytes = make_coinbase(&contract_sk, 5, 1000);
+                let tx_bytes = make_coinbase(&contract_sk, 5, 1000, CHAIN_ID_TESTNET);
                 let tx =
                     StacksTransaction::consensus_deserialize(&mut tx_bytes.as_slice()).unwrap();
                 let e = chain_state
@@ -823,7 +917,14 @@ fn mempool_setup_chainstate() {
                 microblock_1.sign(&secret_key).unwrap();
                 microblock_2.sign(&secret_key).unwrap();
 
-                let tx_bytes = make_poison(&contract_sk, 5, 1000, microblock_1, microblock_2);
+                let tx_bytes = make_poison(
+                    &contract_sk,
+                    5,
+                    1000,
+                    CHAIN_ID_TESTNET,
+                    microblock_1,
+                    microblock_2,
+                );
                 let tx =
                     StacksTransaction::consensus_deserialize(&mut tx_bytes.as_slice()).unwrap();
                 let e = chain_state
@@ -848,6 +949,7 @@ fn mempool_setup_chainstate() {
                     &contract_sk,
                     5,
                     250,
+                    CHAIN_ID_TESTNET,
                     &contract_addr,
                     "use-trait-contract",
                     "baz",
@@ -875,6 +977,7 @@ fn mempool_setup_chainstate() {
                     &contract_sk,
                     5,
                     250,
+                    CHAIN_ID_TESTNET,
                     &contract_addr,
                     "use-trait-contract",
                     "baz",
diff --git a/testnet/stacks-node/src/tests/mod.rs b/testnet/stacks-node/src/tests/mod.rs
index ba88584f39..2c555e7232 100644
--- a/testnet/stacks-node/src/tests/mod.rs
+++ b/testnet/stacks-node/src/tests/mod.rs
@@ -23,6 +23,7 @@ use clarity::vm::events::STXEventType;
 use clarity::vm::types::PrincipalData;
 use clarity::vm::{ClarityName, ClarityVersion, ContractName, Value};
 use lazy_static::lazy_static;
+use neon_integrations::test_observer::EVENT_OBSERVER_PORT;
 use rand::Rng;
 use stacks::chainstate::burn::ConsensusHash;
 use stacks::chainstate::stacks::db::StacksChainState;
@@ -94,13 +95,18 @@ lazy_static! {
         .unwrap(),
         0,
         10,
+        CHAIN_ID_TESTNET,
         "store",
         STORE_CONTRACT
     );
 }
 
 lazy_static! {
-    static ref USED_PORTS: Mutex<HashSet<u16>> = Mutex::new(HashSet::new());
+    static ref USED_PORTS: Mutex<HashSet<u16>> = Mutex::new({
+        let mut set = HashSet::new();
+        set.insert(EVENT_OBSERVER_PORT);
+        set
+    });
 }
 
 /// Generate a random port number between 1024 and 65534 (inclusive) and insert it into the USED_PORTS set.
@@ -134,6 +140,7 @@ pub fn serialize_sign_sponsored_sig_tx_anchor_mode_version(
     sender_nonce: u64,
     payer_nonce: u64,
     tx_fee: u64,
+    chain_id: u32,
     anchor_mode: TransactionAnchorMode,
     version: TransactionVersion,
 ) -> Vec<u8> {
@@ -144,6 +151,7 @@ pub fn serialize_sign_sponsored_sig_tx_anchor_mode_version(
         sender_nonce,
         Some(payer_nonce),
         tx_fee,
+        chain_id,
         anchor_mode,
         version,
     )
@@ -154,12 +162,14 @@ pub fn serialize_sign_standard_single_sig_tx(
     sender: &StacksPrivateKey,
     nonce: u64,
     tx_fee: u64,
+    chain_id: u32,
 ) -> Vec<u8> {
     serialize_sign_standard_single_sig_tx_anchor_mode(
         payload,
         sender,
         nonce,
         tx_fee,
+        chain_id,
         TransactionAnchorMode::OnChainOnly,
     )
 }
@@ -169,6 +179,7 @@ pub fn serialize_sign_standard_single_sig_tx_anchor_mode(
     sender: &StacksPrivateKey,
     nonce: u64,
     tx_fee: u64,
+    chain_id: u32,
     anchor_mode: TransactionAnchorMode,
 ) -> Vec<u8> {
     serialize_sign_standard_single_sig_tx_anchor_mode_version(
@@ -176,6 +187,7 @@ pub fn serialize_sign_standard_single_sig_tx_anchor_mode(
         sender,
         nonce,
         tx_fee,
+        chain_id,
         anchor_mode,
         TransactionVersion::Testnet,
     )
@@ -186,6 +198,7 @@ pub fn serialize_sign_standard_single_sig_tx_anchor_mode_version(
     sender: &StacksPrivateKey,
     nonce: u64,
     tx_fee: u64,
+    chain_id: u32,
     anchor_mode: TransactionAnchorMode,
     version: TransactionVersion,
 ) -> Vec<u8> {
@@ -196,6 +209,7 @@ pub fn serialize_sign_standard_single_sig_tx_anchor_mode_version(
         nonce,
         None,
         tx_fee,
+        chain_id,
         anchor_mode,
         version,
     )
@@ -208,6 +222,7 @@ pub fn serialize_sign_tx_anchor_mode_version(
     sender_nonce: u64,
     payer_nonce: Option<u64>,
     tx_fee: u64,
+    chain_id: u32,
     anchor_mode: TransactionAnchorMode,
     version: TransactionVersion,
 ) -> Vec<u8> {
@@ -234,7 +249,7 @@ pub fn serialize_sign_tx_anchor_mode_version(
     let mut unsigned_tx = StacksTransaction::new(version, auth, payload);
     unsigned_tx.anchor_mode = anchor_mode;
     unsigned_tx.post_condition_mode = TransactionPostConditionMode::Allow;
-    unsigned_tx.chain_id = CHAIN_ID_TESTNET;
+    unsigned_tx.chain_id = chain_id;
 
     let mut tx_signer = StacksTransactionSigner::new(&unsigned_tx);
     tx_signer.sign_origin(sender).unwrap();
@@ -255,6 +270,7 @@ pub fn make_contract_publish_versioned(
     sender: &StacksPrivateKey,
     nonce: u64,
     tx_fee: u64,
+    chain_id: u32,
     contract_name: &str,
     contract_content: &str,
     version: Option<ClarityVersion>,
@@ -265,23 +281,33 @@ pub fn make_contract_publish_versioned(
     let payload =
         TransactionPayload::SmartContract(TransactionSmartContract { name, code_body }, version);
 
-    serialize_sign_standard_single_sig_tx(payload, sender, nonce, tx_fee)
+    serialize_sign_standard_single_sig_tx(payload, sender, nonce, tx_fee, chain_id)
 }
 
 pub fn make_contract_publish(
     sender: &StacksPrivateKey,
     nonce: u64,
     tx_fee: u64,
+    chain_id: u32,
     contract_name: &str,
     contract_content: &str,
 ) -> Vec<u8> {
-    make_contract_publish_versioned(sender, nonce, tx_fee, contract_name, contract_content, None)
+    make_contract_publish_versioned(
+        sender,
+        nonce,
+        tx_fee,
+        chain_id,
+        contract_name,
+        contract_content,
+        None,
+    )
 }
 
 pub fn make_contract_publish_microblock_only_versioned(
     sender: &StacksPrivateKey,
     nonce: u64,
     tx_fee: u64,
+    chain_id: u32,
     contract_name: &str,
     contract_content: &str,
     version: Option<ClarityVersion>,
@@ -297,6 +323,7 @@ pub fn make_contract_publish_microblock_only_versioned(
         sender,
         nonce,
         tx_fee,
+        chain_id,
         TransactionAnchorMode::OffChainOnly,
     )
 }
@@ -305,6 +332,7 @@ pub fn make_contract_publish_microblock_only(
     sender: &StacksPrivateKey,
     nonce: u64,
     tx_fee: u64,
+    chain_id: u32,
     contract_name: &str,
     contract_content: &str,
 ) -> Vec<u8> {
@@ -312,6 +340,7 @@ pub fn make_contract_publish_microblock_only(
         sender,
         nonce,
         tx_fee,
+        chain_id,
         contract_name,
         contract_content,
         None,
@@ -392,12 +421,13 @@ pub fn make_stacks_transfer(
     sender: &StacksPrivateKey,
     nonce: u64,
     tx_fee: u64,
+    chain_id: u32,
     recipient: &PrincipalData,
     amount: u64,
 ) -> Vec<u8> {
     let payload =
         TransactionPayload::TokenTransfer(recipient.clone(), amount, TokenTransferMemo([0; 34]));
-    serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, tx_fee)
+    serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, tx_fee, chain_id)
 }
 
 pub fn make_sponsored_stacks_transfer_on_testnet(
@@ -406,6 +436,7 @@ pub fn make_sponsored_stacks_transfer_on_testnet(
     sender_nonce: u64,
     payer_nonce: u64,
     tx_fee: u64,
+    chain_id: u32,
     recipient: &PrincipalData,
     amount: u64,
 ) -> Vec<u8> {
@@ -418,6 +449,7 @@ pub fn make_sponsored_stacks_transfer_on_testnet(
         sender_nonce,
         payer_nonce,
         tx_fee,
+        chain_id,
         TransactionAnchorMode::OnChainOnly,
         TransactionVersion::Testnet,
     )
@@ -427,6 +459,7 @@ pub fn make_stacks_transfer_mblock_only(
     sender: &StacksPrivateKey,
     nonce: u64,
     tx_fee: u64,
+    chain_id: u32,
     recipient: &PrincipalData,
     amount: u64,
 ) -> Vec<u8> {
@@ -437,6 +470,7 @@ pub fn make_stacks_transfer_mblock_only(
         sender,
         nonce,
         tx_fee,
+        chain_id,
         TransactionAnchorMode::OffChainOnly,
     )
 }
@@ -445,22 +479,24 @@ pub fn make_poison(
     sender: &StacksPrivateKey,
     nonce: u64,
     tx_fee: u64,
+    chain_id: u32,
     header_1: StacksMicroblockHeader,
     header_2: StacksMicroblockHeader,
 ) -> Vec<u8> {
     let payload = TransactionPayload::PoisonMicroblock(header_1, header_2);
-    serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, tx_fee)
+    serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, tx_fee, chain_id)
 }
 
-pub fn make_coinbase(sender: &StacksPrivateKey, nonce: u64, tx_fee: u64) -> Vec<u8> {
+pub fn make_coinbase(sender: &StacksPrivateKey, nonce: u64, tx_fee: u64, chain_id: u32) -> Vec<u8> {
     let payload = TransactionPayload::Coinbase(CoinbasePayload([0; 32]), None, None);
-    serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, tx_fee)
+    serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, tx_fee, chain_id)
 }
 
 pub fn make_contract_call(
     sender: &StacksPrivateKey,
     nonce: u64,
     tx_fee: u64,
+    chain_id: u32,
     contract_addr: &StacksAddress,
     contract_name: &str,
     function_name: &str,
@@ -476,13 +512,14 @@ pub fn make_contract_call(
         function_args: function_args.iter().map(|x| x.clone()).collect(),
     };
 
-    serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, tx_fee)
+    serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, tx_fee, chain_id)
 }
 
 pub fn make_contract_call_mblock_only(
     sender: &StacksPrivateKey,
     nonce: u64,
     tx_fee: u64,
+    chain_id: u32,
     contract_addr: &StacksAddress,
     contract_name: &str,
     function_name: &str,
@@ -503,6 +540,7 @@ pub fn make_contract_call_mblock_only(
         sender,
         nonce,
         tx_fee,
+        chain_id,
         TransactionAnchorMode::OffChainOnly,
     )
 }
@@ -921,7 +959,7 @@ fn should_succeed_handling_malformed_and_valid_txs() {
             1 => {
                 // On round 1, publish the KV contract
                 let contract_sk = StacksPrivateKey::from_hex(SK_1).unwrap();
-                let publish_contract = make_contract_publish(&contract_sk, 0, 10, "store", STORE_CONTRACT);
+                let publish_contract = make_contract_publish(&contract_sk, 0, 10, CHAIN_ID_TESTNET, "store", STORE_CONTRACT);
                 tenure.mem_pool.submit_raw(&mut chainstate_copy, &sortdb, &consensus_hash, &header_hash,publish_contract,
                                 &ExecutionCost::max_value(),
                                 &StacksEpochId::Epoch20,
diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs
index 049df9f83a..93f4ac4106 100644
--- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs
+++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs
@@ -91,7 +91,7 @@ use stacks_signer::chainstate::{ProposalEvalConfig, SortitionsView};
 use stacks_signer::signerdb::{BlockInfo, BlockState, ExtraBlockInfo, SignerDb};
 
 use super::bitcoin_regtest::BitcoinCoreController;
-use crate::config::{EventKeyType, EventObserverConfig, InitialBalance};
+use crate::config::{EventKeyType, InitialBalance};
 use crate::nakamoto_node::miner::{
     TEST_BLOCK_ANNOUNCE_STALL, TEST_BROADCAST_STALL, TEST_MINE_STALL, TEST_SKIP_P2P_BROADCAST,
 };
@@ -255,7 +255,7 @@ pub fn check_nakamoto_empty_block_heuristics() {
     }
 }
 
-pub fn get_stacker_set(http_origin: &str, cycle: u64) -> GetStackersResponse {
+pub fn get_stacker_set(http_origin: &str, cycle: u64) -> Result<GetStackersResponse, String> {
     let client = reqwest::blocking::Client::new();
     let path = format!("{http_origin}/v3/stacker_set/{cycle}");
     let res = client
@@ -263,10 +263,9 @@ pub fn get_stacker_set(http_origin: &str, cycle: u64) -> GetStackersResponse {
         .send()
         .unwrap()
         .json::<serde_json::Value>()
-        .unwrap();
+        .map_err(|e| format!("{e}"))?;
     info!("Stacker set response: {res}");
-    let res = serde_json::from_value(res).unwrap();
-    res
+    serde_json::from_value(res).map_err(|e| format!("{e}"))
 }
 
 pub fn get_stackerdb_slot_version(
@@ -849,7 +848,7 @@ pub fn boot_to_epoch_3(
             &signer_sk,
             reward_cycle.into(),
             &Pox4SignatureTopic::StackStx,
-            CHAIN_ID_TESTNET,
+            naka_conf.burnchain.chain_id,
             12_u128,
             u128::MAX,
             1,
@@ -863,6 +862,7 @@ pub fn boot_to_epoch_3(
             &stacker_sk,
             0,
             1000,
+            naka_conf.burnchain.chain_id,
             &StacksAddress::burn_address(false),
             "pox-4",
             "stack-stx",
@@ -886,19 +886,21 @@ pub fn boot_to_epoch_3(
         signers.signer_keys = signer_sks.to_vec();
     }
 
-    let prepare_phase_start = btc_regtest_controller
+    // the reward set is generally calculated in the first block of the prepare phase hence the + 1
+    let reward_set_calculation = btc_regtest_controller
         .get_burnchain()
         .pox_constants
         .prepare_phase_start(
             btc_regtest_controller.get_burnchain().first_block_height,
             reward_cycle,
-        );
+        )
+        + 1;
 
     // Run until the prepare phase
     run_until_burnchain_height(
         btc_regtest_controller,
         &blocks_processed,
-        prepare_phase_start,
+        reward_set_calculation,
         &naka_conf,
     );
 
@@ -909,7 +911,11 @@ pub fn boot_to_epoch_3(
         let aggregate_public_key = clarity::vm::Value::buff_from(aggregate_key)
             .expect("Failed to serialize aggregate public key");
         let signer_sks_unique: HashMap<_, _> = signer_sks.iter().map(|x| (x.to_hex(), x)).collect();
-        let signer_set = get_stacker_set(&http_origin, reward_cycle + 1);
+        wait_for(30, || {
+            Ok(get_stacker_set(&http_origin, reward_cycle + 1).is_ok())
+        })
+        .expect("Timed out waiting for stacker set");
+        let signer_set = get_stacker_set(&http_origin, reward_cycle + 1).unwrap();
         // Vote on the aggregate public key
         for signer_sk in signer_sks_unique.values() {
             let signer_index =
@@ -919,6 +925,7 @@ pub fn boot_to_epoch_3(
                 signer_sk,
                 0,
                 300,
+                naka_conf.burnchain.chain_id,
                 &StacksAddress::burn_address(false),
                 SIGNERS_VOTING_NAME,
                 SIGNERS_VOTING_FUNCTION_NAME,
@@ -1003,7 +1010,7 @@ pub fn boot_to_pre_epoch_3_boundary(
             &signer_sk,
             reward_cycle.into(),
             &Pox4SignatureTopic::StackStx,
-            CHAIN_ID_TESTNET,
+            naka_conf.burnchain.chain_id,
             12_u128,
             u128::MAX,
             1,
@@ -1017,6 +1024,7 @@ pub fn boot_to_pre_epoch_3_boundary(
             &stacker_sk,
             0,
             1000,
+            naka_conf.burnchain.chain_id,
             &StacksAddress::burn_address(false),
             "pox-4",
             "stack-stx",
@@ -1040,19 +1048,21 @@ pub fn boot_to_pre_epoch_3_boundary(
         signers.signer_keys = signer_sks.to_vec();
     }
 
-    let prepare_phase_start = btc_regtest_controller
+    // the reward set is generally calculated in the first block of the prepare phase hence the + 1
+    let reward_set_calculation = btc_regtest_controller
         .get_burnchain()
         .pox_constants
         .prepare_phase_start(
             btc_regtest_controller.get_burnchain().first_block_height,
             reward_cycle,
-        );
+        )
+        + 1;
 
     // Run until the prepare phase
     run_until_burnchain_height(
         btc_regtest_controller,
         &blocks_processed,
-        prepare_phase_start,
+        reward_set_calculation,
         &naka_conf,
     );
 
@@ -1063,7 +1073,11 @@ pub fn boot_to_pre_epoch_3_boundary(
         let aggregate_public_key = clarity::vm::Value::buff_from(aggregate_key)
             .expect("Failed to serialize aggregate public key");
         let signer_sks_unique: HashMap<_, _> = signer_sks.iter().map(|x| (x.to_hex(), x)).collect();
-        let signer_set = get_stacker_set(&http_origin, reward_cycle + 1);
+        wait_for(30, || {
+            Ok(get_stacker_set(&http_origin, reward_cycle + 1).is_ok())
+        })
+        .expect("Timed out waiting for stacker set");
+        let signer_set = get_stacker_set(&http_origin, reward_cycle + 1).unwrap();
         // Vote on the aggregate public key
         for signer_sk in signer_sks_unique.values() {
             let signer_index =
@@ -1073,6 +1087,7 @@ pub fn boot_to_pre_epoch_3_boundary(
                 signer_sk,
                 0,
                 300,
+                naka_conf.burnchain.chain_id,
                 &StacksAddress::burn_address(false),
                 SIGNERS_VOTING_NAME,
                 SIGNERS_VOTING_FUNCTION_NAME,
@@ -1234,7 +1249,7 @@ pub fn setup_epoch_3_reward_set(
             &signer_sk,
             reward_cycle.into(),
             &Pox4SignatureTopic::StackStx,
-            CHAIN_ID_TESTNET,
+            naka_conf.burnchain.chain_id,
             lock_period,
             u128::MAX,
             1,
@@ -1247,6 +1262,7 @@ pub fn setup_epoch_3_reward_set(
             &stacker_sk,
             0,
             1000,
+            naka_conf.burnchain.chain_id,
             &StacksAddress::burn_address(false),
             "pox-4",
             "stack-stx",
@@ -1432,11 +1448,7 @@ fn simple_neon_integration() {
     let stacker_sk = setup_stacker(&mut naka_conf);
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut naka_conf);
 
     let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
     btcd_controller
@@ -1488,16 +1500,19 @@ fn simple_neon_integration() {
     // query for prometheus metrics
     #[cfg(feature = "monitoring_prom")]
     {
-        let prom_http_origin = format!("http://{}", prom_bind);
-        let client = reqwest::blocking::Client::new();
-        let res = client
-            .get(&prom_http_origin)
-            .send()
-            .unwrap()
-            .text()
-            .unwrap();
-        let expected_result = format!("stacks_node_stacks_tip_height {block_height_pre_3_0}");
-        assert!(res.contains(&expected_result));
+        wait_for(10, || {
+            let prom_http_origin = format!("http://{}", prom_bind);
+            let client = reqwest::blocking::Client::new();
+            let res = client
+                .get(&prom_http_origin)
+                .send()
+                .unwrap()
+                .text()
+                .unwrap();
+            let expected_result = format!("stacks_node_stacks_tip_height {block_height_pre_3_0}");
+            Ok(res.contains(&expected_result))
+        })
+        .expect("Prometheus metrics did not update");
     }
 
     info!("Nakamoto miner started...");
@@ -1517,7 +1532,14 @@ fn simple_neon_integration() {
     }
 
     // Submit a TX
-    let transfer_tx = make_stacks_transfer(&sender_sk, 0, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        0,
+        send_fee,
+        naka_conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     let transfer_tx_hex = format!("0x{}", to_hex(&transfer_tx));
 
     let tip = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb)
@@ -1599,19 +1621,30 @@ fn simple_neon_integration() {
     let bhh = u64::from(tip.burn_header_height);
     test_observer::contains_burn_block_range(220..=bhh).unwrap();
 
-    // make sure prometheus returns an updated height
+    // make sure prometheus returns an updated number of processed blocks
     #[cfg(feature = "monitoring_prom")]
     {
-        let prom_http_origin = format!("http://{}", prom_bind);
-        let client = reqwest::blocking::Client::new();
-        let res = client
-            .get(&prom_http_origin)
-            .send()
-            .unwrap()
-            .text()
-            .unwrap();
-        let expected_result = format!("stacks_node_stacks_tip_height {}", tip.stacks_block_height);
-        assert!(res.contains(&expected_result));
+        wait_for(10, || {
+            let prom_http_origin = format!("http://{}", prom_bind);
+            let client = reqwest::blocking::Client::new();
+            let res = client
+                .get(&prom_http_origin)
+                .send()
+                .unwrap()
+                .text()
+                .unwrap();
+            let expected_result_1 = format!(
+                "stacks_node_stx_blocks_processed_total {}",
+                tip.stacks_block_height
+            );
+
+            let expected_result_2 = format!(
+                "stacks_node_stacks_tip_height {}",
+                tip.stacks_block_height - 1
+            );
+            Ok(res.contains(&expected_result_1) && res.contains(&expected_result_2))
+        })
+        .expect("Prometheus metrics did not update");
     }
 
     check_nakamoto_empty_block_heuristics();
@@ -1665,11 +1698,7 @@ fn flash_blocks_on_epoch_3() {
     let stacker_sk = setup_stacker(&mut naka_conf);
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut naka_conf);
 
     let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
     btcd_controller
@@ -1770,7 +1799,14 @@ fn flash_blocks_on_epoch_3() {
     }
 
     // Submit a TX
-    let transfer_tx = make_stacks_transfer(&sender_sk, 0, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        0,
+        send_fee,
+        naka_conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     let transfer_tx_hex = format!("0x{}", to_hex(&transfer_tx));
 
     let tip = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb)
@@ -1932,11 +1968,7 @@ fn mine_multiple_per_tenure_integration() {
     let stacker_sk = setup_stacker(&mut naka_conf);
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut naka_conf);
 
     let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
     btcd_controller
@@ -2012,8 +2044,14 @@ fn mine_multiple_per_tenure_integration() {
                 .get_stacks_blocks_processed();
             // submit a tx so that the miner will mine an extra block
             let sender_nonce = tenure_ix * inter_blocks_per_tenure + interim_block_ix;
-            let transfer_tx =
-                make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+            let transfer_tx = make_stacks_transfer(
+                &sender_sk,
+                sender_nonce,
+                send_fee,
+                naka_conf.burnchain.chain_id,
+                &recipient,
+                send_amt,
+            );
             submit_tx(&http_origin, &transfer_tx);
 
             loop {
@@ -2095,7 +2133,7 @@ fn multiple_miners() {
     let node_2_p2p = 51025;
     let http_origin = format!("http://{}", &naka_conf.node.rpc_bind);
     naka_conf.miner.wait_on_interim_blocks = Duration::from_secs(1);
-    naka_conf.node.pox_sync_sample_secs = 5;
+    naka_conf.node.pox_sync_sample_secs = 30;
     let sender_sk = Secp256k1PrivateKey::new();
     let sender_signer_sk = Secp256k1PrivateKey::new();
     let sender_signer_addr = tests::to_addr(&sender_signer_sk);
@@ -2146,11 +2184,7 @@ fn multiple_miners() {
     );
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut naka_conf);
 
     let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
     btcd_controller
@@ -2268,8 +2302,14 @@ fn multiple_miners() {
                 .get_stacks_blocks_processed();
             // submit a tx so that the miner will mine an extra block
             let sender_nonce = tenure_ix * inter_blocks_per_tenure + interim_block_ix;
-            let transfer_tx =
-                make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+            let transfer_tx = make_stacks_transfer(
+                &sender_sk,
+                sender_nonce,
+                send_fee,
+                naka_conf.burnchain.chain_id,
+                &recipient,
+                send_amt,
+            );
             submit_tx(&http_origin, &transfer_tx);
 
             wait_for(20, || {
@@ -2379,11 +2419,7 @@ fn correct_burn_outs() {
     let signers = TestSigners::new(vec![sender_signer_sk]);
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut naka_conf);
 
     let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
     btcd_controller
@@ -2479,7 +2515,7 @@ fn correct_burn_outs() {
                 &sender_signer_sk,
                 reward_cycle.into(),
                 &Pox4SignatureTopic::StackStx,
-                CHAIN_ID_TESTNET,
+                naka_conf.burnchain.chain_id,
                 1_u128,
                 u128::MAX,
                 1,
@@ -2491,6 +2527,7 @@ fn correct_burn_outs() {
                 &account.0,
                 account.2.nonce,
                 1000,
+                naka_conf.burnchain.chain_id,
                 &StacksAddress::burn_address(false),
                 "pox-4",
                 "stack-stx",
@@ -2552,7 +2589,7 @@ fn correct_burn_outs() {
     info!("first_epoch_3_cycle: {:?}", first_epoch_3_cycle);
 
     let http_origin = format!("http://{}", &naka_conf.node.rpc_bind);
-    let stacker_response = get_stacker_set(&http_origin, first_epoch_3_cycle);
+    let stacker_response = get_stacker_set(&http_origin, first_epoch_3_cycle).unwrap();
     assert!(stacker_response.stacker_set.signers.is_some());
     assert_eq!(
         stacker_response.stacker_set.signers.as_ref().unwrap().len(),
@@ -2702,11 +2739,7 @@ fn block_proposal_api_endpoint() {
 
     // only subscribe to the block proposal events
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::BlockProposal],
-    });
+    test_observer::register(&mut conf, &[EventKeyType::BlockProposal]);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -2836,6 +2869,7 @@ fn block_proposal_api_endpoint() {
             &account_keys[0],
             0,
             100,
+            conf.burnchain.chain_id,
             &to_addr(&account_keys[1]).into(),
             10000,
         );
@@ -3074,11 +3108,10 @@ fn miner_writes_proposed_block_to_stackerdb() {
     let mut signers = TestSigners::new(vec![sender_signer_sk.clone()]);
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::AnyEvent, EventKeyType::MinedBlocks],
-    });
+    test_observer::register(
+        &mut naka_conf,
+        &[EventKeyType::AnyEvent, EventKeyType::MinedBlocks],
+    );
 
     let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
     btcd_controller
@@ -3189,11 +3222,7 @@ fn vote_for_aggregate_key_burn_op() {
     let stacker_sk = setup_stacker(&mut naka_conf);
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut naka_conf);
 
     let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
     btcd_controller
@@ -3437,11 +3466,7 @@ fn follower_bootup() {
     let stacker_sk = setup_stacker(&mut naka_conf);
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut naka_conf);
 
     let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
     btcd_controller
@@ -3522,7 +3547,7 @@ fn follower_bootup() {
             &node_info.node_public_key.unwrap(),
             naka_conf.node.p2p_bind
         ),
-        CHAIN_ID_TESTNET,
+        naka_conf.burnchain.chain_id,
         PEER_VERSION_TESTNET,
     );
 
@@ -3582,8 +3607,14 @@ fn follower_bootup() {
             let sender_nonce = account
                 .nonce
                 .max(last_nonce.as_ref().map(|ln| *ln + 1).unwrap_or(0));
-            let transfer_tx =
-                make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+            let transfer_tx = make_stacks_transfer(
+                &sender_sk,
+                sender_nonce,
+                send_fee,
+                naka_conf.burnchain.chain_id,
+                &recipient,
+                send_amt,
+            );
             submit_tx(&http_origin, &transfer_tx);
 
             last_nonce = Some(sender_nonce);
@@ -3736,6 +3767,7 @@ fn follower_bootup_across_multiple_cycles() {
 
     let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None);
     naka_conf.miner.wait_on_interim_blocks = Duration::from_secs(1);
+    naka_conf.node.pox_sync_sample_secs = 30;
     naka_conf.burnchain.max_rbf = 10_000_000;
 
     let sender_sk = Secp256k1PrivateKey::new();
@@ -3760,11 +3792,7 @@ fn follower_bootup_across_multiple_cycles() {
     let stacker_sk = setup_stacker(&mut naka_conf);
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut naka_conf);
 
     let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
     btcd_controller
@@ -3863,7 +3891,6 @@ fn follower_bootup_across_multiple_cycles() {
     follower_conf.node.p2p_bind = format!("{localhost}:{p2p_port}");
     follower_conf.node.data_url = format!("http://{localhost}:{rpc_port}");
     follower_conf.node.p2p_address = format!("{localhost}:{p2p_port}");
-    follower_conf.node.pox_sync_sample_secs = 30;
 
     let node_info = get_chain_info(&naka_conf);
     follower_conf.node.add_bootstrap_node(
@@ -3872,7 +3899,7 @@ fn follower_bootup_across_multiple_cycles() {
             &node_info.node_public_key.unwrap(),
             naka_conf.node.p2p_bind
         ),
-        CHAIN_ID_TESTNET,
+        naka_conf.burnchain.chain_id,
         PEER_VERSION_TESTNET,
     );
 
@@ -3930,6 +3957,338 @@ fn follower_bootup_across_multiple_cycles() {
     follower_thread.join().unwrap();
 }
 
+/// Boot up a node and a follower with a non-default chain id
+#[test]
+#[ignore]
+fn follower_bootup_custom_chain_id() {
+    if env::var("BITCOIND_TEST") != Ok("1".into()) {
+        return;
+    }
+
+    let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None);
+    naka_conf.burnchain.chain_id = 0x87654321;
+    let http_origin = format!("http://{}", &naka_conf.node.rpc_bind);
+    naka_conf.miner.wait_on_interim_blocks = Duration::from_secs(1);
+    let sender_sk = Secp256k1PrivateKey::new();
+    let sender_signer_sk = Secp256k1PrivateKey::new();
+    let sender_signer_addr = tests::to_addr(&sender_signer_sk);
+    let mut signers = TestSigners::new(vec![sender_signer_sk.clone()]);
+    let tenure_count = 5;
+    let inter_blocks_per_tenure = 9;
+    // setup sender + recipient for some test stx transfers
+    // these are necessary for the interim blocks to get mined at all
+    let sender_addr = tests::to_addr(&sender_sk);
+    let send_amt = 100;
+    let send_fee = 180;
+    naka_conf.add_initial_balance(
+        PrincipalData::from(sender_addr.clone()).to_string(),
+        (send_amt + send_fee) * tenure_count * inter_blocks_per_tenure,
+    );
+    naka_conf.add_initial_balance(
+        PrincipalData::from(sender_signer_addr.clone()).to_string(),
+        100000,
+    );
+    let recipient = PrincipalData::from(StacksAddress::burn_address(false));
+    let stacker_sk = setup_stacker(&mut naka_conf);
+
+    test_observer::spawn();
+    test_observer::register_any(&mut naka_conf);
+
+    let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
+    btcd_controller
+        .start_bitcoind()
+        .expect("Failed starting bitcoind");
+    let mut btc_regtest_controller = BitcoinRegtestController::new(naka_conf.clone(), None);
+    btc_regtest_controller.bootstrap_chain(201);
+
+    let mut run_loop = boot_nakamoto::BootRunLoop::new(naka_conf.clone()).unwrap();
+    let run_loop_stopper = run_loop.get_termination_switch();
+    let Counters {
+        blocks_processed,
+        naka_submitted_commits: commits_submitted,
+        naka_proposed_blocks: proposals_submitted,
+        ..
+    } = run_loop.counters();
+
+    let coord_channel = run_loop.coordinator_channels();
+
+    let run_loop_thread = thread::Builder::new()
+        .name("run_loop".into())
+        .spawn(move || run_loop.start(None, 0))
+        .unwrap();
+    wait_for_runloop(&blocks_processed);
+    boot_to_epoch_3(
+        &naka_conf,
+        &blocks_processed,
+        &[stacker_sk],
+        &[sender_signer_sk],
+        &mut Some(&mut signers),
+        &mut btc_regtest_controller,
+    );
+
+    info!("Bootstrapped to Epoch-3.0 boundary, starting nakamoto miner");
+
+    let burnchain = naka_conf.get_burnchain();
+    let sortdb = burnchain.open_sortition_db(true).unwrap();
+    let (chainstate, _) = StacksChainState::open(
+        naka_conf.is_mainnet(),
+        naka_conf.burnchain.chain_id,
+        &naka_conf.get_chainstate_path_str(),
+        None,
+    )
+    .unwrap();
+
+    let block_height_pre_3_0 =
+        NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb)
+            .unwrap()
+            .unwrap()
+            .stacks_block_height;
+
+    info!("Nakamoto miner started...");
+    blind_signer(&naka_conf, &signers, proposals_submitted);
+
+    wait_for_first_naka_block_commit(60, &commits_submitted);
+
+    let mut follower_conf = naka_conf.clone();
+    follower_conf.node.miner = false;
+    follower_conf.events_observers.clear();
+    follower_conf.node.working_dir = format!("{}-follower", &naka_conf.node.working_dir);
+    follower_conf.node.seed = vec![0x01; 32];
+    follower_conf.node.local_peer_seed = vec![0x02; 32];
+
+    let rpc_port = gen_random_port();
+    let p2p_port = gen_random_port();
+
+    let localhost = "127.0.0.1";
+    follower_conf.node.rpc_bind = format!("{localhost}:{rpc_port}");
+    follower_conf.node.p2p_bind = format!("{localhost}:{p2p_port}");
+    follower_conf.node.data_url = format!("http://{localhost}:{rpc_port}");
+    follower_conf.node.p2p_address = format!("{localhost}:{p2p_port}");
+    follower_conf.node.pox_sync_sample_secs = 30;
+
+    let node_info = get_chain_info(&naka_conf);
+    follower_conf.node.add_bootstrap_node(
+        &format!(
+            "{}@{}",
+            &node_info.node_public_key.unwrap(),
+            naka_conf.node.p2p_bind
+        ),
+        naka_conf.burnchain.chain_id,
+        PEER_VERSION_TESTNET,
+    );
+
+    let mut follower_run_loop = boot_nakamoto::BootRunLoop::new(follower_conf.clone()).unwrap();
+    let follower_run_loop_stopper = follower_run_loop.get_termination_switch();
+    let follower_coord_channel = follower_run_loop.coordinator_channels();
+
+    debug!(
+        "Booting follower-thread ({},{})",
+        &follower_conf.node.p2p_bind, &follower_conf.node.rpc_bind
+    );
+    debug!(
+        "Booting follower-thread: neighbors = {:?}",
+        &follower_conf.node.bootstrap_node
+    );
+
+    // spawn a follower thread
+    let follower_thread = thread::Builder::new()
+        .name("follower-thread".into())
+        .spawn(move || follower_run_loop.start(None, 0))
+        .unwrap();
+
+    debug!("Booted follower-thread");
+
+    // Mine `tenure_count` nakamoto tenures
+    for tenure_ix in 0..tenure_count {
+        debug!("follower_bootup: Miner runs tenure {}", tenure_ix);
+        let commits_before = commits_submitted.load(Ordering::SeqCst);
+        next_block_and_process_new_stacks_block(&mut btc_regtest_controller, 60, &coord_channel)
+            .unwrap();
+
+        let mut last_tip = BlockHeaderHash([0x00; 32]);
+        let mut last_nonce = None;
+
+        debug!(
+            "follower_bootup: Miner mines interum blocks for tenure {}",
+            tenure_ix
+        );
+
+        // mine the interim blocks
+        for _ in 0..inter_blocks_per_tenure {
+            let blocks_processed_before = coord_channel
+                .lock()
+                .expect("Mutex poisoned")
+                .get_stacks_blocks_processed();
+
+            let account = loop {
+                // submit a tx so that the miner will mine an extra block
+                let Ok(account) = get_account_result(&http_origin, &sender_addr) else {
+                    debug!("follower_bootup: Failed to load miner account");
+                    thread::sleep(Duration::from_millis(100));
+                    continue;
+                };
+                break account;
+            };
+
+            let sender_nonce = account
+                .nonce
+                .max(last_nonce.as_ref().map(|ln| *ln + 1).unwrap_or(0));
+            let transfer_tx = make_stacks_transfer(
+                &sender_sk,
+                sender_nonce,
+                send_fee,
+                naka_conf.burnchain.chain_id,
+                &recipient,
+                send_amt,
+            );
+            submit_tx(&http_origin, &transfer_tx);
+
+            last_nonce = Some(sender_nonce);
+
+            let tx = StacksTransaction::consensus_deserialize(&mut &transfer_tx[..]).unwrap();
+
+            debug!("follower_bootup: Miner account: {:?}", &account);
+            debug!("follower_bootup: Miner sent {}: {:?}", &tx.txid(), &tx);
+
+            let now = get_epoch_time_secs();
+            while get_epoch_time_secs() < now + 10 {
+                let Ok(info) = get_chain_info_result(&naka_conf) else {
+                    debug!("follower_bootup: Could not get miner chain info");
+                    thread::sleep(Duration::from_millis(100));
+                    continue;
+                };
+
+                let Ok(follower_info) = get_chain_info_result(&follower_conf) else {
+                    debug!("follower_bootup: Could not get follower chain info");
+                    thread::sleep(Duration::from_millis(100));
+                    continue;
+                };
+
+                if follower_info.burn_block_height < info.burn_block_height {
+                    debug!("follower_bootup: Follower is behind miner's burnchain view");
+                    thread::sleep(Duration::from_millis(100));
+                    continue;
+                }
+
+                if info.stacks_tip == last_tip {
+                    debug!(
+                        "follower_bootup: Miner stacks tip hasn't changed ({})",
+                        &info.stacks_tip
+                    );
+                    thread::sleep(Duration::from_millis(100));
+                    continue;
+                }
+
+                let blocks_processed = coord_channel
+                    .lock()
+                    .expect("Mutex poisoned")
+                    .get_stacks_blocks_processed();
+
+                if blocks_processed > blocks_processed_before {
+                    break;
+                }
+
+                debug!("follower_bootup: No blocks processed yet");
+                thread::sleep(Duration::from_millis(100));
+            }
+
+            // compare chain tips
+            loop {
+                let Ok(info) = get_chain_info_result(&naka_conf) else {
+                    debug!("follower_bootup: failed to load tip info");
+                    thread::sleep(Duration::from_millis(100));
+                    continue;
+                };
+
+                let Ok(follower_info) = get_chain_info_result(&follower_conf) else {
+                    debug!("follower_bootup: Could not get follower chain info");
+                    thread::sleep(Duration::from_millis(100));
+                    continue;
+                };
+                if info.stacks_tip == follower_info.stacks_tip {
+                    debug!(
+                        "follower_bootup: Follower has advanced to miner's tip {}",
+                        &info.stacks_tip
+                    );
+                } else {
+                    debug!(
+                        "follower_bootup: Follower has NOT advanced to miner's tip: {} != {}",
+                        &info.stacks_tip, follower_info.stacks_tip
+                    );
+                }
+
+                last_tip = info.stacks_tip;
+                break;
+            }
+        }
+
+        debug!("follower_bootup: Wait for next block-commit");
+        let start_time = Instant::now();
+        while commits_submitted.load(Ordering::SeqCst) <= commits_before {
+            if start_time.elapsed() >= Duration::from_secs(20) {
+                panic!("Timed out waiting for block-commit");
+            }
+            thread::sleep(Duration::from_millis(100));
+        }
+        debug!("follower_bootup: Block commit submitted");
+    }
+
+    // load the chain tip, and assert that it is a nakamoto block and at least 30 blocks have advanced in epoch 3
+    let tip = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb)
+        .unwrap()
+        .unwrap();
+    info!(
+        "Latest tip";
+        "height" => tip.stacks_block_height,
+        "is_nakamoto" => tip.anchored_header.as_stacks_nakamoto().is_some(),
+    );
+
+    assert!(tip.anchored_header.as_stacks_nakamoto().is_some());
+    assert_eq!(
+        tip.stacks_block_height,
+        block_height_pre_3_0 + ((inter_blocks_per_tenure + 1) * tenure_count),
+        "Should have mined (1 + interim_blocks_per_tenure) * tenure_count nakamoto blocks"
+    );
+
+    // wait for follower to reach the chain tip
+    loop {
+        sleep_ms(1000);
+        let follower_node_info = get_chain_info(&follower_conf);
+
+        info!(
+            "Follower tip is now {}/{}",
+            &follower_node_info.stacks_tip_consensus_hash, &follower_node_info.stacks_tip
+        );
+        if follower_node_info.stacks_tip_consensus_hash == tip.consensus_hash
+            && follower_node_info.stacks_tip == tip.anchored_header.block_hash()
+        {
+            break;
+        }
+    }
+
+    // Verify both nodes have the correct chain id
+    let miner_info = get_chain_info(&naka_conf);
+    assert_eq!(miner_info.network_id, 0x87654321);
+
+    let follower_info = get_chain_info(&follower_conf);
+    assert_eq!(follower_info.network_id, 0x87654321);
+
+    coord_channel
+        .lock()
+        .expect("Mutex poisoned")
+        .stop_chains_coordinator();
+    run_loop_stopper.store(false, Ordering::SeqCst);
+
+    follower_coord_channel
+        .lock()
+        .expect("Mutex poisoned")
+        .stop_chains_coordinator();
+    follower_run_loop_stopper.store(false, Ordering::SeqCst);
+
+    run_loop_thread.join().unwrap();
+    follower_thread.join().unwrap();
+}
+
 #[test]
 #[ignore]
 /// Test out various burn operations being processed in Nakamoto.
@@ -3986,11 +4345,7 @@ fn burn_ops_integration_test() {
     );
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut naka_conf);
 
     let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
     btcd_controller
@@ -4168,6 +4523,7 @@ fn burn_ops_integration_test() {
         &signer_sk_1,
         1,
         500,
+        naka_conf.burnchain.chain_id,
         &StacksAddress::burn_address(false),
         "pox-4",
         "set-signer-key-authorization",
@@ -4370,8 +4726,14 @@ fn burn_ops_integration_test() {
                 .expect("Mutex poisoned")
                 .get_stacks_blocks_processed();
             // submit a tx so that the miner will mine an extra block
-            let transfer_tx =
-                make_stacks_transfer(&sender_sk, sender_nonce, 200, &stacker_addr_1.into(), 10000);
+            let transfer_tx = make_stacks_transfer(
+                &sender_sk,
+                sender_nonce,
+                200,
+                naka_conf.burnchain.chain_id,
+                &stacker_addr_1.into(),
+                10000,
+            );
             sender_nonce += 1;
             submit_tx(&http_origin, &transfer_tx);
 
@@ -4589,11 +4951,10 @@ fn forked_tenure_is_ignored() {
     let http_origin = format!("http://{}", &naka_conf.node.rpc_bind);
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::AnyEvent, EventKeyType::MinedBlocks],
-    });
+    test_observer::register(
+        &mut naka_conf,
+        &[EventKeyType::AnyEvent, EventKeyType::MinedBlocks],
+    );
 
     let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
     btcd_controller
@@ -4782,8 +5143,14 @@ fn forked_tenure_is_ignored() {
 
     // submit a tx so that the miner will mine an extra block
     let sender_nonce = 0;
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        naka_conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     let tx = submit_tx(&http_origin, &transfer_tx);
 
     info!("Submitted tx {tx} in Tenure C to mine a second block");
@@ -4981,6 +5348,7 @@ fn check_block_heights() {
         &sender_sk,
         sender_nonce,
         deploy_fee,
+        naka_conf.burnchain.chain_id,
         contract0_name,
         contract_clarity1,
     );
@@ -5066,6 +5434,7 @@ fn check_block_heights() {
         &sender_sk,
         sender_nonce,
         deploy_fee,
+        naka_conf.burnchain.chain_id,
         contract1_name,
         contract_clarity1,
         Some(ClarityVersion::Clarity2),
@@ -5082,6 +5451,7 @@ fn check_block_heights() {
         &sender_sk,
         sender_nonce,
         deploy_fee,
+        naka_conf.burnchain.chain_id,
         contract3_name,
         contract_clarity3,
     );
@@ -5189,8 +5559,14 @@ fn check_block_heights() {
                 .expect("Mutex poisoned")
                 .get_stacks_blocks_processed();
             // submit a tx so that the miner will mine an extra block
-            let transfer_tx =
-                make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+            let transfer_tx = make_stacks_transfer(
+                &sender_sk,
+                sender_nonce,
+                send_fee,
+                naka_conf.burnchain.chain_id,
+                &recipient,
+                send_amt,
+            );
             sender_nonce += 1;
             submit_tx(&http_origin, &transfer_tx);
 
@@ -5370,11 +5746,7 @@ fn nakamoto_attempt_time() {
 
     // only subscribe to the block proposal events
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::BlockProposal],
-    });
+    test_observer::register(&mut naka_conf, &[EventKeyType::BlockProposal]);
 
     let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
     btcd_controller
@@ -5485,8 +5857,14 @@ fn nakamoto_attempt_time() {
 
             let mut sender_nonce = account.nonce;
             for _ in 0..txs_per_block {
-                let transfer_tx =
-                    make_stacks_transfer(&sender_sk, sender_nonce, tx_fee, &recipient, amount);
+                let transfer_tx = make_stacks_transfer(
+                    &sender_sk,
+                    sender_nonce,
+                    tx_fee,
+                    naka_conf.burnchain.chain_id,
+                    &recipient,
+                    amount,
+                );
                 sender_nonce += 1;
                 submit_tx(&http_origin, &transfer_tx);
             }
@@ -5578,8 +5956,14 @@ fn nakamoto_attempt_time() {
     'submit_txs: loop {
         let acct = &mut account[acct_idx];
         for _ in 0..MAXIMUM_MEMPOOL_TX_CHAINING {
-            let transfer_tx =
-                make_stacks_transfer(&acct.privk, acct.nonce, tx_fee, &recipient, amount);
+            let transfer_tx = make_stacks_transfer(
+                &acct.privk,
+                acct.nonce,
+                tx_fee,
+                naka_conf.burnchain.chain_id,
+                &recipient,
+                amount,
+            );
             submit_tx(&http_origin, &transfer_tx);
             tx_total_size += transfer_tx.len();
             tx_count += 1;
@@ -5667,11 +6051,7 @@ fn clarity_burn_state() {
     let stacker_sk = setup_stacker(&mut naka_conf);
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::MinedBlocks],
-    });
+    test_observer::register(&mut naka_conf, &[EventKeyType::MinedBlocks]);
 
     let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
     btcd_controller
@@ -5733,6 +6113,7 @@ fn clarity_burn_state() {
         &sender_sk,
         sender_nonce,
         deploy_fee,
+        naka_conf.burnchain.chain_id,
         contract_name,
         contract,
     );
@@ -5764,6 +6145,7 @@ fn clarity_burn_state() {
                 &sender_sk,
                 sender_nonce,
                 tx_fee,
+                naka_conf.burnchain.chain_id,
                 &sender_addr,
                 contract_name,
                 "bar",
@@ -5852,6 +6234,7 @@ fn clarity_burn_state() {
                 &sender_sk,
                 sender_nonce,
                 tx_fee,
+                naka_conf.burnchain.chain_id,
                 &sender_addr,
                 contract_name,
                 "bar",
@@ -5939,11 +6322,7 @@ fn signer_chainstate() {
     let stacker_sk = setup_stacker(&mut naka_conf);
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut naka_conf);
 
     let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
     btcd_controller
@@ -5995,15 +6374,18 @@ fn signer_chainstate() {
                 .unwrap()
                 .stacks_block_height;
         let prom_http_origin = format!("http://{}", prom_bind);
-        let client = reqwest::blocking::Client::new();
-        let res = client
-            .get(&prom_http_origin)
-            .send()
-            .unwrap()
-            .text()
-            .unwrap();
-        let expected_result = format!("stacks_node_stacks_tip_height {block_height_pre_3_0}");
-        assert!(res.contains(&expected_result));
+        wait_for(10, || {
+            let client = reqwest::blocking::Client::new();
+            let res = client
+                .get(&prom_http_origin)
+                .send()
+                .unwrap()
+                .text()
+                .unwrap();
+            let expected_result = format!("stacks_node_stacks_tip_height {block_height_pre_3_0}");
+            Ok(res.contains(&expected_result))
+        })
+        .expect("Failed waiting for prometheus metrics to update")
     }
 
     info!("Nakamoto miner started...");
@@ -6018,6 +6400,7 @@ fn signer_chainstate() {
             .clone()
             .unwrap_or("".into()),
         false,
+        CHAIN_ID_TESTNET,
     );
 
     wait_for_first_naka_block_commit(60, &commits_submitted);
@@ -6148,8 +6531,14 @@ fn signer_chainstate() {
 
         // submit a tx to trigger an intermediate block
         let sender_nonce = i;
-        let transfer_tx =
-            make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+        let transfer_tx = make_stacks_transfer(
+            &sender_sk,
+            sender_nonce,
+            send_fee,
+            naka_conf.burnchain.chain_id,
+            &recipient,
+            send_amt,
+        );
         submit_tx(&http_origin, &transfer_tx);
 
         let timer = Instant::now();
@@ -6547,11 +6936,7 @@ fn continue_tenure_extend() {
     let stacker_sk = setup_stacker(&mut naka_conf);
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut naka_conf);
 
     let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
     btcd_controller
@@ -6605,15 +6990,18 @@ fn continue_tenure_extend() {
     #[cfg(feature = "monitoring_prom")]
     {
         let prom_http_origin = format!("http://{}", prom_bind);
-        let client = reqwest::blocking::Client::new();
-        let res = client
-            .get(&prom_http_origin)
-            .send()
-            .unwrap()
-            .text()
-            .unwrap();
-        let expected_result = format!("stacks_node_stacks_tip_height {block_height_pre_3_0}");
-        assert!(res.contains(&expected_result));
+        wait_for(10, || {
+            let client = reqwest::blocking::Client::new();
+            let res = client
+                .get(&prom_http_origin)
+                .send()
+                .unwrap()
+                .text()
+                .unwrap();
+            let expected_result = format!("stacks_node_stacks_tip_height {block_height_pre_3_0}");
+            Ok(res.contains(&expected_result))
+        })
+        .expect("Prometheus metrics did not update");
     }
 
     info!("Nakamoto miner started...");
@@ -6664,7 +7052,14 @@ fn continue_tenure_extend() {
     .unwrap();
 
     // Submit a TX
-    let transfer_tx = make_stacks_transfer(&sender_sk, 0, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        0,
+        send_fee,
+        naka_conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     let transfer_tx_hex = format!("0x{}", to_hex(&transfer_tx));
 
     let tip = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb)
@@ -6801,15 +7196,19 @@ fn continue_tenure_extend() {
     #[cfg(feature = "monitoring_prom")]
     {
         let prom_http_origin = format!("http://{}", prom_bind);
-        let client = reqwest::blocking::Client::new();
-        let res = client
-            .get(&prom_http_origin)
-            .send()
-            .unwrap()
-            .text()
-            .unwrap();
-        let expected_result = format!("stacks_node_stacks_tip_height {}", tip.stacks_block_height);
-        assert!(res.contains(&expected_result));
+        wait_for(10, || {
+            let client = reqwest::blocking::Client::new();
+            let res = client
+                .get(&prom_http_origin)
+                .send()
+                .unwrap()
+                .text()
+                .unwrap();
+            let expected_result =
+                format!("stacks_node_stacks_tip_height {}", tip.stacks_block_height);
+            Ok(res.contains(&expected_result))
+        })
+        .expect("Prometheus metrics did not update");
     }
 
     coord_channel
@@ -6855,11 +7254,7 @@ fn check_block_times() {
     let stacker_sk = setup_stacker(&mut naka_conf);
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut naka_conf);
 
     let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
     btcd_controller
@@ -6896,6 +7291,7 @@ fn check_block_times() {
         &sender_sk,
         sender_nonce,
         deploy_fee,
+        naka_conf.burnchain.chain_id,
         contract0_name,
         contract_clarity1,
     );
@@ -6939,6 +7335,7 @@ fn check_block_times() {
         &sender_sk,
         sender_nonce,
         deploy_fee,
+        naka_conf.burnchain.chain_id,
         contract1_name,
         contract_clarity1,
         Some(ClarityVersion::Clarity2),
@@ -6956,6 +7353,7 @@ fn check_block_times() {
         &sender_sk,
         sender_nonce,
         deploy_fee,
+        naka_conf.burnchain.chain_id,
         contract3_name,
         contract_clarity3,
     );
@@ -7051,8 +7449,14 @@ fn check_block_times() {
     info!("Mining Nakamoto block");
 
     // submit a tx so that the miner will mine an extra block
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        naka_conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     sender_nonce += 1;
     submit_tx(&http_origin, &transfer_tx);
 
@@ -7132,8 +7536,14 @@ fn check_block_times() {
         .get_stacks_blocks_processed();
 
     // submit a tx so that the miner will mine an extra block
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        naka_conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     submit_tx(&http_origin, &transfer_tx);
 
     loop {
@@ -7324,11 +7734,7 @@ fn check_block_info() {
     let stacker_sk = setup_stacker(&mut naka_conf);
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut naka_conf);
 
     let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
     btcd_controller
@@ -7383,6 +7789,7 @@ fn check_block_info() {
         &sender_sk,
         sender_nonce,
         deploy_fee,
+        naka_conf.burnchain.chain_id,
         contract0_name,
         contract_clarity1,
     );
@@ -7421,6 +7828,7 @@ fn check_block_info() {
         &sender_sk,
         sender_nonce,
         deploy_fee,
+        naka_conf.burnchain.chain_id,
         contract1_name,
         contract_clarity1,
         Some(ClarityVersion::Clarity2),
@@ -7453,6 +7861,7 @@ fn check_block_info() {
         &sender_sk,
         sender_nonce,
         deploy_fee,
+        naka_conf.burnchain.chain_id,
         contract3_name,
         contract_clarity3,
     );
@@ -7559,8 +7968,14 @@ fn check_block_info() {
         .get_stacks_blocks_processed();
 
     // submit a tx so that the miner will mine an extra block
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        naka_conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     sender_nonce += 1;
     submit_tx(&http_origin, &transfer_tx);
 
@@ -7666,8 +8081,14 @@ fn check_block_info() {
         .get_stacks_blocks_processed();
 
     // submit a tx so that the miner will mine an extra block
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        naka_conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     submit_tx(&http_origin, &transfer_tx);
 
     loop {
@@ -7844,11 +8265,7 @@ fn check_block_info_rewards() {
     let stacker_sk = setup_stacker(&mut naka_conf);
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut naka_conf);
 
     let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
     btcd_controller
@@ -7896,6 +8313,7 @@ fn check_block_info_rewards() {
         &sender_sk,
         sender_nonce,
         deploy_fee,
+        naka_conf.burnchain.chain_id,
         contract0_name,
         contract_clarity1,
     );
@@ -7934,6 +8352,7 @@ fn check_block_info_rewards() {
         &sender_sk,
         sender_nonce,
         deploy_fee,
+        naka_conf.burnchain.chain_id,
         contract1_name,
         contract_clarity1,
         Some(ClarityVersion::Clarity2),
@@ -7959,6 +8378,7 @@ fn check_block_info_rewards() {
         &sender_sk,
         sender_nonce,
         deploy_fee,
+        naka_conf.burnchain.chain_id,
         contract3_name,
         contract_clarity3,
     );
@@ -7979,8 +8399,14 @@ fn check_block_info_rewards() {
         .get_stacks_blocks_processed();
 
     // submit a tx so that the miner will mine an extra block
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        naka_conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     sender_nonce += 1;
     submit_tx(&http_origin, &transfer_tx);
 
@@ -8006,8 +8432,14 @@ fn check_block_info_rewards() {
         .get_stacks_blocks_processed();
 
     // submit a tx so that the miner will mine an extra block
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        naka_conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     submit_tx(&http_origin, &transfer_tx);
 
     loop {
@@ -8154,6 +8586,7 @@ fn mock_mining() {
 
     let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None);
     naka_conf.miner.wait_on_interim_blocks = Duration::from_secs(1);
+    naka_conf.node.pox_sync_sample_secs = 30;
     let sender_sk = Secp256k1PrivateKey::new();
     let sender_signer_sk = Secp256k1PrivateKey::new();
     let sender_signer_addr = tests::to_addr(&sender_signer_sk);
@@ -8166,16 +8599,16 @@ fn mock_mining() {
     let send_amt = 100;
     let send_fee = 180;
 
-    let node_1_rpc = 51024;
-    let node_1_p2p = 51023;
-    let node_2_rpc = 51026;
-    let node_2_p2p = 51025;
+    let node_1_rpc = gen_random_port();
+    let node_1_p2p = gen_random_port();
+    let node_2_rpc = gen_random_port();
+    let node_2_p2p = gen_random_port();
 
     let localhost = "127.0.0.1";
-    naka_conf.node.rpc_bind = format!("{}:{}", localhost, node_1_rpc);
-    naka_conf.node.p2p_bind = format!("{}:{}", localhost, node_1_p2p);
-    naka_conf.node.data_url = format!("http://{}:{}", localhost, node_1_rpc);
-    naka_conf.node.p2p_address = format!("{}:{}", localhost, node_1_p2p);
+    naka_conf.node.rpc_bind = format!("{localhost}:{node_1_rpc}");
+    naka_conf.node.p2p_bind = format!("{localhost}:{node_1_p2p}");
+    naka_conf.node.data_url = format!("http://{localhost}:{node_1_rpc}");
+    naka_conf.node.p2p_address = format!("{localhost}:{node_1_p2p}");
     let http_origin = format!("http://{}", &naka_conf.node.rpc_bind);
 
     naka_conf.add_initial_balance(
@@ -8190,11 +8623,7 @@ fn mock_mining() {
     let stacker_sk = setup_stacker(&mut naka_conf);
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut naka_conf);
 
     let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
     btcd_controller
@@ -8259,10 +8688,10 @@ fn mock_mining() {
     follower_conf.node.seed = vec![0x01; 32];
     follower_conf.node.local_peer_seed = vec![0x02; 32];
 
-    follower_conf.node.rpc_bind = format!("{}:{}", localhost, node_2_rpc);
-    follower_conf.node.p2p_bind = format!("{}:{}", localhost, node_2_p2p);
-    follower_conf.node.data_url = format!("http://{}:{}", localhost, node_2_rpc);
-    follower_conf.node.p2p_address = format!("{}:{}", localhost, node_2_p2p);
+    follower_conf.node.rpc_bind = format!("{localhost}:{node_2_rpc}");
+    follower_conf.node.p2p_bind = format!("{localhost}:{node_2_p2p}");
+    follower_conf.node.data_url = format!("http://{localhost}:{node_2_rpc}");
+    follower_conf.node.p2p_address = format!("{localhost}:{node_2_p2p}");
 
     let node_info = get_chain_info(&naka_conf);
     follower_conf.node.add_bootstrap_node(
@@ -8271,7 +8700,7 @@ fn mock_mining() {
             &node_info.node_public_key.unwrap(),
             naka_conf.node.p2p_bind
         ),
-        CHAIN_ID_TESTNET,
+        naka_conf.burnchain.chain_id,
         PEER_VERSION_TESTNET,
     );
 
@@ -8336,8 +8765,14 @@ fn mock_mining() {
                 .get_stacks_blocks_processed();
             // submit a tx so that the miner will mine an extra block
             let sender_nonce = tenure_ix * inter_blocks_per_tenure + interim_block_ix;
-            let transfer_tx =
-                make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+            let transfer_tx = make_stacks_transfer(
+                &sender_sk,
+                sender_nonce,
+                send_fee,
+                naka_conf.burnchain.chain_id,
+                &recipient,
+                send_amt,
+            );
             submit_tx(&http_origin, &transfer_tx);
 
             loop {
@@ -8457,11 +8892,7 @@ fn utxo_check_on_startup_panic() {
     naka_conf.miner.wait_on_interim_blocks = Duration::from_secs(1000);
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut naka_conf);
 
     let mut epochs = NAKAMOTO_INTEGRATION_EPOCHS.to_vec();
     let (last, rest) = epochs.split_last_mut().unwrap();
@@ -8537,11 +8968,7 @@ fn utxo_check_on_startup_recover() {
     naka_conf.miner.wait_on_interim_blocks = Duration::from_secs(1000);
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut naka_conf);
 
     let mut epochs = NAKAMOTO_INTEGRATION_EPOCHS.to_vec();
     let (last, rest) = epochs.split_last_mut().unwrap();
@@ -8618,11 +9045,7 @@ fn v3_signer_api_endpoint() {
 
     // only subscribe to the block proposal events
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::BlockProposal],
-    });
+    test_observer::register(&mut conf, &[EventKeyType::BlockProposal]);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -8728,8 +9151,14 @@ fn v3_signer_api_endpoint() {
         .get_stacks_blocks_processed();
     // submit a tx so that the miner will mine an extra stacks block
     let sender_nonce = 0;
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     submit_tx(&http_origin, &transfer_tx);
 
     wait_for(30, || {
@@ -8802,11 +9231,7 @@ fn skip_mining_long_tx() {
     let http_origin = format!("http://{}", &naka_conf.node.rpc_bind);
 
     test_observer::spawn();
-    let observer_port = test_observer::EVENT_OBSERVER_PORT;
-    naka_conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{observer_port}"),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut naka_conf);
 
     let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
     btcd_controller
@@ -8887,6 +9312,7 @@ fn skip_mining_long_tx() {
                 &sender_2_sk,
                 0,
                 9_000,
+                naka_conf.burnchain.chain_id,
                 "large_contract",
                 &format!(
                     "(define-constant INP_LIST (list {input_list}))
@@ -8910,8 +9336,14 @@ fn skip_mining_long_tx() {
 
             TEST_SKIP_P2P_BROADCAST.lock().unwrap().replace(false);
         } else {
-            let transfer_tx =
-                make_stacks_transfer(&sender_1_sk, i - 1, send_fee, &recipient, send_amt);
+            let transfer_tx = make_stacks_transfer(
+                &sender_1_sk,
+                i - 1,
+                send_fee,
+                naka_conf.burnchain.chain_id,
+                &recipient,
+                send_amt,
+            );
             submit_tx(&http_origin, &transfer_tx);
 
             wait_for(30, || {
diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs
index 0294876931..d6373a3b44 100644
--- a/testnet/stacks-node/src/tests/neon_integrations.rs
+++ b/testnet/stacks-node/src/tests/neon_integrations.rs
@@ -53,7 +53,6 @@ use stacks::net::api::getaccount::AccountEntryResponse;
 use stacks::net::api::getcontractsrc::ContractSrcResponse;
 use stacks::net::api::getinfo::RPCPeerInfoData;
 use stacks::net::api::getpoxinfo::RPCPoxInfoData;
-use stacks::net::api::getstackers::GetStackersResponse;
 use stacks::net::api::gettransaction_unconfirmed::UnconfirmedTransactionResponse;
 use stacks::net::api::postblock::StacksBlockAcceptedData;
 use stacks::net::api::postfeerate::RPCFeeEstimateResponse;
@@ -206,7 +205,9 @@ pub mod test_observer {
     use warp::Filter;
     use {tokio, warp};
 
+    use crate::config::{EventKeyType, EventObserverConfig};
     use crate::event_dispatcher::{MinedBlockEvent, MinedMicroblockEvent, MinedNakamotoBlockEvent};
+    use crate::Config;
 
     pub const EVENT_OBSERVER_PORT: u16 = 50303;
 
@@ -631,6 +632,18 @@ pub mod test_observer {
             Err(format!("Missing the following burn blocks: {missing:?}"))
         }
     }
+
+    pub fn register(config: &mut Config, event_keys: &[EventKeyType]) {
+        config.events_observers.insert(EventObserverConfig {
+            endpoint: format!("localhost:{EVENT_OBSERVER_PORT}"),
+            events_keys: event_keys.to_vec(),
+            timeout_ms: 1000,
+        });
+    }
+
+    pub fn register_any(config: &mut Config) {
+        self::register(config, &[EventKeyType::AnyEvent]);
+    }
 }
 
 const PANIC_TIMEOUT_SECS: u64 = 600;
@@ -1024,11 +1037,7 @@ fn bitcoind_integration_test() {
     conf.burnchain.max_rbf = 1000000;
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -1137,11 +1146,7 @@ fn confirm_unparsed_ongoing_ops() {
     conf.burnchain.max_rbf = 1000000;
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -1437,16 +1442,6 @@ pub fn get_contract_src(
     }
 }
 
-pub fn get_stacker_set(http_origin: &str, reward_cycle: u64) -> GetStackersResponse {
-    let client = reqwest::blocking::Client::new();
-    let path = format!("{}/v3/stacker_set/{}", http_origin, reward_cycle);
-    let res = client.get(&path).send().unwrap();
-
-    info!("Got stacker_set response {:?}", &res);
-    let res = res.json::<GetStackersResponse>().unwrap();
-    res
-}
-
 #[test]
 #[ignore]
 fn deep_contract() {
@@ -1468,11 +1463,7 @@ fn deep_contract() {
     let (mut conf, _miner_account) = neon_integration_test_conf();
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let spender_bal = 10_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64);
 
@@ -1526,6 +1517,7 @@ fn deep_contract() {
         &spender_sk,
         0,
         1000,
+        conf.burnchain.chain_id,
         "test-publish",
         &exceeds_stack_depth_list,
     );
@@ -1572,11 +1564,7 @@ fn bad_microblock_pubkey() {
     let (mut conf, _miner_account) = neon_integration_test_conf();
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -1657,11 +1645,7 @@ fn liquid_ustx_integration() {
     let (mut conf, _miner_account) = neon_integration_test_conf();
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let spender_bal = 10_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64);
 
@@ -1711,11 +1695,25 @@ fn liquid_ustx_integration() {
 
     let _sort_height = channel.get_sortitions_processed();
 
-    let publish = make_contract_publish(&spender_sk, 0, 1000, "caller", caller_src);
+    let publish = make_contract_publish(
+        &spender_sk,
+        0,
+        1000,
+        conf.burnchain.chain_id,
+        "caller",
+        caller_src,
+    );
 
     let replaced_txid = submit_tx(&http_origin, &publish);
 
-    let publish = make_contract_publish(&spender_sk, 0, 1100, "caller", caller_src);
+    let publish = make_contract_publish(
+        &spender_sk,
+        0,
+        1100,
+        conf.burnchain.chain_id,
+        "caller",
+        caller_src,
+    );
     submit_tx(&http_origin, &publish);
 
     let dropped_txs = test_observer::get_memtx_drops();
@@ -1732,6 +1730,7 @@ fn liquid_ustx_integration() {
         &spender_sk,
         1,
         1000,
+        conf.burnchain.chain_id,
         &spender_addr,
         "caller",
         "execute",
@@ -1785,11 +1784,7 @@ fn lockup_integration() {
     let (mut conf, _miner_account) = neon_integration_test_conf();
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -1902,10 +1897,7 @@ fn stx_transfer_btc_integration_test() {
     let (mut conf, _miner_account) = neon_integration_test_conf();
 
     test_observer::spawn();
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     conf.initial_balances.push(InitialBalance {
         address: spender_addr.clone(),
@@ -2171,10 +2163,7 @@ fn stx_delegate_btc_integration_test() {
     conf.burnchain.pox_2_activation = Some(3);
 
     test_observer::spawn();
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -2301,6 +2290,7 @@ fn stx_delegate_btc_integration_test() {
         &recipient_sk,
         0,
         293,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-2",
         "delegate-stack-stx",
@@ -2459,10 +2449,7 @@ fn stack_stx_burn_op_test() {
     conf.burnchain.pox_2_activation = Some(3);
 
     test_observer::spawn();
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -2592,6 +2579,7 @@ fn stack_stx_burn_op_test() {
         &signer_sk_1,
         0,
         500,
+        conf.burnchain.chain_id,
         &boot_code_addr(false),
         POX_4_NAME,
         "set-signer-key-authorization",
@@ -2864,10 +2852,7 @@ fn vote_for_aggregate_key_burn_op_test() {
     conf.burnchain.pox_2_activation = Some(3);
 
     test_observer::spawn();
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -2956,6 +2941,7 @@ fn vote_for_aggregate_key_burn_op_test() {
         &spender_sk,
         0,
         500,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox-4",
         "stack-stx",
@@ -3178,6 +3164,7 @@ fn bitcoind_resubmission_test() {
             &spender_sk,
             0,
             100,
+            conf.burnchain.chain_id,
             &PrincipalData::from(StacksAddress::burn_address(false)),
             1000,
         );
@@ -3465,11 +3452,7 @@ fn microblock_fork_poison_integration_test() {
     conf.miner.subsequent_attempt_time_ms = i64::MAX as u64;
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -3525,12 +3508,24 @@ fn microblock_fork_poison_integration_test() {
     info!("Test microblock");
 
     let recipient = StacksAddress::from_string(ADDR_4).unwrap();
-    let unconfirmed_tx_bytes =
-        make_stacks_transfer_mblock_only(&spender_sk, 0, 1000, &recipient.into(), 1000);
+    let unconfirmed_tx_bytes = make_stacks_transfer_mblock_only(
+        &spender_sk,
+        0,
+        1000,
+        conf.burnchain.chain_id,
+        &recipient.into(),
+        1000,
+    );
     let unconfirmed_tx =
         StacksTransaction::consensus_deserialize(&mut &unconfirmed_tx_bytes[..]).unwrap();
-    let second_unconfirmed_tx_bytes =
-        make_stacks_transfer_mblock_only(&second_spender_sk, 0, 1000, &recipient.into(), 1500);
+    let second_unconfirmed_tx_bytes = make_stacks_transfer_mblock_only(
+        &second_spender_sk,
+        0,
+        1000,
+        conf.burnchain.chain_id,
+        &recipient.into(),
+        1500,
+    );
     let second_unconfirmed_tx =
         StacksTransaction::consensus_deserialize(&mut &second_unconfirmed_tx_bytes[..]).unwrap();
 
@@ -3706,11 +3701,7 @@ fn microblock_integration_test() {
     conf.node.wait_time_for_microblocks = 0;
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -3763,7 +3754,14 @@ fn microblock_integration_test() {
 
     // okay, let's push a transaction that is marked microblock only!
     let recipient = StacksAddress::from_string(ADDR_4).unwrap();
-    let tx = make_stacks_transfer_mblock_only(&spender_sk, 0, 1000, &recipient.into(), 1000);
+    let tx = make_stacks_transfer_mblock_only(
+        &spender_sk,
+        0,
+        1000,
+        conf.burnchain.chain_id,
+        &recipient.into(),
+        1000,
+    );
     submit_tx(&http_origin, &tx);
 
     info!("Try to mine a microblock-only tx");
@@ -3793,12 +3791,24 @@ fn microblock_integration_test() {
 
     // push another two transactions that are marked microblock only
     let recipient = StacksAddress::from_string(ADDR_4).unwrap();
-    let unconfirmed_tx_bytes =
-        make_stacks_transfer_mblock_only(&spender_sk, 1, 1000, &recipient.into(), 1000);
+    let unconfirmed_tx_bytes = make_stacks_transfer_mblock_only(
+        &spender_sk,
+        1,
+        1000,
+        conf.burnchain.chain_id,
+        &recipient.into(),
+        1000,
+    );
     let unconfirmed_tx =
         StacksTransaction::consensus_deserialize(&mut &unconfirmed_tx_bytes[..]).unwrap();
-    let second_unconfirmed_tx_bytes =
-        make_stacks_transfer_mblock_only(&second_spender_sk, 0, 1000, &recipient.into(), 1500);
+    let second_unconfirmed_tx_bytes = make_stacks_transfer_mblock_only(
+        &second_spender_sk,
+        0,
+        1000,
+        conf.burnchain.chain_id,
+        &recipient.into(),
+        1500,
+    );
     let second_unconfirmed_tx =
         StacksTransaction::consensus_deserialize(&mut &second_unconfirmed_tx_bytes[..]).unwrap();
 
@@ -4121,6 +4131,7 @@ fn microblock_integration_test() {
             &spender_sk,
             next_nonce,
             1000,
+            conf.burnchain.chain_id,
             &recipient.into(),
             1000,
         );
@@ -4204,6 +4215,14 @@ fn filter_low_fee_tx_integration_test() {
         .collect();
     let spender_addrs: Vec<PrincipalData> = spender_sks.iter().map(|x| to_addr(x).into()).collect();
 
+    let (mut conf, _) = neon_integration_test_conf();
+    for spender_addr in spender_addrs.iter() {
+        conf.initial_balances.push(InitialBalance {
+            address: spender_addr.clone(),
+            amount: 1049230,
+        });
+    }
+
     let txs: Vec<_> = spender_sks
         .iter()
         .enumerate()
@@ -4212,22 +4231,28 @@ fn filter_low_fee_tx_integration_test() {
 
             if ix < 5 {
                 // low-fee
-                make_stacks_transfer(&spender_sk, 0, 1000 + (ix as u64), &recipient.into(), 1000)
+                make_stacks_transfer(
+                    &spender_sk,
+                    0,
+                    1000 + (ix as u64),
+                    conf.burnchain.chain_id,
+                    &recipient.into(),
+                    1000,
+                )
             } else {
                 // high-fee
-                make_stacks_transfer(&spender_sk, 0, 2000 + (ix as u64), &recipient.into(), 1000)
+                make_stacks_transfer(
+                    &spender_sk,
+                    0,
+                    2000 + (ix as u64),
+                    conf.burnchain.chain_id,
+                    &recipient.into(),
+                    1000,
+                )
             }
         })
         .collect();
 
-    let (mut conf, _) = neon_integration_test_conf();
-    for spender_addr in spender_addrs.iter() {
-        conf.initial_balances.push(InitialBalance {
-            address: spender_addr.clone(),
-            amount: 1049230,
-        });
-    }
-
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
         .start_bitcoind()
@@ -4298,15 +4323,6 @@ fn filter_long_runtime_tx_integration_test() {
         .collect();
     let spender_addrs: Vec<PrincipalData> = spender_sks.iter().map(|x| to_addr(x).into()).collect();
 
-    let txs: Vec<_> = spender_sks
-        .iter()
-        .enumerate()
-        .map(|(ix, spender_sk)| {
-            let recipient = StacksAddress::from_string(ADDR_4).unwrap();
-            make_stacks_transfer(&spender_sk, 0, 1000 + (ix as u64), &recipient.into(), 1000)
-        })
-        .collect();
-
     let (mut conf, _) = neon_integration_test_conf();
     for spender_addr in spender_addrs.iter() {
         conf.initial_balances.push(InitialBalance {
@@ -4319,6 +4335,22 @@ fn filter_long_runtime_tx_integration_test() {
     conf.miner.first_attempt_time_ms = 0;
     conf.miner.subsequent_attempt_time_ms = 0;
 
+    let txs: Vec<_> = spender_sks
+        .iter()
+        .enumerate()
+        .map(|(ix, spender_sk)| {
+            let recipient = StacksAddress::from_string(ADDR_4).unwrap();
+            make_stacks_transfer(
+                &spender_sk,
+                0,
+                1000 + (ix as u64),
+                conf.burnchain.chain_id,
+                &recipient.into(),
+                1000,
+            )
+        })
+        .collect();
+
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
         .start_bitcoind()
@@ -4384,8 +4416,6 @@ fn miner_submit_twice() {
        (define-private (bar)
          (foo 56))
     ";
-    let tx_1 = make_contract_publish(&spender_sk, 0, 50_000, "first-contract", contract_content);
-    let tx_2 = make_contract_publish(&spender_sk, 1, 50_000, "second-contract", contract_content);
 
     let (mut conf, _) = neon_integration_test_conf();
     conf.initial_balances.push(InitialBalance {
@@ -4398,6 +4428,23 @@ fn miner_submit_twice() {
     conf.miner.first_attempt_time_ms = 20;
     conf.miner.subsequent_attempt_time_ms = 30_000;
 
+    let tx_1 = make_contract_publish(
+        &spender_sk,
+        0,
+        50_000,
+        conf.burnchain.chain_id,
+        "first-contract",
+        contract_content,
+    );
+    let tx_2 = make_contract_publish(
+        &spender_sk,
+        1,
+        50_000,
+        conf.burnchain.chain_id,
+        "second-contract",
+        contract_content,
+    );
+
     // note: this test depends on timing of how long it takes to assemble a block,
     //  but it won't flake if the miner behaves correctly: a correct miner should
     //  always be able to mine both transactions by the end of this test. an incorrect
@@ -4476,18 +4523,28 @@ fn size_check_integration_test() {
         .collect();
     let spender_addrs: Vec<PrincipalData> = spender_sks.iter().map(|x| to_addr(x).into()).collect();
 
+    let (mut conf, miner_account) = neon_integration_test_conf();
+
     // make a bunch of txs that will only fit one per block.
     let txs: Vec<_> = spender_sks
         .iter()
         .enumerate()
         .map(|(ix, spender_sk)| {
             if ix % 2 == 0 {
-                make_contract_publish(spender_sk, 0, 1049230, "large-0", &giant_contract)
+                make_contract_publish(
+                    spender_sk,
+                    0,
+                    1049230,
+                    conf.burnchain.chain_id,
+                    "large-0",
+                    &giant_contract,
+                )
             } else {
                 let tx = make_contract_publish_microblock_only(
                     spender_sk,
                     0,
                     1049230,
+                    conf.burnchain.chain_id,
                     "large-0",
                     &giant_contract,
                 );
@@ -4498,8 +4555,6 @@ fn size_check_integration_test() {
         })
         .collect();
 
-    let (mut conf, miner_account) = neon_integration_test_conf();
-
     for spender_addr in spender_addrs.iter() {
         conf.initial_balances.push(InitialBalance {
             address: spender_addr.clone(),
@@ -4644,6 +4699,8 @@ fn size_overflow_unconfirmed_microblocks_integration_test() {
         .collect();
     let spender_addrs: Vec<PrincipalData> = spender_sks.iter().map(|x| to_addr(x).into()).collect();
 
+    let (mut conf, miner_account) = neon_integration_test_conf();
+
     let txs: Vec<Vec<_>> = spender_sks
         .iter()
         .enumerate()
@@ -4654,6 +4711,7 @@ fn size_overflow_unconfirmed_microblocks_integration_test() {
                     spender_sk,
                     0,
                     1100000,
+                    conf.burnchain.chain_id,
                     "large-0",
                     &giant_contract,
                 )]
@@ -4664,6 +4722,7 @@ fn size_overflow_unconfirmed_microblocks_integration_test() {
                         spender_sk,
                         i as u64,
                         1100000,
+                        conf.burnchain.chain_id,
                         &format!("small-{}", i),
                         &small_contract,
                     );
@@ -4674,8 +4733,6 @@ fn size_overflow_unconfirmed_microblocks_integration_test() {
         })
         .collect();
 
-    let (mut conf, miner_account) = neon_integration_test_conf();
-
     for spender_addr in spender_addrs.iter() {
         conf.initial_balances.push(InitialBalance {
             address: spender_addr.clone(),
@@ -4692,10 +4749,7 @@ fn size_overflow_unconfirmed_microblocks_integration_test() {
     conf.miner.subsequent_attempt_time_ms = i64::MAX as u64;
 
     test_observer::spawn();
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -4854,20 +4908,6 @@ fn size_overflow_unconfirmed_stream_microblocks_integration_test() {
         .collect();
     let spender_addrs: Vec<PrincipalData> = spender_sks.iter().map(|x| to_addr(x).into()).collect();
 
-    let txs: Vec<_> = spender_sks
-        .iter()
-        .map(|spender_sk| {
-            let tx = make_contract_publish_microblock_only(
-                spender_sk,
-                0,
-                600000,
-                "small",
-                &small_contract,
-            );
-            tx
-        })
-        .collect();
-
     let (mut conf, miner_account) = neon_integration_test_conf();
 
     for spender_addr in spender_addrs.iter() {
@@ -4887,11 +4927,23 @@ fn size_overflow_unconfirmed_stream_microblocks_integration_test() {
     conf.miner.first_attempt_time_ms = i64::MAX as u64;
     conf.miner.subsequent_attempt_time_ms = i64::MAX as u64;
 
+    let txs: Vec<_> = spender_sks
+        .iter()
+        .map(|spender_sk| {
+            let tx = make_contract_publish_microblock_only(
+                spender_sk,
+                0,
+                600000,
+                conf.burnchain.chain_id,
+                "small",
+                &small_contract,
+            );
+            tx
+        })
+        .collect();
+
     test_observer::spawn();
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -5044,20 +5096,6 @@ fn size_overflow_unconfirmed_invalid_stream_microblocks_integration_test() {
         .collect();
     let spender_addrs: Vec<PrincipalData> = spender_sks.iter().map(|x| to_addr(x).into()).collect();
 
-    let txs: Vec<Vec<_>> = spender_sks
-        .iter()
-        .map(|spender_sk| {
-            let tx = make_contract_publish_microblock_only(
-                spender_sk,
-                0,
-                1149230,
-                "small",
-                &small_contract,
-            );
-            tx
-        })
-        .collect();
-
     let (mut conf, miner_account) = neon_integration_test_conf();
 
     for spender_addr in spender_addrs.iter() {
@@ -5074,6 +5112,21 @@ fn size_overflow_unconfirmed_invalid_stream_microblocks_integration_test() {
     conf.node.max_microblocks = 65536;
     conf.burnchain.max_rbf = 1000000;
 
+    let txs: Vec<Vec<_>> = spender_sks
+        .iter()
+        .map(|spender_sk| {
+            let tx = make_contract_publish_microblock_only(
+                spender_sk,
+                0,
+                1149230,
+                conf.burnchain.chain_id,
+                "small",
+                &small_contract,
+            );
+            tx
+        })
+        .collect();
+
     let mut epochs = core::STACKS_EPOCHS_REGTEST.to_vec();
     epochs[1].block_limit = core::BLOCK_LIMIT_MAINNET_20;
     conf.burnchain.epochs = Some(epochs);
@@ -5082,10 +5135,7 @@ fn size_overflow_unconfirmed_invalid_stream_microblocks_integration_test() {
     conf.miner.subsequent_attempt_time_ms = i64::MAX as u64;
 
     test_observer::spawn();
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -5210,6 +5260,27 @@ fn runtime_overflow_unconfirmed_microblocks_integration_test() {
     let spender_addrs_c32: Vec<StacksAddress> =
         spender_sks.iter().map(|x| to_addr(x).into()).collect();
 
+    let (mut conf, miner_account) = neon_integration_test_conf();
+
+    for spender_addr in spender_addrs.iter() {
+        conf.initial_balances.push(InitialBalance {
+            address: spender_addr.clone(),
+            amount: 1049230,
+        });
+    }
+
+    conf.node.mine_microblocks = true;
+    conf.node.wait_time_for_microblocks = 0;
+    conf.node.microblock_frequency = 15000;
+    conf.miner.microblock_attempt_time_ms = 120_000;
+
+    conf.miner.first_attempt_time_ms = i64::MAX as u64;
+    conf.miner.subsequent_attempt_time_ms = i64::MAX as u64;
+
+    let mut epochs = core::STACKS_EPOCHS_REGTEST.to_vec();
+    epochs[1].block_limit = core::BLOCK_LIMIT_MAINNET_20;
+    conf.burnchain.epochs = Some(epochs);
+
     let txs: Vec<Vec<_>> = spender_sks
         .iter()
         .enumerate()
@@ -5220,6 +5291,7 @@ fn runtime_overflow_unconfirmed_microblocks_integration_test() {
                     spender_sk,
                     0,
                     1049230,
+                    conf.burnchain.chain_id,
                     &format!("large-{}", ix),
                     &format!("
                         ;; a single one of these transactions consumes over half the runtime budget
@@ -5274,6 +5346,7 @@ fn runtime_overflow_unconfirmed_microblocks_integration_test() {
                         spender_sk,
                         i as u64,
                         210000,
+                        conf.burnchain.chain_id,
                         &format!("small-{}-{}", ix, i),
                         &format!("
                             ;; a single one of these transactions consumes over half the runtime budget
@@ -5326,32 +5399,8 @@ fn runtime_overflow_unconfirmed_microblocks_integration_test() {
         })
         .collect();
 
-    let (mut conf, miner_account) = neon_integration_test_conf();
-
-    for spender_addr in spender_addrs.iter() {
-        conf.initial_balances.push(InitialBalance {
-            address: spender_addr.clone(),
-            amount: 1049230,
-        });
-    }
-
-    conf.node.mine_microblocks = true;
-    conf.node.wait_time_for_microblocks = 0;
-    conf.node.microblock_frequency = 15000;
-    conf.miner.microblock_attempt_time_ms = 120_000;
-
-    conf.miner.first_attempt_time_ms = i64::MAX as u64;
-    conf.miner.subsequent_attempt_time_ms = i64::MAX as u64;
-
-    let mut epochs = core::STACKS_EPOCHS_REGTEST.to_vec();
-    epochs[1].block_limit = core::BLOCK_LIMIT_MAINNET_20;
-    conf.burnchain.epochs = Some(epochs);
-
     test_observer::spawn();
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -5519,11 +5568,7 @@ fn block_replay_integration_test() {
     conf.miner.subsequent_attempt_time_ms = i64::MAX as u64;
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -5571,7 +5616,14 @@ fn block_replay_integration_test() {
     assert_eq!(account.nonce, 0);
 
     let recipient = StacksAddress::from_string(ADDR_4).unwrap();
-    let tx = make_stacks_transfer(&spender_sk, 0, 1000, &recipient.into(), 1000);
+    let tx = make_stacks_transfer(
+        &spender_sk,
+        0,
+        1000,
+        conf.burnchain.chain_id,
+        &recipient.into(),
+        1000,
+    );
     submit_tx(&http_origin, &tx);
 
     next_block_and_wait(&mut btc_regtest_controller, &blocks_processed);
@@ -5651,11 +5703,7 @@ fn cost_voting_integration() {
     conf.node.wait_time_for_blocks = 1_000;
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let spender_bal = 10_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64);
 
@@ -5713,9 +5761,30 @@ fn cost_voting_integration() {
     assert_eq!(res.nonce, 0);
 
     let transactions = vec![
-        make_contract_publish(&spender_sk, 0, 1000, "cost-definer", cost_definer_src),
-        make_contract_publish(&spender_sk, 1, 1000, "caller", caller_src),
-        make_contract_publish(&spender_sk, 2, 1000, "voter", power_vote_src),
+        make_contract_publish(
+            &spender_sk,
+            0,
+            1000,
+            conf.burnchain.chain_id,
+            "cost-definer",
+            cost_definer_src,
+        ),
+        make_contract_publish(
+            &spender_sk,
+            1,
+            1000,
+            conf.burnchain.chain_id,
+            "caller",
+            caller_src,
+        ),
+        make_contract_publish(
+            &spender_sk,
+            2,
+            1000,
+            conf.burnchain.chain_id,
+            "voter",
+            power_vote_src,
+        ),
     ];
 
     for tx in transactions.into_iter() {
@@ -5729,6 +5798,7 @@ fn cost_voting_integration() {
         &spender_sk,
         3,
         1000,
+        conf.burnchain.chain_id,
         &spender_addr,
         "voter",
         "propose-vote-confirm",
@@ -5739,6 +5809,7 @@ fn cost_voting_integration() {
         &spender_sk,
         4,
         1000,
+        conf.burnchain.chain_id,
         &spender_addr,
         "caller",
         "execute-2",
@@ -5790,6 +5861,7 @@ fn cost_voting_integration() {
         &spender_sk,
         5,
         1000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "cost-voting",
         "confirm-miners",
@@ -5840,6 +5912,7 @@ fn cost_voting_integration() {
         &spender_sk,
         6,
         1000,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "cost-voting",
         "confirm-miners",
@@ -5884,6 +5957,7 @@ fn cost_voting_integration() {
         &spender_sk,
         7,
         1000,
+        conf.burnchain.chain_id,
         &spender_addr,
         "caller",
         "execute-2",
@@ -5945,11 +6019,6 @@ fn mining_events_integration_test() {
     let spender_sk_2 = StacksPrivateKey::from_hex(SK_2).unwrap();
     let addr_2 = to_addr(&spender_sk_2);
 
-    let tx = make_contract_publish(&spender_sk, 0, 600000, "small", &small_contract);
-    let tx_2 = make_contract_publish(&spender_sk, 1, 610000, "small", &small_contract);
-    let mb_tx =
-        make_contract_publish_microblock_only(&spender_sk_2, 0, 620000, "small", &small_contract);
-
     let (mut conf, _) = neon_integration_test_conf();
 
     conf.initial_balances.push(InitialBalance {
@@ -5968,16 +6037,40 @@ fn mining_events_integration_test() {
     conf.miner.first_attempt_time_ms = i64::MAX as u64;
     conf.miner.subsequent_attempt_time_ms = i64::MAX as u64;
 
-    test_observer::spawn();
+    let tx = make_contract_publish(
+        &spender_sk,
+        0,
+        600000,
+        conf.burnchain.chain_id,
+        "small",
+        &small_contract,
+    );
+    let tx_2 = make_contract_publish(
+        &spender_sk,
+        1,
+        610000,
+        conf.burnchain.chain_id,
+        "small",
+        &small_contract,
+    );
+    let mb_tx = make_contract_publish_microblock_only(
+        &spender_sk_2,
+        0,
+        620000,
+        conf.burnchain.chain_id,
+        "small",
+        &small_contract,
+    );
 
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![
+    test_observer::spawn();
+    test_observer::register(
+        &mut conf,
+        &[
             EventKeyType::AnyEvent,
             EventKeyType::MinedBlocks,
             EventKeyType::MinedMicroblocks,
         ],
-    });
+    );
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -6208,15 +6301,6 @@ fn block_limit_hit_integration_test() {
     let third_spender_sk = StacksPrivateKey::new();
     let third_spender_addr: PrincipalData = to_addr(&third_spender_sk).into();
 
-    // included in first block
-    let tx = make_contract_publish(&spender_sk, 0, 555_000, "over", &oversize_contract_src);
-    // contract limit hit; included in second block
-    let tx_2 = make_contract_publish(&spender_sk, 1, 555_000, "over-2", &oversize_contract_src);
-    // skipped over since contract limit was hit; included in second block
-    let tx_3 = make_contract_publish(&second_spender_sk, 0, 150_000, "max", &max_contract_src);
-    // included in first block
-    let tx_4 = make_stacks_transfer(&third_spender_sk, 0, 180, &PrincipalData::from(addr), 100);
-
     let (mut conf, _miner_account) = neon_integration_test_conf();
 
     conf.initial_balances.push(InitialBalance {
@@ -6239,12 +6323,45 @@ fn block_limit_hit_integration_test() {
     conf.miner.first_attempt_time_ms = i64::MAX as u64;
     conf.miner.subsequent_attempt_time_ms = i64::MAX as u64;
 
-    test_observer::spawn();
+    // included in first block
+    let tx = make_contract_publish(
+        &spender_sk,
+        0,
+        555_000,
+        conf.burnchain.chain_id,
+        "over",
+        &oversize_contract_src,
+    );
+    // contract limit hit; included in second block
+    let tx_2 = make_contract_publish(
+        &spender_sk,
+        1,
+        555_000,
+        conf.burnchain.chain_id,
+        "over-2",
+        &oversize_contract_src,
+    );
+    // skipped over since contract limit was hit; included in second block
+    let tx_3 = make_contract_publish(
+        &second_spender_sk,
+        0,
+        150_000,
+        conf.burnchain.chain_id,
+        "max",
+        &max_contract_src,
+    );
+    // included in first block
+    let tx_4 = make_stacks_transfer(
+        &third_spender_sk,
+        0,
+        180,
+        conf.burnchain.chain_id,
+        &PrincipalData::from(addr),
+        100,
+    );
 
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::spawn();
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -6396,39 +6513,6 @@ fn microblock_limit_hit_integration_test() {
     let third_spender_sk = StacksPrivateKey::new();
     let third_spender_addr: PrincipalData = to_addr(&third_spender_sk).into();
 
-    // included in the first block
-    let tx = make_contract_publish_microblock_only(
-        &spender_sk,
-        0,
-        555_000,
-        "over",
-        &oversize_contract_src,
-    );
-    // contract limit hit; included in second block
-    let tx_2 = make_contract_publish_microblock_only(
-        &spender_sk,
-        1,
-        555_000,
-        "over-2",
-        &oversize_contract_src,
-    );
-    // skipped over since contract limit was hit; included in second block
-    let tx_3 = make_contract_publish_microblock_only(
-        &second_spender_sk,
-        0,
-        150_000,
-        "max",
-        &max_contract_src,
-    );
-    // included in first block
-    let tx_4 = make_stacks_transfer_mblock_only(
-        &third_spender_sk,
-        0,
-        180,
-        &PrincipalData::from(addr),
-        100,
-    );
-
     let (mut conf, _) = neon_integration_test_conf();
 
     conf.initial_balances.push(InitialBalance {
@@ -6494,12 +6578,45 @@ fn microblock_limit_hit_integration_test() {
     ]);
     conf.burnchain.pox_2_activation = Some(10_003);
 
-    test_observer::spawn();
+    // included in the first block
+    let tx = make_contract_publish_microblock_only(
+        &spender_sk,
+        0,
+        555_000,
+        conf.burnchain.chain_id,
+        "over",
+        &oversize_contract_src,
+    );
+    // contract limit hit; included in second block
+    let tx_2 = make_contract_publish_microblock_only(
+        &spender_sk,
+        1,
+        555_000,
+        conf.burnchain.chain_id,
+        "over-2",
+        &oversize_contract_src,
+    );
+    // skipped over since contract limit was hit; included in second block
+    let tx_3 = make_contract_publish_microblock_only(
+        &second_spender_sk,
+        0,
+        150_000,
+        conf.burnchain.chain_id,
+        "max",
+        &max_contract_src,
+    );
+    // included in first block
+    let tx_4 = make_stacks_transfer_mblock_only(
+        &third_spender_sk,
+        0,
+        180,
+        conf.burnchain.chain_id,
+        &PrincipalData::from(addr),
+        100,
+    );
 
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::spawn();
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -6639,17 +6756,9 @@ fn block_large_tx_integration_test() {
     let spender_sk = StacksPrivateKey::new();
     let spender_addr = to_addr(&spender_sk);
 
-    // higher fee for tx means it will get mined first
-    let tx = make_contract_publish(&spender_sk, 0, 671_000, "small", &small_contract_src);
-    let tx_2 = make_contract_publish(&spender_sk, 1, 670_000, "over", &oversize_contract_src);
-
     let (mut conf, miner_account) = neon_integration_test_conf();
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     conf.initial_balances.push(InitialBalance {
         address: spender_addr.clone().into(),
@@ -6667,6 +6776,24 @@ fn block_large_tx_integration_test() {
     conf.miner.first_attempt_time_ms = i64::MAX as u64;
     conf.miner.subsequent_attempt_time_ms = i64::MAX as u64;
 
+    // higher fee for tx means it will get mined first
+    let tx = make_contract_publish(
+        &spender_sk,
+        0,
+        671_000,
+        conf.burnchain.chain_id,
+        "small",
+        &small_contract_src,
+    );
+    let tx_2 = make_contract_publish(
+        &spender_sk,
+        1,
+        670_000,
+        conf.burnchain.chain_id,
+        "over",
+        &oversize_contract_src,
+    );
+
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
         .start_bitcoind()
@@ -6765,29 +6892,10 @@ fn microblock_large_tx_integration_test_FLAKY() {
     let spender_sk = StacksPrivateKey::new();
     let addr = to_addr(&spender_sk);
 
-    let tx = make_contract_publish_microblock_only(
-        &spender_sk,
-        0,
-        150_000,
-        "small",
-        &small_contract_src,
-    );
-    let tx_2 = make_contract_publish_microblock_only(
-        &spender_sk,
-        1,
-        670_000,
-        "over",
-        &oversize_contract_src,
-    );
-
     let (mut conf, miner_account) = neon_integration_test_conf();
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     conf.initial_balances.push(InitialBalance {
         address: addr.clone().into(),
@@ -6806,6 +6914,23 @@ fn microblock_large_tx_integration_test_FLAKY() {
     conf.burnchain.max_rbf = 10_000_000;
     conf.node.wait_time_for_blocks = 1_000;
 
+    let tx = make_contract_publish_microblock_only(
+        &spender_sk,
+        0,
+        150_000,
+        conf.burnchain.chain_id,
+        "small",
+        &small_contract_src,
+    );
+    let tx_2 = make_contract_publish_microblock_only(
+        &spender_sk,
+        1,
+        670_000,
+        conf.burnchain.chain_id,
+        "over",
+        &oversize_contract_src,
+    );
+
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
         .start_bitcoind()
@@ -6918,15 +7043,11 @@ fn pox_integration_test() {
     let (mut conf, miner_account) = neon_integration_test_conf();
 
     test_observer::spawn();
+    test_observer::register_any(&mut conf);
 
     // required for testing post-sunset behavior
     conf.node.always_use_affirmation_maps = false;
 
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
-
     let first_bal = 6_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64);
     let second_bal = 2_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64);
     let third_bal = 2_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64);
@@ -7063,6 +7184,7 @@ fn pox_integration_test() {
         &spender_sk,
         0,
         260,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox",
         "stack-stx",
@@ -7178,6 +7300,7 @@ fn pox_integration_test() {
         &spender_2_sk,
         0,
         260,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox",
         "stack-stx",
@@ -7201,6 +7324,7 @@ fn pox_integration_test() {
         &spender_3_sk,
         0,
         260,
+        conf.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "pox",
         "stack-stx",
@@ -7470,6 +7594,7 @@ fn atlas_integration_test() {
         .insert(EventObserverConfig {
             endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
             events_keys: vec![EventKeyType::AnyEvent],
+            timeout_ms: 1000,
         });
 
     conf_follower_node.node.always_use_affirmation_maps = false;
@@ -7542,6 +7667,7 @@ fn atlas_integration_test() {
             &user_1,
             0,
             260,
+            conf_bootstrap_node.burnchain.chain_id,
             &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
             "bns",
             "namespace-preorder",
@@ -7601,6 +7727,7 @@ fn atlas_integration_test() {
             &user_1,
             1,
             1000,
+            conf_bootstrap_node.burnchain.chain_id,
             &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
             "bns",
             "namespace-reveal",
@@ -7663,6 +7790,7 @@ fn atlas_integration_test() {
             &user_1,
             2,
             500,
+            conf_bootstrap_node.burnchain.chain_id,
             &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
             "bns",
             "name-import",
@@ -7780,6 +7908,7 @@ fn atlas_integration_test() {
     };
 
     let burnchain_config = Burnchain::regtest(&conf_follower_node.get_burn_db_path());
+    let chain_id = conf_follower_node.burnchain.chain_id;
     let http_origin = format!("http://{}", &conf_follower_node.node.rpc_bind);
 
     eprintln!("Chain bootstrapped...");
@@ -7858,6 +7987,7 @@ fn atlas_integration_test() {
             &user_1,
             2 + i,
             500,
+            chain_id,
             &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
             "bns",
             "name-import",
@@ -8010,6 +8140,7 @@ fn antientropy_integration_test() {
         .insert(EventObserverConfig {
             endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
             events_keys: vec![EventKeyType::AnyEvent],
+            timeout_ms: 1000,
         });
 
     conf_follower_node.node.mine_microblocks = true;
@@ -8325,6 +8456,7 @@ fn atlas_stress_integration_test() {
         &user_1,
         0,
         1000,
+        conf_bootstrap_node.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "bns",
         "namespace-preorder",
@@ -8384,6 +8516,7 @@ fn atlas_stress_integration_test() {
         &user_1,
         1,
         1000,
+        conf_bootstrap_node.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "bns",
         "namespace-reveal",
@@ -8472,6 +8605,7 @@ fn atlas_stress_integration_test() {
                 &user_1,
                 2 + (batch_size * i + j) as u64,
                 1000,
+                conf_bootstrap_node.burnchain.chain_id,
                 &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
                 "bns",
                 "name-import",
@@ -8541,6 +8675,7 @@ fn atlas_stress_integration_test() {
         &user_1,
         2 + (batch_size as u64) * (batches as u64),
         1000,
+        conf_bootstrap_node.burnchain.chain_id,
         &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
         "bns",
         "namespace-ready",
@@ -8601,6 +8736,7 @@ fn atlas_stress_integration_test() {
                 &users[batches * batch_size + j],
                 0,
                 1000,
+                conf_bootstrap_node.burnchain.chain_id,
                 &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
                 "bns",
                 "name-preorder",
@@ -8660,6 +8796,7 @@ fn atlas_stress_integration_test() {
                 &users[batches * batch_size + j],
                 1,
                 1000,
+                conf_bootstrap_node.burnchain.chain_id,
                 &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
                 "bns",
                 "name-register",
@@ -8723,6 +8860,7 @@ fn atlas_stress_integration_test() {
                 &users[batches * batch_size + j],
                 2,
                 1000,
+                conf_bootstrap_node.burnchain.chain_id,
                 &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
                 "bns",
                 "name-update",
@@ -8785,6 +8923,7 @@ fn atlas_stress_integration_test() {
                 &users[batches * batch_size + j],
                 3,
                 1000,
+                conf_bootstrap_node.burnchain.chain_id,
                 &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(),
                 "bns",
                 "name-renewal",
@@ -9009,10 +9148,7 @@ fn fuzzed_median_fee_rate_estimation_test(window_size: u64, expected_final_value
         amount: 10000000000,
     });
     test_observer::spawn();
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -9043,6 +9179,7 @@ fn fuzzed_median_fee_rate_estimation_test(window_size: u64, expected_final_value
             &spender_sk,
             0,
             110000,
+            conf.burnchain.chain_id,
             "increment-contract",
             &max_contract_src,
         ),
@@ -9060,6 +9197,7 @@ fn fuzzed_median_fee_rate_estimation_test(window_size: u64, expected_final_value
                 &spender_sk,
                 i,          // nonce
                 i * 100000, // payment
+                conf.burnchain.chain_id,
                 &spender_addr.into(),
                 "increment-contract",
                 "increment-many",
@@ -9188,11 +9326,7 @@ fn use_latest_tip_integration_test() {
     conf.node.microblock_frequency = 1_000;
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -9259,15 +9393,27 @@ fn use_latest_tip_integration_test() {
 
     // Make microblock with two transactions.
     let recipient = StacksAddress::from_string(ADDR_4).unwrap();
-    let transfer_tx =
-        make_stacks_transfer_mblock_only(&spender_sk, 0, 1000, &recipient.into(), 1000);
+    let transfer_tx = make_stacks_transfer_mblock_only(
+        &spender_sk,
+        0,
+        1000,
+        conf.burnchain.chain_id,
+        &recipient.into(),
+        1000,
+    );
 
     let caller_src = "
      (define-public (execute)
         (ok stx-liquid-supply))
      ";
-    let publish_tx =
-        make_contract_publish_microblock_only(&spender_sk, 1, 1000, "caller", caller_src);
+    let publish_tx = make_contract_publish_microblock_only(
+        &spender_sk,
+        1,
+        1000,
+        conf.burnchain.chain_id,
+        "caller",
+        caller_src,
+    );
 
     let tx_1 = StacksTransaction::consensus_deserialize(&mut &transfer_tx[..]).unwrap();
     let tx_2 = StacksTransaction::consensus_deserialize(&mut &publish_tx[..]).unwrap();
@@ -9592,11 +9738,7 @@ fn test_problematic_txs_are_not_stored() {
     conf.burnchain.ast_precheck_size_height = Some(0);
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
 
@@ -9618,6 +9760,7 @@ fn test_problematic_txs_are_not_stored() {
         &spender_sk_1,
         0,
         (tx_edge_body.len() * 100) as u64,
+        conf.burnchain.chain_id,
         "test-edge",
         &tx_edge_body,
     );
@@ -9635,6 +9778,7 @@ fn test_problematic_txs_are_not_stored() {
         &spender_sk_2,
         0,
         (tx_exceeds_body.len() * 100) as u64,
+        conf.burnchain.chain_id,
         "test-exceeds",
         &tx_exceeds_body,
     );
@@ -9652,6 +9796,7 @@ fn test_problematic_txs_are_not_stored() {
         &spender_sk_3,
         0,
         (tx_high_body.len() * 100) as u64,
+        conf.burnchain.chain_id,
         "test-high",
         &tx_high_body,
     );
@@ -9745,10 +9890,7 @@ fn spawn_follower_node(
         conf.burnchain.peer_version,
     );
 
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     conf.initial_balances = initial_conf.initial_balances.clone();
     conf.burnchain.epochs = initial_conf.burnchain.epochs.clone();
@@ -9843,11 +9985,7 @@ fn test_problematic_blocks_are_not_mined() {
     conf.burnchain.ast_precheck_size_height = Some(210);
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -9868,6 +10006,7 @@ fn test_problematic_blocks_are_not_mined() {
         &spender_sk_2,
         0,
         (tx_exceeds_body.len() * 100) as u64,
+        conf.burnchain.chain_id,
         "test-exceeds",
         &tx_exceeds_body,
     );
@@ -9885,6 +10024,7 @@ fn test_problematic_blocks_are_not_mined() {
         &spender_sk_3,
         0,
         (tx_high_body.len() * 100) as u64,
+        conf.burnchain.chain_id,
         "test-high",
         &tx_high_body,
     );
@@ -10200,11 +10340,7 @@ fn test_problematic_blocks_are_not_relayed_or_stored() {
     conf.burnchain.ast_precheck_size_height = Some(210);
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -10225,6 +10361,7 @@ fn test_problematic_blocks_are_not_relayed_or_stored() {
         &spender_sk_2,
         0,
         (tx_exceeds_body.len() * 100) as u64,
+        conf.burnchain.chain_id,
         "test-exceeds",
         &tx_exceeds_body,
     );
@@ -10241,6 +10378,7 @@ fn test_problematic_blocks_are_not_relayed_or_stored() {
         &spender_sk_3,
         0,
         (tx_high_body.len() * 100) as u64,
+        conf.burnchain.chain_id,
         "test-high",
         &tx_high_body,
     );
@@ -10599,11 +10737,7 @@ fn test_problematic_microblocks_are_not_mined() {
     conf.node.wait_time_for_microblocks = 0;
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -10624,6 +10758,7 @@ fn test_problematic_microblocks_are_not_mined() {
         &spender_sk_2,
         0,
         (tx_exceeds_body.len() * 100) as u64,
+        conf.burnchain.chain_id,
         "test-exceeds",
         &tx_exceeds_body,
     );
@@ -10642,6 +10777,7 @@ fn test_problematic_microblocks_are_not_mined() {
         &spender_sk_3,
         0,
         (tx_high_body.len() * 100) as u64,
+        conf.burnchain.chain_id,
         "test-high",
         &tx_high_body,
     );
@@ -10983,11 +11119,7 @@ fn test_problematic_microblocks_are_not_relayed_or_stored() {
     conf.connection_options.inv_sync_interval = 3;
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let mut btcd_controller = BitcoinCoreController::new(conf.clone());
     btcd_controller
@@ -11008,6 +11140,7 @@ fn test_problematic_microblocks_are_not_relayed_or_stored() {
         &spender_sk_2,
         0,
         (tx_exceeds_body.len() * 100) as u64,
+        conf.burnchain.chain_id,
         "test-exceeds",
         &tx_exceeds_body,
     );
@@ -11026,6 +11159,7 @@ fn test_problematic_microblocks_are_not_relayed_or_stored() {
         &spender_sk_3,
         0,
         (tx_high_body.len() * 100) as u64,
+        conf.burnchain.chain_id,
         "test-high",
         &tx_high_body,
     );
@@ -11328,10 +11462,7 @@ fn push_boot_receipts() {
     }
 
     let (mut conf, _) = neon_integration_test_conf();
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let burnchain_config = Burnchain::regtest(&conf.get_burn_db_path());
 
@@ -11376,10 +11507,7 @@ fn run_with_custom_wallet() {
     }
 
     let (mut conf, _) = neon_integration_test_conf();
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     // custom wallet
     conf.burnchain.wallet_name = "test_with_custom_wallet".to_string();
@@ -11505,6 +11633,7 @@ enum TxChainStrategy {
 pub fn make_expensive_tx_chain(
     privk: &StacksPrivateKey,
     fee_plus: u64,
+    chain_id: u32,
     mblock_only: bool,
 ) -> Vec<Vec<u8>> {
     let addr = to_addr(&privk);
@@ -11519,6 +11648,7 @@ pub fn make_expensive_tx_chain(
                 privk,
                 nonce,
                 1049230 + nonce + fee_plus,
+                chain_id,
                 &contract_name,
                 &make_runtime_sized_contract(256, nonce, &addr_prefix),
             )
@@ -11527,6 +11657,7 @@ pub fn make_expensive_tx_chain(
                 privk,
                 nonce,
                 1049230 + nonce + fee_plus,
+                chain_id,
                 &contract_name,
                 &make_runtime_sized_contract(256, nonce, &addr_prefix),
             )
@@ -11539,6 +11670,7 @@ pub fn make_expensive_tx_chain(
 pub fn make_random_tx_chain(
     privk: &StacksPrivateKey,
     fee_plus: u64,
+    chain_id: u32,
     mblock_only: bool,
 ) -> Vec<Vec<u8>> {
     let addr = to_addr(&privk);
@@ -11564,6 +11696,7 @@ pub fn make_random_tx_chain(
                 privk,
                 nonce,
                 1049230 + nonce + fee_plus + random_extra_fee,
+                chain_id,
                 &contract_name,
                 &make_runtime_sized_contract(random_iters, nonce, &addr_prefix),
             )
@@ -11572,6 +11705,7 @@ pub fn make_random_tx_chain(
                 privk,
                 nonce,
                 1049230 + nonce + fee_plus + random_extra_fee,
+                chain_id,
                 &contract_name,
                 &make_runtime_sized_contract(random_iters, nonce, &addr_prefix),
             )
@@ -11581,7 +11715,7 @@ pub fn make_random_tx_chain(
     chain
 }
 
-fn make_mblock_tx_chain(privk: &StacksPrivateKey, fee_plus: u64) -> Vec<Vec<u8>> {
+fn make_mblock_tx_chain(privk: &StacksPrivateKey, fee_plus: u64, chain_id: u32) -> Vec<Vec<u8>> {
     let addr = to_addr(&privk);
     let mut chain = vec![];
 
@@ -11604,6 +11738,7 @@ fn make_mblock_tx_chain(privk: &StacksPrivateKey, fee_plus: u64) -> Vec<Vec<u8>>
             privk,
             nonce,
             1049230 + nonce + fee_plus + random_extra_fee,
+            chain_id,
             &contract_name,
             &make_runtime_sized_contract(1, nonce, &addr_prefix),
         );
@@ -11663,6 +11798,8 @@ fn test_competing_miners_build_on_same_chain(
         confs.push(conf);
     }
 
+    let chain_id = confs[0].burnchain.chain_id;
+
     let node_privkey_1 = Secp256k1PrivateKey::from_seed(&confs[0].node.local_peer_seed);
     for i in 1..num_miners {
         let chain_id = confs[0].burnchain.chain_id;
@@ -11790,8 +11927,12 @@ fn test_competing_miners_build_on_same_chain(
         .iter()
         .enumerate()
         .map(|(i, pk)| match chain_strategy {
-            TxChainStrategy::Expensive => make_expensive_tx_chain(pk, (25 * i) as u64, mblock_only),
-            TxChainStrategy::Random => make_random_tx_chain(pk, (25 * i) as u64, mblock_only),
+            TxChainStrategy::Expensive => {
+                make_expensive_tx_chain(pk, (25 * i) as u64, chain_id, mblock_only)
+            }
+            TxChainStrategy::Random => {
+                make_random_tx_chain(pk, (25 * i) as u64, chain_id, mblock_only)
+            }
         })
         .collect();
     let mut cnt = 0;
@@ -11871,6 +12012,7 @@ fn test_competing_miners_build_anchor_blocks_and_microblocks_on_same_chain() {
 #[ignore]
 fn microblock_miner_multiple_attempts() {
     let (mut conf, miner_account) = neon_integration_test_conf();
+    let chain_id = conf.burnchain.chain_id;
 
     conf.node.mine_microblocks = true;
     conf.miner.microblock_attempt_time_ms = 2_000;
@@ -11939,7 +12081,7 @@ fn microblock_miner_multiple_attempts() {
     let all_txs: Vec<_> = privks
         .iter()
         .enumerate()
-        .map(|(i, pk)| make_mblock_tx_chain(pk, (25 * i) as u64))
+        .map(|(i, pk)| make_mblock_tx_chain(pk, (25 * i) as u64, chain_id))
         .collect();
 
     let _handle = thread::spawn(move || {
@@ -11975,11 +12117,7 @@ fn min_txs() {
     let (mut conf, _miner_account) = neon_integration_test_conf();
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     conf.miner.min_tx_count = 4;
     conf.miner.first_attempt_time_ms = 0;
@@ -12043,6 +12181,7 @@ fn min_txs() {
             &spender_sk,
             i as u64,
             1000,
+            conf.burnchain.chain_id,
             &format!("test-publish-{}", &i),
             &code,
         );
@@ -12081,11 +12220,7 @@ fn filter_txs_by_type() {
     let (mut conf, _miner_account) = neon_integration_test_conf();
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     conf.miner.min_tx_count = 4;
     conf.miner.first_attempt_time_ms = 0;
@@ -12150,6 +12285,7 @@ fn filter_txs_by_type() {
             &spender_sk,
             i as u64,
             1000,
+            conf.burnchain.chain_id,
             &format!("test-publish-{}", &i),
             &code,
         );
@@ -12197,11 +12333,7 @@ fn filter_txs_by_origin() {
     let (mut conf, _miner_account) = neon_integration_test_conf();
 
     test_observer::spawn();
-
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     conf.miner.min_tx_count = 4;
     conf.miner.first_attempt_time_ms = 0;
@@ -12264,6 +12396,7 @@ fn filter_txs_by_origin() {
             &spender_sk,
             i as u64,
             1000,
+            conf.burnchain.chain_id,
             &format!("test-publish-{}", &i),
             &code,
         );
diff --git a/testnet/stacks-node/src/tests/signer/mod.rs b/testnet/stacks-node/src/tests/signer/mod.rs
index 454ccde780..2e67234285 100644
--- a/testnet/stacks-node/src/tests/signer/mod.rs
+++ b/testnet/stacks-node/src/tests/signer/mod.rs
@@ -173,6 +173,7 @@ impl<S: Signer<T> + Send + 'static, T: SignerEventTrait + 'static> SignerTest<Sp
             Some(100_000),
             None,
             Some(9000),
+            None,
         )
         .into_iter()
         .map(|toml| {
@@ -649,6 +650,7 @@ fn setup_stx_btc_node<G: FnMut(&mut NeonConfig) -> ()>(
                 EventKeyType::BlockProposal,
                 EventKeyType::BurnchainBlocks,
             ],
+            timeout_ms: 1000,
         });
     }
 
@@ -663,6 +665,7 @@ fn setup_stx_btc_node<G: FnMut(&mut NeonConfig) -> ()>(
             EventKeyType::MinedBlocks,
             EventKeyType::BurnchainBlocks,
         ],
+        timeout_ms: 1000,
     });
 
     // The signers need some initial balances in order to pay for epoch 2.5 transaction votes
diff --git a/testnet/stacks-node/src/tests/signer/v0.rs b/testnet/stacks-node/src/tests/signer/v0.rs
index 6bffea2749..89192d274d 100644
--- a/testnet/stacks-node/src/tests/signer/v0.rs
+++ b/testnet/stacks-node/src/tests/signer/v0.rs
@@ -139,6 +139,7 @@ impl SignerTest<SpawnedSigner> {
                 &stacker_sk,
                 0,
                 1000,
+                self.running_nodes.conf.burnchain.chain_id,
                 &StacksAddress::burn_address(false),
                 "pox-4",
                 "stack-stx",
@@ -558,18 +559,22 @@ fn miner_gather_signatures() {
     // Test prometheus metrics response
     #[cfg(feature = "monitoring_prom")]
     {
-        let metrics_response = signer_test.get_signer_metrics();
-
-        // Because 5 signers are running in the same process, the prometheus metrics
-        // are incremented once for every signer. This is why we expect the metric to be
-        // `5`, even though there is only one block proposed.
-        let expected_result = format!("stacks_signer_block_proposals_received {}", num_signers);
-        assert!(metrics_response.contains(&expected_result));
-        let expected_result = format!(
-            "stacks_signer_block_responses_sent{{response_type=\"accepted\"}} {}",
-            num_signers
-        );
-        assert!(metrics_response.contains(&expected_result));
+        wait_for(30, || {
+            let metrics_response = signer_test.get_signer_metrics();
+
+            // Because 5 signers are running in the same process, the prometheus metrics
+            // are incremented once for every signer. This is why we expect the metric to be
+            // `10`, even though there are only two blocks proposed.
+            let expected_result_1 =
+                format!("stacks_signer_block_proposals_received {}", num_signers * 2);
+            let expected_result_2 = format!(
+                "stacks_signer_block_responses_sent{{response_type=\"accepted\"}} {}",
+                num_signers * 2
+            );
+            Ok(metrics_response.contains(&expected_result_1)
+                && metrics_response.contains(&expected_result_2))
+        })
+        .expect("Failed to advance prometheus metrics");
     }
 }
 
@@ -1136,8 +1141,14 @@ fn forked_tenure_testing(
         let start_time = Instant::now();
         // submit a tx so that the miner will mine an extra block
         let sender_nonce = 0;
-        let transfer_tx =
-            make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+        let transfer_tx = make_stacks_transfer(
+            &sender_sk,
+            sender_nonce,
+            send_fee,
+            naka_conf.burnchain.chain_id,
+            &recipient,
+            send_amt,
+        );
         let tx = submit_tx(&http_origin, &transfer_tx);
         info!("Submitted tx {tx} in Tenure C to mine a second block");
         while mined_blocks.load(Ordering::SeqCst) <= blocks_before {
@@ -1468,7 +1479,7 @@ fn multiple_miners() {
             config.node.data_url = format!("http://{localhost}:{node_1_rpc}");
             config.node.p2p_address = format!("{localhost}:{node_1_p2p}");
             config.miner.wait_on_interim_blocks = Duration::from_secs(5);
-            config.node.pox_sync_sample_secs = 5;
+            config.node.pox_sync_sample_secs = 30;
 
             config.node.seed = btc_miner_1_seed.clone();
             config.node.local_peer_seed = btc_miner_1_seed.clone();
@@ -1763,7 +1774,7 @@ fn miner_forking() {
             config.node.local_peer_seed = btc_miner_1_seed.clone();
             config.burnchain.local_mining_public_key = Some(btc_miner_1_pk.to_hex());
             config.miner.mining_key = Some(Secp256k1PrivateKey::from_seed(&[1]));
-            config.node.pox_sync_sample_secs = 5;
+            config.node.pox_sync_sample_secs = 30;
 
             config.events_observers.retain(|listener| {
                 let Ok(addr) = std::net::SocketAddr::from_str(&listener.endpoint) else {
@@ -2079,8 +2090,14 @@ fn end_of_tenure() {
     let start_height = info.stacks_tip_height;
     // submit a tx so that the miner will mine an extra block
     let sender_nonce = 0;
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        signer_test.running_nodes.conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     submit_tx(&http_origin, &transfer_tx);
 
     info!("Submitted transfer tx and waiting for block proposal");
@@ -2190,8 +2207,14 @@ fn retry_on_rejection() {
     let start_time = Instant::now();
     // submit a tx so that the miner will mine a stacks block
     let mut sender_nonce = 0;
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        signer_test.running_nodes.conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     let tx = submit_tx(&http_origin, &transfer_tx);
     sender_nonce += 1;
     info!("Submitted tx {tx} in to mine the first Nakamoto block");
@@ -2227,8 +2250,14 @@ fn retry_on_rejection() {
         .load(Ordering::SeqCst);
 
     // submit a tx so that the miner will mine a block
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        signer_test.running_nodes.conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     submit_tx(&http_origin, &transfer_tx);
 
     info!("Submitted transfer tx and waiting for block proposal");
@@ -2334,8 +2363,14 @@ fn signers_broadcast_signed_blocks() {
 
     // submit a tx so that the miner will mine a blockn
     let sender_nonce = 0;
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        signer_test.running_nodes.conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     submit_tx(&http_origin, &transfer_tx);
 
     debug!("Transaction sent; waiting for block-mining");
@@ -2480,8 +2515,14 @@ fn empty_sortition() {
 
     // submit a tx so that the miner will mine an extra block
     let sender_nonce = 0;
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        signer_test.running_nodes.conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     submit_tx(&http_origin, &transfer_tx);
 
     std::thread::sleep(block_proposal_timeout.add(Duration::from_secs(1)));
@@ -2949,6 +2990,7 @@ fn signer_set_rollover() {
         Some(100_000),
         None,
         Some(9000 + num_signers),
+        None,
     );
 
     let new_spawned_signers: Vec<_> = (0..new_num_signers)
@@ -2981,6 +3023,7 @@ fn signer_set_rollover() {
                         EventKeyType::BlockProposal,
                         EventKeyType::BurnchainBlocks,
                     ],
+                    timeout_ms: 1000,
                 });
             }
             naka_conf.node.rpc_bind = rpc_bind.clone();
@@ -3035,8 +3078,14 @@ fn signer_set_rollover() {
     info!("---- Mining a block to trigger the signer set -----");
     // submit a tx so that the miner will mine an extra block
     let sender_nonce = 0;
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        signer_test.running_nodes.conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     submit_tx(&http_origin, &transfer_tx);
     signer_test.mine_nakamoto_block(short_timeout);
     let mined_block = test_observer::get_mined_nakamoto_blocks().pop().unwrap();
@@ -3086,6 +3135,7 @@ fn signer_set_rollover() {
             &stacker_sk,
             0,
             1000,
+            signer_test.running_nodes.conf.burnchain.chain_id,
             &StacksAddress::burn_address(false),
             "pox-4",
             "stack-stx",
@@ -3158,8 +3208,14 @@ fn signer_set_rollover() {
 
     info!("---- Mining a block to verify new signer set -----");
     let sender_nonce = 1;
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        signer_test.running_nodes.conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     submit_tx(&http_origin, &transfer_tx);
     signer_test.mine_nakamoto_block(short_timeout);
     let mined_block = test_observer::get_mined_nakamoto_blocks().pop().unwrap();
@@ -3243,8 +3299,14 @@ fn min_gap_between_blocks() {
 
     // Submit a tx so that the miner will mine a block
     let sender_nonce = 0;
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        signer_test.running_nodes.conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     submit_tx(&http_origin, &transfer_tx);
 
     info!("Submitted transfer tx and waiting for block to be processed. Ensure it does not arrive before the gap is exceeded");
@@ -3439,7 +3501,7 @@ fn multiple_miners_with_nakamoto_blocks() {
             config.node.data_url = format!("http://{localhost}:{node_1_rpc}");
             config.node.p2p_address = format!("{localhost}:{node_1_p2p}");
             config.miner.wait_on_interim_blocks = Duration::from_secs(5);
-            config.node.pox_sync_sample_secs = 5;
+            config.node.pox_sync_sample_secs = 30;
 
             config.node.seed = btc_miner_1_seed.clone();
             config.node.local_peer_seed = btc_miner_1_seed.clone();
@@ -3569,8 +3631,14 @@ fn multiple_miners_with_nakamoto_blocks() {
             let blocks_processed_before =
                 blocks_mined1.load(Ordering::SeqCst) + blocks_mined2.load(Ordering::SeqCst);
             // submit a tx so that the miner will mine an extra block
-            let transfer_tx =
-                make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+            let transfer_tx = make_stacks_transfer(
+                &sender_sk,
+                sender_nonce,
+                send_fee,
+                signer_test.running_nodes.conf.burnchain.chain_id,
+                &recipient,
+                send_amt,
+            );
             sender_nonce += 1;
             submit_tx(&http_origin, &transfer_tx);
 
@@ -3702,7 +3770,7 @@ fn partial_tenure_fork() {
             config.node.data_url = format!("http://{localhost}:{node_1_rpc}");
             config.node.p2p_address = format!("{localhost}:{node_1_p2p}");
             config.miner.wait_on_interim_blocks = Duration::from_secs(5);
-            config.node.pox_sync_sample_secs = 5;
+            config.node.pox_sync_sample_secs = 30;
 
             config.node.seed = btc_miner_1_seed.clone();
             config.node.local_peer_seed = btc_miner_1_seed.clone();
@@ -3910,8 +3978,14 @@ fn partial_tenure_fork() {
 
             // submit a tx so that the miner will mine an extra block
             let sender_nonce = (btc_blocks_mined - 1) * inter_blocks_per_tenure + interim_block_ix;
-            let transfer_tx =
-                make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+            let transfer_tx = make_stacks_transfer(
+                &sender_sk,
+                sender_nonce,
+                send_fee,
+                signer_test.running_nodes.conf.burnchain.chain_id,
+                &recipient,
+                send_amt,
+            );
             // This may fail if the forking miner wins too many tenures and this account's
             // nonces get too high (TooMuchChaining)
             match submit_tx_fallible(&http_origin, &transfer_tx) {
@@ -4095,8 +4169,14 @@ fn locally_accepted_blocks_overriden_by_global_rejection() {
     let blocks_before = mined_blocks.load(Ordering::SeqCst);
     // submit a tx so that the miner will mine a stacks block
     let mut sender_nonce = 0;
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        signer_test.running_nodes.conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     let tx = submit_tx(&http_origin, &transfer_tx);
     info!("Submitted tx {tx} in to mine block N");
     wait_for(short_timeout_secs, || {
@@ -4138,8 +4218,14 @@ fn locally_accepted_blocks_overriden_by_global_rejection() {
         .unwrap()
         .replace(rejecting_signers.clone());
     test_observer::clear();
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        signer_test.running_nodes.conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     let tx = submit_tx(&http_origin, &transfer_tx);
     sender_nonce += 1;
     info!("Submitted tx {tx} to mine block N+1");
@@ -4166,8 +4252,14 @@ fn locally_accepted_blocks_overriden_by_global_rejection() {
         .unwrap()
         .replace(Vec::new());
 
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        signer_test.running_nodes.conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     let tx = submit_tx(&http_origin, &transfer_tx);
     info!("Submitted tx {tx} to mine block N+1'");
 
@@ -4266,8 +4358,14 @@ fn locally_rejected_blocks_overriden_by_global_acceptance() {
 
     // submit a tx so that the miner will mine a stacks block N
     let mut sender_nonce = 0;
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        signer_test.running_nodes.conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     let tx = submit_tx(&http_origin, &transfer_tx);
     sender_nonce += 1;
     info!("Submitted tx {tx} in to mine block N");
@@ -4320,8 +4418,14 @@ fn locally_rejected_blocks_overriden_by_global_acceptance() {
         .stacks_client
         .get_peer_info()
         .expect("Failed to get peer info");
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        signer_test.running_nodes.conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     let tx = submit_tx(&http_origin, &transfer_tx);
     sender_nonce += 1;
     info!("Submitted tx {tx} in to mine block N+1");
@@ -4376,8 +4480,14 @@ fn locally_rejected_blocks_overriden_by_global_acceptance() {
         .replace(Vec::new());
 
     // submit a tx so that the miner will mine a stacks block N+2 and ensure ALL signers accept it
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        signer_test.running_nodes.conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     let tx = submit_tx(&http_origin, &transfer_tx);
     info!("Submitted tx {tx} in to mine block N+2");
     wait_for(30, || {
@@ -4469,8 +4579,14 @@ fn reorg_locally_accepted_blocks_across_tenures_succeeds() {
 
     // submit a tx so that the miner will mine a stacks block
     let mut sender_nonce = 0;
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        signer_test.running_nodes.conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     let tx = submit_tx(&http_origin, &transfer_tx);
     sender_nonce += 1;
     info!("Submitted tx {tx} in to mine block N");
@@ -4511,8 +4627,14 @@ fn reorg_locally_accepted_blocks_across_tenures_succeeds() {
     test_observer::clear();
 
     // submit a tx so that the miner will ATTEMPT to mine a stacks block N+1
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        signer_test.running_nodes.conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     let tx = submit_tx(&http_origin, &transfer_tx);
 
     info!("Submitted tx {tx} in to attempt to mine block N+1");
@@ -4679,8 +4801,14 @@ fn miner_recovers_when_broadcast_block_delay_across_tenures_occurs() {
 
     // submit a tx so that the miner will mine a stacks block
     let mut sender_nonce = 0;
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        signer_test.running_nodes.conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     let tx = submit_tx(&http_origin, &transfer_tx);
     info!("Submitted tx {tx} in to mine block N");
 
@@ -4722,8 +4850,14 @@ fn miner_recovers_when_broadcast_block_delay_across_tenures_occurs() {
         .get_peer_info()
         .expect("Failed to get peer info");
 
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        signer_test.running_nodes.conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
     sender_nonce += 1;
 
     let tx = submit_tx(&http_origin, &transfer_tx);
@@ -4885,8 +5019,14 @@ fn miner_recovers_when_broadcast_block_delay_across_tenures_occurs() {
     }
 
     // Induce block N+2 to get mined
-    let transfer_tx =
-        make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
+    let transfer_tx = make_stacks_transfer(
+        &sender_sk,
+        sender_nonce,
+        send_fee,
+        signer_test.running_nodes.conf.burnchain.chain_id,
+        &recipient,
+        send_amt,
+    );
 
     let tx = submit_tx(&http_origin, &transfer_tx);
     info!("Submitted tx {tx} in to attempt to mine block N+2");
diff --git a/testnet/stacks-node/src/tests/stackerdb.rs b/testnet/stacks-node/src/tests/stackerdb.rs
index e24b5c5c24..aa620d349b 100644
--- a/testnet/stacks-node/src/tests/stackerdb.rs
+++ b/testnet/stacks-node/src/tests/stackerdb.rs
@@ -25,7 +25,7 @@ use {reqwest, serde_json};
 
 use super::bitcoin_regtest::BitcoinCoreController;
 use crate::burnchains::BurnchainController;
-use crate::config::{EventKeyType, EventObserverConfig, InitialBalance};
+use crate::config::{EventKeyType, InitialBalance};
 use crate::tests::neon_integrations::{
     neon_integration_test_conf, next_block_and_wait, submit_tx, test_observer, wait_for_runloop,
 };
@@ -113,10 +113,7 @@ fn test_stackerdb_load_store() {
     }
 
     let (mut conf, _) = neon_integration_test_conf();
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::AnyEvent],
-    });
+    test_observer::register_any(&mut conf);
 
     let privks = vec![
         // ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R
@@ -209,7 +206,14 @@ fn test_stackerdb_load_store() {
     let http_origin = format!("http://{}", &conf.node.rpc_bind);
 
     eprintln!("Send contract-publish...");
-    let tx = make_contract_publish(&privks[0], 0, 10_000, "hello-world", stackerdb_contract);
+    let tx = make_contract_publish(
+        &privks[0],
+        0,
+        10_000,
+        conf.burnchain.chain_id,
+        "hello-world",
+        stackerdb_contract,
+    );
     submit_tx(&http_origin, &tx);
 
     // mine it
@@ -246,10 +250,7 @@ fn test_stackerdb_event_observer() {
     }
 
     let (mut conf, _) = neon_integration_test_conf();
-    conf.events_observers.insert(EventObserverConfig {
-        endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT),
-        events_keys: vec![EventKeyType::StackerDBChunks],
-    });
+    test_observer::register(&mut conf, &[EventKeyType::StackerDBChunks]);
 
     let privks = vec![
         // ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R
@@ -342,7 +343,14 @@ fn test_stackerdb_event_observer() {
     let http_origin = format!("http://{}", &conf.node.rpc_bind);
 
     eprintln!("Send contract-publish...");
-    let tx = make_contract_publish(&privks[0], 0, 10_000, "hello-world", stackerdb_contract);
+    let tx = make_contract_publish(
+        &privks[0],
+        0,
+        10_000,
+        conf.burnchain.chain_id,
+        "hello-world",
+        stackerdb_contract,
+    );
     submit_tx(&http_origin, &tx);
 
     // mine it