Skip to main content

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}