Skip to content

Commit 8e75867

Browse files
committed
Add block_proposal_max_age_secs config option
Signed-off-by: Jacinta Ferrant <[email protected]>
1 parent b108d09 commit 8e75867

File tree

5 files changed

+68
-5
lines changed

5 files changed

+68
-5
lines changed

CHANGELOG.md

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

1010
### Added
11+
- Added configuration option `connections.block_proposal_max_age_secs` to prevent processing stale block proposals
1112

1213
### Changed
14+
- The RPC endpoint `/v3/block_proposal` no longer will evaluate block proposals more than `block_proposal_max_age_secs` old
1315

1416
## [3.1.0.0.1]
1517

stackslib/src/net/api/postblock_proposal.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,20 @@ impl RPCRequestHandler for RPCBlockProposalRequestHandler {
742742
NetError::SendError("Proposal currently being evaluated".into()),
743743
));
744744
}
745+
746+
if block_proposal
747+
.block
748+
.header
749+
.timestamp
750+
.wrapping_add(network.get_connection_opts().block_proposal_max_age_secs)
751+
< get_epoch_time_secs()
752+
{
753+
return Err((
754+
422,
755+
NetError::SendError("Block proposal is too old to process.".into()),
756+
));
757+
}
758+
745759
let (chainstate, _) = chainstate.reopen().map_err(|e| (400, NetError::from(e)))?;
746760
let sortdb = sortdb.reopen().map_err(|e| (400, NetError::from(e)))?;
747761
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
@@ -334,9 +334,9 @@ fn test_try_make_response() {
334334
request.add_header("authorization".into(), "password".into());
335335
requests.push(request);
336336

337-
// Set the timestamp to a value in the past
337+
// Set the timestamp to a value in the past (but NOT BEFORE timeout)
338338
let mut early_time_block = good_block.clone();
339-
early_time_block.header.timestamp -= 10000;
339+
early_time_block.header.timestamp -= 400;
340340
rpc_test
341341
.peer_1
342342
.miner
@@ -382,16 +382,42 @@ fn test_try_make_response() {
382382
request.add_header("authorization".into(), "password".into());
383383
requests.push(request);
384384

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

389410
info!("Run requests with observer");
390-
let mut responses = rpc_test.run_with_observer(requests, Some(&observer));
411+
let responses = rpc_test.run_with_observer(requests, Some(&observer));
391412

392-
let response = responses.remove(0);
413+
for response in responses.iter().take(3) {
414+
assert_eq!(response.preamble().status_code, 202);
415+
}
416+
let response = &responses[3];
417+
assert_eq!(response.preamble().status_code, 422);
393418

394-
// Wait for the results of all 3 requests
419+
// Wait for the results of all 3 PROCESSED requests
420+
let start = std::time::Instant::now();
395421
loop {
396422
info!("Wait for results to be non-empty");
397423
if proposal_observer
@@ -407,6 +433,10 @@ fn test_try_make_response() {
407433
} else {
408434
break;
409435
}
436+
assert!(
437+
start.elapsed().as_secs() < 60,
438+
"Timed out waiting for results"
439+
);
410440
}
411441

412442
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 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: 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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2875,6 +2875,7 @@ fn block_proposal_api_endpoint() {
28752875
const HTTP_ACCEPTED: u16 = 202;
28762876
const HTTP_TOO_MANY: u16 = 429;
28772877
const HTTP_NOT_AUTHORIZED: u16 = 401;
2878+
const HTTP_UNPROCESSABLE: u16 = 422;
28782879
let test_cases = [
28792880
(
28802881
"Valid Nakamoto block proposal",
@@ -2924,6 +2925,16 @@ fn block_proposal_api_endpoint() {
29242925
Some(Err(ValidateRejectCode::ChainstateError)),
29252926
),
29262927
("Not authorized", sign(&proposal), HTTP_NOT_AUTHORIZED, None),
2928+
(
2929+
"Unprocessable entity",
2930+
{
2931+
let mut p = proposal.clone();
2932+
p.block.header.timestamp = 0;
2933+
sign(&p)
2934+
},
2935+
HTTP_UNPROCESSABLE,
2936+
None,
2937+
),
29272938
];
29282939

29292940
// Build HTTP client

0 commit comments

Comments
 (0)