From b6076b995df1ae943187c9b9f11f8ed33c72e9d9 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 2 Dec 2024 17:00:49 -0500 Subject: [PATCH 1/8] fix: expose locally-overriden hint-replicas data for each stackerdb replica --- stackslib/src/net/connection.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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, From d4a0c5c65750c51e4fb4171307d3f3ac05f42efa Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 2 Dec 2024 17:01:16 -0500 Subject: [PATCH 2/8] chore: API sync --- stackslib/src/net/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"); From 28134bf535560bcebbaae4b14a4cf7061c315f7d Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 2 Dec 2024 17:01:29 -0500 Subject: [PATCH 3/8] chore: API sync; only count authenticated outbound nodes --- stackslib/src/net/p2p.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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(()) } From 19e3cb58b4f957ee12385fbddbd4e4565490d226 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 2 Dec 2024 17:01:53 -0500 Subject: [PATCH 4/8] chore: override stackerdb smart contract hint-replicas with local hint-replicas if given --- stackslib/src/net/stackerdb/config.rs | 181 ++++++++++++++------------ 1 file changed, 99 insertions(+), 82 deletions(-) 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) } From 6794642a7e55ea2027dcdd106e91932944446abe Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 2 Dec 2024 17:02:17 -0500 Subject: [PATCH 5/8] chore: API sync --- stackslib/src/net/stackerdb/mod.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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() From 8a44ece596b2dfbfe6d8382c4628b139d2580835 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 2 Dec 2024 17:02:33 -0500 Subject: [PATCH 6/8] chore: test local hint-replicas override --- stackslib/src/net/stackerdb/tests/config.rs | 121 +++++++++++++++++++- 1 file changed, 120 insertions(+), 1 deletion(-) 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(); +} From 979fefe9d20d0765d34e70ef7101288194ef61b2 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 2 Dec 2024 17:02:46 -0500 Subject: [PATCH 7/8] chore: add config setting for stackerdb_hint_replicas; also make private_neighbors false by default --- testnet/stacks-node/src/config.rs | 27 +++++++++++++++++++++++++-- testnet/stacks-node/src/neon_node.rs | 2 +- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 785ce057e5..4e7ec7bc88 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_lib::boot::boot_code_id; @@ -2223,6 +2223,7 @@ pub struct ConnectionOptionsFile { pub auth_token: Option, pub antientropy_retry: Option, pub reject_blocks_pushed: Option, + pub stackerdb_hint_replicas: Option, } impl ConnectionOptionsFile { @@ -2352,12 +2353,34 @@ 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()? + .and_then(|stackerdb_replicas_list| { + // coalesce to a hashmap, but don't worry about duplicate entries + // (garbage in, garbage out) + let stackerdb_hint_replicas: HashMap< + QualifiedContractIdentifier, + Vec, + > = stackerdb_replicas_list.into_iter().collect(); + + Some(stackerdb_hint_replicas) + }) + .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 b688db100d..9dba79a2c5 100644 --- a/testnet/stacks-node/src/neon_node.rs +++ b/testnet/stacks-node/src/neon_node.rs @@ -4810,7 +4810,7 @@ impl StacksNode { &mut chainstate, &sortdb, stackerdb_configs, - config.connection_options.num_neighbors, + &config.connection_options, ) .unwrap(); From f06321673295df889fdc34592bccc4c29548c3f2 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Tue, 3 Dec 2024 18:02:50 -0500 Subject: [PATCH 8/8] chore: PR feedback --- testnet/stacks-node/src/config.rs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 4e7ec7bc88..ad780fa17c 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -2370,16 +2370,7 @@ impl ConnectionOptionsFile { hint_replicas_res }) .transpose()? - .and_then(|stackerdb_replicas_list| { - // coalesce to a hashmap, but don't worry about duplicate entries - // (garbage in, garbage out) - let stackerdb_hint_replicas: HashMap< - QualifiedContractIdentifier, - Vec, - > = stackerdb_replicas_list.into_iter().collect(); - - Some(stackerdb_hint_replicas) - }) + .map(HashMap::from_iter) .unwrap_or(default.stackerdb_hint_replicas), ..default })