-
Notifications
You must be signed in to change notification settings - Fork 401
fix(Rosetta): DEFI-2902: Enforce a 24h ingress window in Rosetta /construction/payloads #10547
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mbjorkqvist
wants to merge
4
commits into
master
Choose a base branch
from
mathias/DEFI-2902-ingress-window-validation
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+207
−2
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
6fc7051
test: characterize missing ingress-window validation in Rosetta /cons…
mbjorkqvist d453979
fix: enforce a 24h ingress window in Rosetta /construction/payloads
mbjorkqvist 031693e
test: assert ingress windows beyond the 24h bound are rejected
mbjorkqvist bd5d728
fix: address review on ingress-window validation
mbjorkqvist File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,7 @@ use super::utils::{ | |
| handle_construction_hash, handle_construction_parse, handle_construction_payloads, | ||
| handle_construction_submit, | ||
| }; | ||
| use crate::common::constants::INGRESS_INTERVAL_OVERLAP; | ||
| use crate::common::constants::{INGRESS_INTERVAL_OVERLAP, MAX_INGRESS_WINDOW}; | ||
| use crate::common::types::Error; | ||
| use candid::Principal; | ||
| use ic_base_types::{CanisterId, PrincipalId}; | ||
|
|
@@ -108,6 +108,32 @@ pub fn construction_combine( | |
| .map_err(|err| Error::processing_construction_failed(&err)) | ||
| } | ||
|
|
||
| /// Validate the caller-provided ingress window before it is expanded into a | ||
| /// list of ingress expiries. | ||
| /// | ||
| /// Rejects windows that would make the expiry loop iterate an excessive (or, on | ||
| /// arithmetic wraparound, unbounded) number of times (see ICPBB-134 / | ||
| /// DEFI-2902). Two independent bounds are enforced: | ||
| /// - the span `ingress_end - ingress_start` must not exceed 24h, and | ||
| /// - `ingress_end` must not be more than 24h in the future, which also rejects | ||
| /// the near-`u64::MAX` payloads that would otherwise wrap the loop counter. | ||
| fn validate_ingress_window(now: u64, ingress_start: u64, ingress_end: u64) -> Result<(), Error> { | ||
| let max_window = MAX_INGRESS_WINDOW.as_nanos() as u64; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Contrary to the validating function in ICP Rosetta the check seems to be missing, is that on purpose? |
||
| if ingress_end.saturating_sub(ingress_start) > max_window { | ||
| return Err(Error::processing_construction_failed(&format!( | ||
| "The ingress window (ingress_end - ingress_start) must not exceed {} hours: Start: {ingress_start}, End: {ingress_end}", | ||
| MAX_INGRESS_WINDOW.as_secs() / 3600 | ||
| ))); | ||
| } | ||
| if ingress_end.saturating_sub(now) > max_window { | ||
| return Err(Error::processing_construction_failed(&format!( | ||
| "ingress_end must not be more than {} hours in the future: Current time: {now}, End: {ingress_end}", | ||
| MAX_INGRESS_WINDOW.as_secs() / 3600 | ||
| ))); | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
| pub fn construction_payloads( | ||
| operations: Vec<Operation>, | ||
| metadata: Option<ConstructionPayloadsRequestMetadata>, | ||
|
|
@@ -157,6 +183,8 @@ pub fn construction_payloads( | |
| ))); | ||
| } | ||
|
|
||
| validate_ingress_window(now, ingress_start, ingress_end)?; | ||
|
|
||
| // Every ingress message sent to the IC has an expiry timestamp until which the signature associated with that message is valid | ||
| // To support a longer overall timeframe than one interval, we can send multiple ingress messages with two signable contents each | ||
| let mut ingress_expiries = vec![]; | ||
|
|
@@ -261,6 +289,53 @@ mod tests { | |
| const NUM_TEST_CASES: u32 = 100; | ||
| const NUM_BLOCKS: usize = 1; | ||
|
|
||
| const HOUR_NANOS: u64 = 60 * 60 * 1_000_000_000; | ||
| // A realistic "now" (~2023) that is well away from the u64 boundary. | ||
| const NOW_NANOS: u64 = 1_700_000_000 * 1_000_000_000; | ||
|
|
||
| // Vector A (ICPBB-134): a window far larger than the documented 24h bound is | ||
| // rejected before the loop. | ||
| #[test] | ||
| fn oversized_ingress_window_is_rejected() { | ||
| assert!( | ||
| validate_ingress_window(NOW_NANOS, NOW_NANOS, NOW_NANOS + 48 * HOUR_NANOS).is_err() | ||
| ); | ||
| } | ||
|
|
||
| // Vector A' (ICPBB-134): a tiny start with a near-`u64::MAX` end (an enormous | ||
| // span) is rejected. | ||
| #[test] | ||
| fn unbounded_ingress_span_is_rejected() { | ||
| assert!(validate_ingress_window(NOW_NANOS, 0, u64::MAX).is_err()); | ||
| } | ||
|
|
||
| // Vector B (ICPBB-134): the near-`u64::MAX` wrap payload has a tiny span but a | ||
| // far-future end, so it is rejected by the future bound. | ||
| #[test] | ||
| fn near_u64_max_ingress_window_is_rejected() { | ||
| assert!( | ||
| validate_ingress_window(NOW_NANOS, u64::MAX - 50 * 1_000_000_000, u64::MAX).is_err() | ||
| ); | ||
| } | ||
|
|
||
| // Negative control: a realistic window (5 minutes ahead of now) is accepted. | ||
| #[test] | ||
| fn valid_ingress_window_is_accepted() { | ||
| assert!( | ||
| validate_ingress_window(NOW_NANOS, NOW_NANOS, NOW_NANOS + 5 * 60 * 1_000_000_000) | ||
| .is_ok() | ||
| ); | ||
| } | ||
|
|
||
| // Boundary: a window of exactly 24h ending exactly 24h from now is accepted, | ||
| // one nanosecond more is rejected. | ||
| #[test] | ||
| fn ingress_window_boundary_is_inclusive() { | ||
| let max = MAX_INGRESS_WINDOW.as_nanos() as u64; | ||
| assert!(validate_ingress_window(NOW_NANOS, NOW_NANOS, NOW_NANOS + max).is_ok()); | ||
| assert!(validate_ingress_window(NOW_NANOS, NOW_NANOS, NOW_NANOS + max + 1).is_err()); | ||
| } | ||
|
|
||
| fn call_construction_derive<T: RosettaSupportedKeyPair>(key_pair: &T) { | ||
| let principal_id = key_pair.generate_principal_id().unwrap(); | ||
| let public_key = ic_rosetta_test_utils::to_public_key(key_pair); | ||
|
|
@@ -461,6 +536,17 @@ mod tests { | |
| } | ||
| (_, _) => {} | ||
| } | ||
| // Windows wider than the permitted maximum, or with an | ||
| // ingress_end too far in the future, are rejected before | ||
| // the loop (ICPBB-134 / DEFI-2902). | ||
| let eff_start = payloads_metadata.ingress_start.unwrap_or(now); | ||
| let eff_end = payloads_metadata | ||
| .ingress_end | ||
| .unwrap_or(eff_start + ingress_interval); | ||
| if validate_ingress_window(now, eff_start, eff_end).is_err() { | ||
| assert!(construction_payloads_response.is_err()); | ||
| continue; | ||
| } | ||
|
Copilot marked this conversation as resolved.
|
||
| let construction_parse_response = construction_parse( | ||
| construction_payloads_response | ||
| .clone() | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: do we want the bugbounty number to appear in the code? Maybe better to keep it only in the Jira ticket?