Skip to content

SIMD-0177: Program Runtime ABI v2#177

Open
Lichtso wants to merge 22 commits intosolana-foundation:mainfrom
Lichtso:program-runtime-abiv2
Open

SIMD-0177: Program Runtime ABI v2#177
Lichtso wants to merge 22 commits intosolana-foundation:mainfrom
Lichtso:program-runtime-abiv2

Conversation

@Lichtso
Copy link
Copy Markdown
Contributor

@Lichtso Lichtso commented Oct 1, 2024

No description provided.

@Lichtso Lichtso force-pushed the program-runtime-abiv2 branch from ae4514d to 7148fdf Compare October 2, 2024 09:40
- Readonly instruction accounts get no growth padding.
- For writable instruction accounts additional capacity is allocated and mapped
for potential account growth. The maximum capacity is the length of the account
payload at the beginning of the transaction plus 10 KiB. CPI can not grow
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are independent. SIMD-0163 is about the program being called, that is not affected in this SIMD.

Copy link
Copy Markdown
Contributor

@buffalojoec buffalojoec Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could increase the account realloc / resize limit if there are no interactions of ABI v0/v1 and ABI v2 programs in the CPI call tree of a top level instruction. See the discussion with Sean below.

@Lichtso Lichtso force-pushed the program-runtime-abiv2 branch from 7148fdf to 375deef Compare October 2, 2024 15:50
@Lichtso Lichtso force-pushed the program-runtime-abiv2 branch 4 times, most recently from caa91dd to 6b641c8 Compare October 16, 2024 14:07
- Magic: `u32`: `0x76494241` ("ABIv" encoded in ASCII)
- ABI version `u32`: `0x00000002`
- Pointer to instruction data: `u64`
- Length of instruction data: `u32`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about this and I thought that if data was presented in a way which makes sense to rust, e.g. regular slices with u64 ptr and u64 length, then rust programs do not have to do any entry processing at all, and can just cast 4GiB address to a type and be done.

@Lichtso Lichtso force-pushed the program-runtime-abiv2 branch 2 times, most recently from 0d9976c to 9084d79 Compare February 22, 2025 23:28
@Lichtso Lichtso force-pushed the program-runtime-abiv2 branch from 9084d79 to e3e3e19 Compare February 25, 2025 17:58
Comment on lines +68 to +73
- Key: `[u8; 32]`
- Owner: `[u8; 32]`
- Lamports: `u64`
- Account payload: `&[u8]` which is composed of:
- Pointer to account payload: `u64`
- Account payload length: `u64`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Programs also have access to the booleans writable, signer and executable. Are we serializing these ones as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are per instruction not per transaction. See the "flags bitfield" in "Per Instruction Serialization".

Comment on lines +67 to +70
- For each transaction account:
- Key: `[u8; 32]`
- Owner: `[u8; 32]`
- Lamports: `u64`
Copy link
Copy Markdown
Contributor

@LucasSte LucasSte May 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a couple of discussions with @Lichtso, we thought the feedback from developer relations would be important here.

Today programs can only see the accounts passed to them in the instruction being executed. This layout change entails that programs (and every CPIs program invoked from them) will now be able to access metadata from all the accounts passed in the transaction, regardless whether they were passed in the instruction or not. We still intend to maintain the account payload hidden, though, if it is not an instruction account.

Would this change have any unintended consequences on the developer side?

(cc. @joncinque and @jacobcreech )

The `AccountInfo` parameter of the CPI syscalls (`sol_invoke_signed_c` and
`sol_invoke_signed_rust`) will be ignored if ABI v2 is in use. Instead the
changes to account metadata will be communicated explicitly through separate
syscalls `sol_set_account_owner`, `sol_set_account_lamports` and
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we need to mention the expected cost of sol_set_account_lamports to update lamports of an account – this is a quite common operation in programs.

The first one is a readonly region starting at `0x600000000`. It must be
updated at each CPI call edge. The contents of this region are the following:

- For each instruction in transaction:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Are CPIs added to the middle or end of the array?
  2. Is the array updated in the middle of an instruction or only when entering an instruction?
  3. Is the length in TransactionMetadata the number of top level instructions or top level instructions + executed cpis? (the former would be useful to add if the later is true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. To the end.
  2. It would be updated during the CPI syscall. When you invoke sol_invoke_signed_v2, the runtime would update the array. These changes would only be visible to the caller when the CPI finished and the control flow is returned.
  3. Thanks for bringing that up. I'll specify what the number refers to and add the number of CPI instructions in the array.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for bringing that up. I'll specify what the number refers to and add the number of CPI instructions in the array.

Fixed in 0aee740

Comment on lines +158 to +159
area to be the CPI scratch pad, i.e. at address `0x10900000000` plus
`0x100000000` times the number of instructions in the transaction. Its purpose
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be a static location? Removes a math operation to get the cpi scratchpad ptr

@Buzzec
Copy link
Copy Markdown

Buzzec commented Dec 1, 2025

I've been doing a bit of experimenting with how a program api would work: https://github.com/Buzzec/solana_abi_v2

LucasSte and others added 2 commits February 2, 2026 16:50
Co-authored-by: Alex Kahn <43892045+alnoki@users.noreply.github.com>
Changes to the account metadata must now be communicated with specific
syscalls, as detailed below:

- `sol_assign_owner`: Dst account, new owner as `&[u8; 32]`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@febo cc @igor56D @jacobcreech per 2026-02-25 mtnDAO discussion:

To maintain CU parity with ABI v1, transfer_lamports syscall should cost at most 6CUs:

r3 := lamports to transfer # calculated separately
# load, increment, store lamports for recipient
ldxdw r2, [r1 + ACCT_TO_INCREMENT_LAMPORTS_OFF] 
add64 r2, r3
stxdw [r1 + ACCT_TO_INCREMENT_LAMPORTS_OFF], r2
# repeat for decrement to sender, 3 more CUs

Comment on lines +131 to +133
For each unique (meaning deduplicated) instruction account the payload must
be mapped in at `0x800000000` plus `0x100000000` times the index of the
**transaction** account (not the index of the instruction account). Only if the
Copy link
Copy Markdown

@alnoki alnoki Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Lichtso (cc @febo @igor56D @jacobcreech @deanmlittle @arihantbansal)

per mtnDAO discussion 2026-02-26

TL;DR - non-deterministic account data addressing on a per-instruction basis is akin to a hard drive that starts up with randomized data pointers on every boot

E.g. if account data for account at index x in instruction y is non-deterministic, then direct pointer addressing in data structures breaks, introduction significant overhead for ABI v2.

For example the below binary search tree, which works with absolute addressing in v1, breaks in v2 (assuming non-deterministic instruction account payload addressing) and then has to use much more expensive offset calculations:

Implementation
#[repr(C, packed)]
/// Tree account data header. Contains pointer to tree root and top of free node stack.
pub struct TreeHeader {
    /// Aboslute pointer to tree root in memory map.
    pub root: *mut TreeNode,
    /// Absolute pointer to stack top in memory map.
    pub top: *mut StackNode,
    /// Absolute pointer to where the next node should be allocated in memory map.
    pub next: *mut TreeNode,
}

#[array_fields]
#[repr(C, packed)]
pub struct TreeNode {
    /// Absolute pointer to parent node in memory map.
    pub parent: *mut TreeNode,
    /// Absolute pointers to child nodes in memory map.
    pub child: [*mut TreeNode; tree::N_CHILDREN],
    pub key: u16,
    pub value: u16,
    pub color: Color,
}

#[repr(C, packed)]
/// Nodes removed from tree are pushed onto stack.
pub struct StackNode {
    pub next: *mut StackNode,
}

Suggested updates per discussion with @febo 2026-02-27:

  1. Densely packed account headers in read-only region at 0x500000000, laid out via instruction account index, without pointer to account region (40 bytes per instruction account)
  2. Account payloads deterministically translated on a per-instruction basis using instruction index, at 0x800000000 plus 0x100000000 times the index of the instruction account, containing:
    1. Owner
    2. Lamports (40 bytes up until this field, per instruction account)
    3. Data
  3. Account payloads are either read-only or writable depending on writable status of account in instruction

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About the data structure you just mentioned, does it span across multiple accounts' payloads, or is it supposed to stay contained within a single account?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LucasSte it is within a single account, but the layout requires a deterministic ordering of accounts at the instruction level. E.g. [user_account, tree_account] for direct pointer addressing. This constraint is met by ABI v1

However in ABI v2 as currently written, instruction accounts are not deterministically laid out. E.g. if txn accounts are [tree_account, user_account], the layout is broken during a CPI to the program in question

@febo is also well aware of the problem and can probably help explain further on internal Anza channels

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In ABIv0/v1 the pointers to instruction accounts (beyond the first) are not stable either because the caller can pick aliasing accounts which shifts everything by the one byte alias marker. Yes, you would probably abort in that case. Just saying we are not guaranteeing address stability.

@febo I don't see any mentions of instruction account aliasing in the suggestion, so I imagine you haven't tackled that problem yet.

Densely packed account headers in read-only region at 0x500000000 ...
deterministically translated on a per-instruction basis ...

This would bring us back to per instruction serialization, which we want to avoid entirely. The cost / complexity doesn't vanish if it is moved to the program runtime, then we still have to charge for it. Conceptually there are two paths:

  1. Do the maximal / worst-case thing in the program runtime and charge everybody for all and everything, even things they don't want / use.
  2. Do only what you need in the program, be charged for what you use. This is IBRL because less compute is wasted, the price reflects the actual resource usage closer and more transactions can be packed in the same time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the metadata of instruction accounts that (gathering them from transaction accounts) would be the same cost to do inside programs as it would be for the program runtime. For the payload it is a different story because the program can't remap that efficiently from the inside.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the caller can pick aliasing accounts

@Lichtso are you referring to the non-dup field from acct serialization? Yes it's easy enough to ensure addressable offsets in ABI v1 by just requiring NON_DUP_MARKER

And as far as the instruction serialization schema, I don't know if it is strictly necessary to re-serialize everything; the existing #### Instruction area section should already work fine except for the fat pointers to the non-deterministic payload area: in this case, what about simply shifting around the ### Accounts area addressing so that account payloads are in same order as instruction area?

This could be a simple offset applied to every store/load for the instruction, not dissimilar from translation already required for VM, and then saves a pointer in the InstructionAccount: less pointer loads as a result, and deterministic layout

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the core tenets behind ABIv2 was to reduce fees to a minimum. One way to achieve this is to share the same data structures between the validator and programs without any need to re-organize them. The validator loads accounts for the transaction and uses the account index in transaction for most operations, hence the idea to unify accounts around such an index.

Converting from index in instruction to index in transaction has a cost to the validator too. (1)

Densely packed account headers in read-only region at 0x500000000, laid out via instruction account index, without pointer to account region (40 bytes per instruction account)

Doing this requires sorting the accounts in a dense area for each top level instruction and twice for each CPI. (2)

what about simply shifting around the ### Accounts area addressing so that account payloads are in same order as instruction area?

This idea means that we cannot maintain the address space constant throughout the transaction. Consequently, we need to re-create it for each top-level instruction and twice for each CPI. (3)

Doing either (1), (2), (3), or any mix between them entails higher base costs and a possible cost per account in both top level and CPI instructions. That may offset any gains you might have from a predictable address space.

The question we should be discussing is whether it is worth adopting a suggestion that helps your use case, and potentially someone else's, while raising CU costs for everybody.

And as far as the instruction serialization schema, I don't know if it is strictly necessary to re-serialize everything; the existing #### Instruction area section should already work fine except for the fat pointers to the non-deterministic payload area: in this case, what about simply shifting around the ### Accounts area addressing so that account payloads are in same order as instruction area?

Another point this might bring confusion is the fact that the index of account in transaction would be used for accessing the account metadata and passed on to CPI, but the access of the account payload would have to use the index of the account in the instruction. I believed a unified index is more straightforward.

It is worth pointing too that the layout in this proposal obviates the syscall GetProcessedSiblingInstruction, since all instructions are provided, together with all the accounts metadata. Reordering accounts by index in instruction would need an effort to rethink this idea.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One idea that we discussed was that everything can stay as it is, but there is an extra mapping per instructions to access account payloads. Instead of having to calculate the address of the payload using the account index, there would be instruction specific addresses.

A simplistic view for this would be to map the payload of accounts to a new 0x990000... region (or any other that is available) and space them out by 10MiB. This way the payloads for instruction accounts are in a deterministic address based on their instruction index. Note that this does not mean to copy the content, just creating a mapping for a VM address that takes you directly to each account payload. Would this be feasible? And if yes, is it costly?

Copy link
Copy Markdown
Contributor Author

@Lichtso Lichtso Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be feasible?

Assuming you mean having two mappings for each account: One in order of the transaction, one in order of the instruction. Yes, it is easy to do in the program runtime, but it causes a different issue inside the program:

In Rust one can only track multiple aliasing references to the same address, but there is no concept of having multiple aliasing memory mappings (views) of the same underlying memory at different addresses. This would thus break borrow checking and pointer provenance rules if a program ever uses both. A way to circumvent this is by having a cfg feature which selects and only exposes one of the two in a SDK.

Edit: Thinking about it some more it wouldn't even work in Rust with the cfg feature, because the instruction account ordering is aliasing in itself. The instruction to transaction mapping does not just reorder but also deduplicate the mappings.

And if yes, is it costly?

After SIMD-0339 it is possible to pass in all transaction accounts in an instruction. Meaning, in the worst case, this would double the program runtime work of adjusting the memory mappings for each instruction.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be feasible?

Assuming you mean having two mappings for each account: One in order of the transaction, one in order of the instruction. Yes, it is easy to do in the program runtime, but it causes a different issue inside the program:

In Rust one can only track multiple aliasing references to the same address, but there is no concept of having multiple aliasing memory mappings (views) of the same underlying memory at different addresses. This would thus break borrow checking and pointer provenance rules if a program ever uses both

For high-perf programs that rely only on pointers, though, this wouldn't be an issue, and as high-perf methods become more dominant, predictable addressing ensures that foundational data structures work as expected without excessive pointer arithmetic

I think this secondary mapping is a useful idea, especially if it can be optimized to only do payloads for example

@Lichtso
Copy link
Copy Markdown
Contributor Author

Lichtso commented Mar 2, 2026

To maintain CU parity with ABI v1, transfer_lamports syscall should cost at most 6CUs

This is only half the story, the other half is the high cost of CPI. That is because every CPI and the deserialization at the end of the instruction have to check every instruction accounts metadata to see if the program changed something and verify if it was allowed to do so.

Having syscalls to update account metadata (size, lamport, owner) allows us to charge on a per use basis instead of having to charge a flat rate for the worst case of changing everything, which basically never happens. And yes, the CU charging is currently all over the place over and under charging. This will be adjusted in ABIv0/v1 to fit the actual cost, after ABIv2 is available and programs have a viable alternative.

Co-authored-by: Alex Kahn <43892045+alnoki@users.noreply.github.com>
Comment on lines +77 to +78
- Total number of instructions in transaction (including CPIs and top level
instructions): `u32`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about this member. Would it be helpful if it were instead a VmSlice<InstructionFrame>?

@Lichtso
Copy link
Copy Markdown
Contributor Author

Lichtso commented Mar 11, 2026

In general the SIMD still needs to define the CU charging for the four syscalls and for the number of instruction accounts.


The runtime must only map the payload for accounts that belong in the current
executing instruction. The payload for accounts belonging to sibling instructions
must NOT be mapped.
Copy link
Copy Markdown
Contributor Author

@Lichtso Lichtso Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be easier to always map in all accounts which are not referenced in an instruction as readonly. That way we wouldn't even have to hide / reveal them on every instruction, thus it is less work for the validator and more available data for the programs.

Also, we already load all sysvar accounts, might as well expose them here too. That would however either rise the maximum transaction account number beyond 255 or require a new range of transaction accounts, but that is harder to pull of because of possible aliasing with sysvars which were mentioned in the message.

LucasSte and others added 3 commits March 27, 2026 16:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.