dash_platform_queries/documents/document_ranked_entries.rs
1//! `FromProof` + `Fetch` for [`DocumentRankedEntries`] — the
2//! **ranked** (`GROUP BY … ORDER BY <aggregate> LIMIT n [OFFSET m]`)
3//! view of the unified `getDocuments` endpoint.
4//!
5//! A ranked query answers "which `n` groups score highest (or lowest)
6//! on an aggregate?" — *top 5 restaurants by average grade* — in
7//! `O(log n + k)`, with a proof. It reads a pre-sorted per-axis
8//! *secondary* Merk maintained by the write path (grovedb PR #657)
9//! rather than walking value trees, which is why it is cheap and why
10//! its shape is so constrained.
11//!
12//! Per-request resolution (which axis, which direction, how many
13//! groups, how many ranks to skip, which index covers them) lives in
14//! [`super::ranked_proof_helpers`]; this module is the thin
15//! `Fetch`-side wrapper.
16//!
17//! ## Request shape
18//!
19//! Exactly one aggregate `select`, exactly one `group_by` property,
20//! exactly one `ORDER BY` clause naming that select's aggregate, and a
21//! `LIMIT` — plus an optional `OFFSET`. `where` clauses are pins on a
22//! covering compound ranked index's leading properties (one per leading
23//! property, selecting which prefix's own ranking the walk reads) —
24//! absent for a single-property index. Each pin is an equality, except
25//! that **at most one** may be an `IN` of 2..=10 distinct elements: one
26//! walk per element, merged by `(aggregate, encoded pin, group key)`,
27//! with each merged entry carrying the encoded branch segment in
28//! `in_key` (unset on single-branch responses; a single-element `IN`
29//! normalizes to the equality pin). A non-zero `OFFSET` cannot combine
30//! with the `IN`, nor can a `null` pin on another property. No
31//! `having`, no `start_at`: each of those is rejected rather than
32//! ignored, on both sides, because a ranked walk cannot honour them and
33//! silently answering a different question is worse than an error.
34//!
35//! [`DocumentQuery::order_by_selected_aggregate`] builds the ordering
36//! clause, deriving the ordered field from the `select` through
37//! rs-drive's own key mapping (`SUM(f)` / `AVG(f)` are named by `f`,
38//! `COUNT(*)` by the `$count` sentinel), so there is no way to name it
39//! wrong by hand.
40//!
41//! ## Contract prerequisites
42//!
43//! The index must opt in with `rankedCountable` / `rankedSummable` /
44//! `rankedAverageable` (meta-schema v3, **protocol version 14+**). The
45//! index may be single-property (`group_by` its property, no `where`)
46//! or compound (`group_by` its trailing property, pin every leading one
47//! — equality pins, at most one of them an `IN`, as above). Against a
48//! protocol-version-13
49//! node the request is refused — v13's query table has no ranked path
50//! and rejects the ordering as `Unsupported`. That is the intended
51//! activation gate, not a bug: a v13 node and a v14 node must disagree
52//! here and nowhere else, which is what lets a mixed-version network
53//! run through the upgrade.
54//!
55//! ## Ranks, offsets, and the empty ranking
56//!
57//! The fetch result carries
58//! [`starting_rank`](drive_proof_verifier::DocumentRankedEntries::starting_rank)
59//! alongside the entries: entry `i` is the group at rank
60//! `starting_rank + i`, which is what makes `LIMIT 1 OFFSET 4`
61//! meaningful as "the 5th best" rather than "some entry". On the proved
62//! path that number is re-derived from the proof's counted subtree
63//! commitments, not taken from the node.
64//!
65//! An offset past the end of the ranking is a legitimate, provable
66//! answer: no entries, and `starting_rank` equal to the ranking's whole
67//! attested population. **Empty rankings prove too** — grovedb's
68//! paginated prover emits a guaranteed-empty range against an empty
69//! axis secondary rather than refusing — so querying a freshly
70//! registered contract with `prove = true` returns an empty page rather
71//! than an error, and the proved and unproven paths agree.
72//!
73//! ## Reading the values
74//!
75//! Entries come back in ranking order; **do not re-sort**. Averages
76//! are fixed-point integers: divide by
77//! [`RANKED_AVG_SCALE`](drive_proof_verifier::RANKED_AVG_SCALE) — a
78//! re-export of grovedb's own constant, which moved from `10^15` to
79//! `10^19` before release, so never hardcode the literal — or call
80//! [`RankedEntryValue::as_f64`](drive_proof_verifier::RankedEntryValue::as_f64),
81//! which does that division for you.
82//!
83//! **How exact the average is depends on the path.** Fetched with a
84//! proof (the default, and what the examples below do), the fixed point
85//! is the integer grovedb committed to and ranked on. Fetched without
86//! one, the wire carries only an `f64` of the average and the SDK
87//! re-scales it back, so the digits past `f64`'s ~15–16 significant
88//! decimals are reconstruction noise — fine to render, not something to
89//! compare for equality. Ranking *order* is exact either way.
90//!
91//! ## Example: top 5 restaurants by average grade
92//!
93//! `SELECT AVG(grade) GROUP BY restaurantId ORDER BY avg(grade) DESC LIMIT 5`
94//!
95//! ```rust,ignore
96//! use dash_sdk::{Sdk, platform::{DataContract, DocumentQuery, Fetch, Identifier}};
97//! use dash_sdk::drive::query::SelectProjection;
98//! use dash_sdk::platform::documents::document_query::RankingDirection;
99//! use drive_proof_verifier::{DocumentRankedEntries, RankedEntryValue, RANKED_AVG_SCALE};
100//! use futures::executor::block_on;
101//!
102//! # const RESTAURANTS_CONTRACT_ID: [u8; 32] = [0; 32];
103//! let sdk = Sdk::new_mock();
104//! let contract = block_on(DataContract::fetch(&sdk, Identifier::new(RESTAURANTS_CONTRACT_ID)))
105//! .expect("fetch contract")
106//! .expect("contract exists");
107//!
108//! let query = DocumentQuery::new(contract, "review")
109//! .expect("document type exists")
110//! .with_select(SelectProjection::avg("grade"))
111//! .with_group_by("restaurantId")
112//! .order_by_selected_aggregate(RankingDirection::Descending)
113//! .with_limit(5);
114//!
115//! let ranked = block_on(DocumentRankedEntries::fetch(&sdk, query))
116//! .expect("fetch succeeds")
117//! .expect("a well-formed ranked query always answers");
118//!
119//! // Entry order IS the ranking order — best first.
120//! for (offset, entry) in ranked.entries.iter().enumerate() {
121//! let rank = ranked.starting_rank + offset as u64;
122//! let restaurant = String::from_utf8_lossy(&entry.key);
123//! if let RankedEntryValue::AvgFixedPoint(fixed_point) = entry.value {
124//! // `as_f64()` is this same division, for when you only want
125//! // to display the number:
126//! // let average = entry.value.as_f64();
127//! // Keep the `fixed_point` itself when you need the exact
128//! // integer the proof committed to — comparing two groups,
129//! // reproducing the ranking, storing it. On a `prove = false`
130//! // fetch that integer is a reconstruction from the wire's
131//! // double, so it is only as precise as an `f64`.
132//! let average = (fixed_point as f64) / (RANKED_AVG_SCALE as f64);
133//! println!("#{}: {restaurant}: {average}", rank + 1);
134//! }
135//! }
136//! ```
137//!
138//! ## Example: the 5th-best restaurant
139//!
140//! `SELECT AVG(grade) GROUP BY restaurantId ORDER BY avg(grade) DESC LIMIT 1 OFFSET 4`
141//!
142//! ```rust,ignore
143//! # use dash_sdk::platform::{DataContract, DocumentQuery};
144//! # use dash_sdk::platform::documents::document_query::RankingDirection;
145//! # use dash_sdk::drive::query::SelectProjection;
146//! # fn example(contract: DataContract) -> Result<(), dash_sdk::Error> {
147//! let query = DocumentQuery::new(contract, "review")?
148//! .with_select(SelectProjection::avg("grade"))
149//! .with_group_by("restaurantId")
150//! .order_by_selected_aggregate(RankingDirection::Descending)
151//! .with_limit(1)
152//! .with_offset(4);
153//! # Ok(())
154//! # }
155//! ```
156
157use crate::documents::document_query::DocumentQuery;
158use crate::documents::ranked_proof_helpers::verify_ranked_query;
159use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata};
160use dash_context_provider::ContextProvider;
161use dpp::dashcore::Network;
162use dpp::version::PlatformVersion;
163use drive_proof_verifier::{DocumentRankedEntries, FromProof};
164
165impl FromProof<DocumentQuery> for DocumentRankedEntries {
166 type Request = DocumentQuery;
167 type Response = GetDocumentsResponse;
168
169 fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
170 request: I,
171 response: O,
172 _network: Network,
173 platform_version: &PlatformVersion,
174 provider: &'a dyn ContextProvider,
175 ) -> Result<(Option<Self>, ResponseMetadata, Proof), drive_proof_verifier::Error>
176 where
177 Self: 'a,
178 {
179 let request: Self::Request = request.into();
180 let response: Self::Response = response.into();
181 // Unlike the count / sum / average impls there is no separate
182 // `assert_select_is_*` pre-check here: the ranked grammar
183 // check is the first step of resolution and returns the
184 // resolved mode, so it runs once, inside the helper. See
185 // `ranked_proof_helpers::assert_ranked_shape`.
186 let (page, mtd, proof) =
187 verify_ranked_query(request, response, platform_version, provider)?;
188 Ok((page.map(DocumentRankedEntries::from_verified), mtd, proof))
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 //! Offline tests for the ranked client surface: the ordering
195 //! builder, the request→wire encoding, and the client-side grammar
196 //! mirror.
197 //!
198 //! Proof verification needs a populated Drive and a real grovedb
199 //! proof, so it is exercised where those exist: rs-drive's
200 //! `drive_document_ranked_query::tests` runs prover and verifier
201 //! against a live Drive (including a bit-flip sweep proving no
202 //! tamper survives with the honest root hash), and rs-drive-abci's
203 //! `ranked_tests` pins the wire encoding of the same values. The
204 //! SDK's own network-backed suites live in `tests/fetch/` and run
205 //! against recorded vectors; there is no ranked vector yet because
206 //! recording one needs a protocol-version-14 network, which does
207 //! not exist offline.
208
209 use super::*;
210 use crate::documents::document_query::RankingDirection;
211 use crate::documents::ranked_proof_helpers::assert_ranked_shape;
212 use dapi_grpc::platform::v0::get_documents_request::get_documents_request_v1::select as proto_select;
213 use dapi_grpc::platform::v0::get_documents_request::{
214 order_clause, GetDocumentsRequestV1, OrderClause as ProtoOrderClause,
215 Version as RequestVersion,
216 };
217 use dapi_grpc::platform::v0::GetDocumentsRequest;
218 use dpp::data_contract::DataContract;
219 use dpp::tests::fixtures::get_data_contract_fixture;
220 use dpp::version::TryFromPlatformVersioned;
221 use drive::query::{
222 HavingAggregate, HavingAggregateFunction, HavingClause, HavingOperator, HavingRightOperand,
223 SelectProjection, WhereClause, WhereOperator, RANKED_COUNT_ORDER_KEY,
224 };
225 use std::sync::Arc;
226
227 /// The protocol version the ranked surface activates at. Pinned
228 /// as a literal so a future bump of `PlatformVersion::latest()`
229 /// past 14 doesn't silently change what these tests encode.
230 fn platform_version() -> &'static PlatformVersion {
231 PlatformVersion::latest()
232 }
233
234 /// Any contract with a known document type — the wire encoder is
235 /// schema-agnostic (it never resolves `select` / `group_by` /
236 /// `order_by` field names against the contract; the *server* does
237 /// that), so the fixture's document type carries the encoding
238 /// test fine while the field names stay the headline example's,
239 /// keeping this comparable to rs-drive-abci's `ranked_tests`.
240 fn contract() -> Arc<DataContract> {
241 Arc::new(
242 get_data_contract_fixture(None, 0, platform_version().protocol_version)
243 .data_contract_owned(),
244 )
245 }
246
247 /// `SELECT AVG(grade) GROUP BY restaurantId ORDER BY grade DESC LIMIT 5`.
248 fn top_five_by_avg_grade() -> DocumentQuery {
249 DocumentQuery::new(contract(), "niceDocument")
250 .expect("the fixture has this document type")
251 .with_select(SelectProjection::avg("grade"))
252 .with_group_by("restaurantId")
253 .order_by_selected_aggregate(RankingDirection::Descending)
254 .with_limit(5)
255 }
256
257 fn encode(query: DocumentQuery) -> GetDocumentsRequest {
258 GetDocumentsRequest::try_from_platform_versioned(query, platform_version())
259 .expect("a ranked query encodes onto the V1 wire")
260 }
261
262 fn v1_of(request: GetDocumentsRequest) -> GetDocumentsRequestV1 {
263 match request.version.expect("the encoder always sets a version") {
264 RequestVersion::V1(v1) => v1,
265 RequestVersion::V0(_) => {
266 panic!("a ranked query must encode onto the V1 wire; V0 has no `order_by` targets")
267 }
268 }
269 }
270
271 /// The ordered field name, asserted through the wire's `target`
272 /// oneof so a future change to the aggregate-target spelling can't
273 /// pass this test by accident.
274 fn ordered_field(clause: &ProtoOrderClause) -> &str {
275 match clause.target.as_ref().expect("the target is always set") {
276 order_clause::Target::Field(field) => field.as_str(),
277 other => panic!("a ranked ORDER BY rides the field target, got {other:?}"),
278 }
279 }
280
281 /// The headline round-trip: the SDK must put a ranked query on the
282 /// wire in **exactly** the shape rs-drive-abci's
283 /// `avg_axis_top_k_returns_fixed_point_entries` proved the server
284 /// accepts. Asserted field by field rather than with a single
285 /// struct comparison so a failure names the field that drifted.
286 #[test]
287 fn ranked_query_encodes_the_proven_wire_shape() {
288 let v1 = v1_of(encode(top_five_by_avg_grade()));
289
290 // One aggregate select, naming the averaged property.
291 assert_eq!(v1.selects.len(), 1, "a ranked query has exactly one select");
292 assert_eq!(v1.selects[0].function, proto_select::Function::Avg as i32);
293 assert_eq!(v1.selects[0].field, "grade");
294
295 // One GROUP BY property — the ranked index's only property.
296 assert_eq!(v1.group_by, vec!["restaurantId".to_string()]);
297
298 // One ORDER BY clause naming the select's aggregate, descending.
299 assert_eq!(
300 v1.order_by.len(),
301 1,
302 "a ranked query has exactly one ordering clause — it is the ranking"
303 );
304 assert_eq!(ordered_field(&v1.order_by[0]), "grade");
305 assert!(
306 !v1.order_by[0].ascending,
307 "Descending is the `top n` reading and must not invert on the wire"
308 );
309
310 // The ranking's n rides `limit`.
311 assert_eq!(v1.limit, Some(5));
312
313 // Everything else must be at its "unset" wire value: a ranked
314 // request that carried any of these would be rejected.
315 assert!(v1.where_clauses.is_empty());
316 assert!(v1.having.is_empty());
317 assert_eq!(v1.offset, None);
318 assert!(v1.start.is_none());
319 assert!(v1.prove, "the Fetch path always requests a proof");
320 }
321
322 /// **The 5th-best group.** `LIMIT 1 OFFSET 4` is the whole point of
323 /// the offset surface, and the offset has to survive onto the wire
324 /// — a dropped one silently answers "the best" instead.
325 #[test]
326 fn the_fifth_best_encodes_limit_one_offset_four() {
327 let query = DocumentQuery::new(contract(), "niceDocument")
328 .expect("the fixture has this document type")
329 .with_select(SelectProjection::avg("grade"))
330 .with_group_by("restaurantId")
331 .order_by_selected_aggregate(RankingDirection::Descending)
332 .with_limit(1)
333 .with_offset(4);
334
335 let v1 = v1_of(encode(query.clone()));
336 assert_eq!(v1.limit, Some(1));
337 assert_eq!(v1.offset, Some(4));
338 assert!(!v1.order_by[0].ascending);
339
340 // And the client-side grammar resolves it to the same page the
341 // prover will produce.
342 let mode = assert_ranked_shape(&query, platform_version())
343 .expect("LIMIT 1 OFFSET 4 is well-formed");
344 assert_eq!(mode.k, 1);
345 assert_eq!(mode.offset, 4);
346 assert!(mode.descending);
347 }
348
349 /// An offset far past any plausible population is **not** capped:
350 /// grovedb counts the skipped region from the subtree aggregates
351 /// rather than walking it, on both `prove` settings, so the cost of
352 /// a deep page does not grow with the offset — `O(log n)` in the
353 /// size of the ranking, not in how far you page — and there is
354 /// nothing for a cap to protect.
355 #[test]
356 fn a_very_deep_offset_is_not_capped() {
357 let query = top_five_by_avg_grade().with_limit(1).with_offset(u32::MAX);
358 assert_eq!(v1_of(encode(query.clone())).offset, Some(u32::MAX));
359 let mode = assert_ranked_shape(&query, platform_version())
360 .expect("an offset past the end is a provable answer, not an error");
361 assert_eq!(mode.offset, u32::MAX);
362 }
363
364 /// `COUNT(*)` rankings are ordered by the **`$count` sentinel**,
365 /// not by a property name — the axis counts documents per group, so
366 /// there is no property to name. The builder derives it from the
367 /// select through rs-drive's own mapping, which is the only way the
368 /// client and the server can be guaranteed to agree.
369 #[test]
370 fn count_star_ranking_orders_by_the_count_sentinel() {
371 let query = DocumentQuery::new(contract(), "niceDocument")
372 .expect("the fixture has this document type")
373 .with_select(SelectProjection::count_star())
374 .with_group_by("restaurantId")
375 .order_by_selected_aggregate(RankingDirection::Descending)
376 .with_limit(2);
377
378 let v1 = v1_of(encode(query));
379 assert_eq!(v1.selects[0].function, proto_select::Function::Count as i32);
380 assert_eq!(v1.selects[0].field, "");
381 assert_eq!(ordered_field(&v1.order_by[0]), RANKED_COUNT_ORDER_KEY);
382 assert_eq!(
383 RANKED_COUNT_ORDER_KEY, "$count",
384 "the sentinel is wire-visible; changing it is a protocol change"
385 );
386 }
387
388 /// `Ascending` is the "bottom n" reading and must reach the wire as
389 /// `ascending: true`. Pinned separately from the descending case
390 /// because a builder that ignored its argument would still pass the
391 /// headline test.
392 #[test]
393 fn ascending_encodes_the_bottom_n_reading() {
394 let query = top_five_by_avg_grade()
395 .order_by_selected_aggregate(RankingDirection::Ascending)
396 .with_limit(1);
397 let v1 = v1_of(encode(query.clone()));
398 assert!(v1.order_by[0].ascending);
399
400 let mode = assert_ranked_shape(&query, platform_version()).expect("ASC LIMIT 1 is valid");
401 assert!(!mode.descending, "ASC ranks lowest-first");
402 assert_eq!(mode.k, 1, "ASC LIMIT 1 is the single worst-ranked group");
403 }
404
405 /// `order_by_selected_aggregate` **replaces** rather than appends.
406 /// A ranked query takes exactly one ordering clause, so a builder
407 /// that pushed would turn a second call — or a call after an
408 /// unrelated `with_order_by` — into a request the server rejects.
409 #[test]
410 fn order_by_selected_aggregate_replaces_any_prior_ordering() {
411 let query = top_five_by_avg_grade()
412 .with_order_by(drive::query::OrderClause {
413 field: "restaurantId".to_string(),
414 ascending: true,
415 })
416 .order_by_selected_aggregate(RankingDirection::Descending);
417
418 assert_eq!(query.order_by_clauses.len(), 1);
419 assert_eq!(query.order_by_clauses[0].field, "grade");
420 assert!(assert_ranked_shape(&query, platform_version()).is_ok());
421 }
422
423 /// The client-side grammar must resolve the same
424 /// `(axis, descending, k, offset)` tuple the server does — that
425 /// tuple goes into the proof envelope and is re-checked by the
426 /// verifier, so a client that resolved it differently could not
427 /// verify an honest proof.
428 #[test]
429 fn assert_ranked_shape_resolves_the_ranking() {
430 let mode = assert_ranked_shape(&top_five_by_avg_grade(), platform_version())
431 .expect("the headline query is well-formed");
432 assert!(mode.descending, "DESC ranks highest-first");
433 assert_eq!(mode.k, 5);
434 assert_eq!(mode.offset, 0, "an unset OFFSET is rank 0");
435 assert_eq!(mode.group_by_property, "restaurantId");
436 assert_eq!(mode.aggregate_field, "grade");
437 }
438
439 /// Every knob a ranked walk cannot honour is rejected **client
440 /// side**, before a round trip. Each of these is also rejected by
441 /// the server; mirroring them here turns a network error into an
442 /// immediate, specific one.
443 #[test]
444 fn assert_ranked_shape_rejects_what_a_ranking_cannot_honour() {
445 let base = top_five_by_avg_grade();
446
447 // An ordering on something other than the selected aggregate
448 // asks for an order the secondary cannot produce.
449 let wrong_order = {
450 let mut q = base.clone();
451 q.order_by_clauses = vec![drive::query::OrderClause {
452 field: "restaurantId".to_string(),
453 ascending: true,
454 }];
455 q
456 };
457 assert!(
458 assert_ranked_shape(&wrong_order, platform_version()).is_err(),
459 "ordering by the GROUP BY property is not a ranking by the aggregate"
460 );
461
462 // No ordering at all: a plain grouped aggregate, and the caller
463 // wanted `DocumentSplitAverages`.
464 let no_order = {
465 let mut q = base.clone();
466 q.order_by_clauses = Vec::new();
467 q
468 };
469 assert!(
470 assert_ranked_shape(&no_order, platform_version()).is_err(),
471 "a query with no ordering is not a ranked query"
472 );
473
474 let with_where = {
475 let mut q = base.clone();
476 q.where_clauses = vec![WhereClause {
477 field: "restaurantId".to_string(),
478 operator: WhereOperator::GreaterThan,
479 value: dpp::platform_value::Value::Text("a".to_string()),
480 }];
481 q
482 };
483 let err = assert_ranked_shape(&with_where, platform_version())
484 .expect_err("the axis secondary cannot rank a filtered subset");
485 assert!(format!("{err}").contains("where"));
486
487 // HAVING is a boolean per-group predicate and the ranked
488 // executor cannot drop groups from the middle of its walk.
489 let with_having = base.clone().with_having(vec![HavingClause {
490 aggregate: HavingAggregate {
491 function: HavingAggregateFunction::Avg,
492 field: "grade".to_string(),
493 },
494 operator: HavingOperator::GreaterThan,
495 right: HavingRightOperand::Value(dpp::platform_value::Value::U64(4)),
496 }]);
497 assert!(
498 assert_ranked_shape(&with_having, platform_version()).is_err(),
499 "a ranking cannot also filter its groups"
500 );
501 }
502
503 /// The ranking's `n` rides `limit`, and an out-of-range one is
504 /// rejected rather than clamped: `k` is part of the traversal the
505 /// client rebuilds to verify, so a silent clamp would produce a
506 /// page the client's reconstruction did not ask for.
507 #[test]
508 fn assert_ranked_shape_rejects_an_out_of_range_limit() {
509 // `0` is `DocumentQuery`'s "unset" sentinel, and a ranking with
510 // no `n` has no size; `101` is past `MAX_RANKED_LIMIT`.
511 for limit in [0u32, 101] {
512 let query = top_five_by_avg_grade().with_limit(limit);
513 assert!(
514 assert_ranked_shape(&query, platform_version()).is_err(),
515 "LIMIT {limit} is outside 1..=100 and must be rejected, not clamped"
516 );
517 }
518 }
519
520 /// The V0 wire has no `offset` field at all, so encoding one there
521 /// has to fail loudly. Dropping it would page from rank 0 while the
522 /// caller believed they had skipped ahead.
523 #[test]
524 fn a_v0_encode_refuses_to_silently_drop_an_offset() {
525 let mut v0_version = PlatformVersion::latest().clone();
526 v0_version
527 .drive_abci
528 .query
529 .document_query
530 .default_current_version = 0;
531
532 let query = DocumentQuery::new(contract(), "niceDocument")
533 .expect("doctype exists")
534 .with_offset(4);
535
536 let err = GetDocumentsRequest::try_from_platform_versioned(query, &v0_version)
537 .expect_err("V0 cannot carry an offset");
538 assert!(
539 format!("{err}").contains("offset"),
540 "the refusal must name the field that cannot be carried, got: {err}"
541 );
542 }
543}