Skip to content

Add DeclareEntrypoint#16

Closed
DaviRain-Su wants to merge 109 commits into
joncinque:mainfrom
DaviRain-Su:main
Closed

Add DeclareEntrypoint#16
DaviRain-Su wants to merge 109 commits into
joncinque:mainfrom
DaviRain-Su:main

Conversation

@DaviRain-Su

Copy link
Copy Markdown

Add pub fn declareEntrypoint(comptime process_instruction: processInstruction) void ; so we can just only write processInstruction

@joncinque joncinque left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for the contribution! I like the concept, we just need to figure out what to do for the error type

Comment thread src/error.zig Outdated
Comment on lines +1 to +6
pub const ProgramError = error{
AlreadyInUse,
InvalidAccountType,
Uninitialized,
IncorrectSize,
};

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I have fix

DaviRain-Su and others added 26 commits January 3, 2026 14:03
- 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
DaviRain-Su and others added 28 commits May 12, 2026 13:50
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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants