Skip to content
Open
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e3e3e19
First draft
Lichtso Oct 1, 2024
39486ca
Changes to account metadata updating syscalls.
Lichtso May 30, 2025
75cb2fd
Removes the magic header and renames the scratchpad back to return-data.
Lichtso Jun 11, 2025
d6d9221
Redesign memory regions
LucasSte Jun 13, 2025
6fdcac4
Use index and update CPI steps
LucasSte Jun 17, 2025
18c05b1
Updates "Motivation" section.
Lichtso Jun 20, 2025
8c9336e
Add CPI nesting level and index of parent instruction
LucasSte Jun 20, 2025
0b21855
Remove unnecessary syscall
LucasSte Jun 25, 2025
aa600a7
Use u8 instead of u16 bitfield
LucasSte Jul 3, 2025
5053e9a
Split account metadata and update addresses
LucasSte Jul 3, 2025
4d65a18
Update parameters to sol_invoke_signed_v2
LucasSte Sep 8, 2025
fab4724
Use u32 indexes for alignment
LucasSte Dec 4, 2025
0aee740
Add number of CPIs
LucasSte Dec 4, 2025
b849c2d
Update instruction accounts area
LucasSte Dec 4, 2025
c022960
Nagisa's suggestions
LucasSte Jan 6, 2026
c3303db
Update instruction area with u16s
LucasSte Jan 7, 2026
a94b38c
Update proposals/0177-program-runtime-abiv2.md
LucasSte Feb 2, 2026
2b64135
Update variable names and their purpose
LucasSte Feb 26, 2026
ccec371
Update proposals/0177-program-runtime-abiv2.md
LucasSte Mar 3, 2026
47cb8f3
Update proposals/0177-program-runtime-abiv2.md
LucasSte Mar 27, 2026
ebe6e36
Include recent suggestions
LucasSte Mar 27, 2026
47f1435
Add sysvar accounts area
LucasSte Mar 27, 2026
da90cd3
Update proposals/0177-program-runtime-abiv2.md
LucasSte Apr 22, 2026
53f2fd5
@febo's suggestions
LucasSte Apr 22, 2026
e5d8bf6
Update CPI design
LucasSte Apr 22, 2026
d5d1f7e
Use stable layout for CPI types
LucasSte Apr 22, 2026
f73bab9
Update proposals/0177-program-runtime-abiv2.md
LucasSte May 1, 2026
4a84d69
Add information about payer
LucasSte May 8, 2026
91ab453
Update InstructionAccount and CPI
LucasSte May 8, 2026
a510cdd
Clarify syscall parameters
LucasSte May 19, 2026
050f8e7
Remove pointer field
LucasSte May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
364 changes: 364 additions & 0 deletions proposals/0177-program-runtime-abiv2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,364 @@
---
simd: '0177'
title: Program Runtime ABI v2
authors:
- Alexander Meißner
- Lucas Stuernagel
category: Standard
type: Core
status: Idea
created: 2025-02-23
feature: TBD
extends: SIMD-0219
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 extends: SIMD-0219 references a non-existent proposal

SIMD-0219 does not exist in this repository. The Motivation section states "Direct mapping of the account payload data is enabled by SIMD-0219," making SIMD-0219 a hard prerequisite for this design. Referencing a proposal that hasn't been filed creates an incomplete dependency chain — reviewers have no way to evaluate whether the foundation this SIMD builds on is sound or accepted.

---

## Summary

Align the layout of the virtual address space to large pages in order to
simplify the address translation logic and allow for easy direct mapping.

## Motivation

Direct mapping of the account payload data is enabled by SIMD-0219.
However, there remains a big optimization potential for both programs and the
program runtime:

- Instruction data could be mapped directly as well
- Return data could be mapped directly too
- Account payload could be resized freely (no more 10 KiB growth limit)
- CPI could become cheaper in terms of CU consumption
- Most structures could be shared between programs and program runtime,
requiring only a single serialization at the beginning of a transaction and
only small adjustments after
- Per instruction serialization before a program runs could be removed entriely
- Per instruction deserialization after a program runs could be removed too
- Deserialization inside the dApp could be reduced to a minimum
- programs would only have to pay for what they use, not having to deserialize
all instruction accounts which were passed in
- Scanning sibling instructions would not require a syscall
- Memory regions (and thus address translation) which SIMD-0219 made unaligned
could be aligned (to 4 GiB) again

All of these however do necessitate a major change in the layout how the
program runtime and programs interface (ABI).

## Alternatives Considered

None.

## New Terminology

None.

## Detailed Design

Programs signal that they expect ABIv2 through their SBPF version field being
v4 or above.

