Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,126 changes: 3,126 additions & 0 deletions crates/zeroclaw-solana-core/Cargo.lock

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions crates/zeroclaw-solana-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
[package]
name = "zeroclaw-solana-core"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
description = "Zero-solana-sdk, wasm32-wasip2-friendly Solana core: base58, borsh, versioned-transaction construction, durable-nonce handling, JSON-RPC over waki. Shared substrate for ZeroClaw Solana tool plugins."
publish = false

[lib]
crate-type = ["rlib"]

[dependencies]
borsh = { version = "1.5", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
bs58 = "0.5"
base64 = "0.22"
waki = { version = "0.5", optional = true }

[dev-dependencies]
proptest = "1.5"
# Dev-only, differential-testing dependencies: verify our hand-rolled
# zero-copy Token-2022 parser against the canonical SPL/Solana crates'
# own layout constants and pack/extension APIs. Neither of these are a
# dependency of the shipped rlib -- `cargo build`/`cargo check` without
# `--tests` never touches them, and the wasm32-wasip2 plugin builds don't
# either, so this does not reintroduce solana-program into the plugin binaries.
spl-token-2022 = "6"
solana-program = "2"
# Lightweight, standalone crate (no solana-program pulled in) exposing the
# canonical compact-u16 shortvec codec; used to differentially test our
# hand-rolled ShortVec encode/decode against ground truth. bincode drives
# its `Serialize` impl to get raw encoded bytes out.
solana-short-vec = "2"
bincode = "1"

[features]
default = []
# Enabled only by the wasm32-wasip2 plugin shims; keeps `waki` (and its
# wasi:http component bindings) entirely out of the default host build so
# `cargo test` never needs a wasm toolchain or network access.
waki-transport = ["dep:waki"]

[profile.release]
opt-level = "s"
lto = true
strip = true
codegen-units = 1

# Standalone crate: not part of a parent Cargo workspace, matching every
# plugin crate in zeroclaw-labs/zeroclaw-plugins.
[workspace]
21 changes: 21 additions & 0 deletions crates/zeroclaw-solana-core/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 The Solana-Edge Trio Contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
170 changes: 170 additions & 0 deletions crates/zeroclaw-solana-core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# zeroclaw-solana-core

**Track E entry** (shared core / infrastructure prize): a clean,
MIT/Apache-2.0-licensed, `wasm32-wasip2`-friendly Solana substrate — base58,
Borsh, hand-rolled versioned-transaction construction, durable-nonce
handling, zero-copy Token-2022 parsing, and JSON-RPC shaping over an
injected `HttpTransport` — that both [`token-risk-check`](../../plugins/token-risk-check)
and [`depin-attest`](../../plugins/depin-attest) actually import for their
real logic, not just link against for show.

## Why this exists

> "`solana-client` is not going to give it to you inside a WASM component...
> Expect real friction compiling the standard stack for `wasm32-wasip2`
> inside a WIT component." — this bounty's own "traps" section.

`solana-sdk`/`solana-client`/`solana-program` are not `wasm32-wasip2`
components-friendly. This crate has zero dependency on any of them — only
`borsh`, `serde`/`serde_json`, `bs58`, and `base64` — and carries no
tool-specific orchestration or WIT/`wit-bindgen` code at all. It builds and
tests on a plain host target with `cargo test`; a plugin importing it never
needs a wasm toolchain just to run its own tests.

## What's in here

- **`crypto`** — `Pubkey`/`Signature`/`Blockhash` newtypes over fixed-size
arrays, base58 in/out, the `SysvarRecentBlockhashes` constant.
- **`transaction`** — the actual Solana wire format, hand-rolled:
`MessageHeader`, `CompiledInstruction`, `LegacyMessage`, `MessageV0`,
`VersionedMessage`, `VersionedTransaction`, plus `Instruction`/
`AccountMeta` builder types, account-ordering/compilation
(`compile_legacy_message`), and `build_durable_nonce_transaction` — the
answer to the bounty's blockhash-expiry "trap": a normal blockhash expires
in ~150 blocks, which a transaction sitting in a human approval queue will
blow constantly; a durable nonce doesn't expire until explicitly advanced.
- **`rpc`** — an `HttpTransport` trait (each plugin supplies its own `waki`-
backed implementation, gated to the wasm build only — this crate never
depends on `waki` at all), JSON-RPC request shaping for `getAccountInfo`/
`getTokenLargestAccounts`, and a zero-copy Token-2022 mint parser.
- **`guardrails`** — `enforce_limits`/`enforce_destination`/
`GuardrailContext`: structural spend/destination caps for any future
transfer-shaped plugin built on this crate. (Neither current plugin needs
these directly — `token-risk-check` is read-only and `depin-attest` moves
zero lamports — but they're part of the reusable substrate for Tracks A/B.)

**Byte-compatible transactions without `#[derive]` everywhere.** Solana's
wire format uses "compact-u16" (shortvec) length prefixes, not Borsh's
default u32-LE prefix, and a legacy message carries no version-tag byte at
all. `ShortVec<T>` and `VersionedMessage` therefore have hand-written
`BorshSerialize`/`BorshDeserialize` impls; everything else derives normally.

**Untrusted-byte parsing never indexes or allocates on faith.** Every read
in the Token-2022 parser goes through `.get()`/`checked_add` instead of
direct indexing or unchecked arithmetic, and `ShortVec`'s Borsh decode never
pre-allocates based on an attacker-claimed length. Both were fixed after
being caught by differential/property testing, not designed in from the
start — see "Hardening pass" below.

## Building

```bash
cargo test # host tests, no wasm toolchain needed

# Supply-chain / license policy (see deny.toml)
cargo install cargo-audit cargo-deny
cargo audit
cargo deny check

# Fuzzing (Linux/macOS only -- see the fuzz feasibility note below)
cargo install cargo-fuzz
cargo +nightly fuzz run shortvec_differential
cargo +nightly fuzz run token2022_parser_no_panic
```

This crate is a plain library (`crate-type = ["rlib"]`), not a wasm
component itself — there's nothing to `cargo build --target wasm32-wasip2`
here directly; that happens in each consuming plugin.

## ✅ Verified build status

Compiled and tested with `rustc`/`cargo 1.97.1`:

| Command | Result |
|---|---|
| `cargo test` (host) | **40/40 passed**, 0 failed |
| `cargo clippy --all-targets` | clean, 0 lints |
| `cargo deny check` | advisories/bans/licenses/sources all ok |
| `cargo audit` | 0 vulnerabilities (3 informational, all dev-only, see below) |

### Hardening pass

Adversarial-input testing found four real bugs — none reachable through
either plugin's tool flow as originally called, but all real divergences
from correct/safe behavior:

- **`ShortVec` premature allocation.** `Vec::with_capacity(len)` sized a
buffer directly off an attacker-claimed shortvec length (up to `u16::MAX`)
before validating a single byte existed. Nested `ShortVec`s (e.g. every
`CompiledInstruction` inside a `ShortVec` of instructions) could amplify a
tiny malicious payload into many large upfront allocations. Fixed by
growing the `Vec` as bytes are actually read.
- **`decode_shortvec_len` diverged from the canonical wire format** in three
ways, found by differentially fuzzing it against the real `solana-short-vec`
crate (via `proptest`, and a `cargo-fuzz` target for CI): it accepted
non-minimal ("aliased") encodings like `[0x80, 0x00]` for the value 0,
accepted a third byte with the continuation bit still set, and never
validated the accumulated value actually fit in `u16`. All three are now
rejected, matching the canonical decoder exactly (see the doc comment on
`decode_shortvec_len`).
- **An off-by-one in the Token-2022 mint bounds check** required 1 fewer byte
than the subsequent field read actually used — unreachable in practice
(the outer `MINT_BASE_LEN` guard already ensures enough bytes), but latent.
Fixed by rewriting the whole parser to use `.get()`/`checked_add`
throughout instead of hand-verified index arithmetic.
- **A self-referential guardrail** in an earlier draft of `depin-attest`
checked a caller-supplied fee against a ceiling built from that *same*
caller-supplied value, so the check could never fail. Fixed by moving all
account identities to trusted config-only resolution (see that plugin's
own README for the current, correct design — the fee-cap concept itself
was later removed entirely once memo instructions, which move zero
lamports, replaced the placeholder attestation-program design).

Verification methods used, and why each was chosen:

- **`proptest`** (runs on stable Rust, no special tooling): round-trips and
differential checks against `solana-short-vec` and `spl-token-2022`, plus
a "never panics on arbitrary bytes" property for the Token-2022 parser —
512 randomized cases per run.
- **`spl-token-2022`/`solana-program`/`solana-short-vec` as dev-only
dependencies**: differentially verifies `MINT_BASE_LEN`/`ACCOUNT_TYPE_OFFSET`/
the six extension-type constants and parsed field values (including
`supply`) against the canonical crates' own pack/extension APIs — not
just self-consistency with hand-built fixtures. Confirmed via
`cargo tree -e normal` that none of these reach the shipped rlib or
either plugin's `.wasm` binary; they're dev-dependencies only.
- **`cargo-fuzz`**: the fuzz targets exist and build cleanly (`fuzz/`), and
run in CI on Linux, but **do not work on this Windows authoring host** —
verified directly, not assumed: the ASan build links (there is an MSVC
toolchain present, just not on `PATH`) but the compiled binary fails at
launch with `STATUS_DLL_NOT_FOUND` (the ASan runtime DLL needs a separate
LLVM/Clang install); the no-sanitizer build fails to *link* at all
(`__start___sancov_cntrs` unresolved — SanitizerCoverage's counter-section
registration is a PE/COFF-vs-ELF incompatibility, not fixable by
installing one more component). `proptest` is the practical local
substitute; the fuzz targets are still real and run in CI.
- **`wasm-opt` (Binaryen)**: does not support the Component Model binary
format at all yet ([binaryen#6728](https://github.com/WebAssembly/binaryen/issues/6728))
— confirmed locally, it refuses to even parse a `wasm32-wasip2` component.
Size discipline instead comes from each plugin's own release profile
(`opt-level = "s"`, LTO, strip, `codegen-units = 1`), which is working
well: both plugins land under 25% of the 1.5MB budget. See each plugin's
own build output for current numbers.
- **`cargo-audit`/`cargo-deny`**: both installed and run locally against
this crate's actual dependency tree (not configured blind); `deny.toml`
reflects the real license set and dependency shape observed. The 3
informational `cargo-audit` warnings (`bincode`/`libsecp256k1` unmaintained,
`rand` 0.7.3 unsound) are all reachable *exclusively* through the
differential-testing dev-dependencies (`spl-token-2022`/`solana-program`),
never through the shipped code.

### Still worth independently verifying

Nothing here blocks a build, but this is an assumption that couldn't be
checked against a live network or a canonical crate:

- **The `SysvarRecentB1ockHashes11111111111111111111` constant** is correct
per the documented Solana format, but (unlike the Token-2022 TLV offsets
and extension-type constants, which are differentially verified against
`spl-token-2022`) there's no equivalent canonical-crate check available
for a bare sysvar address constant.
68 changes: 68 additions & 0 deletions crates/zeroclaw-solana-core/deny.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# cargo-deny configuration. Run with `cargo deny check`.
#
# Every entry below was derived from real `cargo deny check` output against
# this workspace's actual Cargo.lock, not copied from a template blind --
# see the comments on each ignored advisory / allowed license for why.

[graph]
all-features = false
no-default-features = false

[advisories]
ignore = [
# All three are reachable *exclusively* through the dev-only
# differential-testing dependencies (spl-token-2022, solana-program,
# solana-short-vec -- see zeroclaw-solana-core/Cargo.toml) used to verify
# our hand-rolled Borsh/TLV/shortvec parsers against canonical ground
# truth. None of them are reachable from the shipped rlib or either
# wasm32-wasip2 plugin binary (verified: `cargo tree -p
# zeroclaw-solana-core -e normal` shows none of these).
{ id = "RUSTSEC-2025-0141", reason = "unmaintained bincode 1.3.3, pulled in only by solana-program (dev-dependency via spl-token-2022); not in the shipped build" },
{ id = "RUSTSEC-2025-0161", reason = "unmaintained libsecp256k1 0.6.0, pulled in only by solana-program (dev-dependency via spl-token-2022); not in the shipped build" },
# RUSTSEC-2026-0097 (unsound rand 0.7.3, same dev-only chain) is caught
# by `cargo audit` but cargo-deny 0.20.2 doesn't flag it in this tree at
# all ("no crate matched advisory criteria") -- the two tools apply
# different classification rules for "unsound"-severity notices. No
# ignore entry needed here since cargo-deny never raises it, but
# `cargo audit` (run separately in CI) still surfaces it for visibility.
]

[licenses]
confidence-threshold = 0.8
allow = [
"MIT",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"BSD-2-Clause",
"Unicode-3.0",
"Unlicense",
]

[licenses.private]
ignore = false

[bans]
multiple-versions = "warn"
# Left at the default ("allow") rather than "deny": cargo-deny 0.20.2 has a
# self-acknowledged bug (`bug[unresolved-workspace-dependency]` in its own
# output) resolving `{ workspace = true }` path dependencies, which makes it
# misidentify our internal `zeroclaw-solana-core` path dependency (used by
# both plugin crates) as an unpinned wildcard. It isn't one -- the version is
# pinned via `version.workspace = true` -- this is a tool limitation with a
# newer Cargo feature, not a real risk to gate on.
wildcards = "allow"
skip-tree = [
# `waki` (our wasi:http transport) pins `wit-bindgen ^0.34` for its own
# internal binding generation, while the plugin shims use `wit-bindgen
# 0.36` directly for their WIT world. These are two independent
# proc-macro invocations at build time -- harmless duplication (larger
# dependency graph / compile time only, verified no effect on the
# shipped .wasm size or correctness), not worth forcing a version
# unification that could destabilize a working build.
{ crate = "wit-bindgen@0.34" },
]

[sources]
unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
4 changes: 4 additions & 0 deletions crates/zeroclaw-solana-core/fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
target
corpus
artifacts
coverage
Loading