dlp_api/pod_view.rs
1use pinocchio::error::ProgramError;
2
3/// SAFETY
4///
5/// This program uses POD structs for account and instruction data that are
6/// read/written via raw byte copies (bytemuck, raw pointers, or direct memory
7/// views). This document establishes the safety and correctness rules for such
8/// usage, with special attention to endianness, alignment, and representation.
9///
10/// Solana BPF Memory Model & Endianness Guarantees
11/// ===============================================
12///
13/// The Solana BPF VM uses a little-endian memory model. As a result, all
14/// multi-byte scalar types (integers and floats) are stored in memory using
15/// little-endian byte order.
16///
17/// When this program serializes POD types by copying their raw memory layout
18/// (e.g. via bytemuck::bytes_of or ptr::copy_nonoverlapping), the resulting
19/// byte sequence is also little-endian, because it directly reflects the
20/// machine-level representation.
21///
22/// But also note that although the Solana VM is little-endian, Solana does not mandate any
23/// serialization format or endianness for account or instruction data. A program
24/// may choose to serialize its data in any order it wants. The only rule is: the
25/// reader must interpret the data consistently with the writer.
26///
27/// However, in this codebase we serialize POD types by copying their raw memory
28/// bytes directly. Since the machine representation is little-endian, this implicitly
29/// produces a little-endian serialization format.
30///
31/// Alignment
32/// =========
33///
34/// POD structs rely on correct alignment whenever they are accessed through "typed"
35/// pointers i.e both during serialization and deserialization (even though in POD's case,
36/// serialization/deserialization are merely reinterpretion of the raw memory).
37///
38/// That basically means, if a struct has alignment n, then:
39///
40/// - The starting address of account buffer must be aligned to n-byte boundary.
41/// - The struct's fields must not cause unintended padding beyond what #[repr(C)]
42/// automatically defines and each field must be aligned to their own alignment.
43/// - In our codebase, we however explicitly define the padding for two reasons:
44/// - The padding is visible.
45/// - And since the padding is visible and has a name, we can easily initialize it
46/// with all zeroes without using any dirty trick. Note that uninitialized paddings
47/// are dangerous and would invoke UB (Rust must be inheriting this UB behaviour
48/// from C++ indirectly through LLVM).
49///
50/// Our codebase assumes that both `account data` and `instruction data` are aligned to
51/// 8-byte boundary and therefore all structs/types have either 8-byte alignment requirement
52/// or weaker one. The `bytemuck` crates enforces the alignment requirements, so we do not
53/// have to do it ourselves.
54///
55/// Avoid char and bool
56/// ===================
57///
58/// Two Rust primitives must be AVOIDED in low-level serialization and POD
59/// layouts:
60/// - char
61/// - bool
62///
63/// These two are "disgusting types" in the context of raw byte-level
64/// programming because NOT all bit-patterns are valid values for them:
65/// - bool only permits 0 and 1; all other bytes are invalid.
66/// - char permits only valid Unicode scalar values; most u32 patterns
67/// represent invalid char.
68///
69/// As a result, neither char nor bool can be safely reinterpreted from
70/// raw bytes, cannot be Pod, and must not appear in account or instruction
71/// layouts. Use u8 instead. Note that `#[repr(u8)] enum` is disgusting
72/// type as well, so we cannot use that either.
73///
74/// Ref: https://docs.rs/bytemuck/latest/bytemuck/trait.Pod.html
75///
76pub trait PodView {
77 /// The exact size of the POD type in bytes.
78 ///
79 /// This is used to create new accounts and to ensure that the buffer
80 /// we write to or read from matches the expected layout exactly.
81 const SPACE: usize;
82 const ALIGN: usize;
83
84 fn to_bytes(&self) -> Vec<u8>
85 where
86 Self: bytemuck::Pod,
87 {
88 bytemuck::bytes_of(self).to_vec()
89 }
90
91 /// Copy the raw bytes of Self into the given mutable buffer.
92 ///
93 /// This is a *copy*, not a cast. The buffer must be exactly Self::SPACE
94 /// bytes long. On success, the buffer will contain a byte-for-byte
95 /// representation of this struct, else it will return ErrorCode::SizeMismatch.
96 fn try_copy_to(&self, buffer: &mut [u8]) -> Result<(), ProgramError>;
97
98 /// This function performs a zero-copy cast from [u8] to &Self.
99 ///
100 /// The buffer must:
101 /// - be exactly Self::SPACE bytes long
102 /// - be properly aligned (guaranteed for Solana account data)
103 /// - contain valid POD bytes for Self
104 fn try_view_from(buffer: &[u8]) -> Result<&Self, ProgramError>;
105
106 /// Mutable version of try_view_from.
107 fn try_view_from_mut(buffer: &mut [u8]) -> Result<&mut Self, ProgramError>;
108
109 /// During testing, the account/ix data may not be properly aligned.
110 /// In that case, we could create a "copy" instead of a "view" using this
111 /// function that takes care of provided possibly-unaligned_buffer.
112 #[cfg(feature = "unit_test_config")]
113 fn try_from_unaligned(
114 unaligned_buffer: &[u8],
115 ) -> Result<Self, ProgramError>
116 where
117 Self: Sized;
118}
119
120impl<T: bytemuck::Pod> PodView for T {
121 const SPACE: usize = core::mem::size_of::<T>();
122 const ALIGN: usize = core::mem::align_of::<T>();
123
124 fn try_copy_to(&self, buffer: &mut [u8]) -> Result<(), ProgramError> {
125 if buffer.len() != Self::SPACE {
126 return Err(ProgramError::InvalidArgument);
127 }
128 let src = bytemuck::bytes_of(self);
129 buffer.copy_from_slice(src);
130 Ok(())
131 }
132
133 fn try_view_from(buffer: &[u8]) -> Result<&Self, ProgramError> {
134 bytemuck::try_from_bytes(buffer)
135 .map_err(|_| ProgramError::InvalidArgument)
136 }
137
138 fn try_view_from_mut(buffer: &mut [u8]) -> Result<&mut Self, ProgramError> {
139 bytemuck::try_from_bytes_mut(buffer)
140 .map_err(|_| ProgramError::InvalidArgument)
141 }
142
143 #[cfg(feature = "unit_test_config")]
144 fn try_from_unaligned(
145 possibly_unaligned_buffer: &[u8],
146 ) -> Result<Self, ProgramError> {
147 bytemuck::try_pod_read_unaligned(possibly_unaligned_buffer)
148 .map_err(|_| ProgramError::InvalidArgument)
149 }
150}