Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions docs/BACKEND_INTERFACE.md
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,21 @@ Runtime traps, error codes and check order are a stable contract:
compile-time diagnostics above are a separate matter. They come only from
`Check`, so they are identical for every backend.

<a id="runtime-word-access"></a>
## Runtime word access

SBF loads and stores at any alignment, so on SBF builds (`__bpf__`) the C
runtime reads and writes whole words where it used to loop over bytes:
`sol_read64`/`sol_write64` (and 32-bit forms) are a fixed-size
`__builtin_memcpy`, which clang lowers to one `ldxdw`/`stxdw`, and 32-byte key
comparisons (`IsTokenProgram`, `IsPDA`, the owner check behind `ResizeAccount` and
`CloseAccount`, `CloseAccount`'s same-key check) fold four
word XORs. `CreateAddress` passes the caller's output straight to
`sol_create_program_address`, which reads every seed before it writes and
writes only on success, so there is no staging copy. Results, error codes and
check order are unchanged; host C keeps the byte loops.
[Measurements](../results/compiler/2026-10-19-runtime-word-access/README.md).

## Equivalence evidence

[`internal/compiler/check_corpus_test.go`](../internal/compiler/check_corpus_test.go)
Expand Down
2 changes: 1 addition & 1 deletion internal/compiler/accounts.c.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ u64 entrypoint(u8 *input) {
#ifdef GOSVM_SDK2
// Mirror the pinned SDK: original length occupies the padding
// immediately before the key and survives resize/CPI changes.
for(u64 j=0;j<4;j++) p[4+j]=(u8)(a->data_len>>(j*8));
sol_write32(p+4,(u32)a->data_len);
#endif
p=a->data+a->data_len+10240;
p=(u8 *)(((u64)p+7)&~7ULL);
Expand Down
30 changes: 29 additions & 1 deletion internal/compiler/check_corpus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,34 @@ func withoutMemorySyscalls(c []byte) []byte {
return []byte(strings.Join(out, ""))
}

// runtimeWordEdits are the runtime template changes of
// docs/BACKEND_INTERFACE.md#runtime-word-access as {new, old} text.
// withoutRuntimeWords undoes them, so the corpus proves they are the only
// change to the emitted C.
var runtimeWordEdits = [][2]string{
{" sol_write32(p+4,(u32)a->data_len);\n", " for(u64 j=0;j<4;j++) p[4+j]=(u8)(a->data_len>>(j*8));\n"},
{" sol_write64(output.ptr,rent.rate);sol_write64(output.ptr+8,rent.threshold_bits);\n", " for(u64 j=0;j<8;j++) {output.ptr[j]=(u8)(rent.rate>>(j*8));output.ptr[8+j]=(u8)(rent.threshold_bits>>(j*8));}\n"},
{" return sol_eq32(a->owner,c->program.ptr) ? 0 : 2012;\n", " for(u64 j=0;j<32;j++) if(a->owner[j]!=c->program.ptr[j]) return 2012;\n return 0;\n"},
{" return sol_read32(a->key-4);\n", " u64 n=0;for(u64 j=0;j<4;j++) n|=(u64)(a->key-4)[j]<<(j*8);return n;\n"},
{" sol_write64(a->data-8,n);\n", " for(u64 j=0;j<8;j++) (a->data-8)[j]=(u8)(n>>(j*8));\n"},
{" if(sol_eq32(a->key,b->key)) return 2013;\n", " boolean same=1;for(u64 j=0;j<32;j++) if(a->key[j]!=b->key[j]) same=0;\n if(same) return 2013;\n"},
{" sol_set_data_len(c,a,0);for(u64 j=0;j<32;j+=8) sol_write64(a->owner+j,0);\n", " sol_set_data_len(c,a,0);for(u64 j=0;j<32;j++) a->owner[j]=0;\n"},
{" // The syscall reads every seed before it writes, and writes the address\n // only on success, so output needs no staging copy.\n return sol_create_pda(seeds,count,program.ptr,output.ptr);\n", " u8 address[32];u64 code=sol_create_pda(seeds,count,program.ptr,address);\n if(code) return code;\n for(u64 j=0;j<32;j++) output.ptr[j]=address[j];\n return 0;\n"},
{"#ifdef __bpf__\n// SBF loads and stores at any alignment, and clang lowers a fixed-size\n// __builtin_memcpy to whole-word ldx/stx: one instruction per 8 bytes instead\n// of a byte loop (docs/BACKEND_INTERFACE.md#runtime-word-access).\nstatic inline u64 sol_read64(const u8 *p) {u64 v;__builtin_memcpy(&v,p,8);return v;}\nstatic inline void sol_write64(u8 *p,u64 v) {__builtin_memcpy(p,&v,8);}\nstatic inline u64 sol_read32(const u8 *p) {u32 v;__builtin_memcpy(&v,p,4);return v;}\nstatic inline void sol_write32(u8 *p,u32 v) {__builtin_memcpy(p,&v,4);}\nstatic inline boolean sol_eq32(const u8 *a,const u8 *b) {\n u64 x[4],y[4];__builtin_memcpy(x,a,32);__builtin_memcpy(y,b,32);\n return ((x[0]^y[0])|(x[1]^y[1])|(x[2]^y[2])|(x[3]^y[3]))==0;\n}\n#else\n", ""},
{"static void sol_write64(u8 *p,u64 v) {for(u64 i=0;i<8;i++) p[i]=(u8)(v>>(i*8));}\nstatic u64 sol_read32(const u8 *p) {u64 v=0;for(u64 i=0;i<4;i++) v|=(u64)p[i]<<(i*8);return v;}\nstatic void sol_write32(u8 *p,u32 v) {for(u64 i=0;i<4;i++) p[i]=(u8)(v>>(i*8));}\nstatic boolean sol_eq32(const u8 *a,const u8 *b) {for(u64 j=0;j<32;j++) if(a[j]!=b[j]) return 0;return 1;}\n#endif\n", ""},
{" return sol_eq32(a->key,id);\n", " for(u64 j=0;j<32;j++) if(a->key[j]!=id[j]) return 0;\n return 1;\n"},
{" return sol_eq32(sol_account(c,i)->key,address);\n", " u8 *key=sol_account(c,i)->key;\n for(u64 j=0;j<32;j++) if(key[j]!=address[j]) return 0;\n return 1;\n"},
{" sol_write64(data+1,amount);\n", " for(u64 i=0;i<8;i++) data[1+i]=(u8)(amount>>(i*8));\n"},
}

func withoutRuntimeWords(c []byte) []byte {
text := string(c)
for _, e := range runtimeWordEdits {
text = strings.ReplaceAll(text, e[0], e[1])
}
return []byte(text)
}

// corpusChanges lists the recorded programs whose verdict changed on purpose
// after the corpus was frozen, with the outcome the current compiler must
// produce instead. The .gz is never edited; every other entry must still match
Expand Down Expand Up @@ -401,7 +429,7 @@ func TestCorpusMatchesPreCheckCompiler(t *testing.T) {
continue
}
if err == nil {
sum := sha256.Sum256(withoutMemorySyscalls(out))
sum := sha256.Sum256(withoutMemorySyscalls(withoutRuntimeWords(out)))
if hex.EncodeToString(sum[:]) != e.CSHA256 {
t.Errorf("entry %d: emitted C changed (sha256 %x)", i, sum)
}
Expand Down
21 changes: 9 additions & 12 deletions internal/compiler/cpi.c.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,21 @@ extern u64 sol_get_rent_sysvar(GosvmRent *);
static u64 sol_Rent(Context *c,slice output) {
if(output.len!=17) return 2015;
GosvmRent rent;u64 code=gosvm_get_rent(&rent);if(code) return code;
for(u64 j=0;j<8;j++) {output.ptr[j]=(u8)(rent.rate>>(j*8));output.ptr[8+j]=(u8)(rent.threshold_bits>>(j*8));}
sol_write64(output.ptr,rent.rate);sol_write64(output.ptr+8,rent.threshold_bits);
output.ptr[16]=rent.burn;return 0;
}

static u64 sol_owned_writable(Context *c,u64 index) {
if(index>=c->count) return 2005;
AccountInfo *a=&c->accounts[index];if(!a->writable || a->executable) return 2011;
if(c->program.len!=32) return 2012;
for(u64 j=0;j<32;j++) if(a->owner[j]!=c->program.ptr[j]) return 2012;
return 0;
return sol_eq32(a->owner,c->program.ptr) ? 0 : 2012;
}
static u64 sol_original_len(AccountInfo *a) {
u64 n=0;for(u64 j=0;j<4;j++) n|=(u64)(a->key-4)[j]<<(j*8);return n;
return sol_read32(a->key-4);
}
static void sol_set_data_len(Context *c,AccountInfo *a,u64 n) {
for(u64 j=0;j<8;j++) (a->data-8)[j]=(u8)(n>>(j*8));
sol_write64(a->data-8,n);
for(u64 j=0;j<c->count;j++) if(c->accounts[j].data==a->data) c->accounts[j].data_len=n;
}
static u64 sol_ResizeAccount(Context *c,u64 index,u64 n) {
Expand All @@ -73,12 +72,11 @@ static u64 sol_CloseAccount(Context *c,u64 index,u64 destination) {
if(destination>=c->count) return 2005;
AccountInfo *a=&c->accounts[index],*b=&c->accounts[destination];
if(!b->writable || b->executable) return 2011;
boolean same=1;for(u64 j=0;j<32;j++) if(a->key[j]!=b->key[j]) same=0;
if(same) return 2013;
if(sol_eq32(a->key,b->key)) return 2013;
if(*a->lamports>~0ULL-*b->lamports) return 2014;
if(sol_original_len(a)>10485760) return 2010;
*b->lamports+=*a->lamports;*a->lamports=0;
sol_set_data_len(c,a,0);for(u64 j=0;j<32;j++) a->owner[j]=0;
sol_set_data_len(c,a,0);for(u64 j=0;j<32;j+=8) sol_write64(a->owner+j,0);
return 0;
}

Expand All @@ -99,10 +97,9 @@ static u64 sol_CreateAddress(Context *c,slice b,slice program,slice output) {
if(program.len!=32 || output.len!=32 || b.len>529) return 2007;
Seed seeds[16];u64 off=0,count=0;
if(!sol_seed_group(b,&off,seeds,&count) || off!=(u64)b.len) return 2007;
u8 address[32];u64 code=sol_create_pda(seeds,count,program.ptr,address);
if(code) return code;
for(u64 j=0;j<32;j++) output.ptr[j]=address[j];
return 0;
// The syscall reads every seed before it writes, and writes the address
// only on success, so output needs no staging copy.
return sol_create_pda(seeds,count,program.ptr,output.ptr);
}

// Outgoing CPI bounds (solana/api.go Invoke, docs/SDK2.md#cpi-bounds): 64
Expand Down
160 changes: 160 additions & 0 deletions internal/compiler/runtimewords_sbf_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package compiler

import (
"bytes"
"crypto/sha256"
"encoding/binary"
"fmt"
"os"
"testing"

"gosvm/internal/targets"
"gosvm/internal/testvm"
"gosvm/sdk/fixture"
)

// runtimeWordsSource drives the runtime routines that read, write and compare
// whole words on SBF (docs/BACKEND_INTERFACE.md#runtime-word-access). Account
// 0 is the 1 KiB program-owned output. Input: [mode][program 32][group...].
//
// mode 0: CreateAddress into out[8:40] (status in out[0:8]); out[40:72] is
// pre-filled with 0xee and must survive a failed derivation.
// mode 1: CreateAddress whose output is the group's own bytes (aliasing).
// mode 2: ResizeAccount(0, 1024), CloseAccount(0, 0), Rent and IsPDA.
const runtimeWordsSource = `package main

import "gosvm/solana"

func put(out []byte, at int, v uint64) {
for i := 0; i < 8; i++ {
out[at+i] = byte(v >> (8 * uint64(i)))
}
}

func Process(c solana.Context) uint64 {
in := solana.Instruction(c)
out := solana.Data(c, 0)
program := in[1:33]
group := make([]byte, len(in)-33)
copy(group, in[33:])
switch in[0] {
case 0:
for i := 8; i < 72; i++ {
out[i] = 0xee
}
put(out, 0, solana.CreateAddress(c, group, program, out[8:40]))
case 1:
buf := make([]byte, len(group)+32)
copy(buf, group)
// The output overlaps the group's last 32 bytes (the key seed).
code := solana.CreateAddress(c, buf[:len(group)], program, buf[len(group)-32:len(group)])
put(out, 0, code)
copy(out[8:40], buf[len(group)-32:len(group)])
case 2:
put(out, 0, solana.ResizeAccount(c, 0, 1024))
put(out, 8, solana.CloseAccount(c, 0, 0))
put(out, 16, solana.Rent(c, out[24:41]))
if solana.IsPDA(c, 0, group, 7) {
out[48] = 1
} else {
out[48] = 2
}
}
return 0
}
`

func pdaHash(program []byte, seeds ...[]byte) fixture.Key {
h := sha256.New()
for _, s := range seeds {
h.Write(s)
}
h.Write(program)
h.Write([]byte("ProgramDerivedAddress"))
var k fixture.Key
copy(k[:], h.Sum(nil))
return k
}

// TestRuntimeWordsSBF runs runtimeWordsSource on fogo-v0 and solana-v3 against
// host-computed results.
func TestRuntimeWordsSBF(t *testing.T) {
if testing.Short() {
t.Skip("SBF runs are skipped in -short mode")
}
llvm := modernLLVM()
if llvm == "" {
t.Skip("pinned platform-tools v1.57 backend required")
}
var cases []allocCase
group := func(seeds ...[]byte) []byte {
g := []byte{byte(len(seeds))}
for _, s := range seeds {
g = append(append(g, byte(len(s))), s...)
}
return g
}
key := bytes.Repeat([]byte{0x5a}, 32)
for j := 0; j < 16; j++ {
program := bytes.Repeat([]byte{byte(j + 1)}, 32)
var id fixture.Key
copy(id[:], program)
// One off-curve and one on-curve bump per program.
var good, bad int = -1, -1
for b := 255; b >= 0 && (good < 0 || bad < 0); b-- {
if fixture.OnCurve(pdaHash(program, []byte("vault"), key, []byte{byte(b)})) {
if bad < 0 {
bad = b
}
} else if good < 0 {
good = b
}
}
for _, b := range []int{good, bad} {
g := group([]byte("vault"), key, []byte{byte(b)})
out := make([]byte, 1024)
for i := 8; i < 72; i++ {
out[i] = 0xee
}
if b == good {
want := pdaHash(program, []byte("vault"), key, []byte{byte(b)})
copy(out[8:40], want[:])
} else {
binary.LittleEndian.PutUint64(out, 1)
}
cases = append(cases, allocCase{name: fmt.Sprintf("create-%02d-bump%03d", j, b), in: append(append([]byte{0}, program...), g...), output: out})
}
// Aliasing: the key seed is last, so the output overlaps it.
g := group([]byte("vault"), []byte{byte(good)}, key)
out := make([]byte, 1024)
want := pdaHash(program, []byte("vault"), []byte{byte(good)}, key)
if fixture.OnCurve(want) {
binary.LittleEndian.PutUint64(out, 1)
copy(out[8:40], key)
} else {
copy(out[8:40], want[:])
}
cases = append(cases, allocCase{name: fmt.Sprintf("alias-%02d", j), in: append(append([]byte{1}, program...), g...), output: out})
}
// The runners' Rent sysvars: LiteSVM 0.8.2 (fogo-v0) has 3,480 lamports
// per byte-year at threshold 2.0, LiteSVM 0.13.0 (solana-v3) 6,960 at 1.0.
rent := map[string][2]uint64{target.Fogo: {3480, 0x4000000000000000}, target.Solana: {6960, 0x3ff0000000000000}}
keep := os.Getenv("GOSVM_WORDS_RESULTS")
for _, profile := range []string{target.Fogo, target.Solana} {
t.Run(profile, func(t *testing.T) {
out := make([]byte, 1024)
binary.LittleEndian.PutUint64(out[8:], 2013) // refund account is the source
binary.LittleEndian.PutUint64(out[24:], rent[profile][0])
binary.LittleEndian.PutUint64(out[32:], rent[profile][1])
out[40] = 50
out[48] = 2
cases := append(cases, allocCase{name: "lifecycle", in: append(append([]byte{2}, key...), 's'), output: out})
runner, err := testvm.RunnerFor("", profile)
if err != nil {
t.Skipf("runner unavailable: %v", err)
}
result := runAllocProgram(t, llvm, runner, profile, "runtime-words", []byte(runtimeWordsSource), 0, cases, keep)
t.Logf("%s: %d cases match the host (ELF %s); create-00 %v CU, lifecycle %v CU", profile, len(cases), result["elf_sha256"], result[cases[0].name], result["lifecycle"])
})
}
}
27 changes: 21 additions & 6 deletions internal/compiler/solana.c.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,25 @@ typedef struct { Seed *ptr; u64 len; } Seeds;
_Static_assert(sizeof(AccountInfo)==56, "C account ABI");
_Static_assert(sizeof(AccountMeta)==16, "C meta ABI");
_Static_assert(sizeof(Instruction)==40, "C instruction ABI");
#ifdef __bpf__
// SBF loads and stores at any alignment, and clang lowers a fixed-size
// __builtin_memcpy to whole-word ldx/stx: one instruction per 8 bytes instead
// of a byte loop (docs/BACKEND_INTERFACE.md#runtime-word-access).
static inline u64 sol_read64(const u8 *p) {u64 v;__builtin_memcpy(&v,p,8);return v;}
static inline void sol_write64(u8 *p,u64 v) {__builtin_memcpy(p,&v,8);}
static inline u64 sol_read32(const u8 *p) {u32 v;__builtin_memcpy(&v,p,4);return v;}
static inline void sol_write32(u8 *p,u32 v) {__builtin_memcpy(p,&v,4);}
static inline boolean sol_eq32(const u8 *a,const u8 *b) {
u64 x[4],y[4];__builtin_memcpy(x,a,32);__builtin_memcpy(y,b,32);
return ((x[0]^y[0])|(x[1]^y[1])|(x[2]^y[2])|(x[3]^y[3]))==0;
}
#else
static u64 sol_read64(const u8 *p) {u64 v=0;for(u64 i=0;i<8;i++) v|=(u64)p[i]<<(i*8);return v;}
static void sol_write64(u8 *p,u64 v) {for(u64 i=0;i<8;i++) p[i]=(u8)(v>>(i*8));}
static u64 sol_read32(const u8 *p) {u64 v=0;for(u64 i=0;i<4;i++) v|=(u64)p[i]<<(i*8);return v;}
static void sol_write32(u8 *p,u32 v) {for(u64 i=0;i<4;i++) p[i]=(u8)(v>>(i*8));}
static boolean sol_eq32(const u8 *a,const u8 *b) {for(u64 j=0;j<32;j++) if(a[j]!=b[j]) return 0;return 1;}
#endif
static AccountInfo *sol_account(Context *c,u64 i) {if(i>=c->count) abort();return &c->accounts[i];}
static u64 sol_Count(Context *c) {return c->count;}
static slice sol_Data(Context *c,u64 i) {AccountInfo *a=sol_account(c,i);return GOSVM_SLICE(a->data,a->data_len);}
Expand All @@ -32,8 +50,7 @@ static slice sol_ProgramID(Context *c) {return c->program;}
static boolean sol_IsTokenProgram(Context *c,u64 i) {
static const u8 id[32]={6,221,246,225,215,101,161,147,217,203,225,70,206,235,121,172,28,180,133,237,95,91,55,145,58,140,245,133,126,255,0,169};
AccountInfo *a=sol_account(c,i);if(!a->executable) return 0;
for(u64 j=0;j<32;j++) if(a->key[j]!=id[j]) return 0;
return 1;
return sol_eq32(a->key,id);
}
#ifdef GOSVM_SBF_V3
#define sol_create_pda ((u64 (*)(Seed *,u64,u8 *,u8 *))2474062396ULL)
Expand All @@ -56,17 +73,15 @@ static boolean sol_IsPDA(Context *c,u64 i,slice seed,u8 bump) {
if(seed.len>32) return 0;
u8 address[32];Seed seeds[2]={{seed.ptr,seed.len},{&bump,1}};
if(sol_create_pda(seeds,2,c->program.ptr,address)!=0) return 0;
u8 *key=sol_account(c,i)->key;
for(u64 j=0;j<32;j++) if(key[j]!=address[j]) return 0;
return 1;
return sol_eq32(sol_account(c,i)->key,address);
}
static __attribute__((always_inline)) inline u64 sol_transfer(
Context *c,u64 program,u64 source,u64 destination,u64 authority,u64 amount,
slice seed,u8 bump,boolean signed_pda) {
if(!sol_IsTokenProgram(c,program)) return 2001;
AccountInfo *src=sol_account(c,source),*dst=sol_account(c,destination),*auth=sol_account(c,authority);
u8 data[9];data[0]=3; // SPL Token Transfer, little-endian amount.
for(u64 i=0;i<8;i++) data[1+i]=(u8)(amount>>(i*8));
sol_write64(data+1,amount);
AccountMeta metas[3]={{src->key,1,0},{dst->key,1,0},{auth->key,0,1}};
Instruction ix={sol_account(c,program)->key,metas,3,data,9};
Seed seed_parts[2]={{seed.ptr,seed.len},{&bump,1}};
Expand Down
Loading
Loading