# Changelog

All notable changes to Hopper land here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) once
1.0 ships; pre-1.0 minor versions may break the API.

## [Unreleased]

## [0.2.0] - 2026-05-15

### Added

- **`hopper-svm` Tier 4 — Language bindings (TypeScript / Python).**
  Closes the SVM roadmap by exposing the harness to non-Rust
  test code. Three pieces:

  **`hopper-svm-ffi`** — new C-ABI wrapper crate. Single
  `lib.rs` (~700 lines) exposes the `HopperSvm` surface as
  `extern "C"` functions through opaque handle types
  (`HopperSvmHandle`, `ExecutionResultHandle`). Builds three
  artifacts via `cargo build -p hopper-svm-ffi`: `cdylib` for
  dynamic loading, `staticlib` for embedding in compiled
  extensions, `rlib` for Rust integration tests. Forwards the
  `bpf-execution` feature so host languages can pick the
  Phase 1 (lightweight) or Phase 2 (real `.so` execution)
  build. Surface coverage:
  - Lifecycle: `hopper_svm_new`, `hopper_svm_with_solana_runtime`,
    `hopper_svm_set_compute_budget`, `hopper_svm_free`.
  - Account state: `hopper_svm_set_account`,
    `hopper_svm_get_lamports`, `hopper_svm_get_account_data`.
    The FFI keeps an internal account cache aligned with the
    Rust crate's stateless `process_instruction(ix, accounts)`
    model — host code seeds accounts once, dispatches feed the
    cache through, post-state replaces the cache.
  - Dispatch: `hopper_svm_dispatch` returns an opaque outcome
    handle; eight accessor functions read out logs, error,
    consumed units, transaction fee, post-accounts, return
    data.
  - String marshalling: `HopperFfiString` (`{ ptr, len }`)
    instead of null-terminated C strings — log lines may
    contain interior nulls. `hopper_svm_string_free` releases
    every owned string.
  - Version probe: `hopper_svm_ffi_version` returns the crate
    semver as a static null-terminated string for host-side
    compatibility checks.

  6 unit tests pin the FFI edges: string round-trip + empty
  handling, lifecycle smoke (new → with_solana_runtime →
  set_compute_budget → free), `u64::MAX` sentinel for unknown
  lamports, set + get account round-trip, replace-existing-
  account-in-place, version-string format.

  **`bindings/typescript/`** — `@hopper/svm` npm package.
  Minimal scaffold (no compile step beyond `tsc`):
  - `package.json` declaring `koffi` as the only runtime dep.
  - `tsconfig.json` strict-mode, ES2022 target.
  - `src/index.ts` (~450 lines) — wraps the FFI through
    `koffi.func` declarations, exposes high-level `HopperSvm`
    + `ExecutionResult` classes with `dispose()` + identical
    semantics to the Rust crate. Includes a hand-written
    base58 codec (no `bs58` dep) since pubkey display is the
    only place it's needed.
  - `README.md` covering build setup
    (`HOPPER_SVM_LIB_PATH` env var) + a quick-start example.

  **`bindings/python/`** — `hopper-svm` PyPI package via
  `cffi` ABI mode (no compile step):
  - `pyproject.toml` declaring `cffi >= 1.16` as the only
    runtime dep, Python ≥ 3.10.
  - `hopper_svm/__init__.py` re-exports the public API.
  - `hopper_svm/core.py` (~400 lines) — `cffi.FFI` `cdef`
    declarations matching the C surface exactly, plus
    Pythonic `HopperSvm` + `ExecutionResult` classes that
    are context managers (`with` blocks free the handle).
    Also includes a tiny base58 codec.
  - `README.md` mirroring the TS shape with Python idioms.

  Both bindings drive the *same* shared library — one source
  of truth in Rust, two host-language ergonomic shapes. Adding
  a new FFI export means: edit `hopper-svm-ffi/src/lib.rs`,
  add the koffi `lib.func` line in TS, add the cffi `cdef`
  line in Python. The pattern is mechanical enough that future
  surface growth doesn't drift between the bindings.

