Skip to content

Commit fd95e2e

Browse files
authored
feat(flexible-outcalls): CON-1640 extend CanisterHttpRequestContext for flexible calls (#9001)
Extends the CanisterHttpRequestContext for flexible outcalls. This is the very first step to enable [flexible HTTPS outcalls](dfinity/portal#5890).
1 parent af5d32c commit fd95e2e

File tree

5 files changed

+112
-38
lines changed

5 files changed

+112
-38
lines changed

rs/https_outcalls/consensus/src/payload_builder.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,10 @@ impl CanisterHttpPayloadBuilderImpl {
306306
.find(|share| share.signature.signer == *node_id)
307307
.map(|correct_share| (metadata, vec![*correct_share]))
308308
}
309+
Some(Replication::Flexible { .. }) => {
310+
// TODO(flexible-http-outcalls): implement Flexible payload construction
311+
None
312+
}
309313
None | Some(Replication::FullyReplicated) => {
310314
let signers: BTreeSet<_> =
311315
shares.iter().map(|share| share.signature.signer).collect();
@@ -529,6 +533,11 @@ impl CanisterHttpPayloadBuilderImpl {
529533
..
530534
}) => (vec![*node_id], 1),
531535
None
536+
// TODO(flexible-http-outcalls): implement Flexible payload validation
537+
| Some(&CanisterHttpRequestContext {
538+
replication: Replication::Flexible { .. },
539+
..
540+
})
532541
| Some(&CanisterHttpRequestContext {
533542
replication: Replication::FullyReplicated,
534543
..

rs/https_outcalls/consensus/src/pool_manager.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,8 @@ impl CanisterHttpPoolManagerImpl {
527527
));
528528
}
529529
}
530+
// TODO(flexible-http-outcalls): implement proper Flexible validation
531+
Replication::Flexible { .. } => {}
530532
}
531533

532534
let node_is_in_committee = self

rs/protobuf/def/state/metadata/v1/metadata.proto

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,16 @@ message Replication {
175175
oneof replication_type {
176176
google.protobuf.Empty fully_replicated = 1;
177177
types.v1.NodeId non_replicated = 2;
178+
FlexibleReplication flexible = 3;
178179
}
179180
}
180181

182+
message FlexibleReplication {
183+
repeated types.v1.NodeId committee = 1;
184+
uint32 min_responses = 2;
185+
uint32 max_responses = 3;
186+
}
187+
181188
message CanisterHttpRequestContextTree {
182189
uint64 callback_id = 1;
183190
CanisterHttpRequestContext context = 2;

rs/protobuf/src/gen/state/state.metadata.v1.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ pub mod pricing_version {
247247
}
248248
#[derive(Clone, PartialEq, ::prost::Message)]
249249
pub struct Replication {
250-
#[prost(oneof = "replication::ReplicationType", tags = "1, 2")]
250+
#[prost(oneof = "replication::ReplicationType", tags = "1, 2, 3")]
251251
pub replication_type: ::core::option::Option<replication::ReplicationType>,
252252
}
253253
/// Nested message and enum types in `Replication`.
@@ -258,9 +258,20 @@ pub mod replication {
258258
FullyReplicated(()),
259259
#[prost(message, tag = "2")]
260260
NonReplicated(super::super::super::super::types::v1::NodeId),
261+
#[prost(message, tag = "3")]
262+
Flexible(super::FlexibleReplication),
261263
}
262264
}
263265
#[derive(Clone, PartialEq, ::prost::Message)]
266+
pub struct FlexibleReplication {
267+
#[prost(message, repeated, tag = "1")]
268+
pub committee: ::prost::alloc::vec::Vec<super::super::super::types::v1::NodeId>,
269+
#[prost(uint32, tag = "2")]
270+
pub min_responses: u32,
271+
#[prost(uint32, tag = "3")]
272+
pub max_responses: u32,
273+
}
274+
#[derive(Clone, PartialEq, ::prost::Message)]
264275
pub struct CanisterHttpRequestContextTree {
265276
#[prost(uint64, tag = "1")]
266277
pub callback_id: u64,

rs/types/types/src/canister_http.rs

Lines changed: 82 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,13 @@ pub enum Replication {
168168
FullyReplicated,
169169
/// The request is not replicated, i.e. only the node with the given `NodeId` will attempt the http request.
170170
NonReplicated(NodeId),
171+
/// The request is sent to a committee of nodes that all attempt the http request.
172+
/// The canister receives between `min_responses` and `max_responses` (potentially differing) responses.
173+
Flexible {
174+
committee: BTreeSet<NodeId>,
175+
min_responses: u32,
176+
max_responses: u32,
177+
},
171178
}
172179

173180
#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize, FromRepr)]
@@ -179,15 +186,30 @@ pub enum PricingVersion {
179186

180187
impl From<&CanisterHttpRequestContext> for pb_metadata::CanisterHttpRequestContext {
181188
fn from(context: &CanisterHttpRequestContext) -> Self {
182-
let replication_type = match context.replication {
189+
let replication_type = match &context.replication {
183190
Replication::FullyReplicated => {
184191
pb_metadata::replication::ReplicationType::FullyReplicated(())
185192
}
186193
Replication::NonReplicated(node_id) => {
187194
pb_metadata::replication::ReplicationType::NonReplicated(node_id_into_protobuf(
188-
node_id,
195+
*node_id,
189196
))
190197
}
198+
Replication::Flexible {
199+
committee,
200+
min_responses,
201+
max_responses,
202+
} => pb_metadata::replication::ReplicationType::Flexible(
203+
pb_metadata::FlexibleReplication {
204+
committee: committee
205+
.iter()
206+
.copied()
207+
.map(node_id_into_protobuf)
208+
.collect(),
209+
min_responses: *min_responses,
210+
max_responses: *max_responses,
211+
},
212+
),
191213
};
192214

193215
let replication_message = pb_metadata::Replication {
@@ -269,6 +291,17 @@ impl TryFrom<pb_metadata::CanisterHttpRequestContext> for CanisterHttpRequestCon
269291
Some(pb_metadata::replication::ReplicationType::NonReplicated(node_id)) => {
270292
Replication::NonReplicated(node_id_try_from_protobuf(node_id)?)
271293
}
294+
Some(pb_metadata::replication::ReplicationType::Flexible(flexible)) => {
295+
Replication::Flexible {
296+
committee: flexible
297+
.committee
298+
.into_iter()
299+
.map(node_id_try_from_protobuf)
300+
.collect::<Result<BTreeSet<NodeId>, ProxyDecodeError>>()?,
301+
min_responses: flexible.min_responses,
302+
max_responses: flexible.max_responses,
303+
}
304+
}
272305
None => Replication::FullyReplicated,
273306
},
274307
None => Replication::FullyReplicated,
@@ -1016,43 +1049,55 @@ mod tests {
10161049

10171050
#[test]
10181051
fn canister_http_request_context_proto_round_trip() {
1019-
let initial = CanisterHttpRequestContext {
1020-
url: "https://example.com".to_string(),
1021-
headers: vec![CanisterHttpHeader {
1022-
name: "Content-Type".to_string(),
1023-
value: "application/json".to_string(),
1024-
}],
1025-
body: Some(b"{\"hello\":\"world\"}".to_vec()),
1026-
max_response_bytes: Some(NumBytes::from(1234)),
1027-
http_method: CanisterHttpMethod::POST,
1028-
transform: Some(Transform {
1029-
method_name: "transform_response".to_string(),
1030-
context: vec![1, 2, 3],
1031-
}),
1032-
request: Request {
1033-
receiver: CanisterId::ic_00(),
1034-
sender: CanisterId::ic_00(),
1035-
sender_reply_callback: CallbackId::from(3),
1036-
payment: Cycles::new(10),
1037-
method_name: "transform".to_string(),
1038-
method_payload: Vec::new(),
1039-
metadata: Default::default(),
1040-
deadline: NO_DEADLINE,
1041-
},
1042-
time: UNIX_EPOCH,
1043-
replication: Replication::NonReplicated(node_test_id(42)),
1044-
pricing_version: PricingVersion::PayAsYouGo,
1045-
refund_status: RefundStatus {
1046-
refundable_cycles: Cycles::new(13_000_000),
1047-
per_replica_allowance: Cycles::new(1_000_000),
1048-
refunded_cycles: Cycles::new(123),
1049-
refunding_nodes: BTreeSet::from([node_test_id(1), node_test_id(2)]),
1052+
let replications = [
1053+
Replication::FullyReplicated,
1054+
Replication::NonReplicated(node_test_id(42)),
1055+
Replication::Flexible {
1056+
committee: BTreeSet::from([node_test_id(1), node_test_id(2), node_test_id(3)]),
1057+
min_responses: 2,
1058+
max_responses: 3,
10501059
},
1051-
};
1060+
];
1061+
1062+
for replication in replications {
1063+
let initial = CanisterHttpRequestContext {
1064+
url: "https://example.com".to_string(),
1065+
headers: vec![CanisterHttpHeader {
1066+
name: "Content-Type".to_string(),
1067+
value: "application/json".to_string(),
1068+
}],
1069+
body: Some(b"{\"hello\":\"world\"}".to_vec()),
1070+
max_response_bytes: Some(NumBytes::from(1234)),
1071+
http_method: CanisterHttpMethod::POST,
1072+
transform: Some(Transform {
1073+
method_name: "transform_response".to_string(),
1074+
context: vec![1, 2, 3],
1075+
}),
1076+
request: Request {
1077+
receiver: CanisterId::ic_00(),
1078+
sender: CanisterId::ic_00(),
1079+
sender_reply_callback: CallbackId::from(3),
1080+
payment: Cycles::new(10),
1081+
method_name: "transform".to_string(),
1082+
method_payload: Vec::new(),
1083+
metadata: Default::default(),
1084+
deadline: NO_DEADLINE,
1085+
},
1086+
time: UNIX_EPOCH,
1087+
replication,
1088+
pricing_version: PricingVersion::PayAsYouGo,
1089+
refund_status: RefundStatus {
1090+
refundable_cycles: Cycles::new(13_000_000),
1091+
per_replica_allowance: Cycles::new(1_000_000),
1092+
refunded_cycles: Cycles::new(123),
1093+
refunding_nodes: BTreeSet::from([node_test_id(1), node_test_id(2)]),
1094+
},
1095+
};
10521096

1053-
let pb: pb_metadata::CanisterHttpRequestContext = (&initial).into();
1054-
let round_trip: CanisterHttpRequestContext = pb.try_into().unwrap();
1055-
assert_eq!(initial, round_trip);
1097+
let pb: pb_metadata::CanisterHttpRequestContext = (&initial).into();
1098+
let round_trip: CanisterHttpRequestContext = pb.try_into().unwrap();
1099+
assert_eq!(initial, round_trip);
1100+
}
10561101
}
10571102

10581103
#[test]

0 commit comments

Comments
 (0)