diff --git a/stackslib/src/net/connection.rs b/stackslib/src/net/connection.rs
index 4eeec0daaf..0e58adb36e 100644
--- a/stackslib/src/net/connection.rs
+++ b/stackslib/src/net/connection.rs
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-use std::collections::VecDeque;
+use std::collections::{HashMap, VecDeque};
use std::io::{Read, Write};
use std::ops::{Deref, DerefMut};
use std::sync::mpsc::{
@@ -24,7 +24,7 @@ use std::time::Duration;
use std::{io, net};
use clarity::vm::costs::ExecutionCost;
-use clarity::vm::types::BOUND_VALUE_SERIALIZATION_HEX;
+use clarity::vm::types::{QualifiedContractIdentifier, BOUND_VALUE_SERIALIZATION_HEX};
use stacks_common::codec::{StacksMessageCodec, MAX_MESSAGE_LEN};
use stacks_common::types::net::PeerAddress;
use stacks_common::util::hash::to_hex;
@@ -44,7 +44,8 @@ use crate::net::neighbors::{
WALK_SEED_PROBABILITY, WALK_STATE_TIMEOUT,
};
use crate::net::{
- Error as net_error, MessageSequence, Preamble, ProtocolFamily, RelayData, StacksHttp, StacksP2P,
+ Error as net_error, MessageSequence, NeighborAddress, Preamble, ProtocolFamily, RelayData,
+ StacksHttp, StacksP2P,
};
/// Receiver notification handle.
@@ -433,6 +434,8 @@ pub struct ConnectionOptions {
pub nakamoto_unconfirmed_downloader_interval_ms: u128,
/// The authorization token to enable privileged RPC endpoints
pub auth_token: Option,
+ /// StackerDB replicas to talk to for a particular smart contract
+ pub stackerdb_hint_replicas: HashMap>,
// fault injection
/// Disable neighbor walk and discovery
@@ -565,6 +568,7 @@ impl std::default::Default for ConnectionOptions {
nakamoto_inv_sync_burst_interval_ms: 1_000, // wait 1 second after a sortition before running inventory sync
nakamoto_unconfirmed_downloader_interval_ms: 5_000, // run unconfirmed downloader once every 5 seconds
auth_token: None,
+ stackerdb_hint_replicas: HashMap::new(),
// no faults on by default
disable_neighbor_walk: false,
diff --git a/stackslib/src/net/mod.rs b/stackslib/src/net/mod.rs
index 89e56fe29c..4af4d2a397 100644
--- a/stackslib/src/net/mod.rs
+++ b/stackslib/src/net/mod.rs
@@ -3141,7 +3141,7 @@ pub mod test {
&mut stacks_node.chainstate,
&sortdb,
old_stackerdb_configs,
- config.connection_opts.num_neighbors,
+ &config.connection_opts,
)
.expect("Failed to refresh stackerdb configs");
diff --git a/stackslib/src/net/p2p.rs b/stackslib/src/net/p2p.rs
index 71ca82f8bf..13f7ad7fac 100644
--- a/stackslib/src/net/p2p.rs
+++ b/stackslib/src/net/p2p.rs
@@ -841,6 +841,9 @@ impl PeerNetwork {
) -> usize {
let mut count = 0;
for (_, convo) in self.peers.iter() {
+ if !convo.is_authenticated() {
+ continue;
+ }
if !convo.is_outbound() {
continue;
}
@@ -4158,7 +4161,7 @@ impl PeerNetwork {
chainstate,
sortdb,
stacker_db_configs,
- self.connection_opts.num_neighbors,
+ &self.connection_opts,
)?;
Ok(())
}
diff --git a/stackslib/src/net/stackerdb/config.rs b/stackslib/src/net/stackerdb/config.rs
index 97f8214913..fbc1f28245 100644
--- a/stackslib/src/net/stackerdb/config.rs
+++ b/stackslib/src/net/stackerdb/config.rs
@@ -285,6 +285,94 @@ impl StackerDBConfig {
Ok(ret)
}
+ /// Evaluate contract-given hint-replicas
+ fn eval_hint_replicas(
+ contract_id: &QualifiedContractIdentifier,
+ hint_replicas_list: Vec,
+ ) -> Result, NetError> {
+ let mut hint_replicas = vec![];
+ for hint_replica_value in hint_replicas_list.into_iter() {
+ let hint_replica_data = hint_replica_value.expect_tuple()?;
+
+ let addr_byte_list = hint_replica_data
+ .get("addr")
+ .expect("FATAL: missing 'addr'")
+ .clone()
+ .expect_list()?;
+ let port = hint_replica_data
+ .get("port")
+ .expect("FATAL: missing 'port'")
+ .clone()
+ .expect_u128()?;
+ let pubkey_hash_bytes = hint_replica_data
+ .get("public-key-hash")
+ .expect("FATAL: missing 'public-key-hash")
+ .clone()
+ .expect_buff_padded(20, 0)?;
+
+ let mut addr_bytes = vec![];
+ for byte_val in addr_byte_list.into_iter() {
+ let byte = byte_val.expect_u128()?;
+ if byte > (u8::MAX as u128) {
+ let reason = format!(
+ "Contract {} stipulates an addr byte above u8::MAX",
+ contract_id
+ );
+ warn!("{}", &reason);
+ return Err(NetError::InvalidStackerDBContract(
+ contract_id.clone(),
+ reason,
+ ));
+ }
+ addr_bytes.push(byte as u8);
+ }
+ if addr_bytes.len() != 16 {
+ let reason = format!(
+ "Contract {} did not stipulate a full 16-octet IP address",
+ contract_id
+ );
+ warn!("{}", &reason);
+ return Err(NetError::InvalidStackerDBContract(
+ contract_id.clone(),
+ reason,
+ ));
+ }
+
+ if port < 1024 || port > u128::from(u16::MAX - 1) {
+ let reason = format!(
+ "Contract {} stipulates a port lower than 1024 or above u16::MAX - 1",
+ contract_id
+ );
+ warn!("{}", &reason);
+ return Err(NetError::InvalidStackerDBContract(
+ contract_id.clone(),
+ reason,
+ ));
+ }
+ // NOTE: port is now known to be in range [1024, 65535]
+
+ let mut pubkey_hash_slice = [0u8; 20];
+ pubkey_hash_slice.copy_from_slice(&pubkey_hash_bytes[0..20]);
+
+ let peer_addr = PeerAddress::from_slice(&addr_bytes).expect("FATAL: not 16 bytes");
+ if peer_addr.is_in_private_range() {
+ debug!(
+ "Ignoring private IP address '{}' in hint-replicas",
+ &peer_addr.to_socketaddr(port as u16)
+ );
+ continue;
+ }
+
+ let naddr = NeighborAddress {
+ addrbytes: peer_addr,
+ port: port as u16,
+ public_key_hash: Hash160(pubkey_hash_slice),
+ };
+ hint_replicas.push(naddr);
+ }
+ Ok(hint_replicas)
+ }
+
/// Evaluate the contract to get its config
fn eval_config(
chainstate: &mut StacksChainState,
@@ -293,6 +381,7 @@ impl StackerDBConfig {
tip: &StacksBlockId,
signers: Vec<(StacksAddress, u32)>,
local_max_neighbors: u64,
+ local_hint_replicas: Option>,
) -> Result {
let value =
chainstate.eval_read_only(burn_dbconn, tip, contract_id, "(stackerdb-get-config)")?;
@@ -394,91 +483,17 @@ impl StackerDBConfig {
max_neighbors = u128::from(local_max_neighbors);
}
- let hint_replicas_list = config_tuple
- .get("hint-replicas")
- .expect("FATAL: missing 'hint-replicas'")
- .clone()
- .expect_list()?;
- let mut hint_replicas = vec![];
- for hint_replica_value in hint_replicas_list.into_iter() {
- let hint_replica_data = hint_replica_value.expect_tuple()?;
-
- let addr_byte_list = hint_replica_data
- .get("addr")
- .expect("FATAL: missing 'addr'")
+ let hint_replicas = if let Some(replicas) = local_hint_replicas {
+ replicas.clone()
+ } else {
+ let hint_replicas_list = config_tuple
+ .get("hint-replicas")
+ .expect("FATAL: missing 'hint-replicas'")
.clone()
.expect_list()?;
- let port = hint_replica_data
- .get("port")
- .expect("FATAL: missing 'port'")
- .clone()
- .expect_u128()?;
- let pubkey_hash_bytes = hint_replica_data
- .get("public-key-hash")
- .expect("FATAL: missing 'public-key-hash")
- .clone()
- .expect_buff_padded(20, 0)?;
- let mut addr_bytes = vec![];
- for byte_val in addr_byte_list.into_iter() {
- let byte = byte_val.expect_u128()?;
- if byte > (u8::MAX as u128) {
- let reason = format!(
- "Contract {} stipulates an addr byte above u8::MAX",
- contract_id
- );
- warn!("{}", &reason);
- return Err(NetError::InvalidStackerDBContract(
- contract_id.clone(),
- reason,
- ));
- }
- addr_bytes.push(byte as u8);
- }
- if addr_bytes.len() != 16 {
- let reason = format!(
- "Contract {} did not stipulate a full 16-octet IP address",
- contract_id
- );
- warn!("{}", &reason);
- return Err(NetError::InvalidStackerDBContract(
- contract_id.clone(),
- reason,
- ));
- }
-
- if port < 1024 || port > u128::from(u16::MAX - 1) {
- let reason = format!(
- "Contract {} stipulates a port lower than 1024 or above u16::MAX - 1",
- contract_id
- );
- warn!("{}", &reason);
- return Err(NetError::InvalidStackerDBContract(
- contract_id.clone(),
- reason,
- ));
- }
- // NOTE: port is now known to be in range [1024, 65535]
-
- let mut pubkey_hash_slice = [0u8; 20];
- pubkey_hash_slice.copy_from_slice(&pubkey_hash_bytes[0..20]);
-
- let peer_addr = PeerAddress::from_slice(&addr_bytes).expect("FATAL: not 16 bytes");
- if peer_addr.is_in_private_range() {
- debug!(
- "Ignoring private IP address '{}' in hint-replicas",
- &peer_addr.to_socketaddr(port as u16)
- );
- continue;
- }
-
- let naddr = NeighborAddress {
- addrbytes: peer_addr,
- port: port as u16,
- public_key_hash: Hash160(pubkey_hash_slice),
- };
- hint_replicas.push(naddr);
- }
+ Self::eval_hint_replicas(contract_id, hint_replicas_list)?
+ };
Ok(StackerDBConfig {
chunk_size: chunk_size as u64,
@@ -497,6 +512,7 @@ impl StackerDBConfig {
sortition_db: &SortitionDB,
contract_id: &QualifiedContractIdentifier,
max_neighbors: u64,
+ local_hint_replicas: Option>,
) -> Result {
let chain_tip =
NakamotoChainState::get_canonical_block_header(chainstate.db(), sortition_db)?
@@ -578,6 +594,7 @@ impl StackerDBConfig {
&chain_tip_hash,
signers,
max_neighbors,
+ local_hint_replicas,
)?;
Ok(config)
}
diff --git a/stackslib/src/net/stackerdb/mod.rs b/stackslib/src/net/stackerdb/mod.rs
index bbbec21290..9d1b25af51 100644
--- a/stackslib/src/net/stackerdb/mod.rs
+++ b/stackslib/src/net/stackerdb/mod.rs
@@ -133,6 +133,7 @@ use crate::chainstate::burn::db::sortdb::SortitionDB;
use crate::chainstate::nakamoto::NakamotoChainState;
use crate::chainstate::stacks::boot::MINERS_NAME;
use crate::chainstate::stacks::db::StacksChainState;
+use crate::net::connection::ConnectionOptions;
use crate::net::neighbors::NeighborComms;
use crate::net::p2p::PeerNetwork;
use crate::net::{
@@ -285,8 +286,9 @@ impl StackerDBs {
chainstate: &mut StacksChainState,
sortdb: &SortitionDB,
stacker_db_configs: HashMap,
- num_neighbors: u64,
+ connection_opts: &ConnectionOptions,
) -> Result, net_error> {
+ let num_neighbors = connection_opts.num_neighbors;
let existing_contract_ids = self.get_stackerdb_contract_ids()?;
let mut new_stackerdb_configs = HashMap::new();
let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())?;
@@ -314,6 +316,10 @@ impl StackerDBs {
&sortdb,
&stackerdb_contract_id,
num_neighbors,
+ connection_opts
+ .stackerdb_hint_replicas
+ .get(&stackerdb_contract_id)
+ .cloned(),
)
.unwrap_or_else(|e| {
if matches!(e, net_error::NoSuchStackerDB(_)) && stackerdb_contract_id.is_boot()
diff --git a/stackslib/src/net/stackerdb/tests/config.rs b/stackslib/src/net/stackerdb/tests/config.rs
index a075d7b974..cff4ca1059 100644
--- a/stackslib/src/net/stackerdb/tests/config.rs
+++ b/stackslib/src/net/stackerdb/tests/config.rs
@@ -528,7 +528,7 @@ fn test_valid_and_invalid_stackerdb_configs() {
ContractName::try_from(format!("test-{}", i)).unwrap(),
);
peer.with_db_state(|sortdb, chainstate, _, _| {
- match StackerDBConfig::from_smart_contract(chainstate, sortdb, &contract_id, 32) {
+ match StackerDBConfig::from_smart_contract(chainstate, sortdb, &contract_id, 32, None) {
Ok(config) => {
let expected = result
.clone()
@@ -551,3 +551,122 @@ fn test_valid_and_invalid_stackerdb_configs() {
.unwrap();
}
}
+
+#[test]
+fn test_hint_replicas_override() {
+ let AUTO_UNLOCK_HEIGHT = 12;
+ let EXPECTED_FIRST_V2_CYCLE = 8;
+ // the sim environment produces 25 empty sortitions before
+ // tenures start being tracked.
+ let EMPTY_SORTITIONS = 25;
+
+ let mut burnchain = Burnchain::default_unittest(
+ 0,
+ &BurnchainHeaderHash::from_hex(BITCOIN_REGTEST_FIRST_BLOCK_HASH).unwrap(),
+ );
+ burnchain.pox_constants.reward_cycle_length = 5;
+ burnchain.pox_constants.prepare_length = 2;
+ burnchain.pox_constants.anchor_threshold = 1;
+ burnchain.pox_constants.v1_unlock_height = AUTO_UNLOCK_HEIGHT + EMPTY_SORTITIONS;
+
+ let first_v2_cycle = burnchain
+ .block_height_to_reward_cycle(burnchain.pox_constants.v1_unlock_height as u64)
+ .unwrap()
+ + 1;
+
+ assert_eq!(first_v2_cycle, EXPECTED_FIRST_V2_CYCLE);
+
+ let epochs = StacksEpoch::all(0, 0, EMPTY_SORTITIONS as u64 + 10);
+
+ let observer = TestEventObserver::new();
+
+ let (mut peer, mut keys) = instantiate_pox_peer_with_epoch(
+ &burnchain,
+ "test_valid_and_invalid_stackerdb_configs",
+ Some(epochs.clone()),
+ Some(&observer),
+ );
+
+ let contract_owner = keys.pop().unwrap();
+ let contract_id = QualifiedContractIdentifier::new(
+ StacksAddress::from_public_keys(
+ 26,
+ &AddressHashMode::SerializeP2PKH,
+ 1,
+ &vec![StacksPublicKey::from_private(&contract_owner)],
+ )
+ .unwrap()
+ .into(),
+ ContractName::try_from("test-0").unwrap(),
+ );
+
+ peer.config.check_pox_invariants =
+ Some((EXPECTED_FIRST_V2_CYCLE, EXPECTED_FIRST_V2_CYCLE + 10));
+
+ let override_replica = NeighborAddress {
+ addrbytes: PeerAddress([2u8; 16]),
+ port: 123,
+ public_key_hash: Hash160([3u8; 20]),
+ };
+
+ let mut coinbase_nonce = 0;
+ let mut txs = vec![];
+
+ let config_contract = r#"
+ (define-public (stackerdb-get-signer-slots)
+ (ok (list { signer: 'ST2TFVBMRPS5SSNP98DQKQ5JNB2B6NZM91C4K3P7B, num-slots: u3 })))
+
+ (define-public (stackerdb-get-config)
+ (ok {
+ chunk-size: u123,
+ write-freq: u4,
+ max-writes: u56,
+ max-neighbors: u7,
+ hint-replicas: (list
+ {
+ addr: (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0 u255 u255 u142 u150 u80 u100),
+ port: u8901,
+ public-key-hash: 0x0123456789abcdef0123456789abcdef01234567
+ })
+ }))
+ "#;
+
+ let expected_config = StackerDBConfig {
+ chunk_size: 123,
+ signers: vec![(
+ StacksAddress {
+ version: 26,
+ bytes: Hash160::from_hex("b4fdae98b64b9cd6c9436f3b965558966afe890b").unwrap(),
+ },
+ 3,
+ )],
+ write_freq: 4,
+ max_writes: 56,
+ hint_replicas: vec![override_replica.clone()],
+ max_neighbors: 7,
+ };
+
+ let tx = make_smart_contract("test-0", &config_contract, &contract_owner, 0, 10000);
+ txs.push(tx);
+
+ peer.tenure_with_txs(&txs, &mut coinbase_nonce);
+
+ peer.with_db_state(|sortdb, chainstate, _, _| {
+ match StackerDBConfig::from_smart_contract(
+ chainstate,
+ sortdb,
+ &contract_id,
+ 32,
+ Some(vec![override_replica.clone()]),
+ ) {
+ Ok(config) => {
+ assert_eq!(config, expected_config);
+ }
+ Err(e) => {
+ panic!("Unexpected error: {:?}", &e);
+ }
+ }
+ Ok(())
+ })
+ .unwrap();
+}
diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs
index f3773ff611..4ad793a4c3 100644
--- a/testnet/stacks-node/src/config.rs
+++ b/testnet/stacks-node/src/config.rs
@@ -50,7 +50,7 @@ use stacks::cost_estimates::metrics::{CostMetric, ProportionalDotProduct, UnitMe
use stacks::cost_estimates::{CostEstimator, FeeEstimator, PessimisticEstimator, UnitEstimator};
use stacks::net::atlas::AtlasConfig;
use stacks::net::connection::ConnectionOptions;
-use stacks::net::{Neighbor, NeighborKey};
+use stacks::net::{Neighbor, NeighborAddress, NeighborKey};
use stacks::types::chainstate::BurnchainHeaderHash;
use stacks::types::EpochList;
use stacks::util::hash::to_hex;
@@ -2233,6 +2233,7 @@ pub struct ConnectionOptionsFile {
pub auth_token: Option,
pub antientropy_retry: Option,
pub reject_blocks_pushed: Option,
+ pub stackerdb_hint_replicas: Option,
}
impl ConnectionOptionsFile {
@@ -2362,12 +2363,25 @@ impl ConnectionOptionsFile {
handshake_timeout: self.handshake_timeout.unwrap_or(5),
max_sockets: self.max_sockets.unwrap_or(800) as usize,
antientropy_public: self.antientropy_public.unwrap_or(true),
- private_neighbors: self.private_neighbors.unwrap_or(true),
+ private_neighbors: self.private_neighbors.unwrap_or(false),
auth_token: self.auth_token,
antientropy_retry: self.antientropy_retry.unwrap_or(default.antientropy_retry),
reject_blocks_pushed: self
.reject_blocks_pushed
.unwrap_or(default.reject_blocks_pushed),
+ stackerdb_hint_replicas: self
+ .stackerdb_hint_replicas
+ .map(|stackerdb_hint_replicas_json| {
+ let hint_replicas_res: Result<
+ Vec<(QualifiedContractIdentifier, Vec)>,
+ String,
+ > = serde_json::from_str(&stackerdb_hint_replicas_json)
+ .map_err(|e| format!("Failed to decode `stackerdb_hint_replicas`: {e:?}"));
+ hint_replicas_res
+ })
+ .transpose()?
+ .map(HashMap::from_iter)
+ .unwrap_or(default.stackerdb_hint_replicas),
..default
})
}
diff --git a/testnet/stacks-node/src/neon_node.rs b/testnet/stacks-node/src/neon_node.rs
index d2143f4986..c74ce3d878 100644
--- a/testnet/stacks-node/src/neon_node.rs
+++ b/testnet/stacks-node/src/neon_node.rs
@@ -4809,7 +4809,7 @@ impl StacksNode {
&mut chainstate,
&sortdb,
stackerdb_configs,
- config.connection_options.num_neighbors,
+ &config.connection_options,
)
.unwrap();