From f4054a09ac95b4c93cec2109862f1f447571951e Mon Sep 17 00:00:00 2001 From: BowTiedDevOps <157840260+BowTiedDevOps@users.noreply.github.com> Date: Mon, 4 Mar 2024 18:19:07 +0200 Subject: [PATCH 1/3] feat: replace old rustfmt action with custom one for alias input --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 552196e4c..434c977a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,9 @@ jobs: - name: Rustfmt id: rustfmt - uses: actions-rust-lang/rustfmt@2d1d4e9f72379428552fa1def0b898733fb8472d # v1.1.0 + uses: stacks-network/actions/rustfmt@main + with: + alias: "fmt-stacks" ###################################################################################### ## Create a tagged github release From c1a6f2fe6483047a6e293b13c1d6864fce75ed30 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Mon, 4 Mar 2024 11:29:23 -0500 Subject: [PATCH 2/3] Add config option to provide authorization password to block proposal endpoint Signed-off-by: Jacinta Ferrant --- stacks-signer/src/cli.rs | 3 ++ stacks-signer/src/client/stacks_client.rs | 15 +++++- stacks-signer/src/config.rs | 7 +++ stacks-signer/src/main.rs | 1 + stacks-signer/src/tests/conf/signer-0.toml | 1 + stacks-signer/src/tests/conf/signer-1.toml | 1 + stacks-signer/src/tests/conf/signer-4.toml | 1 + stackslib/src/net/api/mod.rs | 4 +- stackslib/src/net/api/postblock_proposal.rs | 26 +++++----- stackslib/src/net/connection.rs | 3 ++ stackslib/src/net/httpcore.rs | 3 ++ .../src/tests/nakamoto_integrations.rs | 47 ++++++++++++++----- testnet/stacks-node/src/tests/signer.rs | 12 ++++- 13 files changed, 97 insertions(+), 27 deletions(-) diff --git a/stacks-signer/src/cli.rs b/stacks-signer/src/cli.rs index 639b57f3a..af1987567 100644 --- a/stacks-signer/src/cli.rs +++ b/stacks-signer/src/cli.rs @@ -173,6 +173,9 @@ pub struct GenerateFilesArgs { /// The number of milliseconds to wait when polling for events from the stacker-db instance. #[arg(long)] pub timeout: Option, + #[arg(long)] + /// The authorization password to use to connect to the validate block proposal node endpoint + pub password: String, } #[derive(Clone, Debug)] diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs index 054e4fb37..7dd72ae38 100644 --- a/stacks-signer/src/client/stacks_client.rs +++ b/stacks-signer/src/client/stacks_client.rs @@ -34,6 +34,7 @@ use blockstack_lib::net::api::postblock_proposal::NakamotoBlockProposal; use blockstack_lib::util_lib::boot::{boot_code_addr, boot_code_id}; use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier}; use clarity::vm::{ClarityName, ContractName, Value as ClarityValue}; +use reqwest::header::AUTHORIZATION; use serde_json::json; use slog::slog_debug; use stacks_common::codec::StacksMessageCodec; @@ -63,6 +64,8 @@ pub struct StacksClient { mainnet: bool, /// The Client used to make HTTP connects stacks_node_client: reqwest::blocking::Client, + /// the auth password for the stacks node + auth_password: String, } impl From<&GlobalConfig> for StacksClient { @@ -75,13 +78,19 @@ impl From<&GlobalConfig> for StacksClient { chain_id: config.network.to_chain_id(), stacks_node_client: reqwest::blocking::Client::new(), mainnet: config.network.is_mainnet(), + auth_password: config.auth_password.clone(), } } } impl StacksClient { - /// Create a new signer StacksClient with the provided private key, stacks node host endpoint, and version - pub fn new(stacks_private_key: StacksPrivateKey, node_host: SocketAddr, mainnet: bool) -> Self { + /// Create a new signer StacksClient with the provided private key, stacks node host endpoint, version, and auth password + pub fn new( + stacks_private_key: StacksPrivateKey, + node_host: SocketAddr, + auth_password: String, + mainnet: bool, + ) -> Self { let pubkey = StacksPublicKey::from_private(&stacks_private_key); let tx_version = if mainnet { TransactionVersion::Mainnet @@ -102,6 +111,7 @@ impl StacksClient { chain_id, stacks_node_client: reqwest::blocking::Client::new(), mainnet, + auth_password, } } @@ -214,6 +224,7 @@ impl StacksClient { self.stacks_node_client .post(self.block_proposal_path()) .header("Content-Type", "application/json") + .header(AUTHORIZATION, self.auth_password.clone()) .json(&block_proposal) .send() .map_err(backoff::Error::transient) diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs index d8c7b4a8e..9bbdc4f98 100644 --- a/stacks-signer/src/config.rs +++ b/stacks-signer/src/config.rs @@ -194,6 +194,8 @@ pub struct GlobalConfig { pub sign_timeout: Option, /// the STX tx fee to use in uSTX pub tx_fee_ustx: u64, + /// the authorization password for the block proposal endpoint + pub auth_password: String, } /// Internal struct for loading up the config file @@ -222,6 +224,8 @@ struct RawConfigFile { pub sign_timeout_ms: Option, /// the STX tx fee to use in uSTX pub tx_fee_ustx: Option, + /// The authorization password for the block proposal endpoint + pub auth_password: String, } impl RawConfigFile { @@ -320,6 +324,7 @@ impl TryFrom for GlobalConfig { nonce_timeout, sign_timeout, tx_fee_ustx: raw_data.tx_fee_ustx.unwrap_or(TX_FEE_USTX), + auth_password: raw_data.auth_password, }) } } @@ -350,6 +355,7 @@ pub fn build_signer_config_tomls( node_host: &str, timeout: Option, network: &Network, + password: &str, ) -> Vec { let mut signer_config_tomls = vec![]; @@ -364,6 +370,7 @@ stacks_private_key = "{stacks_private_key}" node_host = "{node_host}" endpoint = "{endpoint}" network = "{network}" +auth_password = "{password}" "# ); diff --git a/stacks-signer/src/main.rs b/stacks-signer/src/main.rs index e59722dd5..194e7fb48 100644 --- a/stacks-signer/src/main.rs +++ b/stacks-signer/src/main.rs @@ -292,6 +292,7 @@ fn handle_generate_files(args: GenerateFilesArgs) { &args.host.to_string(), args.timeout.map(Duration::from_millis), &args.network, + &args.password, ); debug!("Built {:?} signer config tomls.", signer_config_tomls.len()); for (i, file_contents) in signer_config_tomls.iter().enumerate() { diff --git a/stacks-signer/src/tests/conf/signer-0.toml b/stacks-signer/src/tests/conf/signer-0.toml index 2c30d8232..449392c2e 100644 --- a/stacks-signer/src/tests/conf/signer-0.toml +++ b/stacks-signer/src/tests/conf/signer-0.toml @@ -2,3 +2,4 @@ stacks_private_key = "6a1fc1a3183018c6d79a4e11e154d2bdad2d89ac8bc1b0a021de8b4d28 node_host = "127.0.0.1:20443" endpoint = "localhost:30000" network = "testnet" +auth_password = "12345" diff --git a/stacks-signer/src/tests/conf/signer-1.toml b/stacks-signer/src/tests/conf/signer-1.toml index 99facfc1d..3d293af64 100644 --- a/stacks-signer/src/tests/conf/signer-1.toml +++ b/stacks-signer/src/tests/conf/signer-1.toml @@ -2,3 +2,4 @@ stacks_private_key = "126e916e77359ccf521e168feea1fcb9626c59dc375cae00c746430338 node_host = "127.0.0.1:20444" endpoint = "localhost:30001" network = "testnet" +auth_password = "12345" diff --git a/stacks-signer/src/tests/conf/signer-4.toml b/stacks-signer/src/tests/conf/signer-4.toml index 87cda8332..0e80a1aa6 100644 --- a/stacks-signer/src/tests/conf/signer-4.toml +++ b/stacks-signer/src/tests/conf/signer-4.toml @@ -3,3 +3,4 @@ stacks_private_key = "e427196ae29197b1db6d5495ff26bf0675f48a4f07b200c0814b95734e node_host = "127.0.0.1:20443" endpoint = "localhost:30004" network = "testnet" +auth_password = "12345" diff --git a/stackslib/src/net/api/mod.rs b/stackslib/src/net/api/mod.rs index b6fb21fc2..c1a042aef 100644 --- a/stackslib/src/net/api/mod.rs +++ b/stackslib/src/net/api/mod.rs @@ -112,7 +112,9 @@ impl StacksHttp { liststackerdbreplicas::RPCListStackerDBReplicasRequestHandler::new(), ); self.register_rpc_endpoint(postblock::RPCPostBlockRequestHandler::new()); - self.register_rpc_endpoint(postblock_proposal::RPCBlockProposalRequestHandler::new()); + self.register_rpc_endpoint(postblock_proposal::RPCBlockProposalRequestHandler::new( + self.block_proposal_token.clone(), + )); self.register_rpc_endpoint(postfeerate::RPCPostFeeRateRequestHandler::new()); self.register_rpc_endpoint(postmempoolquery::RPCMempoolQueryRequestHandler::new()); self.register_rpc_endpoint(postmicroblock::RPCPostMicroblockRequestHandler::new()); diff --git a/stackslib/src/net/api/postblock_proposal.rs b/stackslib/src/net/api/postblock_proposal.rs index 72b85c577..50b041529 100644 --- a/stackslib/src/net/api/postblock_proposal.rs +++ b/stackslib/src/net/api/postblock_proposal.rs @@ -342,11 +342,15 @@ impl NakamotoBlockProposal { #[derive(Clone, Default)] pub struct RPCBlockProposalRequestHandler { pub block_proposal: Option, + pub auth: Option, } impl RPCBlockProposalRequestHandler { - pub fn new() -> Self { - Self::default() + pub fn new(auth: Option) -> Self { + Self { + block_proposal: None, + auth, + } } /// Decode a JSON-encoded block proposal @@ -375,24 +379,22 @@ impl HttpRequest for RPCBlockProposalRequestHandler { query: Option<&str>, body: &[u8], ) -> Result { - // Only accept requests from localhost - let is_loopback = match preamble.host { - // Should never be DNS - PeerHost::DNS(..) => false, - PeerHost::IP(addr, ..) => addr.is_loopback(), + // If no authorization is set, then the block proposal endpoint is not enabled + let Some(password) = &self.auth else { + return Err(Error::Http(400, "Bad Request.".into())); }; - - if !is_loopback { - return Err(Error::Http(403, "Forbidden".into())); + let Some(auth_header) = preamble.headers.get("authorization") else { + return Err(Error::Http(401, "Unauthorized".into())); + }; + if auth_header != password { + return Err(Error::Http(401, "Unauthorized".into())); } - if preamble.get_content_length() == 0 { return Err(Error::DecodeError( "Invalid Http request: expected non-zero-length body for block proposal endpoint" .to_string(), )); } - if preamble.get_content_length() > MAX_PAYLOAD_LEN { return Err(Error::DecodeError( "Invalid Http request: BlockProposal body is too big".to_string(), diff --git a/stackslib/src/net/connection.rs b/stackslib/src/net/connection.rs index 25d3ee748..878ab04ef 100644 --- a/stackslib/src/net/connection.rs +++ b/stackslib/src/net/connection.rs @@ -415,6 +415,8 @@ pub struct ConnectionOptions { /// the reward cycle in which Nakamoto activates, and thus needs to run both the epoch /// 2.x and Nakamoto state machines. pub force_nakamoto_epoch_transition: bool, + /// The authorization token to enable the block proposal RPC endpoint + pub block_proposal_token: Option, } impl std::default::Default for ConnectionOptions { @@ -508,6 +510,7 @@ impl std::default::Default for ConnectionOptions { disable_stackerdb_get_chunks: false, force_disconnect_interval: None, force_nakamoto_epoch_transition: false, + block_proposal_token: None, } } } diff --git a/stackslib/src/net/httpcore.rs b/stackslib/src/net/httpcore.rs index 074605bcd..04dd185e1 100644 --- a/stackslib/src/net/httpcore.rs +++ b/stackslib/src/net/httpcore.rs @@ -871,6 +871,8 @@ pub struct StacksHttp { pub maximum_call_argument_size: u32, /// Maximum execution budget of a read-only call pub read_only_call_limit: ExecutionCost, + /// The authorization token to enable the block proposal RPC endpoint + pub block_proposal_token: Option, } impl StacksHttp { @@ -886,6 +888,7 @@ impl StacksHttp { request_handlers: vec![], maximum_call_argument_size: conn_opts.maximum_call_argument_size, read_only_call_limit: conn_opts.read_only_call_limit.clone(), + block_proposal_token: conn_opts.block_proposal_token.clone(), }; http.register_rpc_methods(); http diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index 3b46ce24a..6cd15a4d3 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -21,6 +21,7 @@ use std::{env, thread}; use clarity::vm::ast::ASTRules; use clarity::vm::costs::ExecutionCost; use clarity::vm::types::PrincipalData; +use http_types::headers::AUTHORIZATION; use lazy_static::lazy_static; use libsigner::{SignerSession, StackerDBSession}; use stacks::burnchains::MagicBytes; @@ -1402,6 +1403,8 @@ fn block_proposal_api_endpoint() { } let (mut conf, _miner_account) = naka_neon_integration_conf(None); + let password = "12345".to_string(); + conf.connection_options.block_proposal_token = Some(password.clone()); let account_keys = add_initial_balances(&mut conf, 10, 1_000_000); let stacker_sk = setup_stacker(&mut conf); let sender_signer_sk = Secp256k1PrivateKey::new(); @@ -1593,6 +1596,7 @@ fn block_proposal_api_endpoint() { const HTTP_ACCEPTED: u16 = 202; const HTTP_TOO_MANY: u16 = 429; + const HTTP_NOT_AUTHORIZED: u16 = 401; let test_cases = [ ( "Valid Nakamoto block proposal", @@ -1631,6 +1635,12 @@ fn block_proposal_api_endpoint() { HTTP_ACCEPTED, Some(Err(ValidateRejectCode::ChainstateError)), ), + ( + "Not authorized", + sign(proposal.clone()), + HTTP_NOT_AUTHORIZED, + None, + ), ]; // Build HTTP client @@ -1647,12 +1657,18 @@ fn block_proposal_api_endpoint() { test_cases.iter().enumerate() { // Send POST request - let mut response = client + let request_builder = client .post(&path) .header("Content-Type", "application/json") - .json(block_proposal) - .send() - .expect("Failed to POST"); + .json(block_proposal); + let mut response = if expected_http_code == &HTTP_NOT_AUTHORIZED { + request_builder.send().expect("Failed to POST") + } else { + request_builder + .header(AUTHORIZATION.to_string(), password.to_string()) + .send() + .expect("Failed to POST") + }; let start_time = Instant::now(); while ix != 1 && response.status().as_u16() == HTTP_TOO_MANY { if start_time.elapsed() > Duration::from_secs(30) { @@ -1661,20 +1677,29 @@ fn block_proposal_api_endpoint() { } info!("Waiting for prior request to finish processing, and then resubmitting"); thread::sleep(Duration::from_secs(5)); - response = client + let request_builder = client .post(&path) .header("Content-Type", "application/json") - .json(block_proposal) - .send() - .expect("Failed to POST"); + .json(block_proposal); + response = if expected_http_code == &HTTP_NOT_AUTHORIZED { + request_builder.send().expect("Failed to POST") + } else { + request_builder + .header(AUTHORIZATION.to_string(), password.to_string()) + .send() + .expect("Failed to POST") + }; } let response_code = response.status().as_u16(); - let response_json = response.json::(); - + let response_json = if expected_http_code != &HTTP_NOT_AUTHORIZED { + response.json::().unwrap().to_string() + } else { + "No json response".to_string() + }; info!( "Block proposal submitted and checked for HTTP response"; - "response_json" => %response_json.unwrap(), + "response_json" => response_json, "request_json" => serde_json::to_string(block_proposal).unwrap(), "response_code" => response_code, "test_description" => test_description, diff --git a/testnet/stacks-node/src/tests/signer.rs b/testnet/stacks-node/src/tests/signer.rs index c0c2e72e2..0be42768b 100644 --- a/testnet/stacks-node/src/tests/signer.rs +++ b/testnet/stacks-node/src/tests/signer.rs @@ -105,6 +105,10 @@ impl SignerTest { let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None); naka_conf.miner.self_signing_key = None; + // So the combination is... one, two, three, four, five? That's the stupidest combination I've ever heard in my life! + // That's the kind of thing an idiot would have on his luggage! + let password = "12345"; + naka_conf.connection_options.block_proposal_token = Some(password.to_string()); // Setup the signer and coordinator configurations let signer_configs = build_signer_config_tomls( @@ -112,6 +116,7 @@ impl SignerTest { &naka_conf.node.rpc_bind, Some(Duration::from_millis(128)), // Timeout defaults to 5 seconds. Let's override it to 128 milliseconds. &Network::Testnet, + password, ); let mut running_signers = Vec::new(); @@ -726,7 +731,12 @@ impl SignerTest { ) .unwrap(); - let invalid_stacks_client = StacksClient::new(StacksPrivateKey::new(), host, false); + let invalid_stacks_client = StacksClient::new( + StacksPrivateKey::new(), + host, + "12345".to_string(), // That's amazing. I've got the same combination on my luggage! + false, + ); let invalid_signer_tx = invalid_stacks_client .build_vote_for_aggregate_public_key(0, round, point, reward_cycle, None, 0) .expect("FATAL: failed to build vote for aggregate public key"); From 60c9e333c4ae7ca1c1ed0d0940a330b33017b13a Mon Sep 17 00:00:00 2001 From: Hank Stoever Date: Mon, 4 Mar 2024 11:53:45 -0800 Subject: [PATCH 3/3] fix: add block_proposal_token to file type --- testnet/stacks-node/src/config.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 7e2751d7a..a5adf1e5b 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -180,6 +180,25 @@ mod tests { "ST2TFVBMRPS5SSNP98DQKQ5JNB2B6NZM91C4K3P7B" ); } + + #[test] + fn should_load_block_proposal_token() { + let config = Config::from_config_file( + ConfigFile::from_str( + r#" + [connection_options] + block_proposal_token = "password" + "#, + ) + .unwrap(), + ) + .expect("Expected to be able to parse block proposal token from file"); + + assert_eq!( + config.connection_options.block_proposal_token, + Some("password".to_string()) + ); + } } impl ConfigFile { @@ -2106,6 +2125,7 @@ pub struct ConnectionOptionsFile { pub force_disconnect_interval: Option, pub antientropy_public: Option, pub private_neighbors: Option, + pub block_proposal_token: Option, } impl ConnectionOptionsFile { @@ -2229,6 +2249,7 @@ impl ConnectionOptionsFile { 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), + block_proposal_token: self.block_proposal_token, ..ConnectionOptions::default() }) }