Add DeclareEntrypoint#16
Closed
DaviRain-Su wants to merge 109 commits into
Closed
Conversation
joncinque
reviewed
Oct 12, 2025
joncinque
left a comment
Owner
There was a problem hiding this comment.
Thanks for the contribution! I like the concept, we just need to figure out what to do for the error type
Comment on lines
+1
to
+6
| pub const ProgramError = error{ | ||
| AlreadyInUse, | ||
| InvalidAccountType, | ||
| Uninitialized, | ||
| IncorrectSize, | ||
| }; |
Owner
There was a problem hiding this comment.
If you want to add these and have them work similarly to the program error in the Rust SDK, may as well just copy all the variants at https://github.com/anza-xyz/solana-sdk/blob/2d88eb491e52705ec73ee8204dc41d510c298909/program-error/src/lib.rs#L57, but that will require also setting the values individually, ie:
const ProgramError = enum(u64) {
InvalidArgument = 1 << 32,
InvalidInstructionData = 2 << 32,
};
And so on. That'll be a bit more conformant than this error type.
On the other hand, it's not a proper zig error, which makes me think we might want to omit this entirely, and allow processInstruction to return any error. What do you think?
- Change callconv(.C) to callconv(.c) in entrypoint.zig - Add direct exports in root.zig for convenience (entrypoint, PublicKey, etc.) - Fix print format in pubkey example - Add .surfpool to gitignore
- Replace Zig error type with enum(u64) matching Solana SDK values - Add all 26 builtin errors with correct bit-shifted values (n << 32) - Support custom errors in lower 32 bits - Add ProgramResult union type for cleaner error handling - Add helper methods: toU64(), custom(), getCustomCode(), toString() - Update entrypoint to use new ProcessInstruction function pointer type - Add unit tests verifying compatibility with Rust SDK
Adds `buildProgram(b, options)` for the solana-zig fork (Zig 0.16-dev
with native .sbf target), alongside the existing `buildProgramElf2sbpf`
fallback. The fork path emits `.so` in one compile step using the
built-in lld, matching solana-zig v1.52 baseline CU numbers. The
elf2sbpf path stays available for users without the fork.
Changes:
- build.zig: `buildProgram`, `linkSolanaProgram` (bpf.ld linker script),
`sbf_target` export; retains all elf2sbpf helpers unchanged
- scripts/ensure-solana-zig.sh (new): locate a solana-zig fork binary
via SOLANA_ZIG_BIN / ZIG / local .tools / sibling ../solana-zig-bootstrap
/ PATH
- scripts/bootstrap.sh: print both paths (fork if found, elf2sbpf
always)
- README.md: document dual-path model + when to pick each
- .gitignore: zig-pkg/
Smoke test (SOLANA_ZIG_BIN=/path/to/solana-zig-bootstrap/out-smoke/host/bin/zig):
- `zig build test` (host): 10/10 passes unchanged
- External consumer with `.path = "./sdk"`: produces 1.2 KB .so via
buildProgram (pubkey_cmp sample)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
elf2sbpf removed the --peephole flag on 2026-04-19 after discovering a miscompile on zignocchio escrow. Point CU-critical users at the solana-zig fork (primary path) instead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Amends the previous commit — the comment in buildProgramElf2sbpf still pointed users at the now-removed --peephole flag. Recommend the solana-zig fork instead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Changes: - Remove redundant cpu_features_add from sbf_target/bpf_target v1.53.0's LLVM is stricter about duplicate features - Upgrade base58 dependency: v0.15.0 -> v0.16.0 - program-test/build.zig: switch to buildProgram (fork path) - program-test/test.sh: auto-detect solana-zig fork via 'zig targets' - template/hello-world/build.zig: add -Dsolana-zig option for dual path - README.md: update fork URL to joncinque's v1.53.0 release, add prebuilt binary download instructions
- .github/workflows/main.yml: add solana-zig-fork-0.16 branch, add programs-fork job testing with v1.53.0 prebuilt binary - scripts/ensure-solana-zig.sh: update URL to joncinque's repo - template/hello-world/.github/workflows: add fork path test job - template/hello-world/scripts/bootstrap.sh: add solana-zig fork detection - template/hello-world/program-test/test.sh: auto-detect fork like main repo - template/hello-world/README.md: document dual build paths
build.zig: - Add has_sbf_target comptime flag via @Hasfield - Change sbf_target from const to function sbfTarget() - Change buildProgram compileError to runtime exit for stock Zig program-test/build.zig: - Restore dual-path support (buildProgram vs buildProgramElf2sbpf) - Auto-detect via -Dsolana-zig or SOLANA_ZIG_BIN env var program-test/test.sh: - Pass -Dsolana-zig when fork is detected
- Zero-copy AccountInfo - Lazy parsing entrypoint - Zero dependencies - CU optimization with hint module - System Program CPI wrappers - Sysvar accessors - Phase-by-phase implementation plan
New modules: - src/pubkey.zig — replaces public_key.zig, inline Base58, zero deps - src/account.zig — zero-copy AccountInfo with borrow checking (Ref/RefMut) - src/program_error.zig — standard ProgramError matching Rust error codes - src/entrypoint.zig — replaces context.zig, eager + lazy parsing Removed: - src/public_key.zig (merged into pubkey.zig) - src/context.zig (replaced by entrypoint.zig) - base58 external dependency Changes: - build.zig.zon: zero dependencies - build.zig: remove base58 references - instruction.zig: update imports - clock/rent/slot_hashes/bpf: update imports 15/17 tests pass (2 skipped — need Solana runtime memory layout)
- 修复 deserialize 函数:正确处理 Solana 运行时序列化格式 - 非重复 account: Account 结构直接跟在 8 字节块后 - 重复 account: 8 字节 marker (index + padding) - 跳过 data + 10KB padding + align + rent_epoch - 实现 InstructionContext (懒解析) - init(): 预扫描计算 instruction_data 和 program_id 位置 - nextAccount(): 按需解析下一个 account - skipAccounts(): 跳过指定数量 account - instructionData()/programId(): 直接访问预计算位置 - 添加完整的测试覆盖 - deserialize empty / single account / with data / duplicate - lazy parsing / skip accounts - 所有 20 个测试通过
New modules: - src/hint.zig — branch prediction hints (likely/unlikely/coldPath) - src/memory.zig — memory operations (memcpy/memset/memcmp/fromBytes) - Uses sol_memcpy_/sol_memset_/sol_memcmp_ syscalls on BPF - Falls back to std operations on host - src/allocator.zig — simplified BumpAllocator - Removed ReverseFixedBufferAllocator (simpler API) - Added allocDirect() for bypassing Allocator interface - Added global_allocator for std.mem.Allocator compatibility Updated: - src/root.zig — export hint and memory modules 32/32 tests passed
New modules: - src/cpi.zig — Cross-Program Invocation - invoke/invokeSigned with C-ABI syscall wrappers - setReturnData/getReturnData - AccountMeta and Instruction types - src/system.zig — System Program CPI wrappers - createAccount/createAccountSigned/transfer/assign/allocate/realloc - createAccountWithSeed - src/sysvar.zig — Sysvar accessors - Clock/Rent/EpochSchedule types - getSysvar() helper - src/pda.zig — Program Derived Address computation - createProgramAddress/findProgramAddress/createWithSeed - comptime versions for compile-time PDA calculation Updated: - src/root.zig — export cpi, system, sysvar, pda modules - src/instruction.zig — keep InstructionData helper, update imports 44/44 tests passed
Changes: - src/instruction.zig — simplified to only InstructionData helper - Removed CPI code (now in cpi.zig) - src/log.zig — added new functions and external syscall declarations - log64() — log 5 u64 values - getRemainingComputeUnits() — query remaining CU - Extracted syscall declarations to module level - src/root.zig — enhanced exports - Added sysvar ID aliases (clock_id, rent_id, etc.) - Added system_program_id export 49/49 tests passed
- program-test: Migrate to entrypoint() API, remove base58/clap deps - template/hello-world: Update to new entrypoint API - template/program-test: Simplify build.zig using SDK helpers - Remove base58 dependency from all build.zig.zon files
- template/hello-world/build.zig: Add -Dbuild-program flag (default false) so zig build test does not require elf2sbpf - template/program-test/test.sh: Support ZIG_BUILD_ARGS env var - template/.github/workflows: Pass -Dbuild-program when testing program
The entrypoint tests were using fixed-size buffers (4096 bytes) that were too small for the serialized account data (each account needs ~10KB for padding). This caused memory corruption and ABRT on macOS ARM64. - Fix test buffer sizes to account for Account struct + data_len + MAX_PERMITTED_DATA_INCREASE (10KB) + rent_epoch - .github/workflows/main.yml: Restore macOS in test matrix - build.zig: Use std.Io.Dir.openFileAbsolute for cross-platform file existence check
Four new comptime/typed abstractions, all measured 0 CU regression
against the vault baseline (1334 / 1543 / 1866).
### 1. sol.math — checked integer arithmetic (src/math.zig)
Every DeFi-style Solana program does:
```zig
const new_balance, const ovf = @addWithOverflow(balance, amount);
if (ovf != 0) return error.ArithmeticOverflow;
```
This module collapses it to one of three single-line forms:
```zig
const new = sol.math.tryAdd(a, b) orelse return error.ArithmeticOverflow;
const new = try sol.math.add(a, b);
const new = sol.math.addUnchecked(a, b); // wrapping
```
Same for sub/mul. Works on any integer type (u64, u32, i64, …).
Deposit migrated: 0 CU change (LLVM folds @addWithOverflow + branch
to identical BPF).
**Documented caveat**: for the `if (a < b) err else a - b` shape
(typical withdraw), the hand-written form is ~6 CU cheaper than
`trySub`. BPFv2's @subWithOverflow materializes the carry flag as
a value-to-store-and-test; the hand-written compare-and-branch on
the happy path avoids the materialization. README and inline
vault.zig comment explain the trade-off so users don't blindly
migrate every subtraction.
### 2. AccountInfo.expect(.{...}) (src/account.zig)
Mirrors `parseAccountsWith`'s `AccountExpectation` shape for
one-off assertions:
```zig
try authority.expect(.{ .signer = true, .writable = true });
try mint.expect(.{ .owner = sol.spl_token_program_id });
try rent_sysvar.expect(.{ .key = sol.sysvar.RENT_ID });
```
Each field is comptime-gated via `inline for (info.@"struct".fields)`
— only the requested checks generate code. `key` and `owner` use
the comptime-Pubkey fast path (pubkeyEqComptime: 4 u64 immediate
compares, no rodata lookup). Unknown field names → compile error.
Vault migrated: 8 expectSigner/expectWritable/assertOwnerComptime
chains collapsed to 5 single-line `expect(.{...})` calls.
**0 CU change** on all three vault instructions.
### 3. AccountExpectation.key field (src/entrypoint.zig)
`parseAccountsWith` now accepts `.key` as well as `.owner`:
```zig
const accs = try ctx.parseAccountsWith(.{
.{ "vault", .{ .writable = true, .owner = MY_PROGRAM_ID } },
.{ "rent_sysvar", .{ .key = sol.sysvar.RENT_ID } },
});
```
Same comptime-immediate fast path as `.owner`. Returns
`error.InvalidArgument` on mismatch (matches Solana runtime's
convention for wrong account pubkey).
### 4. AccountInfo.dataAs(T) / dataAsConst(T)
Typed views over account data starting at offset 0:
```zig
const state: *align(1) Layout = account.dataAs(Layout);
state.counter += 1; // direct write
```
Single pointer-cast, no allocation. Use `TypedAccount(T)` when
you want discriminator validation; use `dataAs(T)` for raw
layouts (SPL Token accounts, custom binary formats).
### 5. Bonus: subLamportsChecked / addLamportsChecked
Error-returning lamport-balance arithmetic for the cases where
balance cannot be assumed to cover the operation. Returns
`error.ArithmeticOverflow` on under/overflow.
### Test additions (101 → 119, +18 tests)
- math.zig: 11 tests (happy paths, overflow, wrapping, signed types)
- account.zig: 6 tests (expect happy path + 3 failure modes,
dataAs/dataAsConst type check, lamports checked overflow)
- entrypoint.zig: 2 tests (parseAccountsWith .key match + mismatch)
### Final measurements (12 benchmarks)
| ix | CU |
|-----------------------------------|------|
| vault_initialize | 1334 |
| vault_deposit | 1543 |
| vault_withdraw | 1866 |
| token_dispatch_transfer | 37 |
| token_dispatch_burn | 37 |
| token_dispatch_mint | 38 |
| token_dispatch_unchecked_transfer | 31 |
| pda_runtime | 3025 |
| pda_comptime | 6 |
| pubkey_cmp_* | 24-30|
**All identical to pre-refactor baseline.** API surface improved
without any code-gen regression.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
### 1. Bug fix: ErrorCode.toError discriminator was being lost `MyErr.toError(.SomeVariant)` previously returned `error.Custom` which the default `errorToU64` mapped to `CUSTOM_ZERO` — so **every** custom variant collapsed to the same wire code. The runtime saw the same error number regardless of which `.X` was raised. Fix: have `toError` stash the `@intFromEnum(.X)` value in a module-local `u32` slot before returning `error.Custom`. BPF programs are single-threaded (one invocation per VM), so this is safe. New entrypoint variants read the slot on the `error.Custom` path and emit the correct wire code: - `lazyEntrypointWith` — drop-in replacement for `lazyEntrypoint` - `programEntrypointWith` — drop-in for `programEntrypoint` - `errorToU64WithCustom` — the underlying helper, exported for users with custom entrypoint setups Performance: happy path is **identical** to the plain variants (verified: vault_initialize/deposit/withdraw all at 1334/1543/1866). Error path adds one `ldxw + cmp` to recover the discriminator. Migration: - vault.zig: switched to `lazyEntrypointWith` with inline comment explaining why - README: documented the trap in the new 'Custom error codes' section + entrypoint table now lists both `lazyEntrypoint` variants - ErrorCode test extended to verify discriminator is preserved across two consecutive calls ### 2. Sysvar syscall wrappers Added syscall-based `get()` for three previously-unwrapped sysvars: - `sysvar.EpochSchedule.get()` → sol_get_epoch_schedule_sysvar - `sysvar.LastRestartSlot.get()` → sol_get_last_restart_slot - `sysvar.EpochRewards.get()` → sol_get_epoch_rewards_sysvar These mirror the existing `Clock.get()` and `Rent.get()` shape. Reading via syscall is ~250-300 CU and removes the need for clients to list the sysvar account in the instruction's accounts. Programs needing Instructions / SlotHashes / StakeHistory (which don't have syscalls) still use `getSysvar(T, account)` for the account-based path. 3 new tests verify struct layout / field access. ### Numbers (12 benchmarks, all unchanged) | ix | CU | |-----------------------------------|------| | vault_initialize | 1334 | | vault_deposit | 1543 | | vault_withdraw | 1866 | | token_dispatch_transfer | 37 | | token_dispatch_burn | 37 | | token_dispatch_mint | 38 | | token_dispatch_unchecked_transfer | 31 | | pda_runtime | 3025 | | pda_comptime | 6 | Tests: 119 → 123 host (+4) and 5/5 program tests pass. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Two ergonomic additions, both verified 0 CU vs. the hand-written
struct literal / repeated-pubkeyEqComptime patterns.
### 1. AccountMeta convenience constructors (src/cpi.zig)
The pattern `.{ .pubkey = X.key(), .is_writable = 1, .is_signer = 1 }`
appeared 7+ times in system.zig and is even more annoying for users
writing their own CPIs — the flag fields read like noise. Four
`inline fn` constructors describe the role instead:
```zig
AccountMeta.readonly(key) // is_writable=0, is_signer=0
AccountMeta.writable(key) // is_writable=1, is_signer=0
AccountMeta.signer(key) // is_writable=0, is_signer=1
AccountMeta.signerWritable(key) // is_writable=1, is_signer=1
```
All 7 internal call sites in system.zig migrated. Same BPF
(`inline fn` + LLVM SROA fold the struct construction).
Measured: vault_initialize 1334, deposit 1543, withdraw 1866 —
identical to baseline.
### 2. pubkeyEqAny + AccountInfo.isOwnedByAny / assertOwnerAny
The 'either SPL Token or Token-2022' pattern is common in DeFi
programs. Previously users had to write:
```zig
if (!mint.isOwnedByComptime(spl_token_id) and
!mint.isOwnedByComptime(spl_token_2022_id)) return err;
```
Now:
```zig
if (!mint.isOwnedByAny(&.{
sol.spl_token_program_id,
sol.spl_token_2022_program_id,
})) return err;
```
Or via the expectation API:
```zig
try mint.expect(.{ .owner_any = &.{token_a, token_b} });
```
Implementation: `pubkey.pubkeyEqAny` uses `inline for` over the
comptime slice; each iteration is a `pubkeyEqComptime` (4×u64
immediate compares). N == 1 folds to plain `pubkeyEqComptime`.
Bonus: `AccountInfo.expect` now recognizes `owner_any` and
`key_any` fields too (alongside the existing `owner` / `key`
single-value variants). Unknown fields still emit a compile error.
### Benchmark
New `pubkey_cmp_any_2.zig` benchmark exercises the failure path
(neither allowed key matches):
| benchmark | CU |
|----------------------------|-----|
| pubkey_cmp_comptime (1-way, success path) | 24 |
| pubkey_cmp_any_2 (2-way, failure path) | 18 |
| pubkey_cmp_runtime_const (1-way runtime) | 30 |
The 2-way failure path is *cheaper* than the 1-way success path
because the success branch does extra work (returns ProgramResult);
the comparison itself costs ~6 CU per pubkey on the comptime path.
### Tests (123 → 129, +6 new)
- pubkey.zig: 3 tests (match first/second/none, single-element
fold, empty list)
- account.zig: 2 tests (isOwnedByAny, expect.owner_any/key_any)
- cpi.zig: 1 test (4 constructor flag bytes)
### Final measurements (12 benchmarks)
| ix | CU |
|-----------------------------------|------|
| vault_initialize | 1334 |
| vault_deposit | 1543 |
| vault_withdraw | 1866 |
| token_dispatch_transfer | 37 |
| token_dispatch_burn | 37 |
| token_dispatch_mint | 38 |
| token_dispatch_unchecked_transfer | 31 |
| pubkey_cmp_comptime | 24 |
| pubkey_cmp_any_2 (new) | 18 |
| pubkey_cmp_runtime_const | 30 |
| pda_runtime | 3025 |
| pda_comptime | 6 |
All previously-existing benchmarks unchanged.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Two more ergonomic CPI helpers, both verified 0 CU vs. the prior
literal form.
### 1. cpi.Instruction constructors (src/cpi.zig)
The pattern `cpi.Instruction{ .program_id = X.key(), .accounts = &A,
.data = &D }` appeared 7 times in system.zig. Two `inline fn`
constructors cover the common shapes:
```zig
// When you already have a *const Pubkey:
const ix = sol.cpi.Instruction.init(&program_id, &metas, &ix_data);
// When the program is a parsed CpiAccountInfo (typical for CPI
// helpers that take the program account from their caller):
const ix = sol.cpi.Instruction.fromCpiAccount(program, &metas, &ix_data);
```
All 7 system.zig sites migrated. Same BPF as the struct literal
(`inline` + LLVM SROA fold the construction).
### 2. cpi.Seed.fromPubkey / fromByte (src/cpi.zig)
The PDA-seed setup typically looks like:
```zig
const bump_seed = [_]u8{bump};
const seeds = [_]Seed{
.from("vault"),
.from(authority.key()[0..]),
.from(&bump_seed),
};
```
Two more constructors handle the awkward `.key()[0..]` and bump-
byte patterns:
- **`Seed.fromPubkey(*const Pubkey)`** → 32-byte seed. Reads
more naturally than `.from(pk[0..])`, same generated code.
- **`Seed.fromByte(*const u8)`** → 1-byte seed. Useful when the
bump already lives in an account / struct field (no need to
copy it into a stack-local `[_]u8{bump}` first).
```zig
const seeds = [_]Seed{
.from("vault"),
.fromPubkey(authority.key()), // cleaner
.fromByte(&state.bump), // reuses stored bump
};
```
Vault example migrated to use `.fromPubkey` — 0 CU change on
initialize (1334).
### Final measurements (11 benchmarks)
| ix | CU |
|-----------------------------------|------|
| vault_initialize | 1334 |
| vault_deposit | 1543 |
| vault_withdraw | 1866 |
| token_dispatch_transfer | 37 |
| token_dispatch_burn | 37 |
| token_dispatch_mint | 38 |
| token_dispatch_unchecked_transfer | 31 |
| pubkey_cmp_comptime | 24 |
| pubkey_cmp_any_2 | 18 |
| pda_runtime | 3025 |
| pda_comptime | 6 |
All identical to pre-refactor.
### Tests (129 → 131, +2 new)
- cpi.zig: `Instruction.init builds the struct in one call`
- cpi.zig: `Seed.from / fromByte / fromPubkey produce identical layout`
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
## The critical bug The previous `ErrorCode.toError(.X)` design stashed the u32 discriminator in a module-local `var last_custom_code` and relied on `lazyEntrypointWith` to read it on the error path. This put the slot into the program's `.bss` section. **The SBPFv2 loader rejects programs with `.bss` / `.data` — Solana programs cannot use mutable global state**. Every commit since f170628 (`fix: ErrorCode discriminator preservation`) produced `example_vault.so` files that failed to deploy with "Program is not deployed" / "Unsupported program id". The host tests passed because host *can* have mutable globals; the bench `.so`s in CI succeeded only because of stale `.zig-cache`. The clean-build runtime always failed. Discovered when cleaning `.zig-cache` before adding new examples and finding the same vault that supposedly ran at 1334 CU now had a 4-byte `.bss` and would not deploy. ## The fix — dual-declaration ErrorCode + typed entrypoint `ErrorCode` now takes two arguments: the `enum(u32)` of codes and a parallel `error{...}` set with matching variant names. The two are validated at comptime to share names. The entrypoint's catch block dispatches on the **error name** (not a global slot) to map back to the u32 code: ```zig const VaultErr = sol.ErrorCode( enum(u32) { Unauthorized = 6000, AmountOverflow, InsufficientVaultBalance }, error{ Unauthorized, AmountOverflow, InsufficientVaultBalance }, ); fn process(ctx: *InstructionContext) VaultErr.Error!void { try sol.system.transfer(...); // ProgramError flows if (bad) return VaultErr.toError(.Unauthorized); // custom code } export fn entrypoint(input: [*]u8) u64 { return sol.entrypoint.lazyEntrypointTyped(VaultErr, process)(input); } ``` Zero `.bss`, zero `.data`, full custom-code preservation, normal `try` ergonomics inside the handler. ### API changes (breaking) - **Removed**: `lazyEntrypointWith` / `programEntrypointWith` / `errorToU64WithCustom` / `lastCustomCode` / `resetLastCustomCode`. - **Added**: `lazyEntrypointTyped(ErrCode, fn)` / `programEntrypointTyped(N, ErrCode, fn)`. - **Changed**: `ErrorCode(E)` → `ErrorCode(E, ErrSet)`. The dual declaration is intentional — Zig 0.16's compiler in our fork doesn't expose `@Type` to synthesise error sets, and the comptime cross-validation makes typos a compile error. - **Changed**: `requireHasOneWith(field, expected, err)` now takes `comptime err: anytype` so it works with both `ProgramError` and custom `ErrorSet` variants. - Vault example refactored to the new shape. ### Cost: +1 CU per ix on vault | ix | before | after | |---|---:|---:| | vault_initialize | (broken on-chain) | 1335 | | vault_deposit | (broken on-chain) | 1544 | | vault_withdraw | (broken on-chain) | 1867 | The +1 CU is the cost of the `inline for` name-dispatch on the cold error path. Happy path is unchanged. ## New examples Added 3 standalone example programs alongside the existing `vault.zig` / `token_dispatch.zig`: - **`hello.zig`** (~30 lines): minimal program — `lazyEntrypointRaw` + `sol.log`. The smallest deployable program. - **`counter.zig`** (~210 lines): per-user PDA counter with `Initialize` / `Increment` / `Reset`. Showcases the eager-parse `programEntrypointTyped(3, ...)` shape. - **`escrow.zig`** (~255 lines): classic lamport-for-lamport escrow with `Make` / `Take` / `Refund`. Multi-instruction state machine, direct-lamport account closing pattern. All four (vault + 3 new) compile with **zero `.bss` / `.data`** sections — verified by parsing the ELF. ### Tests (131 → 133) - error_code: comptime variant-count / name-mismatch validation - error_code: catchToU64 maps custom variants to u32 codes - error_code: catchToU64 passes ProgramError variants through - error_code: Error union round-trip ### README updated - Replaced `lazyEntrypointWith` docs with `lazyEntrypointTyped`. - Updated vault CU table (1334/1543/1866 → 1335/1544/1867). - Added "Example programs" matrix listing all five examples and the SDK features each one exercises. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Adds Mollusk-based integration tests for examples/hello.zig,
examples/counter.zig, and examples/escrow.zig so the BPF-runtime
behaviour of the new ErrorCode design is verified end-to-end.
The previous slot-stash ErrorCode passed every host-side unit test
but failed to deploy on-chain (the SBPFv2 loader rejects .bss/.data
sections, which the static `var last_custom_code` introduced).
A stale .zig-cache once let that broken design ship 'green'. Both
test.sh and the CI workflow now `rm -rf .zig-cache zig-out` before
building so the contract — "the .so we test is the .so we ship" —
is enforced on every run.
New coverage:
- hello.rs: smoke (success + non-zero CU)
- counter.rs: initialize + increment happy path, Custom(6001)
Overflow, Custom(6000) NotOwner, reset zeroes count
- escrow.rs: Make→Take, Make→Refund, Custom(6001) on
InsufficientFunds, Custom(6000) on NotMaker
All custom error code assertions verify the **exact wire u32**
(via solana_program_error::Custom(N)) — they regress immediately
if a future change collapses custom codes back to Custom(0).
Total: program-test now runs 17 BPF tests
(counter 5 + cpi 3 + escrow 5 + hello 2 + pubkey 2).
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Move `program-test/cpi/main.zig` and `program-test/pubkey/main.zig`
into `examples/cpi.zig` and `examples/pubkey.zig` so the repo has
a single canonical home for deployable sample programs.
Before this commit, two separate program directories existed:
- `examples/` — the curated learning examples
- `program-test/{cpi,pubkey}/` — historical test-only programs
That split made it unclear where new sample programs should live and
forced contributors to track two build.zig configs. The CPI program
is a useful CPI-101 example in its own right, so it belongs in
`examples/` next to hello/counter/vault/escrow.
`program-test/build.zig` now points every entry at `../examples/*.zig`.
The .so artefact names (`cpi`, `pubkey`, …) are kept stable so the
existing Rust integration tests find their binaries unchanged.
No code or test logic changes — pure relocation + light header
polish on the moved files (de-duplicated docstring on cpi.zig,
added "see also: hello.zig" note on pubkey.zig).
All 17 program-test cases still pass:
counter 5 + cpi 3 + escrow 5 + hello 2 + pubkey 2.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
The previous `pubkey` program was a renamed Hello — it logged one
line and returned. The name was misleading: nothing about it tested
pubkey or PDA functionality. Replace it with a 3-instruction program
that covers the three most important pubkey-shaped operations:
- ix=0 DerivePda `sol.pda.findProgramAddress` syscall;
result surfaced via `setReturnData` so the
test can byte-compare against the off-chain
`Pubkey::find_program_address`. (~1663 CU)
- ix=1 VerifyPda `sol.verifyPda` with client-supplied bump.
Happy path is one SHA-256 (~1574 CU); wrong
bump returns InvalidSeeds. This is the
canonical "don't burn 3000 CU walking bumps"
pattern.
- ix=2 CheckOwner `sol.pubkey.pubkeyEqAny` with the
system_program + bpf_loader_upgradeable
whitelist. Demonstrates the comptime-folded
multi-pubkey compare (~49 CU for a 2-way
check, including dispatch).
`tests/pubkey.rs` now runs 5 assertions across these paths:
- derive returns the address+bump matching off-chain derivation
- verify_pda accepts the canonical bump
- verify_pda rejects bump-1 with InvalidSeeds
- check_owner accepts both whitelisted owners
- check_owner returns IncorrectProgramId for a random owner
The `.so` artefact name stays `pubkey` so test loading is
unchanged. Total program-test count: 5 + 3 + 5 + 2 + 6 = 21.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Closes the longest-standing gap vs Pinocchio / solana-program: now
the SDK can introspect the full transaction (instructions sysvar),
the call stack (stack height + processed sibling instruction), and
manage account lifecycle (resize, assign, close).
- src/sysvar_instructions.zig — zero-copy parser for the instructions
sysvar (`loadCurrentIndexChecked`, `loadInstructionAtChecked`,
`getInstructionRelative`). Required for ed25519/secp256k1 verify-
then-act flows and MEV / sandwich-defence patterns.
- src/stack.zig — `getStackHeight`, `siblingMeta`,
`getProcessedSiblingInstruction(Alloc)`. Lets programs reject CPI
(must be top-level) or assert a specific sibling preceded them.
- AccountInfo.{resize, resizeUnchecked, assign, assignComptime,
close} — Anchor `#[account(close = ...)]` parity, with the
MAX_PERMITTED_DATA_INCREASE budget check. `originalDataLen()`
exposes the runtime-captured length.
- TypedAccount.close — forwards to AccountInfo.close.
- AccountInfo.expectSignerKey(Comptime) — combined signer + key
check, common authority-validation pattern.
- src/hash.zig — full rewrite. Removed stale base58 import from the
old blake3.zig; merged blake3 in and added sha256 + keccak256
syscall wrappers. `Hash` newtype with Base58 formatter.
- README — new sections for sysvar instructions, stack
introspection, resize/close, hash module.
Tests: 146 → 154 host tests pass; all 7 example .so still build
under the solana-zig fork; all 21 program-test integration tests
pass on the SBF runtime.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Adds the four remaining cryptographic syscalls and the StakeHistory sysvar accessor that were still missing from the SDK, grouped under a new sol.crypto aggregate namespace. - src/secp256k1_recover.zig — Ethereum-compatible ecrecover via sol_secp256k1_recover. Typed errors (InvalidHash / InvalidRecoveryId / InvalidSignature) instead of u64 magic codes. - src/alt_bn128.zig — BN254 curve ops via sol_alt_bn128_group_op: G1 add/sub/mul + multi-pairing, with both EIP-197 big-endian and ark-bn254 little-endian variants. Foundation for on-chain Groth16 / PLONK verifiers. - src/poseidon.zig — ZK-friendly hash via sol_poseidon. BN254 X5, 1..12 input elements. Cross-checked SliceHeader 16-byte layout matches the BPF slice-of-slice ABI. - src/stake_history.zig — StakeHistory sysvar accessor (no syscall exists; reads from the sysvar account). Zero-copy parse + binary search by epoch. - src/crypto.zig — aggregate namespace re-exporting hash / secp256k1_recover / alt_bn128 / poseidon. Existing flat aliases (sol.sha256, sol.alt_bn128.…) remain. - README — new `sol.crypto` and StakeHistory sections. Tests: 154 → 171 host tests pass; all 7 example .so build under the solana-zig fork; 21 program-test integrations remain green. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Crypto modules (secp256k1_recover / alt_bn128 / poseidon) intentionally
expose typed error sets distinct from ProgramError so the failure
sub-classification survives logging and conditional branching. But
that breaks naive `try` composition with ProgramResult.
Mirroring the Rust SDK's two idioms — From<E> for u64 (preserve)
and .map_err(|_| ProgramError::X) (collapse) — each module now ships:
- errorToCode(err) -> u32: numeric code matching the syscall's
return value and the Rust SDK's From<E> for u64 impl. Pair with
sol.customError(code) to surface the failure on the wire as
Custom(N).
- errorToProgramError(err) -> ProgramError: opinionated split
between malformed-input failures (-> InvalidInstructionData)
and value failures (-> InvalidArgument). Compose with
`catch |e| return errorToProgramError(e)`.
The SDK doesn't pick a default — call sites declare the policy.
README adds a 'Bridging crypto errors' subsection documenting the
three patterns.
Tests: 171 -> 177 host tests pass; all 7 example .so still build.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…_sysvar Closes the final four uncovered syscalls in the audit, bringing Zig SDK syscall coverage to parity with solana-program 4.x. - src/compute_budget.zig — sol_remaining_compute_units. Lets a program query the CU budget mid-instruction so it can bail before the runtime hard-aborts. Costs ~1 CU. - src/stake.zig — sol_get_epoch_stake. Active delegated lamports this epoch for a given vote account. Returns 0 if not a vote account / no stake. - src/big_mod_exp.zig — sol_big_mod_exp. Big-endian arbitrary-precision base^exp mod modulus, output left-padded to modulus length. Used by RSA-style number-theory verifiers. ABI Params struct matches BigModExpParams (6 × u64-sized fields, 48 bytes). - src/sysvar.zig — getSysvarBytes wrapping sol_get_sysvar. Offset-based generic sysvar read, mapping the runtime's 1/2 return codes to InvalidArgument / UnsupportedSysvar. The only on-chain way to read SlotHashes / StakeHistory without the account being passed in. Flat aliases (sol.remainingComputeUnits / sol.getEpochStake / sol.getSysvarBytes / sol.bigModExp) added so call sites stay short. README runtime-introspection subsection documents the new helpers. Tests: 177 -> 184 host tests pass; all 7 example .so still build; 21 program-test integrations green. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
A deployed Solana program returns a single u64 to the caller, so every
`InvalidArgument` / `InvalidAccountData` / etc. failure site
collapses into the same wire code. Rust SDK / Anchor / SPL paper over
this with msg!(...) immediately before returning, so Explorer / RPC
logs identify which constraint actually fired.
This commit ports that pattern to the Zig SDK and applies it
internally so SDK-emitted failures are self-describing without any
extra effort from user code.
New surface (all comptime-tag, inline → zero overhead on happy path):
src/program_error.zig
fail(comptime tag, err) — the primitive
failFmt(tag, fmt, args, err) — formatted variant
src/require.zig (new)
require(cond, tag, err)
requireEq(a, b, tag, err)
requireNeq(a, b, tag, err)
requireKeysEq(&a, &b, tag, err)
requireKeysNeq(&a, &b, tag, err)
These mirror Anchor's require_* / require_keys_* / require_eq! macro
family one-for-one. KeysEq routes through pubkey.pubkeyEq's tuned
4×u64 compare path.
SDK internals adopted the pattern:
- sysvar.getSysvarBytes — splits rc=1 (offset out of range) vs rc=2
(sysvar not found) with distinct tags instead of folding both
into InvalidArgument.
- sysvar_instructions — relative_underflow / index_out_of_range
- account.expectSignerKey* — signer_key_mismatch
- account.expect{...}.{owner,owner_any,key,key_any} — typed
mismatches with module:reason tags
- entrypoint.parseAccountsWith — every per-slot constraint failure
logs `parse:<field_name>:<reason>` so callers see exactly which
of their declared expectations blew up
- entrypoint.{instructionData,programId} — accounts_not_consumed
- cpi.invokeSigned — too_many_signers / too_many_seeds
Cost analysis (program-test CU, post-change):
counter init+inc: 1674 CU (unchanged)
CPI transfer: 1246 CU (unchanged)
hello: 106 CU (unchanged)
Failure path adds ~100 CU (sol_log_ base) + 1 CU per tag byte. Tags
average 25-30 bytes so failures get +130 CU total. Failures are
panic-level events so this is negligible in practice.
Tests: 184 → 189 host (5 new for fail/failFmt + require family);
all 7 example .so still build; 21 program-test integrations green.
README adds a 'Diagnostic helpers' section with the Anchor parity
table.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Take 2 on the diagnostic helpers — now each fail-site logs '<basename>:<line> <tag>' instead of just '<tag>', matching what Anchor surfaces via msg!(...) when its require_* macros fire. API addition: every diagnostic helper takes @src() as its first parameter. Unlike Rust macros, Zig's @src() expands at function definition site, so the only way for a helper to surface caller-site context is to take it as an explicit argument: return sol.fail(@src(), 'vault:wrong_authority', error.IncorrectAuthority); try sol.requireKeysEq(@src(), a.key(), b, 'vault:auth', error.X); The full '<file>:<line> <tag>' string is built at comptime via std.fmt.comptimePrint + a comptime basename() helper, so the runtime cost stays exactly one sol_log_ syscall. Linux/Windows path separators both handled. All 21 SDK-internal callsites converted (cpi / sysvar / sysvar_ix / account.expect* / entrypoint.parseAccountsWith / ctx.{instructionData, programId}). Failure logs now look like: Program log: account.zig:251 expect:key_mismatch Program log: entrypoint.zig:384 parse:authority:not_signer Program log: sysvar.zig:125 sysvar:offset_out_of_range CU verified unchanged on happy paths: counter init+inc: 1674 (same) CPI transfer: 1246 (same) hello: 106 (same) Tests: 189 → 190 (added basename strip test). All 7 .so still build. 21 program-test integrations pass. README diagnostic section updated with the @src() rationale. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This repo is now a monorepo. The package surfaced at the root remains
solana_program_sdk (the on-chain core SDK); future companion packages
live under packages/.
Three planned sub-packages are stubbed with their READMEs so the
intended layout is visible:
- packages/spl-token — dual (on-chain CPI + off-chain ix builder)
- packages/spl-ata — dual
- packages/spl-memo — dual (simplest, used as scaffold reference)
Naming convention (also documented in ROADMAP):
1. Names containing 'program' → strictly on-chain
(e.g. solana_program_sdk)
2. Names starting with 'spl_' → dual-target; instruction.zig
submodule constructs byte buffers usable both on-chain (CPI)
and off-chain (transaction building), cpi.zig adds the
on-chain invoke() syntactic sugar
3. Other solana_* names → strictly off-chain (future: solana_client,
solana_keypair, solana_tx)
This mirrors the Rust ecosystem's solana-program / solana-sdk /
spl-* split so naming maps one-to-one when porting code.
No code changes — only README / ROADMAP + packages/{README, three
sub-package READMEs}. SDK still passes 190/190 host tests.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
The simplest SPL program, implemented as the reference skeleton for
the rest of the packages/ tree. Mirrors Rust spl-memo's surface
one-for-one and follows the dual-target pattern documented in the
monorepo ROADMAP.
API surface:
- id.PROGRAM_ID (modern v2) / id.PROGRAM_ID_V1 (legacy)
- instruction.memo / instruction.memoNoSigners (dual-target byte builder)
- cpi.memo / cpi.memoNoSigners (on-chain invoke() wrapper)
Allocation-free in both contexts. The memo program's wire format is
the simplest possible (raw UTF-8 instruction data, zero or more
signer-required accounts). Tests: 3 host tests pass; root SDK still
190/190.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…l Mollusk integration test
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Derive canonical ATA metas in allocation-free create builders and cover the wire shape with host tests. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Add ATA CPI create/createIdempotent wrappers that reuse the instruction builders and rebrand the callee from the passed ATA program account. Also add the on-chain demo and program-test dependency/import wiring so example_spl_ata_cpi.so builds alongside the existing SPL package demos. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add pub fn declareEntrypoint(comptime process_instruction: processInstruction) void ; so we can just only write processInstruction