Lecture et Écriture des Données
Lors de la création de programmes Solana optimisés, une sérialisation et une désérialisation efficaces des données peuvent avoir un impact significatif sur les performances.
Bien que Pinocchio ne nécessite pas d'opérations mémoire bas niveau, comprendre comment lire et écrire efficacement les données de compte peut vous aider à créer des programmes plus rapides.
Les techniques présentées dans ce guide fonctionnent avec n'importe quel framework de Solana, que vous utilisiez Pinocchio, Anchor ou le SDK natif. La clé réside dans la conception réfléchie de vos structures de données et dans la gestion sécurisée de la sérialisation.
Quand Utiliser du Code Non Sûr (unsafe)
N'utilisez du code non sécurisé que dans les cas suivants :
- Vous avez besoin d'une performance maximale et avez constaté que les alternatives sûres sont trop lentes
- Vous pouvez vérifier rigoureusement toutes les invariantes de sécurité
- Vous documentez clairement les exigences en matière de sécurité
Principes de Sécurité
Lorsque l'on travaille avec des tableaux d'octets bruts et des opérations mémoire, il faut veiller à éviter tout comportement indéfini. La compréhension de ces principes est essentielle pour écrire un code correct et fiable.
Vérification des Limites du Buffer
Vérifiez toujours que votre buffer est suffisamment grand avant toute opération de lecture ou d'écriture. La lecture ou l'écriture au-delà de la mémoire allouée est un comportement indéfini.
// Good: Check bounds first
if data.len() < size_of::<u64>() {
return Err(ProgramError::InvalidInstructionData);
}
let value = u64::from_le_bytes(data[0..8].try_into().unwrap());
// Bad: No bounds checking - could panic or cause UB
let value = u64::from_le_bytes(data[0..8].try_into().unwrap());
Exigences en Matière d'Alignement
Chaque type dans Rust a une exigence d'alignement qui détermine où il peut être placé dans la mémoire. La lecture d'un type depuis la mémoire qui n'est pas correctement aligné entraîne un comportement indéfini. La plupart des types primitifs nécessitent un alignement égal à leur taille :
u8
: Alignement de 1 octetu16
: Alignement de 2 octetsu32
: Alignement de 4 octetsu64
: Alignement de 8 octets
Cela signifie qu'un u64
doit être stocké à des adresses mémoire divisibles par 8, tandis qu'un u16
doit commencer à des adresses paires.
Si cette règle n'est pas respectée, le compilateur ajoutera automatiquement des octets "de remplissage" invisibles entre les champs de la structure afin de garantir que chaque champ respecte ses exigences d'alignement.
Voici à quoi ressemble une structure mal ordonnée :
#[repr(C)]
struct BadOrder {
small: u8, // 1 byte
// padding: [u8; 7] since `big` needs to be aligned at 8 bytes.
big: u64, // 8 bytes
medium: u16, // 2 bytes
// padding: [u8; 6] since the struct size needs to be aligned to 8 bytes.
}
Le compilateur insère 7 octets de remplissage après small
car big
nécessite un alignement de 8 octets. Il ajoute ensuite 6 octets supplémentaires à la fin pour que la taille totale (24 octets) soit un multiple de 8, ce qui gaspille 13 octets.
Une meilleure solution serait d'ordonner les champs de la structure comme ceci :
#[repr(C)]
struct GoodOrder {
big: u64, // 8 bytes
medium: u16, // 2 bytes
small: u8, // 1 byte
// padding: [u8; 5] since the struct size needs to be aligned to 8 bytes.
}
En plaçant les champs les plus volumineux en premier, nous réduisons le remplissage de 13 octets à seulement 5 octets.
Il existe une autre méthode, plus avancée, pour sérialiser et désérialiser les données afin d'optimiser l'espace disponible. Nous pouvons créer des Structures Zero-Padding (à remplissage nul) où les exigences d'alignement sont entièrement éliminées :
#[repr(C)]
struct ByteArrayStruct {
big: [u8; 8], // represents u64
medium: [u8; 2], // represents u16
small: u8,
}
Dans ce cas, la taille est exactement de 11 octets car tout est aligné d'1 octet.
Modèles de Bits Valides
Tous les modèles de bits ne sont pas valides pour tous les types. Les types tels que bool
, char
et enums
ont des valeurs valides restreintes. La lecture de modèles de bits non valides dans ces types est un comportement indéfini.
Lecture des Données
Il existe plusieurs approches pour lire les données à partir des buffers des compte, chacune présentant des avantages et des inconvénients différents :
Désérialisation Champ par Champ (Recommandé)
L'approche la plus sûre consiste à désérialiser chaque champ individuellement. Cela évite tous les problèmes d'alignement car vous travaillez avec des tableaux d'octets :
pub struct DepositInstructionData {
pub amount: u64,
pub recipient: Pubkey,
}
impl<'a> TryFrom<&'a [u8]> for DepositInstructionData {
type Error = ProgramError;
fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
if data.len() < (size_of::<u64>() + size_of::<Pubkey>()) {
return Err(ProgramError::InvalidInstructionData);
}
// No alignment issues: we're reading bytes and converting
let amount = u64::from_le_bytes(
data[0..8].try_into()
.map_err(|_| ProgramError::InvalidInstructionData)?
);
let recipient = Pubkey::try_from(&data[8..40])
.map_err(|_| ProgramError::InvalidInstructionData)?;
Ok(Self { amount, recipient })
}
}
Désérialisation Zéro-Copie
Cela peut être utilisé pour obtenir des performances maximales avec des structures correctement alignées mais cela nécessite une vérification minutieuse de l'alignement :
#[repr(C)]
pub struct Config {
pub authority: Pubkey,
pub mint_x: Pubkey,
pub mint_y: Pubkey,
pub seed: u64, // This field requires 8-byte alignment
pub fee: u16, // This field requires 2-byte alignment
pub state: u8,
pub config_bump: u8,
}
impl Config {
pub const LEN: usize = size_of::<Self>();
pub fn from_bytes(data: &[u8]) -> Result<&Self, ProgramError> {
if data.len() != Self::LEN {
return Err(ProgramError::InvalidAccountData);
}
// Critical: Check alignment for the most restrictive field (u64 in this case)
if (data.as_ptr() as usize) % core::mem::align_of::<Self>() != 0 {
return Err(ProgramError::InvalidAccountData);
}
// SAFETY: We've verified length and alignment
Ok(unsafe { &*(data.as_ptr() as *const Self) })
}
}
// Alternative: Avoid alignment issues entirely by using byte arrays for types with
// alignment requirement greater than 1 and provide accessor methods
#[repr(C)]
pub struct ConfigSafe {
pub authority: Pubkey,
pub mint_x: Pubkey,
pub mint_y: Pubkey,
seed: [u8; 8], // Convert with u64::from_le_bytes when needed
fee: [u8; 2], // Convert with u16::from_le_bytes when needed
pub state: u8,
pub config_bump: u8,
}
impl ConfigSafe {
pub fn from_bytes(data: &[u8]) -> Result<&Self, ProgramError> {
if data.len() != size_of::<Self>() {
return Err(ProgramError::InvalidAccountData);
}
// SAFETY: No alignment check needed - everything is u8 aligned
Ok(unsafe { &*(data.as_ptr() as *const Self) })
}
pub fn seed(&self) -> u64 {
u64::from_le_bytes(self.seed)
}
pub fn fee(&self) -> u16 {
u16::from_le_bytes(self.fee)
}
}
Comme vous pouvez le constater, les champs seed et fee sont tous deux privés. En effet, nous devons toujours utiliser des méthodes d'accès pour lire les données, car leurs valeurs sont représentées par des tableaux d'octets.
Lorsque vous accédez directement à un champ (config.seed
), le compilateur peut avoir besoin de créer une référence à l'emplacement mémoire de ce champ, même temporairement. Si ce champ n'est pas correctement aligné, la création de la référence est un comportement indéfini même si vous n'utilisez jamais explicitement la référence !
Les méthodes d'accès évitent cela en effectuant l'opération de lecture dans le scope de la méthode où le compilateur peut optimiser toutes les références intermédiaires.
#[repr(C, packed)] // This can cause unaligned fields!
pub struct PackedConfig {
pub state: u8,
pub seed: u64, // This u64 might not be 8-byte aligned due to packing
}
impl PackedConfig {
pub fn seed(&self) -> u64 {
self.seed // Safe: Direct value copy, no reference created
}
}
// Usage:
let config = PackedConfig::load(account)?;
// ❌ UNDEFINED BEHAVIOR: Creates a reference to potentially unaligned field
let seed_ref = &config.seed; // Compiler must create a reference here!
// ❌ UNDEFINED BEHAVIOR: Even this can be problematic
let seed_value = config.seed; // May create temporary reference internally
// ✅ SAFE: Accessor method reads value without creating reference
let seed_value = config.seed(); // No intermediate reference
Dans ce cas, nous n'avons pas de "types spéciaux" mais n'oubliez jamais que certains types nécessitent une attention particulière en raison de motifs de bits non valides :
pub struct StateAccount {
pub is_active: bool,
pub state_type: StateType,
pub data: [u8; 32],
}
#[repr(u8)]
pub enum StateType {
Inactive = 0,
Active = 1,
Paused = 2,
}
impl StateAccount {
pub fn from_bytes(data: &[u8]) -> Result<Self, ProgramError> {
if data.len() < size_of::<Self>() {
return Err(ProgramError::InvalidAccountData);
}
// Safely handle bool (only 0 or 1 are valid)
let is_active = match data[0] {
0 => false,
1 => true,
_ => return Err(ProgramError::InvalidAccountData),
};
// Safely handle enum
let state_type = match data[1] {
0 => StateType::Inactive,
1 => StateType::Active,
2 => StateType::Paused,
_ => return Err(ProgramError::InvalidAccountData),
};
let mut data_array = [0u8; 32];
data_array.copy_from_slice(&data[2..34]);
Ok(Self {
is_active,
state_type,
data: data_array,
})
}
}
Modèles Dangereux à Éviter
Voici quelques modèles courants qui peuvent entraîner un comportement indéfini et qui doivent être évités :
- Utilisation de
transmute()
avec des données non alignées
// ❌ UNDEFINED BEHAVIOR: transmute requires proper alignment
let value: u64 = unsafe { core::mem::transmute(bytes_slice) };
transmute()
suppose que les données source sont correctement alignées pour le type cible. Si vous travaillez avec des slices d'octets arbitraires, cette hypothèse est souvent violée.
- Casting de Pointeurs vers des Structures Compactes
#[repr(C, packed)]
pub struct PackedConfig {
pub state: u8,
pub seed: u64, // This u64 is only 1-byte aligned!
pub authority: Pubkey,
}
// ❌ UNDEFINED BEHAVIOR: Creates references to unaligned fields
let config = unsafe { &*(data.as_ptr() as *const PackedConfig) };
let seed_value = config.seed; // UB: May create reference to unaligned u64
Même si la structure tient dans la mémoire, l'accès aux champs multioctets peut créer des références non alignées.
- Accès Direct aux Champs des Structures Compactes
#[repr(C, packed)]
pub struct PackedStruct {
pub a: u8,
pub b: u64,
}
let packed = /* ... */;
// ❌ UNDEFINED BEHAVIOR: Creates reference to unaligned field
let b_ref = &packed.b;
// ❌ UNDEFINED BEHAVIOR: May create temporary reference
let b_value = packed.b;
- Supposer l'Alignement Sans Vérification
// ❌ UNDEFINED BEHAVIOR: No alignment check
let config = unsafe { &*(data.as_ptr() as *const Config) };
Ce n'est pas parce que les données correspondent qu'elles sont correctement alignées.
- Utilisation Incorrecte de
read_unaligned()
// ❌ WRONG: read_unaligned needs proper layout, not just size
#[repr(Rust)] // Default layout - not guaranteed!
pub struct BadStruct {
pub field: u64,
}
let value = unsafe { (data.as_ptr() as *const BadStruct).read_unaligned() };
read_unaligned()
nécessite toujours que la structure ait une disposition prédictible (#[repr(C)]
).
Écriture des Données
L'écriture des données en toute sécurité suit des principes similaires à ceux de la lecture :
Sérialisation Champ par Champ (Recommandé)
impl Config {
pub fn write_to_buffer(&self, data: &mut [u8]) -> Result<(), ProgramError> {
if data.len() != Self::LEN {
return Err(ProgramError::InvalidAccountData);
}
let mut offset = 0;
// Write authority
data[offset..offset + 32].copy_from_slice(self.authority.as_ref());
offset += 32;
// Write mint_x
data[offset..offset + 32].copy_from_slice(self.mint_x.as_ref());
offset += 32;
// Write mint_y
data[offset..offset + 32].copy_from_slice(self.mint_y.as_ref());
offset += 32;
// Write seed
data[offset..offset + 8].copy_from_slice(&self.seed.to_le_bytes());
offset += 8;
// Write fee
data[offset..offset + 2].copy_from_slice(&self.fee.to_le_bytes());
offset += 2;
// Write state
data[offset] = self.state;
offset += 1;
// Write config_bump
data[offset] = self.config_bump;
Ok(())
}
}
Cette approche est la méthode la plus sûre car elle sérialise explicitement chaque champ dans le buffer d'octets :
- Aucun problème d'alignement : vous écrivez dans un tableau d'octets
- Endianisme explicite : vous contrôlez l'ordre des octets avec to_le_bytes()
- Configuration mémoire claire : facile à déboguer et à comprendre
- Pas de comportement indéfini : Toutes les opérations sont effectuées sur des tableaux d'octets
Mutation Directe (Zéro-Copie)
Pour obtenir des performances optimales, vous pouvez convertir le buffer d'octets en une structure et modifier directement les champs. Cela nécessite que la structure soit correctement alignée :
impl Config {
pub fn from_bytes_mut(data: &mut [u8]) -> Result<&mut Self, ProgramError> {
if data.len() != Self::LEN {
return Err(ProgramError::InvalidAccountData);
}
// Check alignment
if (data.as_ptr() as usize) % core::mem::align_of::<Self>() != 0 {
return Err(ProgramError::InvalidAccountData);
}
// SAFETY: We've verified length and alignment
Ok(unsafe { &mut *(data.as_mut_ptr() as *mut Self) })
}
}
Lorsque l'alignement est vérifié et que la structure utilise #[repr(C)]
, la mutation directe des champs ne crée pas de références non alignées.
Approche par Tableau d'Octets avec des Setters (la plus sûre et la plus rapide)
Le meilleur des deux mondes : nous pouvons utiliser des tableaux d'octets en interne mais fournir des setters ergonomiques :
#[repr(C)]
pub struct ConfigSafe {
pub authority: Pubkey,
pub mint_x: Pubkey,
pub mint_y: Pubkey,
seed: [u8; 8],
fee: [u8; 2],
pub state: u8,
pub config_bump: u8,
}
impl ConfigSafe {
pub fn from_bytes_mut(data: &mut [u8]) -> Result<&mut Self, ProgramError> {
if data.len() != size_of::<Self>() {
return Err(ProgramError::InvalidAccountData);
}
// No alignment check needed - everything is u8 aligned
Ok(unsafe { &mut *(data.as_mut_ptr() as *mut Self) })
}
pub fn seed(&self) -> u64 {
u64::from_le_bytes(self.seed)
}
pub fn fee(&self) -> u16 {
u16::from_le_bytes(self.fee)
}
// Setters that handle endianness correctly
pub fn set_seed(&mut self, seed: u64) {
self.seed = seed.to_le_bytes();
}
pub fn set_fee(&mut self, fee: u16) {
self.fee = fee.to_le_bytes();
}
}
C'est idéal car :
- Aucun problème d'alignement : tous les champs sont alignés
- Mutation directe rapide : aucune surcharge de sérialisation après la configuration initiale
- Endianisme cohérent : les setters gèrent la conversion de l'ordre des octets
- Sécurité des types : les setters acceptent les types attendus et non des tableaux d'octets
Données de Taille Dynamique
Dans la mesure du possible, évitez de stocker des données de taille dynamique directement dans les comptes. Cependant, certains cas d'utilisation l'exigent.
Si votre compte contient des données dynamiques, placez toujours tous les champs de taille statique au début de votre structure et ajoutez les données dynamiques à la fin.
Champ Dynamique Unique
C'est le cas le plus simple : une section de longueur variable à la fin de votre compte :
#[repr(C)]
pub struct DynamicAccount {
pub fixed_data: [u8; 32],
pub counter: u64,
// Dynamic data follows after the struct in memory
// Layout: [fixed_data][counter][dynamic_data...]
}
impl DynamicAccount {
pub const FIXED_SIZE: usize = size_of::<Self>();
/// Safely parse account with dynamic data
pub fn from_bytes_with_dynamic(data: &[u8]) -> Result<(&Self, &[u8]), ProgramError> {
if data.len() < Self::FIXED_SIZE {
return Err(ProgramError::InvalidAccountData);
}
// SAFETY: We've verified the buffer is large enough for the fixed part
// The fixed part only contains [u8; 32] and u64, which have predictable layout
let fixed_part = unsafe { &*(data.as_ptr() as *const Self) };
// Everything after the fixed part is dynamic data
let dynamic_part = &data[Self::FIXED_SIZE..];
Ok((fixed_part, dynamic_part))
}
/// Get mutable references to both parts
pub fn from_bytes_mut_with_dynamic(data: &mut [u8]) -> Result<(&mut Self, &mut [u8]), ProgramError> {
if data.len() < Self::FIXED_SIZE {
return Err(ProgramError::InvalidAccountData);
}
// Split the buffer to avoid borrowing issues
let (fixed_bytes, dynamic_bytes) = data.split_at_mut(Self::FIXED_SIZE);
// SAFETY: We've verified the size and split safely
let fixed_part = unsafe { &mut *(fixed_bytes.as_mut_ptr() as *mut Self) };
Ok((fixed_part, dynamic_bytes))
}
}
/// Writing single dynamic field
impl DynamicAccount {
pub fn write_with_dynamic(
data: &mut [u8],
fixed_data: &[u8; 32],
counter: u64,
dynamic_data: &[u8]
) -> Result<(), ProgramError> {
let total_size = Self::FIXED_SIZE + dynamic_data.len();
if data.len() != total_size {
return Err(ProgramError::InvalidAccountData);
}
// Write fixed part field by field (safest approach)
data[0..32].copy_from_slice(fixed_data);
data[32..40].copy_from_slice(&counter.to_le_bytes());
// Write dynamic part
data[Self::FIXED_SIZE..].copy_from_slice(dynamic_data);
Ok(())
}
/// Update just the dynamic portion
pub fn update_dynamic_data(&mut self, account_data: &mut [u8], new_data: &[u8]) -> Result<(), ProgramError> {
if account_data.len() < Self::FIXED_SIZE + new_data.len() {
return Err(ProgramError::InvalidAccountData);
}
// Write new dynamic data
account_data[Self::FIXED_SIZE..Self::FIXED_SIZE + new_data.len()].copy_from_slice(new_data);
Ok(())
}
}
Pour éviter tout comportement indéfini, vérifiez toujours que la taille du buffer des données du compte est au moins égale à celle de la portion de taille statique. La section dynamique peut être vide, cette vérification est donc essentielle.
Cette disposition garantit que les décalages pour les champs de taille fixe sont toujours connus, quelle que soit la longueur des données dynamiques.
Il existe deux scénarios principaux lors de la lecture de données de taille dynamique :
- Champ dynamique unique à la fin : vous pouvez facilement déterminer la taille et le décalage des données dynamiques lors de l'exécution comme ceci :
const DYNAMIC_DATA_START_OFFSET: usize = size_of::<[u8; 32]>();
#[repr(C)]
pub struct DynamicallySizedAccount {
pub sized_data: [u8; 32],
// pub dynamic_data: &'info [u8], // Not part of the struct, but follows in the buffer
}
impl DynamicallySizedAccount {
/// Returns the length of the dynamic data section.
#[inline(always)]
pub fn get_dynamic_data_len(data: &[u8]) -> Result<usize, ProgramError> {
if data.len().le(&DYNAMIC_DATA_START_OFFSET) {
return Err(ProgramError::InvalidAccountData);
}
Ok(data.len() - DYNAMIC_DATA_START_OFFSET)
}
/// Returns a slice of the dynamic data.
#[inline(always)]
pub fn read_dynamic_data(data: &[u8]) -> Result<&[u8], ProgramError> {
if data.len().le(&DYNAMIC_DATA_START_OFFSET) {
return Err(ProgramError::InvalidAccountData);
}
Ok(&data[DYNAMIC_DATA_START_OFFSET..])
}
}
Champs Dynamiques Multiples
Cette approche est plus complexe car nous aurons besoin d'un moyen de déterminer la longueur de chaque champ dynamique à l'exception du dernier. L'approche la plus courante consiste à préfixer chaque champ dynamique (à l'exception du dernier) avec sa longueur, afin de pouvoir analyser correctement le buffer.
Voici un modèle simple et robuste : stocker la longueur du premier champ dynamique sous forme de u8 (ou u16, etc. si vous avez besoin de tailles plus importantes) immédiatement après les données de taille statique. Le premier champ dynamique suit et le second champ dynamique occupe le reste du buffer.
#[repr(C)]
pub struct MultiDynamicAccount {
pub fixed_data: [u8; 32],
pub timestamp: u64,
// Layout: [fixed_data][timestamp][len1: u8][data1][data2: remainder]
}
impl MultiDynamicAccount {
pub const FIXED_SIZE: usize = size_of::<Self>();
pub const LEN_PREFIX_SIZE: usize = size_of::<u8>();
pub const MIN_SIZE: usize = Self::FIXED_SIZE + Self::LEN_PREFIX_SIZE;
/// Parse account with two dynamic sections
pub fn parse_dynamic_fields(data: &[u8]) -> Result<(&[u8; 32], u64, &[u8], &[u8]), ProgramError> {
if data.len() < Self::MIN_SIZE {
return Err(ProgramError::InvalidAccountData);
}
// Extract fixed data safely
let fixed_data = data[..32].try_into()
.map_err(|_| ProgramError::InvalidAccountData)?;
let timestamp = u64::from_le_bytes(
data[32..40].try_into()
.map_err(|_| ProgramError::InvalidAccountData)?
);
// Read length of first dynamic field (single byte)
let len = data[Self::FIXED_SIZE] as usize;
// Validate we have enough data
if data.len() < Self::MIN_SIZE + len {
return Err(ProgramError::InvalidAccountData);
}
let data_1 = &data[Self::MIN_SIZE..Self::MIN_SIZE + len];
let data_2 = &data[Self::MIN_SIZE + len..]; // Remainder
Ok((fixed_data, timestamp, data_1, data_2))
}
/// Write account with two dynamic sections
pub fn write_with_multiple_dynamic(
buffer: &mut [u8],
fixed_data: &[u8; 32],
timestamp: u64,
data_1: &[u8],
data_2: &[u8]
) -> Result<(), ProgramError> {
let total_size = Self::MIN_SIZE + data_1.len() + data_2.len();
if buffer.len() != total_size {
return Err(ProgramError::InvalidAccountData);
}
// Validate data_1 length fits in u8
if data_1.len() > u8::MAX as usize {
return Err(ProgramError::InvalidInstructionData);
}
let mut offset = 0;
// Write fixed data
buffer[offset..offset + 32].copy_from_slice(fixed_data);
offset += 32;
buffer[offset..offset + 8].copy_from_slice(×tamp.to_le_bytes());
offset += 8;
// Write length prefix for data1 (single byte)
buffer[offset] = data_1.len() as u8;
offset += 1;
// Write data1
buffer[offset..offset + data_1.len()].copy_from_slice(data_1);
offset += data_1.len();
// Write data2 (remainder - no length prefix needed)
buffer[offset..].copy_from_slice(data_2);
Ok(())
}
}
Redimensionner le compte
Chaque fois que vous mettez à jour un champ dynamique, vous devez redimensionner le compte si la taille change. Voici une fonction polyvalente permettant de redimensionner un compte :
pub fn resize_account(
account: &AccountInfo,
payer: &AccountInfo,
new_size: usize,
zero_out: bool,
) -> ProgramResult {
// If the account is already the correct size, return early
if new_size == account.data_len() {
return Ok(());
}
// Calculate rent requirements
let rent = Rent::get()?;
let new_minimum_balance = rent.minimum_balance(new_size);
// Adjust lamports to meet rent-exemption requirements
match new_minimum_balance.cmp(&account.lamports()) {
core::cmp::Ordering::Greater => {
// Need more lamports for rent exemption
let lamports_diff = new_minimum_balance.saturating_sub(account.lamports());
**payer.try_borrow_mut_lamports()? -= lamports_diff;
**account.try_borrow_mut_lamports()? += lamports_diff;
}
core::cmp::Ordering::Less => {
// Return excess lamports to payer
let lamports_diff = account.lamports().saturating_sub(new_minimum_balance);
**account.try_borrow_mut_lamports()? -= lamports_diff;
**payer.try_borrow_mut_lamports()? += lamports_diff;
}
core::cmp::Ordering::Equal => {
// No lamport transfer needed
}
}
// Reallocate the account
account.resize(new_size)?;
Ok(())
}