Skip to main content

xrpl_common_stdlib/fields/
ledger_obj.rs

1//! # Ledger Object Field Retrieval Module (by slot)
2//!
3//! Typed accessors for reading fields from a ledger object that has been cached into a slot (via
4//! [`cache_le`](crate::objects::cache_le)). `get_field` and `get_field_optional` are generic over
5//! any type implementing [`crate::fields::decoder::FromLedger`] — see [`crate::fields::decoder`]
6//! for how a type opts into that.
7
8use crate::fields::decoder::{FromLedger, decode_host_result};
9use crate::host::{Error, Result, le_field};
10use crate::sfield::SField;
11use crate::types::blob::Blob;
12use core::mem::MaybeUninit;
13
14/// Retrieves a field from the ledger object cached in `slot` using an SField constant.
15///
16/// # Returns
17///
18/// Returns a `Result<T>` where:
19/// * `Ok(T)` - The field value for the specified field
20/// * `Err(Error)` - If the field cannot be retrieved, has unexpected size, or fails to decode
21#[inline]
22pub fn get_field<T: FromLedger, const CODE: i32>(slot: i32, _: SField<T, CODE>) -> Result<T> {
23    let mut buf = T::empty_buffer();
24    let n = {
25        let slice = buf.as_mut();
26        unsafe { le_field(slot, CODE, slice.as_mut_ptr(), slice.len()) }
27    };
28    decode_host_result::<T>(buf, n)
29}
30
31/// Retrieves an optionally present field from the ledger object cached in `slot`.
32///
33/// # Returns
34///
35/// Returns a `Result<Option<T>>` where:
36/// * `Ok(Some(T))` - The field value for the specified field
37/// * `Ok(None)` - If the field is not present (i.e., result_code == FIELD_NOT_FOUND)
38/// * `Err(Error)` - If the field cannot be retrieved, has unexpected size, or fails to decode
39#[inline]
40pub fn get_field_optional<T: FromLedger, const CODE: i32>(
41    slot: i32,
42    field: SField<T, CODE>,
43) -> Result<Option<T>> {
44    match get_field(slot, field) {
45        Result::Ok(value) => Result::Ok(Some(value)),
46        Result::Err(Error::FieldNotFound) => Result::Ok(None),
47        Result::Err(e) => Result::Err(e),
48    }
49}
50
51// --- `Blob<N>`-specific accessors ------------------------------------------------------------
52//
53// Why these exist (rather than just calling the generic `get_field`/`get_field_optional` above,
54// which `Blob<N>` is also eligible for via `FromLedger`):
55//
56// The generic path allocates `T::empty_buffer()` as its own local, has the host write into it,
57// and then hands that buffer to `T::decode` to build the returned `T`. For small fixed-size
58// types (`u32`, `AccountID`, ...) that "move" from the local buffer into the returned value is
59// reliably optimized away. For `Blob<N>` it is not: `Blob<N>` is `{ data: [u8; N], len: usize }`,
60// a different memory shape than the bare `[u8; N]` buffer `get_field` allocates, so building the
61// `Blob` from that buffer is a genuine field-by-field reconstruction, not just a reinterpret. We
62// verified with `wasm2wat` against this crate's actual release profile (`opt-level = "s"`,
63// `lto = true`, `codegen-units = 1`) that for `WasmBlob` (N = 4096) this reconstruction compiles
64// to a real ~4092-byte `memcpy` per read — `-Os` does not fuse it away the way `-O2`/`-O3` might.
65//
66// The fix is to give the host nothing to reconstruct from: allocate the `Blob<N>` itself first,
67// and point the host straight at its `data` field, so the bytes land in their final resting
68// place on the very first (and only) write. There is then no second, distinctly-addressed buffer
69// for the optimizer to have to notice is redundant — the copy is structurally absent rather than
70// hoped-away.
71//
72// This is deliberately NOT folded into the generic `get_field`/`get_field_optional`/`FieldDecoder`
73// machinery above, which is shared by every other field type (`u8..u64`, `AccountID`, `Amount`,
74// `UInt<N>`, `TransactionType`) where the buffer is small enough that the copy is already
75// negligible/elided. Scoping the fix to `Blob<N>` alone avoids adding raw-pointer plumbing to
76// code paths that don't need it.
77
78/// Retrieves a `Blob<N>` field from the ledger object cached in `slot`, writing the host's bytes
79/// directly into the returned `Blob<N>`'s own storage. See the comment above this function for
80/// why `Blob<N>` gets a dedicated accessor instead of using [`get_field`].
81#[inline]
82pub fn get_blob_field<const N: usize, const CODE: i32>(
83    slot: i32,
84    _: SField<Blob<N>, CODE>,
85) -> Result<Blob<N>> {
86    let mut blob = MaybeUninit::<Blob<N>>::uninit();
87    // SAFETY: `data_ptr` points at the `data` field inside `blob`'s own (uninitialized) storage;
88    // `blob` outlives this pointer and no other reference to it exists yet.
89    let data_ptr = unsafe { core::ptr::addr_of_mut!((*blob.as_mut_ptr()).data) } as *mut u8;
90    // Zero the destination *before* the host call: a result code of 0 is a legitimate "empty
91    // field" success (not an error), and `Blob::data` is `pub`, so callers may read past `len`
92    // directly. Zeroing in place (rather than in a separate scratch buffer) costs the same one
93    // `memset` the old code paid anyway, just at the final address instead of a temporary one.
94    unsafe { core::ptr::write_bytes(data_ptr, 0u8, N) };
95    let n = unsafe { le_field(slot, CODE, data_ptr, N) };
96    if n < 0 {
97        return Result::Err(Error::from_code(n));
98    }
99    if n as usize > N {
100        // A conformant host never reports writing more bytes than the buffer holds.
101        return Result::Err(Error::PointerOutOfBounds);
102    }
103    // SAFETY: `data` was fully zeroed above and then (partially) overwritten by the host, so
104    // every byte is initialized; `len` is set right before `assume_init`, completing the value.
105    unsafe {
106        core::ptr::addr_of_mut!((*blob.as_mut_ptr()).len).write(n as usize);
107        Result::Ok(blob.assume_init())
108    }
109}
110
111/// Optional variant of [`get_blob_field`]: returns `Ok(None)` if the field is not present,
112/// otherwise behaves identically (including the direct-write zero-copy behavior). Implemented
113/// in terms of [`get_blob_field`] itself rather than duplicating its body — `Error::FieldNotFound`
114/// round-trips exactly through `Error::from_code`/`.code()` (both are just `FIELD_NOT_FOUND`),
115/// so translating that one error case into `None` is all this needs to do.
116#[inline]
117pub fn get_blob_field_optional<const N: usize, const CODE: i32>(
118    slot: i32,
119    field: SField<Blob<N>, CODE>,
120) -> Result<Option<Blob<N>>> {
121    match get_blob_field(slot, field) {
122        Result::Ok(blob) => Result::Ok(Some(blob)),
123        Result::Err(Error::FieldNotFound) => Result::Ok(None),
124        Result::Err(e) => Result::Err(e),
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::{get_blob_field, get_blob_field_optional, get_field, get_field_optional};
131    use crate::host::error_codes::{FIELD_NOT_FOUND, SOME_ERROR};
132    use crate::host::host_bindings_trait::MockHostBindings;
133    use crate::host::setup_mock;
134    use crate::sfield;
135    use crate::types::account_id::{ACCOUNT_ID_SIZE, AccountID};
136    use crate::types::number::Number;
137    use mockall::predicate::{always, eq};
138
139    const SLOT: i32 = 3;
140
141    fn expect_ledger_obj_field(
142        mock: &mut MockHostBindings,
143        slot: i32,
144        field_code: i32,
145        size: usize,
146        times: usize,
147    ) {
148        mock.expect_le_field()
149            .with(eq(slot), eq(field_code), always(), eq(size))
150            .times(times)
151            .returning(move |_, _, _, _| size as i32);
152    }
153
154    #[test]
155    fn test_get_field_success() {
156        let mut mock = MockHostBindings::new();
157        expect_ledger_obj_field(&mut mock, SLOT, sfield::Sequence.into(), 4, 1);
158        expect_ledger_obj_field(&mut mock, SLOT, sfield::Account.into(), ACCOUNT_ID_SIZE, 1);
159        let _guard = setup_mock(mock);
160
161        assert!(get_field::<u32, _>(SLOT, sfield::Sequence).is_ok());
162        assert!(get_field::<AccountID, _>(SLOT, sfield::Account).is_ok());
163    }
164
165    #[test]
166    fn test_get_field_decodes_stnumber_field() {
167        // An `STI_NUMBER` field is 12 bytes; the mock writes a full-width value so this also
168        // exercises `Number`'s buffer size and its `FromLedger` marker.
169        const VALUE: [u8; 12] = [
170            0x00, 0x03, 0x8D, 0x7E, 0xA4, 0xC6, 0x80, 0x00, 0xFF, 0xFF, 0xFF, 0xF1,
171        ];
172        let mut mock = MockHostBindings::new();
173        mock.expect_le_field()
174            .with(
175                eq(SLOT),
176                eq::<i32>(sfield::AssetsTotal.into()),
177                always(),
178                eq(VALUE.len()),
179            )
180            .times(1)
181            .returning(|_, _, out, out_len| {
182                unsafe { out.copy_from_nonoverlapping(VALUE.as_ptr(), VALUE.len()) }
183                out_len as i32
184            });
185        let _guard = setup_mock(mock);
186
187        assert_eq!(
188            get_field(SLOT, sfield::AssetsTotal).unwrap(),
189            Number::from(VALUE)
190        );
191    }
192
193    #[test]
194    fn test_get_field_optional_returns_none_on_field_not_found() {
195        let mut mock = MockHostBindings::new();
196        mock.expect_le_field()
197            .with(
198                eq(SLOT),
199                eq::<i32>(sfield::SourceTag.into()),
200                always(),
201                eq(4),
202            )
203            .times(1)
204            .returning(|_, _, _, _| FIELD_NOT_FOUND);
205        let _guard = setup_mock(mock);
206
207        let result = get_field_optional::<u32, _>(SLOT, sfield::SourceTag);
208        assert!(result.is_ok());
209        assert!(result.unwrap().is_none());
210    }
211
212    #[test]
213    fn test_get_field_optional_returns_some_when_present() {
214        let mut mock = MockHostBindings::new();
215        expect_ledger_obj_field(&mut mock, SLOT, sfield::SourceTag.into(), 4, 1);
216        let _guard = setup_mock(mock);
217
218        let result = get_field_optional::<u32, _>(SLOT, sfield::SourceTag);
219        assert!(result.is_ok());
220        assert!(result.unwrap().is_some());
221    }
222
223    #[test]
224    fn test_get_field_returns_decode_error_on_byte_mismatch() {
225        let mut mock = MockHostBindings::new();
226        mock.expect_le_field()
227            .with(
228                eq(SLOT),
229                eq::<i32>(sfield::Sequence.into()),
230                always(),
231                eq(4),
232            )
233            .times(1)
234            .returning(|_, _, _, _| 3);
235        let _guard = setup_mock(mock);
236
237        let result = get_field::<u32, _>(SLOT, sfield::Sequence);
238        assert!(result.is_err());
239        assert_eq!(
240            result.err().unwrap().code(),
241            crate::host::Error::InvalidDecoding.code()
242        );
243    }
244
245    #[test]
246    fn test_get_field_returns_err_on_internal_error() {
247        let mut mock = MockHostBindings::new();
248        mock.expect_le_field()
249            .with(eq(SLOT), eq::<i32>(sfield::Flags.into()), always(), eq(4))
250            .times(1)
251            .returning(|_, _, _, _| SOME_ERROR);
252        let _guard = setup_mock(mock);
253
254        let result = get_field::<u32, _>(SLOT, sfield::Flags);
255        assert!(result.is_err());
256        assert_eq!(result.err().unwrap().code(), SOME_ERROR);
257    }
258
259    #[test]
260    fn test_get_field_returns_err_when_host_reports_oversized_write() {
261        // A conformant host can't write past the buffer it was handed; a positive count larger
262        // than the buffer is reported as PointerOutOfBounds.
263        let mut mock = MockHostBindings::new();
264        mock.expect_le_field()
265            .with(
266                eq(SLOT),
267                eq::<i32>(sfield::Sequence.into()),
268                always(),
269                eq(4),
270            )
271            .times(1)
272            .returning(|_, _, _, _| 8); // claims 8 bytes into a 4-byte u32 buffer
273        let _guard = setup_mock(mock);
274
275        let result = get_field::<u32, _>(SLOT, sfield::Sequence);
276        assert!(result.is_err());
277        assert_eq!(
278            result.err().unwrap().code(),
279            crate::host::Error::PointerOutOfBounds.code()
280        );
281    }
282
283    #[test]
284    fn test_get_blob_field_writes_bytes_directly_into_blob_data() {
285        let mut mock = MockHostBindings::new();
286        mock.expect_le_field()
287            .with(
288                eq(SLOT),
289                eq::<i32>(sfield::Condition.into()),
290                always(),
291                eq(128),
292            )
293            .times(1)
294            .returning(|_, _, buf, size| {
295                // Simulate the host writing 128 bytes of non-zero data.
296                let slice = unsafe { core::slice::from_raw_parts_mut(buf, size) };
297                slice.fill(0xAB);
298                size as i32
299            });
300        let _guard = setup_mock(mock);
301
302        let blob = get_blob_field(SLOT, sfield::Condition).unwrap();
303        assert_eq!(blob.len(), 128);
304        assert!(blob.as_slice().iter().all(|&b| b == 0xAB));
305    }
306
307    #[test]
308    fn test_get_blob_field_zeroes_tail_when_host_writes_fewer_bytes() {
309        // A short write (e.g. an empty/undersized field) must leave the tail zeroed, not
310        // uninitialized -- `Blob::data` is `pub`, so callers may read past `len` directly.
311        let mut mock = MockHostBindings::new();
312        mock.expect_le_field()
313            .with(
314                eq(SLOT),
315                eq::<i32>(sfield::Condition.into()),
316                always(),
317                eq(128),
318            )
319            .times(1)
320            .returning(|_, _, buf, _size| {
321                let slice = unsafe { core::slice::from_raw_parts_mut(buf, 10) };
322                slice.fill(0xFF);
323                10
324            });
325        let _guard = setup_mock(mock);
326
327        let blob = get_blob_field(SLOT, sfield::Condition).unwrap();
328        assert_eq!(blob.len(), 10);
329        assert_eq!(blob.data[9], 0xFF);
330        assert_eq!(blob.data[10], 0);
331        assert_eq!(blob.data[127], 0);
332    }
333
334    #[test]
335    fn test_get_blob_field_returns_err_on_internal_error() {
336        let mut mock = MockHostBindings::new();
337        mock.expect_le_field()
338            .with(
339                eq(SLOT),
340                eq::<i32>(sfield::Condition.into()),
341                always(),
342                eq(128),
343            )
344            .times(1)
345            .returning(|_, _, _, _| SOME_ERROR);
346        let _guard = setup_mock(mock);
347
348        let result = get_blob_field(SLOT, sfield::Condition);
349        assert!(result.is_err());
350        assert_eq!(result.err().unwrap().code(), SOME_ERROR);
351    }
352
353    #[test]
354    fn test_get_blob_field_returns_err_when_host_reports_oversized_write() {
355        let mut mock = MockHostBindings::new();
356        mock.expect_le_field()
357            .with(
358                eq(SLOT),
359                eq::<i32>(sfield::Condition.into()),
360                always(),
361                eq(128),
362            )
363            .times(1)
364            .returning(|_, _, _, _| 129); // claims 129 bytes into a 128-byte buffer
365        let _guard = setup_mock(mock);
366
367        let result = get_blob_field(SLOT, sfield::Condition);
368        assert!(result.is_err());
369        assert_eq!(
370            result.err().unwrap().code(),
371            crate::host::Error::PointerOutOfBounds.code()
372        );
373    }
374
375    #[test]
376    fn test_get_blob_field_optional_returns_none_on_field_not_found() {
377        let mut mock = MockHostBindings::new();
378        mock.expect_le_field()
379            .with(
380                eq(SLOT),
381                eq::<i32>(sfield::Condition.into()),
382                always(),
383                eq(128),
384            )
385            .times(1)
386            .returning(|_, _, _, _| FIELD_NOT_FOUND);
387        let _guard = setup_mock(mock);
388
389        let result = get_blob_field_optional(SLOT, sfield::Condition);
390        assert!(result.is_ok());
391        assert!(result.unwrap().is_none());
392    }
393
394    #[test]
395    fn test_get_blob_field_optional_returns_some_when_present() {
396        let mut mock = MockHostBindings::new();
397        expect_ledger_obj_field(&mut mock, SLOT, sfield::Condition.into(), 128, 1);
398        let _guard = setup_mock(mock);
399
400        let result = get_blob_field_optional(SLOT, sfield::Condition);
401        assert!(result.is_ok());
402        assert!(result.unwrap().is_some());
403    }
404}