Merge branch 'develop' of https://github.com/stacks-network/stacks-core into chore/block-signature-message-type

This commit is contained in:
Jacinta Ferrant
2024-05-14 11:56:12 -07:00
10 changed files with 181 additions and 67 deletions

View File

@@ -16,6 +16,7 @@
use std::io::{Read, Write};
use clarity::vm::costs::ExecutionCost;
use regex::{Captures, Regex};
use stacks_common::types::chainstate::{
BlockHeaderHash, ConsensusHash, StacksBlockId, StacksPublicKey,
@@ -23,6 +24,7 @@ use stacks_common::types::chainstate::{
use stacks_common::types::net::PeerHost;
use stacks_common::types::StacksPublicKeyBuffer;
use stacks_common::util::hash::{Hash160, Sha256Sum};
use url::form_urlencoded;
use crate::burnchains::affirmation::AffirmationMap;
use crate::burnchains::Txid;
@@ -30,19 +32,23 @@ use crate::chainstate::burn::db::sortdb::SortitionDB;
use crate::chainstate::stacks::db::blocks::MINIMUM_TX_FEE_RATE_PER_BYTE;
use crate::chainstate::stacks::db::StacksChainState;
use crate::core::mempool::MemPoolDB;
use crate::net::api::postfeerate::RPCPostFeeRateRequestHandler;
use crate::net::http::{
parse_json, Error, HttpRequest, HttpRequestContents, HttpRequestPreamble, HttpResponse,
HttpResponseContents, HttpResponsePayload, HttpResponsePreamble,
parse_json, Error, HttpBadRequest, HttpRequest, HttpRequestContents, HttpRequestPreamble,
HttpResponse, HttpResponseContents, HttpResponsePayload, HttpResponsePreamble,
};
use crate::net::httpcore::{
HttpPreambleExtensions, RPCRequestHandler, StacksHttpRequest, StacksHttpResponse,
};
use crate::net::p2p::PeerNetwork;
use crate::net::{Error as NetError, StacksNodeState};
use crate::net::{Error as NetError, HttpServerError, StacksNodeState};
use crate::version_string;
pub(crate) const SINGLESIG_TX_TRANSFER_LEN: u64 = 180;
#[derive(Clone)]
pub struct RPCGetStxTransferCostRequestHandler {}
impl RPCGetStxTransferCostRequestHandler {
pub fn new() -> Self {
Self {}
@@ -74,7 +80,7 @@ impl HttpRequest for RPCGetStxTransferCostRequestHandler {
) -> Result<HttpRequestContents, Error> {
if preamble.get_content_length() != 0 {
return Err(Error::DecodeError(
"Invalid Http request: expected 0-length body for GetInfo".to_string(),
"Invalid Http request: expected 0-length body".to_string(),
));
}
Ok(HttpRequestContents::new().query_string(query))
@@ -92,9 +98,57 @@ impl RPCRequestHandler for RPCGetStxTransferCostRequestHandler {
_contents: HttpRequestContents,
node: &mut StacksNodeState,
) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
// todo -- need to actually estimate the cost / length for token transfers
// right now, it just uses the minimum.
let fee = MINIMUM_TX_FEE_RATE_PER_BYTE;
// NOTE: The estimated length isn't needed per se because we're returning a fee rate, but
// we do need an absolute length to use the estimator (so supply a common one).
let estimated_len = SINGLESIG_TX_TRANSFER_LEN;
let fee_resp = node.with_node_state(|_network, sortdb, _chainstate, _mempool, rpc_args| {
let tip = self.get_canonical_burn_chain_tip(&preamble, sortdb)?;
let stacks_epoch = self.get_stacks_epoch(&preamble, sortdb, tip.block_height)?;
if let Some((_, fee_estimator, metric)) = rpc_args.get_estimators_ref() {
// STX transfer transactions have zero runtime cost
let estimated_cost = ExecutionCost::zero();
let estimations =
RPCPostFeeRateRequestHandler::estimate_tx_fee_from_cost_and_length(
&preamble,
fee_estimator,
metric,
estimated_cost,
estimated_len,
stacks_epoch,
)?
.estimations;
if estimations.len() != 3 {
// logic bug, but treat as runtime error
return Err(StacksHttpResponse::new_error(
&preamble,
&HttpServerError::new(
"Logic error in fee estimation: did not get three estimates".into(),
),
));
}
// safety -- checked estimations.len() == 3 above
let median_estimation = &estimations[1];
// NOTE: this returns the fee _rate_
Ok(median_estimation.fee / estimated_len)
} else {
// unlike `POST /v2/fees/transaction`, this method can't fail due to the
// unavailability of cost estimation, so just assume the minimum fee.
debug!("Fee and cost estimation not configured on this stacks node");
Ok(MINIMUM_TX_FEE_RATE_PER_BYTE)
}
});
let fee = match fee_resp {
Ok(fee) => fee,
Err(response) => {
return response.try_into_contents().map_err(NetError::from);
}
};
let mut preamble = HttpResponsePreamble::ok_json(&preamble);
preamble.set_canonical_stacks_tip_height(Some(node.canonical_stacks_tip_height()));
let body = HttpResponseContents::try_from_json(&fee)?;
@@ -116,13 +170,9 @@ impl HttpResponse for RPCGetStxTransferCostRequestHandler {
impl StacksHttpRequest {
pub fn new_get_stx_transfer_cost(host: PeerHost) -> StacksHttpRequest {
StacksHttpRequest::new_for_peer(
host,
"GET".into(),
"/v2/fees/transfer".into(),
HttpRequestContents::new(),
)
.expect("FATAL: failed to construct request from infallible data")
let mut contents = HttpRequestContents::new();
StacksHttpRequest::new_for_peer(host, "GET".into(), "/v2/fees/transfer".into(), contents)
.expect("FATAL: failed to construct request from infallible data")
}
}

View File

@@ -34,7 +34,9 @@ use crate::chainstate::stacks::db::blocks::MINIMUM_TX_FEE_RATE_PER_BYTE;
use crate::chainstate::stacks::db::StacksChainState;
use crate::chainstate::stacks::TransactionPayload;
use crate::core::mempool::MemPoolDB;
use crate::cost_estimates::FeeRateEstimate;
use crate::core::StacksEpoch;
use crate::cost_estimates::metrics::CostMetric;
use crate::cost_estimates::{CostEstimator, FeeEstimator, FeeRateEstimate};
use crate::net::http::{
parse_json, Error, HttpBadRequest, HttpContentType, HttpNotFound, HttpRequest,
HttpRequestContents, HttpRequestPreamble, HttpResponse, HttpResponseContents,
@@ -92,6 +94,7 @@ pub struct RPCPostFeeRateRequestHandler {
pub estimated_len: Option<u64>,
pub transaction_payload: Option<TransactionPayload>,
}
impl RPCPostFeeRateRequestHandler {
pub fn new() -> Self {
Self {
@@ -99,6 +102,48 @@ impl RPCPostFeeRateRequestHandler {
transaction_payload: None,
}
}
/// Estimate a transaction fee, given its execution cost estimation and length estimation
/// and cost estimators.
/// Returns Ok(fee structure) on success
/// Returns Err(HTTP response) on error
pub fn estimate_tx_fee_from_cost_and_length(
preamble: &HttpRequestPreamble,
fee_estimator: &dyn FeeEstimator,
metric: &dyn CostMetric,
estimated_cost: ExecutionCost,
estimated_len: u64,
stacks_epoch: StacksEpoch,
) -> Result<RPCFeeEstimateResponse, StacksHttpResponse> {
let scalar_cost =
metric.from_cost_and_len(&estimated_cost, &stacks_epoch.block_limit, estimated_len);
let fee_rates = fee_estimator.get_rate_estimates().map_err(|e| {
StacksHttpResponse::new_error(
&preamble,
&HttpBadRequest::new(format!(
"Estimator RPC endpoint failed to estimate fees for tx: {:?}",
&e
)),
)
})?;
let mut estimations = RPCFeeEstimate::estimate_fees(scalar_cost, fee_rates).to_vec();
let minimum_fee = estimated_len * MINIMUM_TX_FEE_RATE_PER_BYTE;
for estimate in estimations.iter_mut() {
if estimate.fee < minimum_fee {
estimate.fee = minimum_fee;
}
}
Ok(RPCFeeEstimateResponse {
estimated_cost,
estimations,
estimated_cost_scalar: scalar_cost,
cost_scalar_change_by_byte: metric.change_per_byte(),
})
}
}
/// Decode the HTTP request
@@ -206,39 +251,14 @@ impl RPCRequestHandler for RPCPostFeeRateRequestHandler {
)
})?;
let scalar_cost = metric.from_cost_and_len(
&estimated_cost,
&stacks_epoch.block_limit,
estimated_len,
);
let fee_rates = fee_estimator.get_rate_estimates().map_err(|e| {
StacksHttpResponse::new_error(
&preamble,
&HttpBadRequest::new(format!(
"Estimator RPC endpoint failed to estimate fees for tx {}: {:?}",
&tx.name(),
&e
)),
)
})?;
let mut estimations =
RPCFeeEstimate::estimate_fees(scalar_cost, fee_rates).to_vec();
let minimum_fee = estimated_len * MINIMUM_TX_FEE_RATE_PER_BYTE;
for estimate in estimations.iter_mut() {
if estimate.fee < minimum_fee {
estimate.fee = minimum_fee;
}
}
Ok(RPCFeeEstimateResponse {
Self::estimate_tx_fee_from_cost_and_length(
&preamble,
fee_estimator,
metric,
estimated_cost,
estimations,
estimated_cost_scalar: scalar_cost,
cost_scalar_change_by_byte: metric.change_per_byte(),
})
estimated_len,
stacks_epoch,
)
} else {
debug!("Fee and cost estimation not configured on this stacks node");
Err(StacksHttpResponse::new_error(

View File

@@ -25,6 +25,7 @@ use stacks_common::types::Address;
use super::test_rpc;
use crate::chainstate::stacks::db::blocks::MINIMUM_TX_FEE_RATE_PER_BYTE;
use crate::core::BLOCK_LIMIT_MAINNET_21;
use crate::net::api::getstxtransfercost::SINGLESIG_TX_TRANSFER_LEN;
use crate::net::api::*;
use crate::net::connection::ConnectionOptions;
use crate::net::httpcore::{
@@ -67,6 +68,7 @@ fn test_try_make_response() {
let mut responses = test_rpc(function_name!(), vec![request]);
assert_eq!(responses.len(), 1);
responses.reverse();
let response = responses.pop().unwrap();
debug!(
@@ -80,5 +82,6 @@ fn test_try_make_response() {
);
let fee_rate = response.decode_stx_transfer_fee().unwrap();
debug!("fee_rate = {:?}", &fee_rate);
assert_eq!(fee_rate, MINIMUM_TX_FEE_RATE_PER_BYTE);
}

View File

@@ -646,6 +646,24 @@ impl PeerNetwork {
Ok(())
}
/// Call `bind()` only if not already bound
/// Returns:
/// - `Ok(true)` if `bind()` call was successful
/// - `Ok(false)` if `bind()` call was skipped
/// - `Err()` if `bind()`` failed
#[cfg_attr(test, mutants::skip)]
pub fn try_bind(
&mut self,
my_addr: &SocketAddr,
http_addr: &SocketAddr,
) -> Result<bool, net_error> {
if self.network.is_some() {
// Already bound
return Ok(false);
}
self.bind(my_addr, http_addr).map(|()| true)
}
/// Get bound neighbor key. This is how this PeerNetwork appears to other nodes.
pub fn bound_neighbor_key(&self) -> &NeighborKey {
&self.bind_nk

View File

@@ -25,6 +25,7 @@ use stacks::chainstate::stacks::Error as ChainstateError;
use stacks::monitoring;
use stacks::monitoring::update_active_miners_count_gauge;
use stacks::net::atlas::AtlasConfig;
use stacks::net::p2p::PeerNetwork;
use stacks::net::relay::Relayer;
use stacks::net::stackerdb::StackerDBs;
use stacks_common::types::chainstate::SortitionId;
@@ -132,6 +133,7 @@ impl StacksNode {
globals: Globals,
// relay receiver endpoint for the p2p thread, so the relayer can feed it data to push
relay_recv: Receiver<RelayerDirective>,
peer_network: Option<PeerNetwork>,
) -> StacksNode {
let config = runloop.config().clone();
let is_miner = runloop.is_miner();
@@ -157,7 +159,8 @@ impl StacksNode {
.connect_mempool_db()
.expect("FATAL: database failure opening mempool");
let mut p2p_net = NeonNode::setup_peer_network(&config, &atlas_config, burnchain);
let mut p2p_net = peer_network
.unwrap_or_else(|| NeonNode::setup_peer_network(&config, &atlas_config, burnchain));
let stackerdbs = StackerDBs::connect(&config.get_stacker_db_file_path(), true)
.expect("FATAL: failed to connect to stacker DB");

View File

@@ -182,8 +182,13 @@ impl PeerThread {
.parse()
.unwrap_or_else(|_| panic!("Failed to parse socket: {}", &config.node.rpc_bind));
net.bind(&p2p_sock, &rpc_sock)
.expect("BUG: PeerNetwork could not bind or is already bound");
let did_bind = net
.try_bind(&p2p_sock, &rpc_sock)
.expect("BUG: PeerNetwork could not bind");
if !did_bind {
info!("`PeerNetwork::bind()` skipped, already bound");
}
let poll_timeout = cmp::min(5000, config.miner.first_attempt_time_ms / 2);

View File

@@ -298,7 +298,7 @@ pub struct StacksNode {
/// True if we're a miner
is_miner: bool,
/// handle to the p2p thread
pub p2p_thread_handle: JoinHandle<()>,
pub p2p_thread_handle: JoinHandle<Option<PeerNetwork>>,
/// handle to the relayer thread
pub relayer_thread_handle: JoinHandle<()>,
}
@@ -4655,7 +4655,10 @@ impl StacksNode {
/// Main loop of the p2p thread.
/// Runs in a separate thread.
/// Continuously receives, until told otherwise.
pub fn p2p_main(mut p2p_thread: PeerThread, event_dispatcher: EventDispatcher) {
pub fn p2p_main(
mut p2p_thread: PeerThread,
event_dispatcher: EventDispatcher,
) -> Option<PeerNetwork> {
let should_keep_running = p2p_thread.globals.should_keep_running.clone();
let (mut dns_resolver, mut dns_client) = DNSResolver::new(10);
@@ -4718,6 +4721,7 @@ impl StacksNode {
thread::sleep(Duration::from_secs(5));
}
info!("P2P thread exit!");
p2p_thread.net
}
/// This function sets the global var `GLOBAL_BURNCHAIN_SIGNER`.
@@ -4814,7 +4818,7 @@ impl StacksNode {
))
.spawn(move || {
debug!("p2p thread ID is {:?}", thread::current().id());
Self::p2p_main(p2p_thread, p2p_event_dispatcher);
Self::p2p_main(p2p_thread, p2p_event_dispatcher)
})
.expect("FATAL: failed to start p2p thread");
@@ -5017,8 +5021,8 @@ impl StacksNode {
}
/// Join all inner threads
pub fn join(self) {
pub fn join(self) -> Option<PeerNetwork> {
self.relayer_thread_handle.join().unwrap();
self.p2p_thread_handle.join().unwrap();
self.p2p_thread_handle.join().unwrap()
}
}

View File

@@ -108,7 +108,7 @@ impl BootRunLoop {
let InnerLoops::Epoch3(ref mut naka_loop) = self.active_loop else {
panic!("FATAL: unexpectedly invoked start_from_naka when active loop wasn't nakamoto");
};
naka_loop.start(burnchain_opt, mine_start)
naka_loop.start(burnchain_opt, mine_start, None)
}
fn start_from_neon(&mut self, burnchain_opt: Option<Burnchain>, mine_start: u64) {
@@ -120,7 +120,7 @@ impl BootRunLoop {
let boot_thread = Self::spawn_stopper(&self.config, neon_loop)
.expect("FATAL: failed to spawn epoch-2/3-boot thread");
neon_loop.start(burnchain_opt.clone(), mine_start);
let peer_network = neon_loop.start(burnchain_opt.clone(), mine_start);
let monitoring_thread = neon_loop.take_monitoring_thread();
// did we exit because of the epoch-3.0 transition, or some other reason?
@@ -150,7 +150,7 @@ impl BootRunLoop {
let InnerLoops::Epoch3(ref mut naka_loop) = self.active_loop else {
panic!("FATAL: unexpectedly found epoch2 loop after setting epoch3 active");
};
naka_loop.start(burnchain_opt, mine_start)
naka_loop.start(burnchain_opt, mine_start, peer_network)
}
fn spawn_stopper(

View File

@@ -31,6 +31,7 @@ use stacks::chainstate::stacks::db::{ChainStateBootData, StacksChainState};
use stacks::chainstate::stacks::miner::{signal_mining_blocked, signal_mining_ready, MinerStatus};
use stacks::core::StacksEpochId;
use stacks::net::atlas::{AtlasConfig, AtlasDB, Attachment};
use stacks::net::p2p::PeerNetwork;
use stacks_common::types::PublicKey;
use stacks_common::util::hash::Hash160;
use stx_genesis::GenesisData;
@@ -392,7 +393,12 @@ impl RunLoop {
/// It will start the burnchain (separate thread), set-up a channel in
/// charge of coordinating the new blocks coming from the burnchain and
/// the nodes, taking turns on tenures.
pub fn start(&mut self, burnchain_opt: Option<Burnchain>, mut mine_start: u64) {
pub fn start(
&mut self,
burnchain_opt: Option<Burnchain>,
mut mine_start: u64,
peer_network: Option<PeerNetwork>,
) {
let (coordinator_receivers, coordinator_senders) = self
.coordinator_channels
.take()
@@ -475,7 +481,7 @@ impl RunLoop {
// Boot up the p2p network and relayer, and figure out how many sortitions we have so far
// (it could be non-zero if the node is resuming from chainstate)
let mut node = StacksNode::spawn(self, globals.clone(), relay_recv);
let mut node = StacksNode::spawn(self, globals.clone(), relay_recv, peer_network);
// Wait for all pending sortitions to process
let burnchain_db = burnchain_config

View File

@@ -21,6 +21,7 @@ use stacks::chainstate::stacks::db::{ChainStateBootData, StacksChainState};
use stacks::chainstate::stacks::miner::{signal_mining_blocked, signal_mining_ready, MinerStatus};
use stacks::core::StacksEpochId;
use stacks::net::atlas::{AtlasConfig, AtlasDB, Attachment};
use stacks::net::p2p::PeerNetwork;
use stacks::util_lib::db::Error as db_error;
use stacks_common::deps_common::ctrlc as termination;
use stacks_common::deps_common::ctrlc::SignalId;
@@ -999,7 +1000,11 @@ impl RunLoop {
/// It will start the burnchain (separate thread), set-up a channel in
/// charge of coordinating the new blocks coming from the burnchain and
/// the nodes, taking turns on tenures.
pub fn start(&mut self, burnchain_opt: Option<Burnchain>, mut mine_start: u64) {
pub fn start(
&mut self,
burnchain_opt: Option<Burnchain>,
mut mine_start: u64,
) -> Option<PeerNetwork> {
let (coordinator_receivers, coordinator_senders) = self
.coordinator_channels
.take()
@@ -1018,12 +1023,12 @@ impl RunLoop {
Ok(burnchain_controller) => burnchain_controller,
Err(burnchain_error::ShutdownInitiated) => {
info!("Exiting stacks-node");
return;
return None;
}
Err(e) => {
error!("Error initializing burnchain: {}", e);
info!("Exiting stacks-node");
return;
return None;
}
};
@@ -1142,11 +1147,11 @@ impl RunLoop {
globals.coord().stop_chains_coordinator();
coordinator_thread_handle.join().unwrap();
node.join();
let peer_network = node.join();
liveness_thread.join().unwrap();
info!("Exiting stacks-node");
break;
break peer_network;
}
let remote_chain_height = burnchain.get_headers_height() - 1;
@@ -1269,7 +1274,7 @@ impl RunLoop {
if !node.relayer_sortition_notify() {
// relayer hung up, exit.
error!("Runloop: Block relayer and miner hung up, exiting.");
return;
return None;
}
}
@@ -1343,7 +1348,7 @@ impl RunLoop {
if !node.relayer_issue_tenure(ibd) {
// relayer hung up, exit.
error!("Runloop: Block relayer and miner hung up, exiting.");
break;
break None;
}
}
}