### Memory Regions

#### Transaction metadata area

At the beginning of a transaction the program runtime must prepare a
readonly memory region starting at `0x400000000`. This region is shared by all
instructions running programs with support to new ABI. It must be updated as
as instructions through out the transaction modify the CPI scratchpad or the
return data. The contents of this memory region are the following:

- Key of the program which wrote to the return-data scratchpad most
recently: `[u8; 32]`
- The return-data scratchpad: `&[u8]`, which is composed of:
- Pointer to return-data scratchpad: `u64`
- Length of return-data scratchpad: `u64`
- The CPI scratchpad: `&[u8]`, which consists of:
- Pointer to CPI scratchpad: `u64`
- Length of CPI scratchpad: `u64`
- The CPI accounts scratchpad: `&[InstructionAccount]`, which consists of:
- Pointer to slice: `u64`
- Number of elements in slice: `u64`
- Index of current executing instruction: `u32`
- Total number of instructions in transaction (including CPIs and top level
instructions): `u32`
Comment on lines +80 to +81
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>?

- Number of CPIs in trace (under execution and finished): `u32`
- The number of transaction accounts: `u32`

##### Payer information

As accounts in this area sorted by their index in transaction, the payer
account must always be the account at index zero, as we surface runtime's
internal account ordering for programs.

#### Account metadata area

This region starts at `0x500000000`, is readonly and holds the metadata for
all accounts in the transaction. It is shared by all instructions running
Comment thread
LucasSte marked this conversation as resolved.
programs with support for ABIv2, and must be updated as instruction modify the
metadata with the provided syscalls (see the `Changes to syscalls` section).
The contents for this region are as follow:

- For each transaction account:
- Key: `[u8; 32]`
- Owner: `[u8; 32]`
- Lamports: `u64`
Comment on lines +99 to +102
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 )

- Account payload: `&[u8]` which is composed of:
- Pointer to account payload: `u64`
- Account payload length: `u64`
Comment on lines +100 to +105
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 +104 to +105
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.

If we are not mapping the payload for all accounts in the transaction, could we move the payload pointer and length to the instruction area? It would be a little inconvenient to offer developers a slice that causes an access violation when they try to access it, in the case of an account without its payload mapped to the virtual machine.

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.

That would then require keeping the payload length field in sync in CPI call and return. That is what I would like to avoid.


#### Instruction area

For each transaction, the program runtime must also prepare two memory regions.
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:
Comment thread
LucasSte marked this conversation as resolved.
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

- Reserved filed for alignment and potential future usage: `u16`
- Index in transaction of program account to be executed: `u16`
- CPI nesting level: `u16`
- Index of parent instruction (`u16::MAX` for top-level instructions): `u16`
- Reference to a slice of instruction accounts `&[InstructionAccount]`,
consisting of:
- Pointer to slice: `u64`
- Number of elements in slice: `u64`
- Instruction data `&[u8]`, which is composed of:
- Pointer to data: `u64`
- Length of data: `u64`
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.

It might be useful for programs to also have the instruction account deduplication map available to them here.

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 initially devised the deduplication map (i.e. a map from index_in_transaction to index_in_instruction) to be runtime only, since that was the only place we cared about account deduplication. Today the SDK adds duplicate accounts to the AccountInfo array, so I'm not sure programs distinguish them.

Maybe @febo can have a broader opinion.

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.

If I understood correctly, the current behaviour will still exist. More specifically, when it says:

Reference to a slice of instruction accounts &[InstructionAccount]

This slice will contain all instruction accounts – e.g., if my instruction expects 2 accounts and both are set to be the same, the program will still received a slice of length 2. In other words, each InstructionAccount will reference (index) the same TransactionAccount.

If that is the case, then access to the deduplication map is not necessary since the slice of accounts would indirectly represent this map right?

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.

Yes it is true that the deduplication map is kinda redundant, however it takes time to calculate and we memorize it anyway, so we might as well give it to programs too.

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.

Agree with this, if it is something that could be available for "free" to programs, we should include it. There might be an use case where you want to pin-point whether an account is duplicated or not without having to do multiple comparisons.


Let `InstructionAccount` contain the following fields:
Comment thread
LucasSte marked this conversation as resolved.

- Index to transaction account: `u16`
- Signer flag: `u8` (1 for signer, 0 for non-signer)
- Writable flag: `u8` (1 for writable, 0 for readonly)

#### Return data scratchpad

A writable memory region starting at `0x700000000` must be mapped in for the
return-data scratchpad.

#### Accounts area

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
Comment on lines +139 to +141
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

