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 u64
s 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
u64
s 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 u64
s 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);
});
});