Skip to content

Commit a70b3d5

Browse files
authored
Merge pull request #5552 from stacks-network/feat/ignore-old-proposals-node
[node] Add "connections.block_proposal_max_age_secs" and ignore block proposals more than the configured amount
2 parents ac666a4 + 9e11894 commit a70b3d5

File tree

8 files changed

+88
-10
lines changed

8 files changed

+88
-10
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
1010
### Added
1111

1212
- Add `tenure_timeout_secs` to the miner for determining when a time-based tenure extend should be attempted.
13+
- Added configuration option `block_proposal_max_age_secs` under `[connection_options]` to prevent processing stale block proposals
1314

1415
### Changed
16+
- The RPC endpoint `/v3/block_proposal` no longer will evaluate block proposals more than `block_proposal_max_age_secs` old
1517

1618
- When a transaction is dropped due to replace-by-fee, the `/drop_mempool_tx` event observer payload now includes `new_txid`, which is the transaction that replaced this dropped transaction. When a transaction is dropped for other reasons, `new_txid` is `null`. [#5381](https://github.com/stacks-network/stacks-core/pull/5381)
1719
- Nodes will assume that all PoX anchor blocks exist by default, and stall initial block download indefinitely to await their arrival (#5502)

stacks-signer/src/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use std::path::PathBuf;
2121
use std::time::Duration;
2222

2323
use blockstack_lib::chainstate::stacks::TransactionVersion;
24+
use blockstack_lib::net::connection::DEFAULT_BLOCK_PROPOSAL_MAX_AGE_SECS;
2425
use clarity::util::hash::to_hex;
2526
use libsigner::SignerEntries;
2627
use serde::Deserialize;
@@ -39,7 +40,6 @@ const BLOCK_PROPOSAL_VALIDATION_TIMEOUT_MS: u64 = 120_000;
3940
const DEFAULT_FIRST_PROPOSAL_BURN_BLOCK_TIMING_SECS: u64 = 60;
4041
const DEFAULT_TENURE_LAST_BLOCK_PROPOSAL_TIMEOUT_SECS: u64 = 30;
4142
const TENURE_IDLE_TIMEOUT_SECS: u64 = 300;
42-
const DEFAULT_BLOCK_PROPOSAL_MAX_AGE_SECS: u64 = 600;
4343

4444
#[derive(thiserror::Error, Debug)]
4545
/// An error occurred parsing the provided configuration

stackslib/src/config/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ use crate::cost_estimates::fee_scalar::ScalarFeeRateEstimator;
5959
use crate::cost_estimates::metrics::{CostMetric, ProportionalDotProduct, UnitMetric};
6060
use crate::cost_estimates::{CostEstimator, FeeEstimator, PessimisticEstimator, UnitEstimator};
6161
use crate::net::atlas::AtlasConfig;
62-
use crate::net::connection::ConnectionOptions;
62+
use crate::net::connection::{ConnectionOptions, DEFAULT_BLOCK_PROPOSAL_MAX_AGE_SECS};
6363
use crate::net::{Neighbor, NeighborAddress, NeighborKey};
6464
use crate::types::chainstate::BurnchainHeaderHash;
6565
use crate::types::EpochList;
@@ -2241,6 +2241,7 @@ pub struct ConnectionOptionsFile {
22412241
pub antientropy_retry: Option<u64>,
22422242
pub reject_blocks_pushed: Option<bool>,
22432243
pub stackerdb_hint_replicas: Option<String>,
2244+
pub block_proposal_max_age_secs: Option<u64>,
22442245
}
22452246

22462247
impl ConnectionOptionsFile {
@@ -2389,6 +2390,9 @@ impl ConnectionOptionsFile {
23892390
.transpose()?
23902391
.map(HashMap::from_iter)
23912392
.unwrap_or(default.stackerdb_hint_replicas),
2393+
block_proposal_max_age_secs: self
2394+
.block_proposal_max_age_secs
2395+
.unwrap_or(DEFAULT_BLOCK_PROPOSAL_MAX_AGE_SECS),
23922396
..default
23932397
})
23942398
}

stackslib/src/net/api/postblock_proposal.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,20 @@ impl RPCRequestHandler for RPCBlockProposalRequestHandler {
747747
NetError::SendError("Proposal currently being evaluated".into()),
748748
));
749749
}
750+
751+
if block_proposal
752+
.block
753+
.header
754+
.timestamp
755+
.saturating_add(network.get_connection_opts().block_proposal_max_age_secs)
756+
< get_epoch_time_secs()
757+
{
758+
return Err((
759+
422,
760+
NetError::SendError("Block proposal is too old to process.".into()),
761+
));
762+
}
763+
750764
let (chainstate, _) = chainstate.reopen().map_err(|e| (400, NetError::from(e)))?;
751765
let sortdb = sortdb.reopen().map_err(|e| (400, NetError::from(e)))?;
752766
let receiver = rpc_args