- **`hopper-svm` Tier 3 — Niche syscalls (introspection,
  obsolete sysvars, curve25519, heavy-crypto clear-error shims).** New
  module `hopper-svm/src/bpf/tier3_syscalls.rs` plus the
  matching adapter shims in `bpf/adapters.rs` and the
  registrations in `bpf/engine.rs`. Closes the syscall surface
  gap — programs that link these names now load and dispatch
  cleanly under `--features bpf-execution`.

  **Introspection (fully implemented):**
  - `sol_get_stack_height` — reports `BpfContext::cpi_depth`.
    Outermost program runs at depth 1; each
    `sol_invoke_signed_*` increments. CU pinned at 100.
  - `sol_remaining_compute_units` — reads the meter post the
    syscall's own charge. CU pinned at 100.
  - `sol_get_processed_sibling_instruction` — Hopper Phase 2
    doesn't yet ledger sibling instructions, so the syscall
    returns `0` (mainnet's empty-list convention) for any
    index. CU pinned at 100.

  **Obsolete-but-referenced sysvars (compat buffers matching a
  fresh-validator state):**
  - `sol_get_slothashes_sysvar` — empty SlotHashes vec
    (`len(u64) = 0`).
  - `sol_get_slothistory_sysvar` — all-zero SlotHistory
    bitvec (16,392 bytes: 16,384 bitvec + 8 next_slot).
  - `sol_get_stakehistory_sysvar` — empty StakeHistory vec
    (`len(u64) = 0`).

  **Generic `sol_get_sysvar` (fully implemented):**
  - Mainnet wire `(sysvar_id_addr, offset, out_addr, length)`.
  - Adapter resolves the 32-byte ID, looks up bytes via the
    existing per-sysvar handlers (Clock, Rent, EpochSchedule,
    EpochRewards, LastRestartSlot, plus the new SlotHashes /
    SlotHistory / StakeHistory compat buffers), copies the requested
    `[offset, offset + length)` slice into `out`. CU pinned at
    100.

  **Curve25519 (fully implemented — uses existing
  `curve25519-dalek` dep):**
  - `sol_curve_validate_point` — Edwards (curve = 0) or
    Ristretto (curve = 1). Returns `0` for valid, `1` for
    invalid (mainnet's inverted-bool convention). Edwards
    validation includes the prime-order subgroup check
    (`is_torsion_free`); Ristretto validation is the
    decompression check. CU pinned at 159.
  - `sol_curve_group_op` — Add (op = 0), Sub (op = 1),
    Mul-by-scalar (op = 2). Edwards or Ristretto. Returns `0`
    on success (writes 32-byte result), `1` on invalid input
    (off-curve operand or non-canonical scalar). CU pinned at
    2,000.

  **Heavy crypto — clear-error shims (Tier 4 work):**
  - `sol_poseidon` — Poseidon hash. Returns a structured
    `Custom` error naming `light-poseidon` as the canonical
    backing crate.
  - `sol_big_mod_exp` — RSA-style modular exponentiation.
    Error names `num-bigint`.
  - `sol_alt_bn128_group_op` / `sol_alt_bn128_compression` —
    BN254 / alt_bn128 ops. Error names `ark-bn254`.

  Why clear-error shims instead of "unknown syscall": mainnet programs
  that link these names but don't always call them at runtime
  (feature-flagged paths, fallback branches) load cleanly
  under Hopper. Programs that DO hit the path see an
  actionable error message naming the missing dep, which
  surfaces the next step instead of a cryptic VM trap. CU
  pinned at 1 per shim call so the meter accounting stays
  honest if a future Tier 4 release wires up real impls
  behind the same names.

  **20+ unit tests** in `tier3_syscalls.rs` pin every layer:
  introspection (stack_height returns cpi_depth, remaining
  reports post-charge), sysvar buffer writes, generic accessor
  copy semantics + out-of-range error, curve validation
  (basepoint valid, junk invalid, unknown curve errors), group
  ops (add basepoint to itself round-trips, unknown op
  errors), shim messages contain actionable crate names, and
  out-of-meter short-circuiting on every syscall.

  Adapter layer in `bpf/adapters.rs` adds 13 new
  `declare_builtin_function!` shims (one per syscall),
  registered against the canonical sbpf names in
  `bpf/engine.rs::build_loader`.

- **`hopper-svm` Tier 2 (e) — Config / Stake / Vote programs.**
  Three native simulators close the validator-side parity gap
  (matching `quasar-svm`'s "default-loaded programs" surface)
  without pulling in any validator crates as dependencies. All
  three are registered automatically by
  `HopperSvm::with_solana_runtime()` and individually
  selectable via `with_config_program()` / `with_stake_program()`
  / `with_vote_program()`.

  - **`hopper-svm/src/spl/config_program.rs`** —
    `ConfigProgramSimulator` for `Config1111111111111111111111111111111111111`.
    Single-instruction shape: parses
    `keys_len(u64) + n * (Pubkey + bool) + user_data`, validates
    every signer-flagged key against the instruction's account
    metas, writes user data into account 0 (padding/zeroing
    trailing bytes so a smaller Store doesn't leak the previous
    write). 3 unit tests pin: writes user data + zeroes tail,
    rejects unsigned signer-flagged key, rejects oversized
    payload. CU pinned at 450 (mainnet baseline).

  - **`hopper-svm/src/spl/stake_program.rs`** —
    `StakeProgramSimulator` for `Stake11111111111111111111111111111111111111`.
    Lifecycle slice: `Initialize` (0), `Authorize` (1),
    `DelegateStake` (2), `Withdraw` (4), `Deactivate` (5).
    Hand-coded 200-byte state layout matches upstream
    `StakeStateV2` bincode encoding so accounts round-trip
    cleanly: discriminator (u32 LE) + Meta
    (rent_exempt_reserve, authorized.staker,
    authorized.withdrawer, lockup.unix_timestamp, lockup.epoch,
    lockup.custodian) + optional Delegation (voter_pubkey,
    stake, activation_epoch, deactivation_epoch,
    warmup_cooldown_rate=0.25, credits_observed). `Withdraw`
    enforces a locked floor of `rent_exempt_reserve +
    active_stake` while delegated and not fully cooled, falling
    to just the rent-exempt floor once deactivation has cleared.
    Variants outside the slice (Split, Merge,
    AuthorizeWithSeed, InitializeChecked, AuthorizeChecked,
    AuthorizeCheckedWithSeed, SetLockup, SetLockupChecked,
    Redelegate, MoveStake, MoveLamports, DeactivateDelinquent)
    return a clear "not supported" error. 5 unit tests pin:
    Initialize writes Meta, full delegate-then-deactivate flow,
    Authorize swaps staker, Withdraw respects locked floor,
    unsupported variant errors. CU pinned at 750.

  - **`hopper-svm/src/spl/vote_program.rs`** —
    `VoteProgramSimulator` for `Vote111111111111111111111111111111111111111`.
    Administrative slice: `InitializeAccount` (0), `Authorize`
    (1), `Withdraw` (3), `UpdateValidatorIdentity` (4),
    `UpdateCommission` (5). Modeled header-slice (112 bytes):
    version (u32 LE) + node_pubkey + authorized_withdrawer +
    commission (u8) + authorized_voter at offset 80. Phase 1
    intentionally diverges from upstream's exact bincode for the
    versioned tail (lockouts, BTreeMap epoch->voter mapping)
    since application programs don't touch the TowerBFT
    machinery. Vote-emitting variants (Vote, VoteSwitch,
    UpdateVoteState{,Switch}, CompactUpdateVoteState,
    TowerSync{,Switch}, AuthorizeChecked, AuthorizeWithSeed)
    return a clear "not supported" error. 5 unit tests pin:
    InitializeAccount writes header, Authorize changes voter,
    Withdraw respects rent-exempt floor, UpdateCommission
    rejects commission > 100, unsupported variant errors. CU
    pinned at 3,000.

  Three new builders: `HopperSvm::with_config_program()`,
  `HopperSvm::with_stake_program()`,
  `HopperSvm::with_vote_program()`.
  `HopperSvm::with_solana_runtime()` now registers the full
  Solana validator-side surface: System (default) + Compute
  Budget + ALT + Config + Stake + Vote + SPL Token + Token-2022
  + ATA — closing the last out-of-the-box parity gap with
  `quasar-svm`.

- **`hopper-svm` Tier 2 (d) — Address Lookup Tables.** Two new
  modules close the v0-transaction support gap:
  - **`hopper-svm/src/alt.rs`** — byte-layout helpers
    + the `LookupTableMeta` struct + `read_meta` / `write_meta` /
    `address_count` / `read_address` / `append_addresses` /
    `resolve_lookup`. Wire format matches mainnet exactly:
    56-byte fixed header (discriminator u32, deactivation_slot
    u64, last_extended_slot u64, last_extended_slot_start_index
    u8, authority COption + 32-byte slot, 2-byte pad), then
    addresses packed 32 bytes each at offsets 56, 88, 120, …
    Hand-coded layout to avoid `bincode` dep growth. Constants
    pinned: `LOOKUP_TABLE_META_SIZE = 56`,
    `LOOKUP_TABLE_MAX_ADDRESSES = 256`,
    `DEACTIVATION_COOLDOWN_SLOTS = 513`. **6 unit tests** pin
    every layout edge: meta round-trips, frozen tables zero
    the authority slot, append-and-read, append rejects
    overflow, resolve writable-then-readonly, resolve rejects
    out-of-bounds, closeable-after-cooldown.
  - **`hopper-svm/src/spl/alt_program.rs`** —
    `AltProgramSimulator`, the BuiltinProgram impl for
    `AddressLookupTab1e1111111111111111111111111111`. Five
    instruction tags:
    - **0 / Create** — derives the PDA from
      `(authority, recent_slot, bump_seed)` under the ALT
      program ID, validates the supplied target address
      matches, allocates the 56-byte meta header, sets the
      authority + active state.
    - **1 / Freeze** — clears the authority field. Frozen
      tables can't Extend / Deactivate / Close.
    - **2 / Extend** — appends a `Vec<Pubkey>` to the data
      region, updates `last_extended_slot` +
      `last_extended_slot_start_index`, charges rent to the
      payer.
    - **3 / Deactivate** — writes the current slot into the
      table's `deactivation_slot`, starting the 513-slot
      cooldown.
    - **4 / Close** — moves lamports out, zeroes the data,
      reassigns ownership to the system program. Requires
      `is_closeable`: deactivated AND
      `current_slot - deactivation_slot > 513`.

  Authority enforcement on every state-mutating instruction —
  Extend / Deactivate / Close all check that `signer ==
  stored_authority` and reject otherwise. Frozen tables are
  immutable.

  **2 end-to-end unit tests** in `alt_program.rs`:
  - `full_alt_lifecycle` — create → extend → deactivate →
    close-rejected-before-cooldown → close-succeeds-after.
    Pins every state-machine transition + the cooldown
    boundary.
  - `freeze_blocks_extend` — Freeze → Extend rejected.

  **`HopperSvm::with_alt_program()`** registers the simulator
  against the canonical ALT program ID. **`HopperSvm::with_solana_runtime()`**
  now includes ALT in the registered set.

  **`HopperSvm::resolve_address_table_lookup(accounts,
  table_address, writable_indexes, readonly_indexes)`** —
  the v0-transaction resolution path. Reads the table from
  the supplied account state, expands writable + readonly
  index lists into concrete `(Vec<Pubkey>, Vec<Pubkey>)`
  pairs. Rejects: missing table account, invalid
  discriminator, table that's been deactivated past the
  cooldown (closed-but-not-yet-Closed state), out-of-bounds
  indexes. Tests that simulate v0 transactions can use this
  to project lookup-table-shaped messages onto the standard
  flat `AccountMeta` shape `process_transaction` consumes.

- **`hopper-svm` Tier 2 (c) — fee-payer accounting +
  transaction-level CU budget.** New
  `hopper-svm/src/fees.rs` ships `FeeCalculator`
  (default 5000 lamports/signature, configurable via
  `set_fee_calculator`), `count_unique_signers` for dedupe
  across the chain, `priority_fee` and `total_fee` formulas.

  New `HopperSvm::process_transaction(ixs, accounts, fee_payer)`
  is the **fee-aware** chain dispatcher:
  1. Computes `total_fee = base_fee + priority_fee`:
      - `base_fee = 5000 * num_unique_signers`
      - `priority_fee = compute_unit_limit * micro_lamports_per_cu / 1_000_000`
  2. Deducts the fee from `fee_payer` up front. If insufficient,
     aborts before the first instruction with
     `HopperSvmError::InsufficientFunds`.
  3. Runs every instruction in the chain; **fee stays
     deducted even if a later instruction fails** (matches
     mainnet's anti-spam rule).
  4. Returns `HopperExecutionResult` with new
     `transaction_fee_paid: u64` field populated.

  Compute-budget integration: the compute-budget program's
  `SetComputeUnitPrice` handler now writes into a shared
  `priority_fee_micro_lamports_per_cu` cell on `HopperSvm` so
  programs that include
  `ComputeBudgetInstruction::set_compute_unit_price(N)` early
  in their chain see the new rate applied to subsequent
  `process_transaction` calls. Setter helpers
  `set_priority_fee_micro_lamports_per_cu` and
  `priority_fee_micro_lamports_per_cu()` exposed for tests
  that want to seed the value directly.

  4 unit tests in `fees.rs` pin: default mainnet rate (5000
  lamports/sig), priority-fee integer-division semantics
  (sub-lamport remainders truncated), unique-signer dedupe
  across instruction-list boundaries, base+priority sum.

  `process_instruction_chain` keeps the no-fee semantics for
  fast unit tests; `process_transaction` is the
  mainnet-equivalent path.

- **`hopper-svm` Tier 2 (b) — system program parity.** All 8
  remaining system-program variants implemented, closing the
  surface to mainnet:
  - **Tag 3 — `CreateAccountWithSeed`**: parses
    base/seed/lamports/space/owner from the body, validates the
    target address against `Pubkey::create_with_seed`, allocates
    + assigns + funds the new account.
  - **Tag 9 — `AssignWithSeed`**: reassigns the owner of a
    seed-derived account; same derivation check.
  - **Tag 10 — `TransferWithSeed`**: debits a seed-derived
    source; the from_base must sign.
  - **Tag 4 — `AdvanceNonceAccount`**: rotates the durable
    nonce; the new nonce is deterministically synthesised from
    the harness's current `clock.slot` (since Hopper doesn't
    track recent_blockhashes — that sysvar is deprecated).
    Same-slot calls produce the same nonce; different-slot
    calls produce different nonces.
  - **Tag 5 — `WithdrawNonceAccount`**: moves lamports out of a
    nonce account; only the stored authority can withdraw.
  - **Tag 6 — `InitializeNonceAccount`**: writes the canonical
    80-byte nonce state with the requested authority and a
    starting durable nonce.
  - **Tag 7 — `AuthorizeNonceAccount`**: changes the stored
    authority; current authority must sign.
  - **Tag 11 — `UpgradeNonceAccount`**: legacy → current
    `Versions` migration (no-op since both layouts share the
    same byte shape in our impl).

  **Nonce-state byte layout** is hand-coded directly:
  ```text
  [0..4]   Versions tag (1 = Current)
  [4..8]   State tag (0 = Uninitialized, 1 = Initialized)
  [8..40]  authority (Pubkey)
  [40..72] durable_nonce (Hash, 32 bytes)
  [72..80] fee_calculator.lamports_per_signature (u64 LE)
  ```
  No `bincode` dep growth — the format is stable on mainnet
  and the 80-byte layout matches the upstream
  `solana_sdk::nonce::state::Versions`. Helper functions
  `read_nonce_state` / `write_nonce_state` /
  `synthesise_nonce_from_slot` keep the wire shape isolated
  in one spot for future maintenance.

  **9 new unit tests** added to `system_program.rs`:
  CreateAccountWithSeed validates derivation + rejects
  address mismatch, AssignWithSeed round trip,
  TransferWithSeed round trip, InitializeNonceAccount writes
  the 80-byte state, AdvanceNonceAccount changes nonce with
  slot, AuthorizeNonceAccount rejects wrong signer,
  WithdrawNonceAccount moves lamports, nonce-state
  read/write round-trip.

  **System program coverage now matches mainnet's hot path
  exactly.** Programs that use `system_instruction::transfer_with_seed`,
  durable-nonce transactions, or any of the other previously-
  rejected variants now run unmodified.

- **`hopper-svm` Tier 2 (a) — quasar-svm DX parity block.**
  Five small wins that close the surface gap with
  `quasar-svm`'s public API:
  - **`ExecutionOutcome::execution_time_us`** — wall-clock
    timing in microseconds, measured around the entire
    dispatch (built-in or BPF + post-validation). Stamped
    onto every outcome via a thin `dispatch_one` /
    `dispatch_one_inner` split. Mirrors quasar's
    `ExecutionResult.execution_time_us`. Read via
    `HopperExecutionResult::execution_time_us()`.
  - **Typed `assert_error(&expected)`** on
    `HopperExecutionResult` — structural equality on
    `HopperSvmError::describe()`. Mirrors quasar's
    `result.assert_error(ProgramError::InsufficientFunds)`.
    `assert_error_contains` (substring match) stays for
    flexible cases.
  - **Sysvar convenience setters** on `HopperSvm`:
    `set_clock`, `set_rent`, `set_epoch_schedule`,
    `set_last_restart_slot`, `set_epoch_rewards`. The big
    one: **`warp_to_slot(N)`** updates `clock.slot`,
    advances `unix_timestamp` by 400ms-per-slot (Solana's
    target slot duration), and recomputes `clock.epoch`
    against the configured `EpochSchedule`. Mirrors
    `quasar-svm`'s `svm.sysvars.warp_to_slot(200)`.
  - **`HopperSvm::with_solana_runtime()`** — chained
    builder that registers the full Solana runtime: system
    program (already from `new()`) + Compute Budget program
    + SPL Token + SPL Token-2022 + Associated Token Account
    simulators. Mirrors quasar's "SPL programs loaded by
    default on `QuasarSvm::new()`" without making `new()`
    itself heavyweight — fault-injection tests keep the
    bare `new()`, full-runtime tests call
    `.with_solana_runtime()`.
  - **`assert_inner_instruction_count(expected)`** on
    `HopperExecutionResult` — pin the CPI count for a
    program's instruction (e.g. "this transfer makes
    exactly 2 CPIs: system create + token init"). Plus
    `format_inner_instructions()` for ad-hoc transcripts.
    Mirrors `mollusk-svm`'s
    `Check::inner_instruction_count(N)` and quasar's
    equivalent.

  Together, these close the surface-level public-API gap
  with quasar-svm: every public method or field quasar
  exposes has a Hopper-side equivalent (and Hopper has
  more — `decode_header`, `hopper_accounts`,
  `decoded_logs`, the validation-policy switch, `Engine`
  trait seam). The remaining quasar-svm gap is bundled
  real `.so` files (we ship simulators) and the
  TypeScript / Python bindings (separate effort tracked as
  Tier 4).

- **`hopper-svm` Tier 1 — pre/post account validation.** New
  `hopper-svm/src/validation.rs` ships
  `validate_post_state(program_id, metas, pre, post, policy)`
  that enforces six structural invariants between an
  instruction's pre- and post-state, mirroring what
  `solana-bpf-loader-program` checks on mainnet:
  1. **Read-only accounts cannot change** — lamports, data,
     owner, executable all immutable when `meta.is_writable`
     is false.
  2. **Lamport conservation** across all metas — the runtime
     does not mint or destroy lamports.
  3. **Data writes require ownership** — `pre.owner == program_id`,
     with an exception for the system program creating an
     account (empty pre, system_program owner).
  4. **Owner reassignment requires ownership** — `pre.owner == program_id`,
     with the same creation exception. Backs
     `system_instruction::assign`.
  5. **Executable flag is immutable** — programs cannot toggle
     it; the BPF loader's `Finalize` is the only legitimate
     setter, and Phase 1 doesn't simulate that path.
  6. **Lamport debits require ownership** — `n.lamports < p.lamports`
     requires `pre.owner == program_id`. Anyone can credit any
     account; only the owner can debit. Combined with rule 2
     this makes "lamport theft" structurally impossible
     without ownership.

  Wired into `HopperSvm::dispatch_one` as a post-execution
  check on every successful instruction, both Phase 1 (built-in)
  and Phase 2 (BPF) paths. Validation failures roll back
  account mutations to the pre-state and surface a structured
  `HopperSvmError::AccountValidationFailed { account, reason }`
  so the test sees the offending account + the specific rule
  that fired. The validation step also writes a
  `Validation failed: <reason>` line into the log transcript
  so snapshot tests catch rule trips.

  **Default `Strict`, opt out via `with_lax_validation()`** for
  fast unit tests where structural invariants don't apply
  (hand-written Rust simulators don't always follow Solana's
  account-mutation rules). New `ValidationPolicy` enum exposed
  through the `hopper_svm` re-export. **9 unit tests** pin
  every rule against both passing and failing fixtures: identity,
  read-only mutation rejection, lamport-conservation rejection,
  legitimate transfer passes, non-owner data write rejection,
  data write on creation allowed, non-owner assign rejection,
  owner-self-assign passes, executable toggle rejection,
  non-owner lamport debit rejection, non-owner lamport credit
  allowed, lax policy disables all checks.

  This is the bug-revealing layer: a Hopper program that
  incorrectly mutates a non-owned account, breaks lamport
  conservation, or toggles the executable flag now FAILS the
  `hopper-svm` test with a clear pointer at the rule that
  fired, instead of silently passing locally and reverting on
  mainnet.

- **`hopper-svm` Tier 1 — Compute Budget program builtin.**
  New `hopper-svm/src/compute_budget_program.rs` ships
  `ComputeBudgetProgramSimulator` registered via
  `HopperSvm::with_compute_budget_program()`. Handles the five
  compute-budget instructions: `RequestUnits` (deprecated, tag
  0), `RequestHeapFrame` (1), `SetComputeUnitLimit` (2),
  `SetComputeUnitPrice` (3), `SetLoadedAccountsDataSizeLimit`
  (4). The signature feature: `SetComputeUnitLimit` writes to
  a shared `pending_cu_limit` cell on `HopperSvm`; the next
  `dispatch_one` reads + applies it as the budget override
  for that instruction. This makes
  `ComputeBudgetInstruction::set_compute_unit_limit(N)` take
  effect for subsequent instructions in
  `process_instruction_chain`, matching mainnet's
  transaction-level budget semantics within Hopper's
  instruction-by-instruction dispatch model. `MAX_COMPUTE_UNIT_LIMIT
  = 1_400_000` enforced (mainnet's hard cap). 4 unit tests
  pin the pending-cell write, the over-max rejection (cell
  untouched on rejection), the `RequestHeapFrame` Phase-1
  warning behavior, and the unknown-tag error message.

- **`hopper-svm` Tier 1 — `inner_instructions` field on
  ExecutionOutcome.** New `inner_instructions: Vec<InnerInstruction>`
  field on `ExecutionOutcome`. Each entry captures `program_id`,
  `accounts: Vec<AccountMeta>`, `data: Vec<u8>`, and
  `stack_height: u32` (1 = outermost; 2 = first-level CPI; …;
  capped at `MAX_CPI_DEPTH = 4`). Populated by
  `bpf::cpi::dispatch_cpi` after each successful inner call —
  the parent CPI is recorded first, then the inner call's own
  `inner_instructions` (CPIs the inner program made) are
  flattened in dispatch order. New `InnerInstruction` type
  exported from `crate::engine`. New
  `HopperExecutionResult::inner_instructions()` accessor.
  Mirrors the `inner_instructions` slice mainnet records on
  transaction metadata. **2 new unit tests** in
  `bpf/cpi.rs`: `dispatch_records_inner_instruction` pins the
  single-call recording with correct stack_height, and
  `nested_cpis_flatten_in_dispatch_order` pins that nested
  CPIs flatten parent-before-child in dispatch order with
  correct stack-height assignment.

- **`hopper-svm` Tier 1 — Token-2022 simulator.** New
  `hopper-svm/src/spl/token_2022.rs` ships
  `SplToken2022Simulator` registered via
  `HopperSvm::with_spl_token_2022_simulator()`. The legacy 9
  tags (0/1/3/4/5/7/8/9) delegate to the SPL Token
  simulator's logic — the on-disk Mint/Account layout is
  identical for non-extension accounts, and the owner check
  inside the Token handlers compares against `ctx.program_id`
  which is the Token-2022 ID at this dispatch site. Extension
  tags (22+) return a structured "Phase 1 doesn't support
  Token-2022 extensions yet" error pointing at
  `add_program(&id, "spl_token_2022")` for the real `.so`
  fallback. 3 unit tests pin transfer-via-delegation,
  extension-tag rejection, and InitializeMint preserving the
  Token-2022 owner.

- **`hopper-svm` Tier 1 — ATA simulator.** New
  `hopper-svm/src/spl/ata.rs` ships `SplAtaSimulator`
  registered via `HopperSvm::with_spl_associated_token_simulator()`.
  Handles `Create` (tag 0) and `CreateIdempotent` (tag 1).
  Validates the derived ATA address via
  `get_associated_token_address_with_program_id`, the system
  + token program addresses, and the mint's owner; allocates
  + initialises the token account inline (no CPI dispatch
  needed because the operation is deterministic and we have
  direct account-state access). Idempotent path validates
  the existing ATA matches the requested `(wallet, mint)`
  pair — wrong-mint reuse is a structured rejection. 4 unit
  tests pin: legacy-Token ATA create, Token-2022 ATA create
  (different derived address), wrong-derived-address
  rejection, idempotent-on-existing no-op + idempotent-on-
  wrong-existing rejection.

  Also adds `HopperSvm::with_spl_simulators()` — convenience
  builder that registers all three SPL simulators in one call.

- **`hopper-svm` Tier 1 — bundled SPL Token simulator.** New
  `hopper-svm/src/spl/token.rs` ships `SplTokenSimulator`,
  a pure-Rust `BuiltinProgram` impl of the 8 most-used SPL
  Token instructions: `InitializeMint` (0), `InitializeAccount`
  (1), `Transfer` (3), `Approve` (4), `Revoke` (5), `MintTo`
  (7), `Burn` (8), `CloseAccount` (9). Register against
  `SPL_TOKEN_PROGRAM_ID` via the new
  `HopperSvm::with_spl_token_simulator()` builder method.

  **Why a simulator and not a bundled `.so`?** Three reasons:
  - **Version stability.** No `.so` bytes to re-vendor on
    every Anza release; the Rust source updates with our
    semver cycle.
  - **Speed.** Phase-1 builtin dispatch is 10-100x faster than
    going through the BPF interpreter for the same
    instructions. Token transfers in tests run essentially
    free.
  - **Hopper-owned.** Every layer is hand-written Rust we can
    audit. Embedding third-party `.so` bytes would be an
    opaque trust dependency.

  Validation matches `spl_token::processor` end-to-end against
  the wire format: account-owner check (must be SPL Token
  program ID), mint-mismatch rejection on `Transfer` and
  `Burn`, signer = owner OR delegate (with delegated-amount
  decrement) on auth paths, `is_initialized` enforcement on
  `Mint` and on token-account state, supply / amount overflow
  detection via checked arithmetic, `CloseAccount` rejecting
  non-empty accounts.

  Unsupported tags (`FreezeAccount`, `*Checked` variants,
  `SetAuthority`, `InitializeMultisig`, etc. — 16 total) return
  a structured `BuiltinError` with the supported-tag list so
  test failures are actionable. Programs that need an
  unsupported instruction can fall back to BPF by registering
  the real SPL `.so` via `HopperSvm::add_program`.

  Token-2022 + ATA simulators land in subsequent passes; the
  same `BuiltinProgram` pattern applies. **3 unit tests** pin
  the happy path (initialize_mint → initialize_account →
  mint_to → transfer → burn against a single state machine
  with every numeric invariant verified), the mint-mismatch
  rejection, the close-account-rejects-non-empty rule, and
  the unsupported-tag error message.

- **Hopper segment system polish — DX cohesion pass.** Audit
  of `crates/hopper-core/src/account/`, `segment_map.rs`, and
  the macro-emitted accessors found three documentation gaps
  worth tightening:
  - Read-only segment escape (`<field>_segment_ref`) and the
    pre-declared `<field>_<seg>_ref` accessor now have the
    same RAII-contract paragraph as their mutable
    counterparts. Authors hovering over the generated
    accessor see the lease semantics inline.
  - Module-level cross-link added between `segment_map`
    (compile-time `StaticSegment`) and `account::registry`
    (runtime `SegmentDescriptor`). Resolves a recurring
    "should I use SegmentMap or SegmentRegistry?" question.
  - The "compile-time vs runtime segments" decision matrix is
    now in both module headers, so a developer landing in
    either file sees both options.

  No code changes — just documentation tightening that's
  safe to ship and immediately improves the DX of the
  segment system.

- **`hopper-svm` Phase 2.3 step 12 — `sol_invoke_signed_rust`
  fully implemented.** The Rust-ABI CPI variant — what
  `solana_program::program::invoke_signed` emits by default —
  now parses through Rust's `Rc<RefCell<&mut u64>>` and
  `Rc<RefCell<&mut [u8]>>` AccountInfo wrappers + the
  three-level-nested `&[&[&[u8]]]` signer-seeds shape, runs
  the same `cpi::verify_signer_seeds` + `cpi::dispatch_cpi`
  pipeline as the C variant, and writes mutations back
  through the resolved Rust-shape pointers. Realloc-across-
  CPI honored via the `data_len` slot inside the
  `RefCell<&mut [u8]>`.

  **Layout pinned in one file.** All version-sensitive
  offsets (`AccountInfo`'s 48-byte struct, `RcInner<T>`'s
  16-byte ref-count header, `RefCell<T>`'s 8-byte borrow
  flag, `Vec<T>`'s 24-byte ptr-cap-len header, the 34-byte
  `AccountMeta` stride, etc.) live in
  `bpf/cpi_rust::layout` as named constants. A future Anza
  toolchain bump that shifts any of these is a one-file
  fixup. Six unit tests pin every constant against its
  expected value so a silent shift produces a test failure
  rather than wrong runtime behaviour.

  **Pointer-chase helpers** in `cpi_rust`:
  - `follow_rc_refcell_u64(rc_ptr) → u64_addr` — walks
    `RcInner` (skip 16) → `RefCell` (skip 8) → `&mut u64`
    (read pointer).
  - `follow_rc_refcell_slice(rc_ptr) → (data_addr, data_len)`
    — same chain, but the value at the end is a 16-byte fat
    pointer.
  - `parse_account_infos`, `parse_instruction`,
    `parse_signer_seeds` — full structured readers for the
    three top-level wire shapes the Rust ABI passes.
  - `build_parsed_cpi` — the entry point the syscall
    adapter calls. Returns a `(ParsedCpi, Vec<RustAccountInfo>)`
    tuple where the second element captures every writeback
    address the post-call sync needs.

  With Rust-ABI in place, `hopper-svm` Phase 2 covers **both
  CPI variants** end-to-end. The BPF surface is now
  feature-complete for the typical Hopper test workload:
  programs that use the standard `invoke_signed`,
  `invoke_signed_unchecked`, all sysvars, all crypto
  syscalls, all log + memory + return-data syscalls, and PDA
  derivation should run unmodified under `hopper-svm` once
  `cargo check --features bpf-execution` clears.

  Phase 2.4+ remaining: integration smoke tests against a
  compiled `.so`, the obsolete sysvars (`SlotHashes`,
  `SlotHistory`, `StakeHistory`, `Fees`,
  `RecentBlockhashes`) most modern Hopper programs don't
  read, the `inner_instructions` slice on `ExecutionOutcome`,
  and JIT execution.
- **`hopper-svm` Phase 2.2 step 11 — CPI polish: log
  threading, realloc-across-CPI, EpochRewards sysvar.** Three
  follow-ups to the step-10 CPI implementation:
  - **Inner-instruction logs land in the outer transcript.**
    The `CpiDispatcher` closure type now takes
    `&mut LogCapture` so the inner program's `Program <id>
    invoke [N+1]`, `Program log:`, and `Program <id> success`
    framing append directly to the outer call's log buffer.
    Test-side snapshot diffs over CPI now read as one
    coherent transcript across the depth boundary. The
    `LogCapture::invoke` / `success` framing already tracks
    depth; sharing the buffer is what makes the depth
    tracking observable. New unit test
    `dispatcher_logs_thread_into_outer_transcript` pins the
    cross-boundary append.
  - **Realloc tail across CPI**. Phase 2.1's writeback
    clamped at the originally-mapped data capacity, so an
    inner program that grew an account's data was silently
    truncated. Phase 2.2 honors the
    `MAX_PERMITTED_DATA_INCREASE = 10240`-byte realloc tail
    the parameter buffer already reserves: the writeback
    accepts up to `original_data_len + 10240` bytes,
    updates the `SolAccountInfo`'s `data_len` field at byte
    offset 16 of its 56-byte record, and emits a
    `Program log: warning: CPI realloc truncated …`
    diagnostic if the inner call exceeded even the tail.
    The writeback path now produces results bit-identical to
    what `solana-bpf-loader-program` does for a CPI that
    reallocs an account.
  - **`sol_get_epoch_rewards_sysvar`** — final sysvar in
    scope. 96-byte wire layout (u64 distribution_starting_block_height
    + u64 num_partitions + 32-byte parent_blockhash + u128
    total_points + u64 total_rewards + u64 distributed_rewards
    + bool active + 15-byte zero pad — `#[repr(C)]`-shaped to
    match `solana_sdk::epoch_rewards::EpochRewards`). New
    `EpochRewards` struct + `Sysvars::epoch_rewards` field
    (defaults to "no rewards distributing"). Registered as
    the 25th syscall in the engine loader. New unit test
    `epoch_rewards_layout_canonical` pins every field offset
    + the 15-byte zero pad against the canonical wire format.

  **Phase 2 cumulative coverage**: 25 syscalls — 12 Phase 2.0
  + 2 PDA + 5 sysvar + 1 heap + 3 crypto + 2 CPI. The BPF
  surface is feature-complete for the typical Hopper test
  workload. Remaining for Phase 2.3+: the Rust-ABI CPI
  variant (`sol_invoke_signed_rust`), inner-instruction
  tracking field on `ExecutionOutcome` (the upstream
  `inner_instructions` slice), and the obsolete sysvars
  (`SlotHashes`, `SlotHistory`, `StakeHistory`, `Fees`,
  `RecentBlockhashes`) most modern Hopper programs don't
  read.
- **`hopper-svm` Phase 2.1 step 10 — CPI wired in.** Adds
  `sol_invoke_signed_c` (full implementation) and
  `sol_invoke_signed_rust` (registered with structured
  "Phase 2.2" error so programs see clean failure rather than
  "missing syscall"). The C-ABI variant ships:
  - **Wire-format parsing** of `SolInstruction` (40 bytes →
    program_id / metas / data pointers + lengths),
    `SolAccountMeta` (16 bytes per entry — pubkey_addr + flags),
    `SolAccountInfo` (56 bytes per entry — key_addr +
    lamports_addr + data_len + data_addr + owner_addr +
    rent_epoch + flags), and the nested `SolSignerSeeds[m]`
    pointing at `SolSignerSeed[q]` pointing at byte slices.
    Every pointer translation goes through `MemoryMapping::map`
    so an out-of-region read fails the syscall cleanly instead
    of segfaulting the host.
  - **Signer-seed verification** — for each meta marked
    `is_signer` in the inner instruction, either the same
    pubkey was a signer in the outer instruction (forwarded-
    signature path), or one of the supplied seed sets derives
    to the signer's pubkey under the calling program's ID
    (PDA-signing path). Reuses `do_sol_create_program_address`
    from step 6 — no new crypto. Returns
    `cpi_err::SIGNER_SEEDS_INVALID = 3` on mismatch.
  - **Recursive dispatch** through a `CpiDispatcher` closure
    on `BpfContext` that the engine populates with a clone of
    the `HopperSvm`. The closure runs `HopperSvm::dispatch_one`
    one depth deeper. **Depth bounded at `MAX_CPI_DEPTH = 4`**
    matching mainnet — `cpi_err::DEPTH_EXCEEDED = 2` past the
    limit.
  - **Account-state writeback** — after the inner call returns,
    every modified writable account's lamports / owner / data
    are written back through the SolAccountInfo pointers the
    caller supplied. Outer program continues reading from
    those addresses on resume so mutations are visible.
    Phase 2.1 clamps writeback at the originally-mapped data
    capacity; Phase 2.2 will plumb the realloc tail into the
    SolAccountInfo's `data_len` so growth across CPI is
    observable.
  - **CU costs** — 1 000 CU per invoke (matches mainnet's
    `invoke_units`).
  - **`MAX_SIGNERS = 16`** enforced — a 17th signer-seed set
    is `cpi_err::TOO_MANY_SIGNERS = 4`.
  - **6 new unit tests** in `bpf/cpi.rs` pin: outer-signer
    pass-through accepted, unauthorised signer rejected,
    `MAX_SIGNERS` enforcement, PDA-derived signer accepted
    (round-trips through `do_sol_create_program_address`),
    `dispatch_cpi` rejects recursion past `MAX_CPI_DEPTH`,
    `dispatch_cpi` returns `FAILED` when no dispatcher is
    configured (Phase 1 path).

  **Phase 2.2 (next session)**: full `sol_invoke_signed_rust`
  parsing through the `Rc<RefCell<…>>` AccountInfo layout
  (Rust-internal layout that drifts between toolchain
  versions; deferring keeps 2.1 stable), realloc tail
  plumbing across CPI boundaries, inner-instruction tracking
  for the `inner_instructions` log field. After 2.2, the BPF
  surface is feature-complete for the typical Hopper test
  workload.

  With CPI in place, `hopper-svm` Phase 2 covers **24 syscalls**
  (12 Phase 2.0 + 2 PDA + 4 sysvar + 1 heap + 3 crypto + 2 CPI),
  matching the typical-program coverage of `mollusk-svm` while
  remaining 100% Hopper-owned above the eBPF interpreter.
- **`hopper-svm` Phase 2.1 step 9 — crypto syscalls wired in.**
  Adds `sol_keccak256_`, `sol_blake3`, and
  `sol_secp256k1_recover_` to the BPF engine. Each delegates to
  a published, well-vetted crate:
  - `sol_keccak256_` → `sha3 = "0.10"`'s `Keccak256` (the legacy
    Ethereum variant — *not* the standardised SHA3-256, which
    differs in padding and produces different digests).
  - `sol_blake3` → `blake3 = "1"`.
  - `sol_secp256k1_recover_` → `secp256k1 = "0.29"` with the
    `recovery` feature.
  Wire shape: hashes accept the same `(addr, len)` chunk-list
  format as `sol_log_data` and the PDA-derive seed list — the
  `translate_seeds` adapter helper is reused. secp256k1 recover
  takes a 32-byte hash, recovery id (0..=3), 64-byte (r || s)
  signature, and writes a 64-byte uncompressed public key
  (X || Y, no leading 0x04 marker — matches upstream's wire).
  CU costs match the production runtime: hash = 85 base + 1
  per 16-byte chunk, secp256k1 recover = 25_000 flat. **8 new
  unit tests** pin known-good digests for Keccak-256
  (empty input → c5d24601…, "abc" → 4e03657a… — pinning these
  exact hex strings catches the SHA3-vs-Keccak swap bug class),
  BLAKE3 empty input → af1349b9…, streaming (multi-chunk) hashing
  matches one-shot, hash CU formula at 0/1/16/17/32 bytes,
  secp256k1 rejects bad signatures, secp256k1 rejects out-of-
  range recovery_id (7), out-of-meter on hash returns OutOfMeter
  without touching the output.

  Phase 2.1 remaining: **CPI** (`sol_invoke_signed_*`) — the
  recursive harness-dispatch one. After that, the BPF surface
  is feature-complete for the typical Hopper test workload.
- **`hopper-svm` Phase 2.1 step 8 — heap allocator wired in.**
  Adds `sol_alloc_free_` to the BPF engine. Bump-allocator
  semantics (matches upstream `agave-syscalls::SyscallAllocFree`):
  alloc returns monotonically-increasing 8-byte-aligned VM
  addresses inside the 32 KiB heap region at `MM_HEAP_START`;
  free is a no-op. Heap exhaustion returns null (0) without
  moving the cursor, so a subsequent alloc that fits can still
  proceed. Cursor lives on `BpfContext::heap_cursor` and resets
  to 0 at every fresh instruction (matches per-instruction
  allocator lifetime). New constants exposed: `HEAP_ALIGN = 8`,
  `HEAP_SIZE = 32 KiB`, `HEAP_VM_START = 0x300000000`. **6 new
  unit tests** pin: monotonic VM addresses with expected offsets
  (16, 16+24, etc.), 1-byte alloc rounds to 8, 9-byte rounds to
  16, free-is-noop (cursor unchanged after a free), exhaustion
  returns null and leaves cursor untouched (subsequent fitting
  allocs still succeed), zero-size alloc returns the cursor
  address without moving it (sentinel pattern), out-of-meter
  returns null without partial cursor movement.

  Phase 2.1 remaining: crypto syscalls (`sol_keccak256_`,
  `sol_secp256k1_recover_`, `sol_blake3`), CPI
  (`sol_invoke_signed_*`).
- **`hopper-svm` Phase 2.1 step 7 — sysvar fetches wired in.**
  Adds `sol_get_clock_sysvar`, `sol_get_rent_sysvar`,
  `sol_get_epoch_schedule_sysvar`, `sol_get_last_restart_slot_sysvar`
  to the BPF engine. Each writes a fixed-size `#[repr(C)]`-shaped
  byte buffer that's wire-compatible with the upstream
  `solana_sdk::sysvar::*` structs:
  - `Clock` — 40 bytes (5 * 8-byte fields).
  - `Rent` — 24 bytes (u64 + f64 + u8 + 7-byte zero pad).
  - `EpochSchedule` — 40 bytes (2 * u64 + bool with 7-byte pad +
    2 * u64).
  - `LastRestartSlot` — 8 bytes (single u64).
  Sysvar state lives on `Sysvars` (extended this release with
  `EpochSchedule` + `LastRestartSlot` fields, defaulted to
  mainnet-typical values). The engine snapshots sysvars onto
  `BpfContext::sysvars` at instruction start so a program sees
  a consistent view even if the outer test code mutates the
  harness mid-chain. **6 new unit tests** pin canonical layout
  for each sysvar (specifically including the zero-padding
  bytes — a future change to `Rent` or `EpochSchedule` can't
  silently leak garbage into the wire format), short-buffer
  rejection, and out-of-meter short-circuit.

  Phase 2.1 remaining: heap alloc (`sol_alloc_free_`), crypto
  syscalls (`sol_keccak256_`, `sol_secp256k1_recover_`,
  `sol_blake3`), CPI (`sol_invoke_signed_*`).
- **`hopper-svm` Phase 2.1 — PDA derivation syscalls wired in.**
  Adds `sol_create_program_address` and `sol_try_find_program_address`
  to the BPF engine's syscall registry. Pure compute (SHA-256 of
  `seed0 || seed1 || ... || program_id || "ProgramDerivedAddress"`,
  followed by an Ed25519 curve-point rejection through
  `curve25519-dalek`); no harness recursion, so there's no
  semantic surprise area. Rejects the same edge cases the
  upstream runtime does: `>16 seeds`, individual seed `>32
  bytes`, candidate landing on the curve. `try_find` walks
  bumps `255 → 0` until [`do_sol_create_program_address`] returns
  Ok, charging 1500 CU per attempt — same per-attempt cost as
  the production runtime. Adds `sha2 = "0.10"` and
  `curve25519-dalek = "4"` (small, Anza-vetted, already used
  elsewhere in the workspace) as deps. **7 new unit tests**:
  determinism across calls, `MAX_SEEDS` enforcement, `MAX_SEED_LEN`
  enforcement, per-call CU charging, `try_find` termination +
  determinism, marker-string pin (`"ProgramDerivedAddress"`),
  `try_find` returns None at the seed-count limit. Phase 2.1
  remaining: sysvar reads (`sol_get_*_sysvar`), heap alloc
  (`sol_alloc_free_`), crypto (`sol_keccak256_`,
  `sol_secp256k1_recover_`, `sol_blake3`), CPI
  (`sol_invoke_signed_*`).
- **`hopper-svm` Phase 2 — real BPF execution wired in.**
  Feature-gated behind `--features bpf-execution` (off by default
  so Phase 1's slim built-in path stays the out-of-the-box
  experience). When enabled, `HopperSvm::add_program(&id, "name")`
  loads `target/deploy/<name>.so`, registers it against `id`, and
  subsequent `process_instruction` calls dispatch real Hopper BPF
  programs through Anza's canonical `solana-sbpf 0.20`
  interpreter.
  - **Five-module split** under `hopper-svm/src/bpf/`:
    `parameter` (canonical Solana parameter-buffer serialiser /
    deserialiser, sbpf-independent, fully tested), `context`
    (`BpfContext` impls `solana_sbpf::vm::ContextObject`),
    `syscalls` (pure-Rust `do_*` logic), `adapters`
    (`declare_builtin_function!` calls — the only file that
    touches sbpf macro/translation API surface), `engine`
    (`BpfEngine` with ELF loading + memory regions + VM lifecycle).
    Drift between sbpf minor versions concentrates in `adapters`
    and `engine`; the rest stays untouched.
  - **12 syscalls in scope this release**: `sol_log_`,
    `sol_log_64_`, `sol_log_pubkey`, `sol_log_compute_units_`,
    `sol_log_data`, `sol_panic_`, `sol_memcpy_`, `sol_memset_`,
    `sol_memcmp_`, `sol_memmove_`, `sol_set_return_data`,
    `sol_get_return_data`. CU costs match the production runtime
    defaults so Phase 2 CU readouts equal mainnet figures.
  - **Phase 2.1 deferred**: CPI (`sol_invoke_signed_*` —
    recursive harness dispatch + signer-seed verification + acct
    remapping each have subtleties warranting focused work),
    sysvar reads (`sol_get_*_sysvar`), PDA derivation
    (`sol_create_program_address` / `sol_try_find_program_address`),
    heap alloc (`sol_alloc_free_`), and the crypto syscalls
    (`sol_keccak256_`, `sol_secp256k1_recover_`, `sol_blake3`).
    Each is documented in `bpf/syscalls.rs`'s module header.
  - **Memory layout** matches the production runtime: rodata at
    `MM_RODATA_START`, stack (256 KiB) at `MM_STACK_START`, heap
    (32 KiB) at `MM_HEAP_START`, parameter buffer at
    `MM_INPUT_START`. Stack and heap match
    `solana_program_runtime`'s defaults so Hopper test CU
    readouts reflect production stack-frame and heap-cost
    accounting.
  - **Failure semantics** match Phase 1: VM error or captured
    `sol_panic_` rolls back partial mutations and surfaces a
    `HopperSvmError::BuiltinError` with the program ID and
    captured message.
  - **27 unit tests** added across the five Phase-2 modules
    (parameter buffer round-trip + duplicate-meta compaction +
    realloc tail observation, BpfContext meter saturation,
    syscall logic for every `do_*` including out-of-meter
    short-circuit and base64 padding for all six length-mod-3
    cases, adapter range-overlap detection, engine ELF-miss
    returns `None`, merge-accounts replace-and-append).
  - **Build verification caveat**: the sandbox running this
    pass had no Rust toolchain, so `cargo check --features
    bpf-execution` was not runnable locally. The `adapters` and
    `engine` modules are written against `solana-sbpf 0.20.0`'s
    docs.rs surface; first-compile fixups (if any) will
    concentrate in three places: (1) `declare_builtin_function!`
    macro syntax for adapters, (2) `MemoryRegion::new_writable`
    / `MemoryMapping::new` argument ordering, (3)
    `Executable::from_elf` + `EbpfVm::new` + `execute_program`
    return shapes. Each is one of small, well-bounded sites the
    user's "I'll keep it up to date" maintenance commitment
    covers.
- **`hopper-svm` crate — Hopper-native execution harness, no
  external SVM wrapper.** Wholesale rewrite of the previous
  `mollusk-svm`-wrapped Phase 1. The harness is now Hopper's
  through and through: built-in program registry, system-program
  processor, compute meter, log buffer, sysvar state (clock + rent
  with deterministic defaults), account input/output, error model,
  and Hopper-aware result decoders are all implemented here from
  scratch. **Zero dependency on `mollusk-svm`, `quasar-svm`, or any
  other framework's harness.** Phase 1 ships a complete working
  built-in execution path:
  - `HopperSvm::new()` registers the system program by default.
  - `BuiltinProgram` trait + `InvokeContext` let users register
    custom built-ins for unit-test simulators or fault injection.
  - `SystemProgram` reference impl covers `CreateAccount`,
    `Transfer`, `Allocate`, `Assign` (the four most-used variants;
    `nonce` and `seed` siblings raise a clear "Phase 2" error so a
    test that hits them fails fast with an actionable message).
  - `Engine` trait is the seam for Phase 2: a future `BpfEngine`
    that wraps `solana-sbpf` lands as one new module plus one
    extra fall-through line in `HopperSvm::dispatch_one` — no
    other module changes.
  - `process_instruction_chain` carries account state forward
    across the chain and emits a chained log transcript with
    `# ix[N]` section dividers. Failure aborts the chain and rolls
    back partial mutations atomically (matching the on-chain
    runtime's all-or-nothing instruction effects).
  - `ComputeBudget` charges the system program a fixed 150 CU
    (matches mainnet) and configurable defaults for custom
    built-ins.
  - `LogCapture` emits the runtime's exact wire format —
    `Program <id> invoke [N]` / `Program log: <msg>` /
    `Program <id> consumed N of M compute units` /
    `Program <id> success` — so snapshot tests stay portable.
  - `HopperExecutionResult::decode_header(&pk)` reads the 16-byte
    Hopper account header by address; `hopper_accounts()` filters
    resulting accounts to only those with a valid Hopper header;
    `decoded_logs()` strips runtime framing and returns only
    program-emitted lines for snapshot stability.
  - Token factories (`create_keyed_system_account`,
    `create_keyed_mint_account[_with_program]`,
    `create_keyed_token_account[_with_program]`,
    `create_keyed_associated_token_account[_with_program]`)
    serialise SPL wire shapes via `Pack` directly — no SVM
    involvement, pure data construction.
  - Native error type `HopperSvmError` covers `UnknownProgram`,
    `BuiltinError`, `OutOfComputeUnits`, `EmptyChain`,
    `AccountIndexOutOfBounds`, `UnknownAccount`,
    `InsufficientFunds`, `AccountNotWritable`, `AccountNotSigner`,
    `Custom(u32)`. Every variant has a `describe()` for clean
    panic messages from `assert_success` / `assert_error_contains`.
  - 20 unit tests across the eight modules pin every behavior listed
    above, including system-transfer end-to-end, transfer-from-wrong-owner
    rejection, CreateAccount-on-initialised-target rejection,
    failure-rollback semantics, runtime log-format compliance,
    compute-meter saturation, ATA-derivation parity with
    `spl-associated-token-account`, and SPL `Pack` round-trip.

  **Phase 2 (planned, not in this release)**: wires
  `solana-sbpf 0.20` (Anza's canonical eBPF interpreter, the
  foundation every Solana SVM is built on) for real `.so` execution,
  the full Solana syscall surface (`sol_log_*`, `sol_panic_`,
  `sol_mem*`, `sol_get_*_sysvar`, `sol_create_program_address`,
  `sol_invoke_signed`, `sol_log_compute_units`, `sol_log_data`),
  CPI dispatch back into the harness, and the parameter-buffer
  serialisation Solana programs expect. The seam (`Engine` trait)
  is in place; the new file is a self-contained engine impl.
- **Interactive HTML flamegraph for `hopper profile elf`.** New
  `--html <out.html>` flag emits a self-contained interactive
  flamegraph — no CDN, no external resources, no JS framework: a
  single HTML file the user can open in any browser. Each symbol is
  a horizontal bar sized proportionally to its byte count, colour-cued
  by delta vs. baseline (lime if shrunk, orange if grown, neutral
  if unchanged). Hover for a tooltip with the full demangled name,
  byte size, percentage, and delta; click to pin a bar; type in the
  search box to filter by substring; `Esc` to clear. `--open` flag
  spawns the user's default browser pointed at the file (uses
  `open` on macOS, `xdg-open` on Linux, `cmd /c start` on Windows).
- **`--baseline <folded.txt>` flag for `hopper profile elf`.** Loads
  a previously-saved Brendan-Gregg folded-stack file and adds a
  delta column to both the terminal output and the HTML
  flamegraph. Per-symbol delta uses a "split at the *last* space"
  parser so symbol names containing spaces (Rust trait-method
  names like `<T as Trait>::method`) round-trip correctly.
- **`#[derive(Accounts)]` — Anchor-spelled drop-in for `#[hopper::context]`.**
  Anchor users porting code now get the spelling they expect without
  losing any of Hopper's account-attr surface. Identical generated code
  to the attribute form: same binder type, same accessors, same
  constraint validation pipeline, same Hopper-specific sugar
  (segment-tagged `mut(field, …)`, `read(...)`, the inline
  `#[hopper::pipeline]` / `#[hopper::receipt]` / `#[hopper::invariant]`
  stack). The full Anchor-grade constraint set is supported in either
  spelling: `init`, `init_if_needed`, `mut`, `signer`, `seeds`, `bump`,
  `payer`, `space`, `has_one`, `owner`, `address`, `constraint`,
  `token::*`, `mint::*`, `associated_token::*`, the Token-2022
  extension gates (`non_transferable`, `immutable_owner`,
  `transfer_hook`, `metadata_pointer`, …), `dup`, `sweep`,
  `executable`, `rent_exempt`, `realloc`, `zero`, `close`. The derive
  registers `account`, `signer`, `instruction`, and `validate` as
  helper attributes so `#[account(...)]`, `#[signer]`,
  `#[instruction(...)]`, and `#[validate]` field/struct annotations
  compile without an extra `use`. Lives in
  `hopper-macros-proc::derive_accounts`, exported through the
  `Accounts` symbol from `hopper::prelude::*`. Implementation reuses
  `context::expand_inner` behind a single `emit_struct: bool` flag —
  zero duplication of the constraint surface.
- **CLI polish pass two — final Quasar parity sweep.** Three commands
  Quasar shipped that Hopper still lacked, plus shared visual polish.
  - **`hopper add [-i|-s|-e <name>]`** — incremental scaffolding for
    an existing project. `-i/--instruction` creates
    `src/instructions/<name>.rs` with a Hopper-shaped context template,
    wires it through `src/instructions/mod.rs` and
    `mod instructions;` in `lib.rs`, and (for projects using the
    `#[hopper::program]` style dispatch) injects a handler template
    `#[instruction(N)]` handler at the next-available
    discriminator. For projects using a manual `match *disc` block,
    prints a "wire it in by hand" hint instead of guessing. `-s/--state`
    creates or appends to `src/state.rs` with a
    `#[hopper::state(disc = N, version = 1)]` struct picking the
    next-unused discriminator. `-e/--error` creates or appends to
    `src/errors.rs` with a discriminated `pub enum` plus a
    `From<...> for ProgramError` impl. All edits idempotent —
    re-running on the same name errors rather than overwriting.
  - **`hopper clean [-a|--all]`** — clear `target/{deploy,idl,client,
    profile,hopper}` while preserving `*-keypair.json` files (losing
    a program keypair means losing the on-chain program address —
    Quasar makes the same exception). With `-a`, also runs
    `cargo clean` for a full target wipe.
  - **Animated `hopper init` opening — leap reveal.** First-time
    interactive runs play a one-second FIGlet `HOPPER` animation:
    each row arrives from below with an ease-out-back bounce,
    leaving a trail of green grass-dots. Quasar has the blue nebula
    sweep; Hopper gets the leap. Auto-disables on subsequent runs
    (the wizard sets `ui.animation = false` after the first save),
    when stdout isn't a TTY, when `NO_COLOR` is set, or when the
    user toggles it off in `~/.hopper/wizard.toml`.
  - **Shared `style` module.** Centralised `bold`, `dim`, `color`,
    `success`, `fail`, `warn`, `step`, `human_size` helpers under
    `tools/hopper-cli/src/style.rs`. Auto-respects `NO_COLOR` and
    TTY detection, with explicit `--no-color` flag and
    `ui.color = false` config override. Replaces the ad-hoc ANSI
    escapes that were sprinkled through `cmd::lifecycle`. Build-size
    delta lines now colour the delta itself: green when shrinking,
    yellow when growing.
- **CLI polish — Quasar parity-plus pass.** `hopper init` now drops into
  an interactive `dialoguer` wizard when invoked without a `<path>`,
  prompting for project name, template, testing framework, and git
  policy. Choices are persisted to `~/.hopper/wizard.toml` so the
  second run skips the prompts. Four templates ship: `minimal`,
  `nft-mint` (uses `hopper-metaplex`), `token-2022-vault` (extension
  screening), `defi-vault` (segment-safe authority + balance with
  PDA verification). Pass `--template <name>` to pick one
  non-interactively, `--yes`/`-y` to use saved defaults without
  prompts, `--no-git` to skip git, `--interactive` to force the
  wizard even with a path.
- **`Hopper.toml` project config.** `hopper init` writes a declarative
  `Hopper.toml` at the project root with `[project]`,
  `[toolchain]`, `[testing]`, and `[backend]` sections. The rest of
  the CLI reads it to know how to build / test / deploy.
- **Binary-size delta on `hopper build`.** SBF builds now print a per-
  artefact summary: `✔ my_program.so   56.6 KiB  (-1.2 KiB)`. New
  binaries print `(new)`; unchanged binaries are silent.
- **Git automation in `hopper init`.** `commit` policy runs `git init`
  + initial commit; `init` policy runs `git init` only; `skip`
  leaves git alone.
- **`hopperzero.dev`** is the canonical project domain. Crate metadata,
  README headers, and footer links now point there.
- **`hopper-metaplex`** crate (optional, behind `--features metaplex`):
  `CreateMetadataAccountV3`, `CreateMasterEditionV3`,
  `UpdateMetadataAccountV2` builders, plus `metadata_pda` /
  `master_edition_pda` derivation helpers and a `BorshTape`
  stack-buffer encoder. Closes the Quasar-parity Metaplex gap.
- **`examples/hopper-nft-mint`** — reference NFT-mint program using the
  new Metaplex builders end-to-end (1-of-1 NFT with locked master
  edition).
- **Benchmark repo split** — Anchor, Anza Pinocchio, and lazy-dispatch
  parity targets now live in the sibling `hopper-bench` product repo with
  competitor locks, raw logs, and runner scripts. Release-facing framework
  docs only publish same-provenance benchmark columns.
- **`examples/hopper-token-2022-transfer-hook`** — Token-2022 transfer
  hook validation reference program.
- **DSL parity additions**: `#[derive(HopperInitSpace)]` standalone
  derive; `#[hopper::access_control(expr)]` handler attribute;
  `executable` and `rent_exempt = enforce|skip` field keywords;
  `init_if_needed` field keyword; `hopper_load!(slice => [a, b])`
  destructuring sugar; `err!` and `error!` short-form aliases in the
  prelude.
- **`hopper schema export --anchor-idl`** — emit Anchor 0.30-shaped
  IDL JSON from a `ProgramManifest`. Codama remains the preferred
  interop path; this exists for the long tail of wallets/explorers
  that still consume Anchor IDL.
- **`hopper-runtime::rent`** — `check_rent_exempt(account)` and
  `minimum_balance(data_len)` helpers backing the
  `rent_exempt = enforce` field keyword.
- **`docs/UNSAFE_INVARIANTS.md`** — supplemented with the full
  hopper-native unsafe surface (every `AccountView` unsafe method,
  `raw_input.rs`, `lazy.rs`, `pda.rs`, `mem.rs`, plus the expanded
  `cpi.rs` invariants).
- **Per-crate READMEs** for every crate under `crates/`, with
  hopperzero.dev / docs.rs / crates.io badges.
- **`AUDIT.md`** — comprehensive deep-review of Hopper vs Pinocchio,
  Quasar, Anchor zero-copy with verified parity findings, gap list,
  and implementation-pass record.

### Changed

- **README.md** Getting Started leads with the proc-macro path
  (`#[hopper::state]` / `#[hopper::program]`) for Anchor refugees;
  declarative `hopper_layout!` is the "Day Two" subsection.
- **Bench column** previously labelled "Pinocchio-style" is now
  "Pinocchio" (Anza). Pre-rename CU numbers retained with a
  `(deprecated)` marker so the historical record stays intact.
- **README** "What You Get" table extended with the new capabilities
  (Metaplex builders, Anchor IDL emit, full guard family,
  constraint vocabulary, Token-2022 extensions, handler attributes).
- **`cpi::invoke_unchecked` / `cpi::invoke_signed_unchecked`** now
  carry explicit seven-item `# Safety` invariant lists.
- **README "Segment-level borrows" row** correctly attributes
  `SegmentBorrowRegistry` to `hopper-runtime` (not `hopper-core`).

### Pre-publication checklist

- LICENSE: Apache-2.0, present at repo root.
- CONTRIBUTING.md: see [CONTRIBUTING.md](CONTRIBUTING.md).
- SECURITY.md: see [SECURITY.md](SECURITY.md).
- All published crates have `homepage = "https://hopperzero.dev"`,
  version-pinned `documentation = "https://docs.rs/crate/<crate>/0.2.0"`, and a per-crate
  `README.md`.

## [0.1.0] - 2026-05-10

### Added

- First public release line for the Hopper framework, CLI, and public companion
  crates. The top-level package target is `hopper-lang` with library crate
  name `hopper`. See [`AUDIT.md`](AUDIT.md) for a full feature audit.
