solana-sbpf-asm
This is a new route distinct from the existing solana-sbpf-linker (Zig) route.
It generates sBPF assembly text (.s) directly from the portable contract IR,
then delegates to the blueshift-gg/sbpf
toolchain for assembly, linking, and packaging into a Solana loader-compatible
ELF.
Route Rationale
Why this route over the Zig/sbpf-linker route
Cost: ProofForge must implement a full lowering backend in Lean that emits sBPF
assembly. This includes register allocation, stack-frame discipline, Solana
account ABI parsing, instruction dispatch, and Borsh-like serialization.
Target Toolchain: blueshift-gg/sbpf
Thesbpf CLI provides everything needed after .s emission:
ProofForge’s role is to produce valid
.s text files that sbpf build
accepts. No further toolchain work is needed in this repo.
SDK Reference Anchors
The Solana SDK completion work tracks these upstream surfaces:- Solana CPI: native programs call other programs through
invoke/invoke_signed, which is the high-level Rust API shape ProofForge lowers tosol_invoke_signed_c. - SPL Token:
TokenInstructiondefines the account schemas and data payloads fortransfer_checked,mint_to,burn,approve,revoke, andset_authority. - Pinocchio: the framework target is a
no_std, zero-copy, no-copy/no-allocation entrypoint style with optional allocator control; ProofForge mirrors that by keeping Solana account parsing, allocator policy, and CPI packing in target lowering rather than portable IR. - pinocchio-tkn: the longer-term token SDK reference is stack-only, zero-allocation CPI helpers spanning SPL Token and Token-2022. ProofForge’s current SPL Token helpers are the first compatible slice of that surface.
Assembler ISA
The sBPF assembly grammar (from the blueshiftsbpf.pest PEG grammar):
Registers: r0–r10 (64 bit), w0–w10 (32‑bit alias for lo‑half).
ALU (64‑bit & 32‑bit):
add64/32, sub64/32, mul64/32, div64/32, or64/32, and64/32,
lsh64/32, rsh64/32, mod64/32, xor64/32, mov64/32, arsh64/32,
neg64/32 — each with immediate and register variants.
Endian byte-swap: le16/32/64, be16/32/64.
Loads/stores:
lddw rD, imm64, ldxb/h/w/dw rD, [rBase ± off],
stb/h/w/dw [rBase ± off], imm, stxb/h/w/xdw [rBase ± off], rS.
Control flow:
ja target, jeq/jne/jgt/jge/jlt/jle/jsgt/jsge/jslt/jsle/jset rA, rB/imm, target,
call <syscall>, callx rA, exit.
Directives: .globl, .equ, .text / .data / .rodata,
.ascii / .byte / .short / .word / .int / .long / .quad.
Identifiers: labels (alphanumeric + underscore), numeric labels (0:, 1: with
0f/0b references).
Available syscalls
Syscall coverage plan
ProofForge treats Solana syscalls as target-extension capabilities, not portable IR primitives. Each syscall family should move through the same evidence ladder: SDK/API shape → capability metadata → sBPF AST helper → assembly smoke →sbpf build → Mollusk/runtime test → Surfpool/Web3.js live
test when the syscall changes observable chain behavior.
Implementation note:
sol_get_epoch_schedule_sysvar returns the runtime struct
layout, not the compact 33-byte sysvar-account serialization. The live
Surfpool/Web3.js gate pins the currently used offsets as slots_per_epoch = 0,
leader_schedule_slot_offset = 8, warmup = 16, first_normal_epoch = 24,
and first_normal_slot = 32.
Implementation note: sol_get_epoch_rewards_sysvar writes the runtime
EpochRewards struct. ProofForge exposes 64-bit state views for every field:
distribution_starting_block_height = 0, num_partitions = 8,
parent_blockhash_word0..3 = 16,24,32,40, total_points_low/high = 48,56,
total_rewards = 64, distributed_rewards = 72, and active = 80.
Runtime allocator
Solana’s Rust SDK entrypoint installs a default heap allocator. The runtime constants areHEAP_START_ADDRESS = 0x300000000 and HEAP_LENGTH = 32 * 1024,
and the allocator is a one-way bump allocator: alloc moves the bump pointer
downward with alignment and dealloc is a no-op. Pinocchio follows the same
shape: entrypoint! expands to the program entrypoint plus
default_allocator! and default_panic_handler!; lower-level macros also let a
program opt out with no_allocator!.
ProofForge mirrors this at the target-extension layer instead of baking it into
portable IR:
bumpAllocator records runtime.allocator with:
noAllocator records kind = "none" and model = "deny-dynamic", matching
the no-heap pattern useful for Pinocchio-style programs that intentionally avoid
dynamic allocation. At this stage the selected allocator is emitted in
manifest.toml, proof-forge-artifact.json, and assembly metadata comments.
Future lowering for heap-backed SDK data structures must route through this
capability before emitting real allocation code.
Solana Contract Model
Solana programs have a single entrypoint:r1) contains a serialized layout:
Instruction Manifest
Solana requires explicit account schemas — a sidecar manifest describing instruction dispatch and account constraints. Below is the proposed TOML format (should live as target metadata, not embedded into the generic Lean source).- Offsets for each account field in the generated
.s(computed by the codegen from the account count and data lengths). - Validation code emitted in the entrypoint adapter (signer, writable, owner checks).
- Instruction dispatch (first‑byte discriminant → handler label).
- Test input generation (the
input.jsonfed tosbpf debugor test runner).
IR Lowering Design
The lowering lives inProofForge/Backend/Solana/SbpfAsm.lean and consumes a
ProofForge.IR.Contract.Module to produce:
- An sBPF assembly text file (
.s) for each contract module. - An instruction manifest (
.toml). - A
proof-forge-artifact.jsonwith artifact metadata.
Module structure
Each Lean contract module compiles to:IR to sBPF lowering walkthrough
For theCounter shared scenario:
IR (IR/Contract.lean level):
StorageBackend = account data at fixed offset):
Register discipline
sBPF has 11 registers (r0–r10). Convention:
For the initial spike, a simple convention suffices:
- r1 = input buffer base (never spilled — needed for all account access).
- r6 = instruction_data base pointer (computed once from r1).
- r7–r9 = scratch for intermediate values.
- Stack (
r10 - N) for spilled locals when registers are exhausted. - Every entrypoint handler is at most ~70 instructions; simple register reuse is acceptable before implementing a proper register allocator.
Expression lowering rules
Each IRExpr node lowers to sBPF instructions that compute the value into a
target register. Example mapping:
All 64‑bit computations should be 32‑bit safe (32‑bit variants for
.u32, no
over-64‑bit intermediate overflows). sol_log_64_ is available for debugging.
Statement lowering rules
Effect lowering: storage
storageScalar / storageArray / storageStructField all map to account data
offsets rather than EVM slot storage. The IR state declarations carry the
owning account index and field offset, computed by the state-layout compiler
pass before codegen.
Storage layout is deterministic and computed at codegen time:
- For each account declared in the manifest, assign the data region start offset.
- For each state variable owned by that account, allocate a fixed offset within the data region, packing fields (u64 aligned).
- Emit
.equconstants for every field offset so the assembly is readable.
Effect lowering: CPI/PDA (Solana-specific SDK extension)
CPI and PDA derivation are Solana‑only concepts (D-027). They do not enter the portable IR. Instead, Solana-specific SDK calls are routed throughProofForge.Solana into target capability calls, gated by the existing
crosscall.cpi and storage.pda capability IDs in Target/Capability.lean:
crosscall.cpi and storage.pda. The generic builder records entrypoint
scope as proof_forge.entrypoint; the Solana backend resolves that metadata
into entrypoint actions and injects helper calls after account validation and
before the portable IR body. The generated assembly preserves r1 around
helper calls so subsequent storage lowering still sees the original Solana
input pointer.
Current CPI/PDA lowering pattern:
- Allocate stack space for
SolInstruction+SolAccountInfo[]+ seeds. - Emit one helper per declared PDA/CPI intent (
sol_pda_derive_<name>,sol_cpi_<name>). - In entrypoint handlers with scoped SDK actions, call the helper and branch
to
error_pda/error_cpiwhenr0 != 0. - Build a module-wide multi-account instruction schema from state, PDA, CPI
accounts, and executable CPI program accounts. This schema is used by
manifest.toml,proof-forge-artifact.json, fixed instruction-data offset computation, and generated signer/writable/program-owner validation. - Build
manifest.tomland artifact metadata with both extension definitions and entrypoint action lists.
solana.cpi.protocol, solana.cpi.data_layout, account metas,
signer seeds, and instruction-data sources into the capability plan, manifest,
and artifact metadata.
The source-facing layer exposes first-class contract_source forms for System
Program transfer, System Program create_account, and SPL Token
transfer_checked plus set_authority. These forms are still a v1 embedded
macro frontend rather than the legacy standalone .learn parser, but they
prevent new examples from dropping back to raw ContractSpec/builder strings
for the core CPI paths.
System and SPL Token helpers now emit the C ABI packing skeleton for
sol_invoke_signed_c: program id bytes, C SolAccountMeta[], standard
instruction-data bytes, C SolInstruction, bound SolAccountInfo[], optional
signer seed tables, and the syscall register contract. system.transfer uses
the bincode-style u32 discriminator=2 + u64 lamports layout;
system.create_account uses u32 discriminator=0 + u64 lamports + u64 space + owner pubkey; SPL Token transfer_checked, mint_to, burn, approve, and
revoke use the standard token instruction tags and amount/decimals layouts;
SPL Token set_authority uses instruction tag 6, authority type 0
(MintTokens), a Some option byte, and a new-authority pubkey copied from
the generated program’s readonly new_authority input account.
Program ids, account meta pubkeys, and SolAccountInfo
key/lamports/data/owner/rent/flag fields are sourced from the generated
multi-account input layout when the account appears in the module schema. CPI
value sources can bind to scalar state offsets, numeric literals, or decoded
entrypoint parameters.
PDA helper metadata now carries both a compatibility seeds list and
target-facing typed seed descriptors. Bare strings remain literal seed bytes for
backward compatibility; SDK helpers such as literalSeed, utf8Seed,
accountSeed, bumpSeed, and paramSeed make the source explicit for Solana
lowering. The Solana target extension consumes those descriptors, appends the
declared bump? as an effective bump seed, and emits typed_seeds in
manifest.toml plus typedSeeds in proof-forge-artifact.json. This remains
a target-extension concern: portable IR and the chain-neutral SDK surface only
see capability intent, while --target solana-sbpf-asm decides how those
capabilities are packed into the Solana syscall ABI.
The current instruction-data ABI reserves byte 0 for the ProofForge entrypoint
tag. Packed scalar parameters start at instruction_data+1, in entrypoint
parameter order, with little-endian U64/U32 loads and one-byte Bool loads.
The generated dispatcher rejects empty instruction data before reading the tag;
each handler also checks the minimum payload length required by its parameter
schema before decoding. The backend decodes those parameters into stack locals
before SDK helper calls and exposes the same absolute input offsets to CPI value
binding, so helpers can pack fields such as SPL Token amount directly from
user instruction data. manifest.toml and proof-forge-artifact.json record
each instruction’s min_data_len/minDataLen plus parameter name, type, offset,
byte size, and encoding. The module-wide helper table only binds a parameter
name when all occurrences share the same offset; duplicate names at conflicting
offsets are intentionally left unbound until per-entrypoint helper
specialization lands.
Remaining work: add dynamic per-entrypoint account parsing, richer
aggregate/string/bytes instruction ABI decoding, return-data decoding, and
runtime tests that exercise live CPI paths.
PDA helper lowering:
- Allocate stack space for seed data + result buffer (32 byte).
- Pack typed seeds into Solana
Slice { ptr, len }entries: literal/UTF-8 seeds are copied into stack buffers, account seeds point at input account pubkeys, bump seeds are one byte, and scalar instruction-data seeds are copied from the decoded fixed input offset. call sol_create_program_address.- Restore the Solana input pointer and, when
account?is declared, compare the 32-byte derived pubkey with the declared account pubkey before returning.
Effect lowering: events
Solana has no chain-level event log like EVM. Options:sol_log_/sol_log_64_— simple but unstructured.sol_log_data— base64 data logs used as the Anchor-style event payload carrier.sol_set_return_dataas a quasi-event mechanism.
eventEmit fields through sol_log_64_ as
[eventTag, fieldIndex, value, 0, 0]. The event tag is a stable 32-bit
compile-time tag derived from the event name so generated Web3.js harnesses can
assert the transaction log without baking in Solana-specific syntax at the
portable SDK layer. Solana-only logAccountPubkey lowers account keys through
sol_log_pubkey, and logStateData lowers fixed state-backed byte payloads
through sol_log_data as the base layer for future Anchor-compatible
discriminator/Borsh event serialization. Future work should add string
sol_log_ payloads, complete Anchor-compatible serialization, and indexed
event forms.
Capability mapping
The target profile must accept or reject each IR capability. The proposedsolana-sbpf-asm profile extends the existing solanaSbpfLinker capability set:
CLI and Build Integration
New CLI flag
--emit-sbpf-asm— emit.swithout invokingsbpf build(development).--emit-sbpf-elfor--solana-elf— emit.sthen invokesbpf build.--solana-sbpf-arch v0|v3— pass the selected sbpf architecture tosbpf build --arch; artifacts record the value undertoolchain.sbpf.arch.
Build pipeline steps
- Lean frontend: Parse contract, resolve LCNF.
- IR extraction: Map LCNF to
ProofForge.IR.Contract.Module. - Capability check: Validate against
solana-sbpf-asmtarget profile. - Storage layout: Compute account data offsets per manifest, assign
.equconstants. - Codegen (
ProofForge.Backend.Solana.SbpfAsm):- Emit instruction dispatch adapter (labeled handlers).
- For each entrypoint: lower body statements + expressions → sBPF text.
- Emit
.rodatafor string constants, event type tags.
- Write
.s: Producesrc/<module>/<module>.s. - Write
manifest.toml: Record instruction metadata, account offsets. sbpf build: Invoke external tool with the selected sbpf architecture, producedeploy/<module>.so.- Artifact metadata: Write
proof-forge-artifact.jsonrecordingirVersion, target id, tool versions, capability subset.
Artifact metadata
State Layout: From IR StateDecl to sBPF .equs
The storage layout compiler (ProofForge.Backend.Solana.StateLayout) takes the
account manifest + IR state declarations and computes fixed offsets for every
state variable per account.
Example state declarations:
account_index maps "counter" → 1 (the Counter account) and the Counter
account data region starts at byte 0 within that account’s data buffer:
Each
.equ constant is added to the per-account data base (COUNTER_DATA in
the dispatch adapter), producing the final memory reference:
[r1 + COUNTER_DATA + COUNTER_COUNT_OFFSET].
Toolchain Dependency
CI should make Solana tests optional (gated on
sbpf, Surfpool, Solana CLI,
and Node tooling) following the same pattern as others (solc, foundry,
dargo per validation-gates.md).
Test Strategy
Spike 1: Static entrypoint
- Generated
entrypointreturns success (r0 = 0). - No account parsing, no storage.
sbpf buildsucceeds,sbpf debugshows the entrypoint executes.- Validate the
.sround‑trips viasbpf disassemble.
Spike 2: Counter (single scalar u64)
- Account manifest with one writable account.
- Dispatch adapter: parse accounts, validate signer, dispatch on instruction tag.
initialize: writeu64(0)to account data at fixed offset.increment: read, add 1, write.sbpf testwith Mollusk.- Surfpool/Web3.js live deploy/invoke smoke.
Spike 3: Multiple instruction types, typed returns
- Add return data (
sol_set_return_data). - Multi‑instruction dispatch.
- Validation gate against the shared Counter scenario.
Spike 4: CPI (System Program)
- Account creation via System Program CPI with signer seeds.
- PDA derivation and validation.
sol_invoke_signed_ccall pattern.
Spike 5: Borsh and structured types
- Struct state with multiple fields.
- Borsh serialization/deserialization primitives (hand‑written sBPF or generated).
- Map storage via sorted entries or sparse buckets.
Spike 6: SPL Token CPI
- Token account create, mint, transfer.
- Associated Token Program integration.
Acceptance criteria
Lean Module Layout
Input/Abstraction: The Lean SDK Layer
Above the sBPF codegen, developers should get convenient Lean abstractions. Proposed API (Phase 3):Registration in Target Profile
New target profile inProofForge/Target/Registry.lean:
Risks and Mitigations
Phased Implementation Plan
Phase 0: Toolchain integration (Spike 1)
- Add
solana-sbpf-asmtoTarget/Registry.lean. - Write a fixed sBPF entrypoint
.sthat returns success. - Run
sbpf build+sbpf debuground‑trip. - CLI flag
--emit-sbpf-asmthat writes the canned.s.
Phase 1: Counter (Spike 2–3)
StateLayout.lean: account offsets from manifest.SbpfAsm.lean: lowering for Module →.stext.- Support:
literal,local,add,effect(storageScalarWrite/Read),letBind,assign,ifElse,return,assert. - Counter scenario passes
sbpf test.
Phase 2: Storage mid‑level (Spike 4-5)
- Storage arrays, structs, and maps.
- CPI (Account creation, SPL Token transfers).
- PDA derivation.
- Bounded loops.
- Instruction manifest TOML generation.
Phase 3: Developer SDK
Phase 3 is split into verifiable SDK completeness levels rather than one large “framework” milestone. Estimates assume one engineer working from the 2026-07-02 baseline, current direct-assembly codegen staying stable, and localsbpf/Surfpool/Solana CLI tooling being available.
The alpha line is the point where a developer should be able to write and
deploy simple Solana programs without hand-written assembly patches. The beta
line is the point where ProofForge output can be compared against reference
Rust/Pinocchio programs for the same account schema. The System transfer,
System
create_account, SPL Token transfer_checked, and SPL Token
mint_to/burn/approve/revoke plus SPL Token set_authority reference
contracts are the first static equivalence anchors for that line, and their
live dual-deploy harnesses are already wired to build/deploy both ELFs when
Solana rustc is available. The
final framework line adds the higher-level
ergonomics expected from Anchor-like and Pinocchio-style workflows without
moving Solana-specific details into portable IR.
References
- sbpf toolchain — assembler, linker, runtime, debugger.
- sbpf-asm-counter — example assembly‑level Counter.
- Solana native program examples (QuickNode) — reference SDK patterns.
- Existing solana-sbf.md — Zig/sbpf-linker route background.
- RFC 0002 — target families and build pipeline design.
- Portable IR doc — IR specification.