stackslib/src/net/api/tests/postblock_proposal.rs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -340,9 +340,9 @@ fn test_try_make_response() {
340340
request.add_header("authorization".into(), "password".into());
341341
requests.push(request);
342342

343-
// Set the timestamp to a value in the past
343+
// Set the timestamp to a value in the past (but NOT BEFORE timeout)
344344
let mut early_time_block = good_block.clone();
345-
early_time_block.header.timestamp -= 10000;
345+
early_time_block.header.timestamp -= 400;
346346
rpc_test
347347
.peer_1
348348
.miner
@@ -388,16 +388,42 @@ fn test_try_make_response() {
388388
request.add_header("authorization".into(), "password".into());
389389
requests.push(request);
390390

391+
// Set the timestamp to a value in the past (BEFORE the timeout)
392+
let mut stale_block = good_block.clone();
393+
stale_block.header.timestamp -= 10000;
394+
rpc_test.peer_1.miner.sign_nakamoto_block(&mut stale_block);
395+
396+
// post the invalid block proposal
397+
let proposal = NakamotoBlockProposal {
398+
block: stale_block,
399+
chain_id: 0x80000000,
400+
};
401+
402+
let mut request = StacksHttpRequest::new_for_peer(
403+
rpc_test.peer_1.to_peer_host(),
404+
"POST".into(),
405+
"/v3/block_proposal".into(),
406+
HttpRequestContents::new().payload_json(serde_json::to_value(proposal).unwrap()),
407+
)
408+
.expect("failed to construct request");
409+
request.add_header("authorization".into(), "password".into());
410+
requests.push(request);
411+
391412
// execute the requests
392413
let observer = ProposalTestObserver::new();
393414
let proposal_observer = Arc::clone(&observer.proposal_observer);
394415

395416
info!("Run requests with observer");
396-
let mut responses = rpc_test.run_with_observer(requests, Some(&observer));
417+
let responses = rpc_test.run_with_observer(requests, Some(&observer));
397418

398-
let response = responses.remove(0);
419+
for response in responses.iter().take(3) {
420+
assert_eq!(response.preamble().status_code, 202);
421+
}
422+
let response = &responses[3];
423+
assert_eq!(response.preamble().status_code, 422);
399424

400-
// Wait for the results of all 3 requests
425+
// Wait for the results of all 3 PROCESSED requests
426+
let start = std::time::Instant::now();
401427
loop {
402428
info!("Wait for results to be non-empty");
403429
if proposal_observer
@@ -413,6 +439,10 @@ fn test_try_make_response() {
413439
} else {
414440
break;
415441
}
442+
assert!(
443+
start.elapsed().as_secs() < 60,
444+
"Timed out waiting for results"
445+
);
416446
}
417447

418448
let observer = proposal_observer.lock().unwrap();

stackslib/src/net/connection.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ use crate::net::{
4848
StacksHttp, StacksP2P,
4949
};
5050

51+
/// The default maximum age in seconds of a block that can be validated by the block proposal endpoint
52+
pub const DEFAULT_BLOCK_PROPOSAL_MAX_AGE_SECS: u64 = 600;
53+
5154
/// Receiver notification handle.
5255
/// When a message with the expected `seq` value arrives, send it to an expected receiver (possibly
5356
/// in another thread) via the given `receiver_input` channel.
@@ -434,6 +437,8 @@ pub struct ConnectionOptions {
434437
pub nakamoto_unconfirmed_downloader_interval_ms: u128,
435438
/// The authorization token to enable privileged RPC endpoints
436439
pub auth_token: Option<String>,
440+
/// The maximum age in seconds of a block that can be validated by the block proposal endpoint
441+
pub block_proposal_max_age_secs: u64,
437442
/// StackerDB replicas to talk to for a particular smart contract
438443
pub stackerdb_hint_replicas: HashMap<QualifiedContractIdentifier, Vec<NeighborAddress>>,
439444

@@ -568,6 +573,7 @@ impl std::default::Default for ConnectionOptions {
568573
nakamoto_inv_sync_burst_interval_ms: 1_000, // wait 1 second after a sortition before running inventory sync
569574
nakamoto_unconfirmed_downloader_interval_ms: 5_000, // run unconfirmed downloader once every 5 seconds
570575
auth_token: None,
576+
block_proposal_max_age_secs: DEFAULT_BLOCK_PROPOSAL_MAX_AGE_SECS,
571577
stackerdb_hint_replicas: HashMap::new(),
572578

573579
// no faults on by default

testnet/stacks-node/src/tests/nakamoto_integrations.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2873,6 +2873,7 @@ fn block_proposal_api_endpoint() {
28732873
const HTTP_ACCEPTED: u16 = 202;
28742874
const HTTP_TOO_MANY: u16 = 429;
28752875
const HTTP_NOT_AUTHORIZED: u16 = 401;
2876+
const HTTP_UNPROCESSABLE: u16 = 422;
28762877
let test_cases = [
28772878
(
28782879
"Valid Nakamoto block proposal",
@@ -2922,6 +2923,16 @@ fn block_proposal_api_endpoint() {
29222923
Some(Err(ValidateRejectCode::ChainstateError)),
29232924
),
29242925
("Not authorized", sign(&proposal), HTTP_NOT_AUTHORIZED, None),
2926+
(
2927+
"Unprocessable entity",
2928+
{
2929+
let mut p = proposal.clone();
2930+
p.block.header.timestamp = 0;
2931+
sign(&p)
2932+
},
2933+
HTTP_UNPROCESSABLE,
2934+
None,
2935+
),
29252936
];
29262937

29272938
// Build HTTP client
@@ -6843,6 +6854,7 @@ fn continue_tenure_extend() {
68436854
let prom_bind = "127.0.0.1:6000".to_string();
68446855
naka_conf.node.prometheus_bind = Some(prom_bind.clone());
68456856
naka_conf.miner.wait_on_interim_blocks = Duration::from_secs(1);
6857+
naka_conf.connection_options.block_proposal_max_age_secs = u64::MAX;
68466858
let http_origin = naka_conf.node.data_url.clone();
68476859
let sender_sk = Secp256k1PrivateKey::new();
68486860
// setup sender + recipient for a test stx transfer

testnet/stacks-node/src/tests/signer/v0.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2956,7 +2956,10 @@ fn idle_tenure_extend_active_mining() {
29562956
|config| {
29572957
config.tenure_idle_timeout = idle_timeout;
29582958
},
2959-
|_| {},
2959+
|config| {
2960+
// accept all proposals in the node
2961+
config.connection_options.block_proposal_max_age_secs = u64::MAX;
2962+
},
29602963
None,
29612964
None,
29622965
);
@@ -2977,7 +2980,7 @@ fn idle_tenure_extend_active_mining() {
29772980
// Add a delay to the block validation process
29782981
TEST_VALIDATE_DELAY_DURATION_SECS.lock().unwrap().replace(3);
29792982

2980-
signer_test.mine_nakamoto_block(Duration::from_secs(30), true);
2983+
signer_test.mine_nakamoto_block(Duration::from_secs(60), true);
29812984

29822985
info!("---- Getting current idle timeout ----");
29832986

@@ -6130,9 +6133,16 @@ fn miner_recovers_when_broadcast_block_delay_across_tenures_occurs() {
61306133
let send_fee = 180;
61316134
let nmb_txs = 3;
61326135
let recipient = PrincipalData::from(StacksAddress::burn_address(false));
6133-
let mut signer_test: SignerTest<SpawnedSigner> = SignerTest::new(
6136+
let mut signer_test: SignerTest<SpawnedSigner> = SignerTest::new_with_config_modifications(
61346137
num_signers,
61356138
vec![(sender_addr, (send_amt + send_fee) * nmb_txs)],
6139+
|_config| {},
6140+
|config| {
6141+
// Accept all block proposals
6142+
config.connection_options.block_proposal_max_age_secs = u64::MAX;
6143+
},
6144+
None,
6145+
None,
61366146
);
61376147
let http_origin = format!("http://{}", &signer_test.running_nodes.conf.node.rpc_bind);
61386148
signer_test.boot_to_epoch_3();

0 commit comments

Comments
 (0)