instruction account has the writable flag set and is owned by the current
program it is mapped in as a writable region. The writability of a region must
be updated as programs through out the transaction modify the account metadata.

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.


#### Instruction payload area

For each instruction, the runtime must map its payload at address
`0x10800000000` plus `0x100000000` times the index of the instruction in the
transaction. All instruction payload mappings are readonly.

One extra writable mapping must be created after the last instruction payload
area to be the CPI scratch pad, i.e. at address `0x10800000000` plus
`0x100000000` times the number of instructions in the transaction. Its purpose
is for programs to write CPI instruction data directly to it and avoid copies.

#### Instruction accounts area

For each instruction, the runtime must map an array of `InstructionAccount`
(as previously defined) at address `0x14800000000` plus `0x100000000` times
the index of the instruction in the transaction. This mapped are is readonly.

Each of these memory regions contain the following for each instruction:

- For each account in instruction:
- `InstructionAccount`, consisting of:
- Index to transaction account: `u16`
- Signer flag: `u8` (1 for signer, 0 for non-singer)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Typo: "non-singer" should be "non-signer", inconsistent with the correct spelling used earlier in the document.

Suggested change
- Signer flag: `u8` (1 for signer, 0 for non-singer)
- Signer flag: `u8` (1 for signer, 0 for non-signer)

- Writable flag: `u8` (1 for writable, 0 for readonly)

One extra writable mapping must be created after the last instruction accounts
area to be the CPI scratch pad, i.e. at address `0x14800000000` plus
`0x100000000` times the number of instructions in the transaction. Its purpose
is for programs to write CPI accounts directly to it and avoid copies.
Comment on lines +150 to +178
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Implicit 63-instruction total cap is undocumented

The instruction payload area starts at 0x10800000000 and the instruction accounts area starts at 0x14800000000, a difference of 0x4000000000 = 64 × 0x100000000. This means the layout physically supports at most 63 instruction payload slots plus 1 CPI scratchpad before the two regions would overlap. The same 64-slot constraint applies symmetrically to the instruction accounts area (capped by the sysvar area at 0x18800000000). The spec tracks "Total number of instructions in transaction (including CPIs and top level instructions)" but never states this hard cap. With Solana's existing CPI nesting depth of 4 and many top-level instructions, worst-case totals can exceed 64 entries.


#### Sysvar accounts area

For each existing (non deprecated) sysvar account, the runtime must map its
payload at address `0x18800000000` plus `0x100000000` times the index of the
sysvar in the following order:

0. Clock
1. Epoch rewards
2. Epoch Schdule
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Typo: "Schdule" should be "Schedule".

Suggested change
2. Epoch Schdule
2. Epoch Schedule

3. Last restart slot
4. Rent
5. Slot hashes
6. Stake history

### VM initialization

During the initilization of the virtual machine, the runtime must load the
Copy link
Copy Markdown
Contributor

@febo febo Apr 22, 2026

Choose a reason for hiding this comment

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

Couple of ideas for this:

  1. We could also add the pointer to the instruction data (R4) and length of it (R5).
  2. I wonder if having only R1 is enough – the other pointers and lengths can be computed with a static offset from R1, so we can "save" those register to other use.

following values to registers:

1. Register R1: A pointer to the metadata of the instruction under execution.
(see section [Instruction area](#instruction-area)).
2. Register R2: A pointer to the instruction accounts slice for the
instruction under execution (see section
[Instruction accounts area](#instruction-accounts-area)).
3. Register R3: The number of instruction accounts for the instruction under
execution.
4. Register R4: A pointer to the instruction payload of the instruction under
execution (see section [Instruction payload area](#instruction-payload-area)).
5. Register R5: The payload lenght for the instruction under execution.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Typo: "lenght" should be "length".

Suggested change
5. Register R5: The payload lenght for the instruction under execution.
5. Register R5: The payload length for the instruction under execution.



### Changes to syscalls

Changes to the account metadata must now be communicated with specific
syscalls, as detailed below:

- `sol_assign_owner(u64, *const [u8; 32])`.
- `u64`: Index in transaction of the account whose owner is changing,
- `*const [u8; 32]`: Pointer to the public key of the new owner.
- `sol_transfer_lamports(u64, u64, u64)`:
- `u64`: Index in transaction of the destination account.
- `u64`: Index in transaction of the source account.
- `u64`: Lamports amount.

Changes to the account payload length and all the scratchpads sections
introduced in this SIMD (the return-data scratchpad and the CPI scratchpad)
must be communicated via a new sycall `set_buffer_length(u64, u64)`, with the
following parameters:

- `u64`: Base address of region to be resized.
- `u64`: New length of region.

The syscall must check if the address matches the base address of either a
writable account payload mapping or one of the scratchpad mappings and return
an error otherwise. Constrains for the maximum resizable limits must also be
verified for each region separetely.

The `set_buffer_length` must charge a base cost (to be determined) plus the
same CU per byte ratio as the `memset` syscall.
Comment on lines +237 to +238
Copy link
Copy Markdown

@nagisa nagisa May 11, 2026

Choose a reason for hiding this comment

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

While implementing this I found the specification to be insufficient here. The obvious interpretation would be to charge the per-byte fee for the additional bytes that are being allocated (and not at all if the buffer size is being reduced) as those are the only bytes that need memsetting to 0.

At the same time, any increase in buffer size may require a realloc and memcpy from previous buffer to the new one. So charging per_byte * new_length any time the buffer size would increase (new_length > current_length) would seem like a more correct option. I think we can get away with a single fee here, as the cost of memcpy and memset is in roughly the same ballpark.

Question is: are we okay with charging a significant fee when the buffer is being resized? This fee can be especially painful to resizes where the base buffer is large and is only increased in size by a little bit every time, but it would correctly reflect the computation load.

EDIT: Alternative to charging for reallocations would be to maintain a pre-allocated pool of buffers such that each region already gets a buffer that's at least as large as needed to contain the largest region requestable. I don't think that's feasible, though, especially not an outlook of having buffers larger than 10MiB.

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.

So charging per_byte * new_length any time the buffer size would increase (new_length > current_length) would seem like a more correct option. I think we can get away with a single fee here, as the cost of memcpy and memset is in roughly the same ballpark.

I believe charging for the entire new size during account growths is the right solution for now. If we can come up with another implementation on the validator side that allows us to decrease costs, we can do so later.

Decreasing costs is always easier than increasing them.


The verifier must reject SBPFv4 programs containing the `sol_invoke_signed_c`
and `sol_invoke_signed_rust`, since they are not compatible with ABIv2. A new
syscall `sol_invoke_signed_v2` must replace them. The parameters for
`sol_invoke_signed_v2` are the following:

- Index in transaction of program ID to be called: `u64`.
Copy link
Copy Markdown
Contributor

@febo febo Jun 19, 2025

Choose a reason for hiding this comment

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

Could this be a reference to the program id, as it is now? Otherwise it seems that a program will need to lookup the index for the callee program. Or is this index easily accessible?

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 will have access to an array containing all accounts in the transaction. They will be aware of the index of the accounts the instruction is referring to. Having said that, the index is easily accessible and easier to manage.

- A pointer to the singer seeds of type `VmSlice<VmSlice<VmSlice<u8>>>`.
- The length of the outer signer seeds slice in
`VmSlice<VmSlice<VmSlice<u8>>>`.

`VmSlice<T>` is a stable layout type defined to share slices between the guest
and the host. It consists of:

- `u64`: Pointer to the data.
- `u64`: Length of data (number of elements `T`)

Programs using `sol_get_return_data` and `sol_set_return_data` must be
rejected by the verfier if ABI v2 is in use.

### Scratchpad management

This SIMD introduces two scratch pad regions: the return-data scratchpad and
the CPI scratchpad. At the beginning of every instruction, these scratchpads
must be empty and their size must be zero.

Programs must set the desired length for them using the `set_buffer_length`
Copy link
Copy Markdown
Contributor

@febo febo Apr 22, 2026

Choose a reason for hiding this comment

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

Since it is possible to statically determine the maximum length for the return-data scratchpad, one alternative is to always have MAX_RETURN_DATA bytes allocated. Programs then write as many bytes as they need, prefixing with the length if needed, without having to set the length. This might save a syscall call on the program side.

syscall. Reads and writes to a region beyond the scratchpad length must
trigger an access violation error.

The management for the writable accounts payload must work similarly, except
that they must not be initialized empty, but instead with the pre-existing
data it holds.

### CPIs

#### Program side

The workflow for cross program invokation on the program side will change.
Instead of programs themselves allocating memory on the heap or the stack for
CPI instruction data and CPI accounts, the program runtime must already
provide the pointers for programs to write to.

The CPI data scratchpad is a region in the
[Instruction payload area](#instruction-payload-area), right after the space
reserved for the last intruction in the instruction trace.
In other words, `0x10800000000` plus `0x100000000` times the number of
instructions in the transaction.

Likewise, the CPI accounts must be written to a runtime provided region
residing in the [Instruction accounts area](#instruction-accounts-area)
at `0x14800000000` plus `0x100000000` times the number of instructions in the
transaction.

Both the aforementioned regions must begin with size zero, and programs must
resize them before writing to them using the `set_buffer_length` syscall.

#### Runtime side

With the new `sol_invoke_signed_v2` syscall, CPIs must be managed
differently. At each CPI call, the runtime must perform the following actions:

1. Verify that all account indexes received in the `InstructionAccount` area
belong in the current executing instruction. Likewise, the prgram ID index
that should be called must also undergo the same verification.
2. Verify that accounts have the correct signer and writer flags set, avoiding
privelege promotion.
3. Append a new instruction at the end of the serialization array kept at
`0x600000000`.
4. Transform the caller CPI scratchpad into a readonly instruction payload
region visible for the callee.
5. Change the visibility and write permissions for the account payload
regions, according to the CPI accounts and their flags.
6. Update the address for the callee CPI scratchpad, the index of current
executing transaction, and the number of instructions in transaction at
address `0x400000000`.
Comment on lines +312 to +314
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Typo: "executing transaction" should be "executing instruction" — the transaction metadata area stores the "Index of current executing instruction" (line 79), not a transaction index.

Suggested change
6. Update the address for the callee CPI scratchpad, the index of current
executing transaction, and the number of instructions in transaction at
address `0x400000000`.
6. Update the address for the callee CPI scratchpad, the index of current
executing instruction, and the number of instructions in transaction at
address `0x400000000`.


When the CPI returns, the runtime must do the following:

1. Update the address for the CPI scratchpad, and keep the previouly used one
in its exsiting address assigned during CPI call. The new CPI scratchpad
address is the same as the previous one plus `0x100000000`.
2. Change the read and write permission for the account payload regions,
Comment on lines +316 to +321
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 CPI-return scratchpad advance logic is ambiguous

Step 1 states: "The new CPI scratchpad address is the same as the previous one plus 0x100000000." It is unclear what "the previous one" refers to — the caller's pre-call scratchpad address or the callee's scratchpad address. With sequential CPIs, the scratchpad address keeps advancing, consuming slots from the instruction payload area. Combined with the undocumented total-instruction cap, a program making many sequential CPIs will silently exhaust the available slots. The spec should define whether this advancement is intentional and what happens when the caller's scratchpad address reaches the boundary of the instruction accounts area.

according to potential changes in account ownership.
3. Update the index of current executing instruction.

CPIs between ABIv0/v1 and ABIv2 program must be allowed, but costs will difer.
A CPI from an ABIv2 to another ABIv2 program must cost less than a CPI from an
ABIv2 to an ABIv0/v1 program, due to the decreased work overhead from program
runtime.

### Changes to CU metering

CPI will no longer charge CUs for the length of account payloads. Instead TBD
CUs will be charged for every instruction account. Also TBD CUs will be charged
for the three new account metadata updating syscalls. TBD will be charged for
resizing a scratchpad.

### Lazy deserialization on the dApp side (inside the SDK)

With this design a program SDK can (but no longer needs to) eagerly deserialize
all account metadata at the entrypoint. Because this layout is strictly aligned
and uses proper arrays, it is possible to directly calculate the offset of a
single accounts metadata with only one indirect lookup and no need to scan all
preceeding metadata. This allows a program SDK to offer a lazy interface which
only interacts with the account metadata fields which are needed, only of the
accounts which are of interest and only when necessary.

## Impact

This change is expected to drastically reduce the CU costs as the cost will no
longer depend on the length of the instruction account payloads or instruction
data.

From the dApp devs perspective almost all changes are hidden inside the SDK.
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 am not sure we can hide the need for sol_set_account_lamports in the SDK. It might also be the case that CUs to update lamports will increase.


## Security Considerations

What security implications/considerations come with implementing this feature?
Are there any implementation-specific guidance or pitfalls?
Comment on lines +355 to +358
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Security Considerations section is unfilled

The Security Considerations section retains only the boilerplate template questions and contains no actual content. For a proposal of this scale — a new VM ABI, shared memory regions across instructions, direct account payload mapping, and new privilege-elevation checks for CPIs — leaving this section empty blocks any meaningful security review. At a minimum it should address: (1) how privilege escalation via sol_invoke_signed_v2 is prevented when the program writing to the CPI scratchpad is not the current executor, (2) what prevents a program from crafting an InstructionAccount entry that references an account index outside its instruction scope, and (3) memory-safety implications of the set_buffer_length syscall if the new length exceeds the backing allocation.


## Drawbacks

This will require parallel code paths for serialization, deserialization, CPI
call edges and CPI return edges. All of these will coexist with the exisiting
ABI v0 and v1 for the forseeable future, until we decide to deprecate them.
Loading