SIMD-0177: Program Runtime ABI v2#177
SIMD-0177: Program Runtime ABI v2#177Lichtso wants to merge 22 commits intosolana-foundation:mainfrom
Conversation
ae4514d to
7148fdf
Compare
| - 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 |
There was a problem hiding this comment.
does this get affected by this other SIMD?
https://github.com/solana-foundation/solana-improvement-documents/pull/163/files
There was a problem hiding this comment.
They are independent. SIMD-0163 is about the program being called, that is not affected in this SIMD.
There was a problem hiding this comment.
@nickfrosty Fwiw, this means the realloc limit is unchanged as well.
There was a problem hiding this comment.
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.
7148fdf to
375deef
Compare
375deef to
1057208
Compare
1057208 to
51f9f75
Compare
caa91dd to
6b641c8
Compare
| - Magic: `u32`: `0x76494241` ("ABIv" encoded in ASCII) | ||
| - ABI version `u32`: `0x00000002` | ||
| - Pointer to instruction data: `u64` | ||
| - Length of instruction data: `u32` |
There was a problem hiding this comment.
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.
0d9976c to
9084d79
Compare
9084d79 to
e3e3e19
Compare
| - Key: `[u8; 32]` | ||
| - Owner: `[u8; 32]` | ||
| - Lamports: `u64` | ||
| - Account payload: `&[u8]` which is composed of: | ||
| - Pointer to account payload: `u64` | ||
| - Account payload length: `u64` |
There was a problem hiding this comment.
Programs also have access to the booleans writable, signer and executable. Are we serializing these ones as well?
There was a problem hiding this comment.
These are per instruction not per transaction. See the "flags bitfield" in "Per Instruction Serialization".
| - For each transaction account: | ||
| - Key: `[u8; 32]` | ||
| - Owner: `[u8; 32]` | ||
| - Lamports: `u64` |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
- Are CPIs added to the middle or end of the array?
- Is the array updated in the middle of an instruction or only when entering an instruction?
- Is the length in
TransactionMetadatathe number of top level instructions or top level instructions + executed cpis? (the former would be useful to add if the later is true)
There was a problem hiding this comment.
- To the end.
- 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. - Thanks for bringing that up. I'll specify what the number refers to and add the number of CPI instructions in the array.
There was a problem hiding this comment.
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
| area to be the CPI scratch pad, i.e. at address `0x10900000000` plus | ||
| `0x100000000` times the number of instructions in the transaction. Its purpose |
There was a problem hiding this comment.
Can this be a static location? Removes a math operation to get the cpi scratchpad ptr
|
I've been doing a bit of experimenting with how a program api would work: https://github.com/Buzzec/solana_abi_v2 |
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]` |
There was a problem hiding this comment.
@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| 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 |
There was a problem hiding this comment.
@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:
- 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) - Account payloads deterministically translated on a per-instruction basis using instruction index, at
0x800000000plus0x100000000times the index of the instruction account, containing:- Owner
- Lamports (40 bytes up until this field, per instruction account)
- Data
- Account payloads are either read-only or writable depending on writable status of account in instruction
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
@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
There was a problem hiding this comment.
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:
- Do the maximal / worst-case thing in the program runtime and charge everybody for all and everything, even things they don't want / use.
- 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
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>
| - Total number of instructions in transaction (including CPIs and top level | ||
| instructions): `u32` |
There was a problem hiding this comment.
I was thinking about this member. Would it be helpful if it were instead a VmSlice<InstructionFrame>?
|
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. |
There was a problem hiding this comment.
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.
Co-authored-by: Alex Kahn <43892045+alnoki@users.noreply.github.com>
No description provided.