Skip to content

Commit 920df80

Browse files
mraszykIDX GitHub Automation
andauthored
feat: enable asynchronous canister creation with subnet ID in the URL (dfinity#9982)
This PR enables to retrieve the status of an (asynchronous) update call via the (existing) public HTTP endpoint `/api/v3/subnet/<effective_subnet_id>/read_state` on the replica. It is specified in this [PR](dfinity/portal#6224). In more detail, this PR is needed if the synchronous update call endpoint `/api/v4/subnet/.../call` returns 202 and thus the user needs to process the update call (to create canister as nothing else is allowed on `/api/v4/subnet/.../call`) asynchronously (by polling `/api/v3/subnet/.../read_state` for the `/request_status/...`). --------- Co-authored-by: IDX GitHub Automation <infra+github-automation@dfinity.org>
1 parent a8beefd commit 920df80

2 files changed

Lines changed: 143 additions & 38 deletions

File tree

rs/http_endpoints/public/src/read_state.rs

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,9 @@ fn verify_paths(
514514
b"request_status",
515515
request_id,
516516
b"status" | b"reply" | b"reject_code" | b"reject_message" | b"error_code",
517-
] if target == Target::Canister => {
517+
] if target == Target::Canister
518+
|| (target == Target::Subnet && version == Version::V3) =>
519+
{
518520
let message_id = MessageId::try_from(*request_id).map_err(|_| HttpError {
519521
status: StatusCode::BAD_REQUEST,
520522
message: format!(
@@ -631,7 +633,11 @@ mod test {
631633
SUBNET_0, SUBNET_1, canister_test_id, subnet_test_id, user_test_id,
632634
};
633635
use ic_types::{
634-
SubnetId, batch::RawQueryStats, consensus::certification::Certification, time::UNIX_EPOCH,
636+
NumBytes, SubnetId,
637+
batch::RawQueryStats,
638+
consensus::certification::Certification,
639+
ingress::{IngressState, IngressStatus},
640+
time::UNIX_EPOCH,
635641
};
636642
use ic_validator::CanisterIdSet;
637643
use rstest::rstest;
@@ -1002,8 +1008,8 @@ mod test {
10021008
Ok(())
10031009
);
10041010

1005-
// request_status not allowed on subnet endpoint
1006-
let err = verify_paths(
1011+
// request_status not allowed on subnet V2 endpoint, allowed on V3
1012+
let result = verify_paths(
10071013
&metrics,
10081014
Target::Subnet,
10091015
version,
@@ -1017,9 +1023,16 @@ mod test {
10171023
&CanisterIdSet::all(),
10181024
APP_SUBNET_ID.get(),
10191025
NNS_SUBNET_ID,
1020-
)
1021-
.expect_err("Should fail");
1022-
assert_eq!(err.status, StatusCode::NOT_FOUND);
1026+
);
1027+
match version {
1028+
Version::V2 => {
1029+
assert_eq!(
1030+
result.expect_err("Should fail").status,
1031+
StatusCode::NOT_FOUND
1032+
)
1033+
}
1034+
Version::V3 => assert_eq!(result, Ok(())),
1035+
}
10231036

10241037
// canister/* not allowed on subnet endpoint
10251038
let err = verify_paths(
@@ -1270,4 +1283,98 @@ mod test {
12701283
1
12711284
);
12721285
}
1286+
1287+
fn state_with_ingress_status(
1288+
message_id: MessageId,
1289+
user_id: UserId,
1290+
receiver: CanisterId,
1291+
) -> ReplicatedState {
1292+
let mut state = fake_replicated_state();
1293+
state.set_ingress_status(
1294+
message_id,
1295+
IngressStatus::Known {
1296+
receiver: receiver.get(),
1297+
user_id,
1298+
time: UNIX_EPOCH,
1299+
state: IngressState::Processing,
1300+
},
1301+
NumBytes::from(u64::MAX),
1302+
|_| {},
1303+
);
1304+
state
1305+
}
1306+
1307+
#[rstest]
1308+
#[case(Target::Canister, Version::V2, canister_test_id(1).get())]
1309+
#[case(Target::Canister, Version::V3, canister_test_id(1).get())]
1310+
#[case(Target::Subnet, Version::V3, APP_SUBNET_ID.get())]
1311+
fn test_request_status_wrong_user_is_rejected(
1312+
#[case] target: Target,
1313+
#[case] version: Version,
1314+
#[case] effective_principal_id: PrincipalId,
1315+
) {
1316+
let metrics = test_metrics();
1317+
let message_id = MessageId::from([0_u8; 32]);
1318+
let state =
1319+
state_with_ingress_status(message_id.clone(), user_test_id(1), canister_test_id(1));
1320+
1321+
let err = verify_paths(
1322+
&metrics,
1323+
target,
1324+
version,
1325+
&state,
1326+
&user_test_id(2),
1327+
&[Path::new(vec![
1328+
Label::from("request_status"),
1329+
message_id.as_bytes().to_vec().into(),
1330+
])],
1331+
&CanisterIdSet::all(),
1332+
effective_principal_id,
1333+
NNS_SUBNET_ID,
1334+
)
1335+
.expect_err("Should fail");
1336+
assert_eq!(err.status, StatusCode::FORBIDDEN);
1337+
assert_eq!(
1338+
err.message,
1339+
"The user tries to access Request ID not signed by the caller."
1340+
);
1341+
}
1342+
1343+
#[rstest]
1344+
#[case(Target::Canister, Version::V2, canister_test_id(1).get())]
1345+
#[case(Target::Canister, Version::V3, canister_test_id(1).get())]
1346+
#[case(Target::Subnet, Version::V3, APP_SUBNET_ID.get())]
1347+
fn test_request_status_receiver_not_in_targets_is_rejected(
1348+
#[case] target: Target,
1349+
#[case] version: Version,
1350+
#[case] effective_principal_id: PrincipalId,
1351+
) {
1352+
let metrics = test_metrics();
1353+
let message_id = MessageId::from([0_u8; 32]);
1354+
let receiver = canister_test_id(1);
1355+
let state = state_with_ingress_status(message_id.clone(), user_test_id(1), receiver);
1356+
let other_canister = canister_test_id(2);
1357+
1358+
let err = verify_paths(
1359+
&metrics,
1360+
target,
1361+
version,
1362+
&state,
1363+
&user_test_id(1),
1364+
&[Path::new(vec![
1365+
Label::from("request_status"),
1366+
message_id.as_bytes().to_vec().into(),
1367+
])],
1368+
&CanisterIdSet::try_from_iter(vec![other_canister]).unwrap(),
1369+
effective_principal_id,
1370+
NNS_SUBNET_ID,
1371+
)
1372+
.expect_err("Should fail");
1373+
assert_eq!(err.status, StatusCode::FORBIDDEN);
1374+
assert_eq!(
1375+
err.message,
1376+
"The user tries to access request IDs for canisters \
1377+
not belonging to sender delegation targets."
1378+
);
1379+
}
12731380
}

rs/tests/networking/read_state_test.rs

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ Success::
1313
. public key and canister ranges for all subnets
1414
. public keys of nodes on the subnet
1515
. no public keys of nodes on other subnets
16-
. Malformed status requests are rejected by /api/{v2,v3}/canister/.../read_state
16+
. Malformed status requests are rejected by /api/{v2,v3}/canister/.../read_state and /api/{v2,v3}/subnet/.../read_state
1717
. Status requests for non-existent requests contain an absence proof by /api/{v2,v3}/canister/.../read_state
18-
. /api/{v2,v3}/canister/.../read_state requests of invalid paths are rejected
18+
and /api/v3/subnet/.../read_state
19+
. /api/{v2,v3}/canister/.../read_state and /api/{v2,v3}/subnet/.../read_state requests of invalid paths are rejected
1920
. A canister's public metadata sections can be read by
2021
. The canister controller
2122
. The anonymous identity
@@ -27,11 +28,13 @@ Success::
2728
. module_hash is absent for empty canisters;
2829
. module_hash is a blob for non-empty canisters;
2930
. controllers are always present for existing canisters and consist of a list of principals
30-
. /api/{v2,v3}/canister/.../read_state requests for the full paths /request_status/R/status and /request_status/R/reply succeed
31-
. /api/{v2,v3}/canister/.../read_state requests for the path /request_status/R are rejected with 403 if signed by a different
32-
principal than who made the original request with request ID R;
33-
. /api/{v2,v3}/canister/.../read_state requests for two paths /request_status/R and /request_status/S with two different request
34-
IDs R and S are rejected with 400 (while requesting each of the two paths in isolation would succeed);
31+
. /api/{v2,v3}/canister/.../read_state and /api/v3/subnet/.../read_state requests for the full paths
32+
/request_status/R/status and /request_status/R/reply succeed
33+
. /api/{v2,v3}/canister/.../read_state and /api/v3/subnet/.../read_state requests for the path /request_status/R
34+
are rejected with 403 if signed by a different principal than who made the original request with request ID R;
35+
. /api/{v2,v3}/canister/.../read_state and /api/v3/subnet/.../read_state requests for two paths /request_status/R
36+
and /request_status/S with two different request IDs R and S are rejected with 400
37+
(while requesting each of the two paths in isolation would succeed);
3538
. Read state requests at `/api/{v2,v3}/subnet/{subnet_id}/read_state` for the path `/canister_ranges/{subnet_id}`
3639
succeed and return a correct list of canister ranges assigned to the subnet.
3740
. Read state requests at `/api/{v2,v3}/canister/{canister_id}/read_state` for the path `/canister_ranges/{subnet_id}`
@@ -271,7 +274,7 @@ fn test_subnet_path(env: TestEnv, endpoint: Endpoint) {
271274
let (app_subnet, other_app_subnet) = get_both_app_subnets(&env);
272275
let app_subnet_id = app_subnet.subnet_id;
273276

274-
// Query the `/subnet` enpoint of the app subnet
277+
// Query the `/subnet` endpoint of the app subnet
275278
let path = vec!["subnet".into()];
276279
let cert = read_state(&env, vec![path], endpoint).expect("Valid request");
277280

@@ -348,26 +351,25 @@ fn test_invalid_request_rejected(env: TestEnv, endpoint: Endpoint) {
348351
let path = vec!["request_status".into(), invalid_request_id.into()];
349352
let error = read_state(&env, vec![path], endpoint).expect_err("Invalid request");
350353
match endpoint {
351-
Endpoint::CanisterReadState(_version) => {
354+
Endpoint::CanisterReadState(_) | Endpoint::SubnetReadState(read_state::Version::V3) => {
352355
assert_matches!(
353356
error,
354357
AgentError::HttpError(error) if error.status == StatusCode::BAD_REQUEST.as_u16(),
355358
"Invalid request id"
356359
)
357360
}
358-
Endpoint::SubnetReadState(_version) => {
361+
Endpoint::SubnetReadState(read_state::Version::V2) => {
359362
assert_matches!(
360363
error,
361364
AgentError::HttpError(error) if error.status == StatusCode::NOT_FOUND.as_u16(),
362-
"request_status is not allowed on subnet read_state endpoint"
365+
"request_status is not allowed on subnet read_state V2 endpoint"
363366
)
364367
}
365368
}
366369
}
367370
}
368371

369-
fn test_absent_request(env: TestEnv, version: read_state::Version) {
370-
let endpoint = Endpoint::CanisterReadState(version);
372+
fn test_absent_request(env: TestEnv, endpoint: Endpoint) {
371373
for absent_request_id in [&[0; 32], &[8; 32], &[255; 32]] {
372374
let path = vec!["request_status".into(), absent_request_id.into()];
373375
let cert = read_state(&env, vec![path], endpoint).expect("Valid request");
@@ -722,8 +724,7 @@ fn make_update_call(agent: &Agent, canister_id: &Principal) -> (RequestId, Vec<u
722724
(request_id, result)
723725
}
724726

725-
fn test_request_path(env: TestEnv, version: read_state::Version) {
726-
let endpoint = Endpoint::CanisterReadState(version);
727+
fn test_request_path(env: TestEnv, endpoint: Endpoint) {
727728
let node = get_first_app_node(&env);
728729
let effective_canister_id = node.effective_canister_id();
729730
let agent = node.build_default_agent();
@@ -760,8 +761,7 @@ fn test_request_path(env: TestEnv, version: read_state::Version) {
760761
assert_eq!(value.to_vec(), result);
761762
}
762763

763-
fn test_request_path_access(env: TestEnv, version: read_state::Version) {
764-
let endpoint = Endpoint::CanisterReadState(version);
764+
fn test_request_path_access(env: TestEnv, endpoint: Endpoint) {
765765
let node = get_first_app_node(&env);
766766
let effective_canister_id = node.effective_canister_id();
767767
let agent = node.build_default_agent();
@@ -809,7 +809,7 @@ fn test_request_path_access(env: TestEnv, version: read_state::Version) {
809809
}
810810

811811
/// Queries the `api/{v2,v3}/canister/{canister_id}/read_state` endpoint for the canister ranges,
812-
/// and makes sure the requests fails.
812+
/// and makes sure the request fails.
813813
fn test_canister_canister_ranges_paths(env: TestEnv, version: read_state::Version) {
814814
let endpoint = Endpoint::CanisterReadState(version);
815815
let subnet = get_first_app_subnet(&env);
@@ -827,7 +827,7 @@ fn test_canister_canister_ranges_paths(env: TestEnv, version: read_state::Versio
827827
assert_matches!(err, AgentError::HttpError(payload) if payload.status == 404);
828828
}
829829

830-
/// Queries the `api/{v2,v3}/subnet/{subnet_id}/read_state` endpoint for the canister ranges.
830+
/// Queries the `api/{v2,v3}/subnet/{subnet_id}/read_state` endpoint for the canister ranges
831831
/// and compares the result with the canister ranges obtained from the registry.
832832
fn test_subnet_canister_ranges_paths(env: TestEnv, version: read_state::Version) {
833833
let endpoint = Endpoint::SubnetReadState(version);
@@ -1096,18 +1096,16 @@ fn main() -> Result<()> {
10961096
.add_test(systest!(test_subnet_canister_ranges_paths; read_state::Version::V3))
10971097
.add_test(systest!(test_canister_canister_ranges_paths; read_state::Version::V2))
10981098
.add_test(systest!(test_canister_canister_ranges_paths; read_state::Version::V3))
1099-
// Only /api/{v2,v3}/canister/read_state endpoints are tested because paths with
1100-
// /request_status prefix are not supported by /api/{v2,v3}/subnet/read_state
1101-
.add_test(systest!(test_request_path; read_state::Version::V2))
1102-
.add_test(systest!(test_request_path; read_state::Version::V3))
1103-
// Only /api/{v2,v3}/canister/read_state endpoints are tested because paths with
1104-
// /request_status prefix are not supported by /api/{v2,v3}/subnet/read_state
1105-
.add_test(systest!(test_request_path_access; read_state::Version::V2))
1106-
.add_test(systest!(test_request_path_access; read_state::Version::V3))
1107-
// Only /api/{v2,v3}/canister/read_state endpoints are tested because paths with
1108-
// /request_status prefix are not supported by /api/{v2,v3}/subnet/read_state
1109-
.add_test(systest!(test_absent_request; read_state::Version::V2))
1110-
.add_test(systest!(test_absent_request; read_state::Version::V3))
1099+
// paths with /request_status prefix are not supported by /api/v2/subnet/read_state
1100+
.add_test(systest!(test_request_path; Endpoint::CanisterReadState(read_state::Version::V2)))
1101+
.add_test(systest!(test_request_path; Endpoint::CanisterReadState(read_state::Version::V3)))
1102+
.add_test(systest!(test_request_path; Endpoint::SubnetReadState(read_state::Version::V3)))
1103+
.add_test(systest!(test_request_path_access; Endpoint::CanisterReadState(read_state::Version::V2)))
1104+
.add_test(systest!(test_request_path_access; Endpoint::CanisterReadState(read_state::Version::V3)))
1105+
.add_test(systest!(test_request_path_access; Endpoint::SubnetReadState(read_state::Version::V3)))
1106+
.add_test(systest!(test_absent_request; Endpoint::CanisterReadState(read_state::Version::V2)))
1107+
.add_test(systest!(test_absent_request; Endpoint::CanisterReadState(read_state::Version::V3)))
1108+
.add_test(systest!(test_absent_request; Endpoint::SubnetReadState(read_state::Version::V3)))
11111109
// Only /api/{v2,v3}/canister/read_state endpoints are tested because paths with
11121110
// /canister prefix are not supported by /api/{v2,v3}/subnet/read_state
11131111
.add_test(systest!(test_canister_path; read_state::Version::V2))

0 commit comments

Comments
 (0)