1

On CPUs of some architectures, memory addresses of variables (including fields of structs) should align with its size. Thus for example, a u64 variable (whose size in bytes is 8) cannot reside in addresses that are not a multiple of 8 (e.g., 0x24).

I'm wondering if this alignment rule should apply to Solana runtime? Can I store a u64 at an address that is not a multiple of 8? If yes, then what is the memory address of an account's data (if there does exist such a fixed address) after the account is loaded by Solana runtime? This question is important, especially when we want to directly modify data in-place (i.e., zero copy) instead of deserializing it then serializing it back.

By "an account's data", I mean the data field in the Solana AccountInfo struct defined as follows:

#[repr(C)]
pub struct AccountInfo<'a> {
    pub key: &'a Pubkey,
    pub lamports: Rc<RefCell<&'a mut u64>>,
    pub data: Rc<RefCell<&'a mut [u8]>>,
    pub owner: &'a Pubkey,
    pub rent_epoch: Epoch,
    pub is_signer: bool,
    pub is_writable: bool,
    pub executable: bool,
}

And I'm asking the value of the &'a mut [u8] (which represents the base address of the whole bunch of data) in the data field.

1 Answer 1

1

Let me answer my own question. Hope this could help somebody.

The short answer is: Solana runtime DOES NOT care about the alignments.

I've written a simple Solana program using Anchor framework for testing. And a simple test script in TypeScript is written to interact with the on-chain program. The source code of both files are at the end of the answer.

I define a struct as:

#[account]
#[derive(Debug, InitSpace)]
pub struct MyStruct {
    pub field1: u8,
    pub field2: u16,
    pub field3: u32,
}

I want to store this struct as well as some extra u64s in the PDA. So the PDA's data layout will be:

  • data[0..8]: The discriminator (used by Anchor) of this struct
  • data[8..9]: A u8 (i.e., the field1)
  • data[9..11]: A u16 (i.e., the field2) in its little endian representation (borsh will serialize integers in little endian)
  • data[11..15]: A u32 (i.e., the field3) in its little endian representation
  • data[15..]: Some u64s in their little endian representations

In the initialize instruction, first I've initialized the 3 fields. These 3 fields of MyStruct is initialized in a variable in memory, and after the instruction ends, Anchor will deserialize the variable back to the account's data.

After initializing the 3 fields, I've initialized the extra u64s in-place (i.e., via a zero-copy approach). Note here I use unsafe std::mem::transmute function. If we use bytemuck's cast_slice_mut function, the instruction will panic at runtime, because bytemuck will check the alignments, which Solana actually doesn't require.

In the test_inst instruction, I've logged the base address of account data. Then, I've logged each byte of the account's data. I've run the test on Solana devnet, and you could see the transaction log here on Solscan. Note that the first u64 following field3 starts at the 15th (zero indexed) byte, and this address is not aligned.

Solana on-chain program source code:

use anchor_lang::prelude::*;

declare_id!("Ver13Cmn9GXqsoPyGXNnq9KW5AaxxV4pea2jZJ1coW9");

#[program]
pub mod testanchor {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        ctx.accounts.my_struct.set_inner(MyStruct {
            field1: 0xA0,
            field2: 0xA2A1,
            field3: 0xA6A5A4A3,
        });

        let account_info = ctx.accounts.my_struct.to_account_info();
        let mut data = account_info.data.borrow_mut();
        let data = data.as_mut();

        // `bytemuck` crate will validate alignments, and will panic.
        // let extra: &mut [u64] = bytemuck::cast_slice_mut(&mut data[16..]);
        let extra: &mut [u64] = unsafe {std::mem::transmute(&mut data[15..])};

        for i in 0..5 {
            let num: u8 = 0xB0 | (i as u8);
            let num = u64::from_le_bytes([num; 8]);
            extra[i] = num;
        }

        Ok(())
    }

    pub fn test_inst(ctx: Context<TestInst>) -> Result<()> {
        let mut data = ctx.accounts.my_struct.data.borrow_mut();
        let data = data.as_mut();

        let base_ptr = &data[0] as *const u8;
        msg!("Base address of data is: {:#?}", base_ptr);

        for i in 0..60 {
            msg!("data[{}] = {:#04x}", i, data[i]);
        }

        Ok(())
    }
}

#[account]
#[derive(Debug, InitSpace)]
pub struct MyStruct {
    pub field1: u8,
    pub field2: u16,
    pub field3: u32,
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(
        init,
        payer = payer,
        seeds = [b"my_struct"],
        bump,
        space = 8 + MyStruct::INIT_SPACE + 200
    )]
    pub my_struct: Account<'info, MyStruct>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct TestInst<'info> {
    /// CHECK: 
    #[account(
        mut,
        seeds = [b"my_struct"],
        bump
    )]
    pub my_struct: UncheckedAccount<'info>,
}

Test script source code:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Testanchor } from "../target/types/testanchor";
import { PublicKey, SystemProgram } from "@solana/web3.js";

describe("testanchor", () => {
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);
  const wallet = provider.wallet as anchor.Wallet;
  console.log(wallet.publicKey);

  const program = anchor.workspace.Testanchor as Program<Testanchor>;

  const [pda, ] = PublicKey.findProgramAddressSync([Buffer.from("my_struct")], program.programId);

  it("tests", async () => {
    // You might want to uncomment this when you are testing on local net.
    // await provider.connection.requestAirdrop(wallet.publicKey, 500e9);

    // Add your test here.
    const tx1 = await program.methods
      .initialize()
      .accounts({
        payer: wallet.publicKey,
        myStruct: pda,
        systemProgram: SystemProgram.programId,
      })
      .signers([wallet.payer])
      .rpc();
    console.log("Initialization transaction signature", tx1);

    const tx2 = await program.methods
      .testInst()
      .accounts({
        myStruct: pda,
      })
      .signers([wallet.payer])
      .rpc();
    console.log("TestInst transaction signature", tx2);

  });
});

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.