diff --git a/solana-alt-ops-skill/.github/workflows/ci.yml b/solana-alt-ops-skill/.github/workflows/ci.yml new file mode 100644 index 0000000..61c4be9 --- /dev/null +++ b/solana-alt-ops-skill/.github/workflows/ci.yml @@ -0,0 +1,17 @@ +name: validate + +on: + pull_request: + push: + branches: + - main + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: bash scripts/validate.sh diff --git a/solana-alt-ops-skill/.gitignore b/solana-alt-ops-skill/.gitignore new file mode 100644 index 0000000..9cb84a7 --- /dev/null +++ b/solana-alt-ops-skill/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.DS_Store +coverage/ +campaigns/ diff --git a/solana-alt-ops-skill/DEMO.md b/solana-alt-ops-skill/DEMO.md new file mode 100644 index 0000000..1ea1384 --- /dev/null +++ b/solana-alt-ops-skill/DEMO.md @@ -0,0 +1,69 @@ +# Demonstration: a safe ALT rollout plan + +This is an offline, fictional example. It deliberately does not create a +table, connect a wallet, submit a transaction, or claim that a transaction +landed on any cluster. + +## Scenario + +`RoutePilot` has a swap route that intermittently exceeds its legacy message +budget. The route reuses three stable, non-signer program accounts, while the +payer and user signer vary per request. + +## 1. Make the decision + +The operator collects an oversized legacy transaction, measures it, and +confirms that a v0 message without a lookup table still cannot carry the +repeated route accounts. The candidate accounts are stable and have recorded +provenance. The result is **v0 with an ALT**, not “put every account in a +table.” + +```text +Static: payer, user signer, user token accounts, dynamic route accounts +ALT: fixed program IDs / fixed route accounts only +Fallback: existing legacy route subset for clients without v0 support +``` + +## 2. Validate the proposed manifest + +```bash +$ node scripts/validate-alt-manifest.mjs --strict fixtures/valid-manifest.json +Valid ALT manifest: 1 table(s), 0 warning(s). +``` + +The manifest identifies a mutable table, its public authority, the multisig +review process, the stable address set, and the static address set. It is only a +review artifact; the operator must still fetch the real deployed table. + +## 3. Required evidence before a signer acts + +```text +1. On-chain table owner and authority match the approved manifest. +2. Table has remaining capacity and is not deactivating. +3. Extension transaction contains only the reviewed incremental addresses. +4. After confirmation, a later slot has passed. +5. Fresh RPC fetch returns the expected address list. +6. Exact v0 route compiles, has the expected static signer map, and simulates. +7. A devnet or low-risk canary succeeds before wider rollout. +``` + +## 4. Rollout gate + +```text +if wallet_v0_capable && table_fresh && v0_simulation_ok: + select ALT-backed v0 route +else: + select existing tested fallback +``` + +The application emits the table version and selected path. If ALT-only compile, +simulation, or signing failures rise, the feature flag disables new ALT traffic +without trying to mutate the table during the incident. + +## 5. Retire later + +When a replacement table is live and no clients select the old version, the +authorized operator deactivates the old table, waits until its deactivation slot +is absent from `SlotHashes`, verifies that state, and closes it to a separately +verified recipient. This never applies to a frozen table, because freezing +removes the authority needed for deactivation and closure. diff --git a/solana-alt-ops-skill/LICENSE b/solana-alt-ops-skill/LICENSE new file mode 100644 index 0000000..4f8fce9 --- /dev/null +++ b/solana-alt-ops-skill/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Anant Garg + +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. diff --git a/solana-alt-ops-skill/README.md b/solana-alt-ops-skill/README.md new file mode 100644 index 0000000..458b2e3 --- /dev/null +++ b/solana-alt-ops-skill/README.md @@ -0,0 +1,123 @@ +# Solana ALT Operations Skill + +Production operating guidance for Solana Address Lookup Tables (ALTs): deciding +when they are justified, creating and extending them safely, composing v0 +transactions, and retiring tables without breaking users. + +This is deliberately not a generic transaction tutorial. It is a runbook for +the awkward lifecycle problems that surface after an ALT becomes production +infrastructure: authority ownership, same-slot activation, compatibility, +rollbacks, observability, deactivation, and rent recovery. + +## Why this skill exists + +An ALT can replace repeated 32-byte account keys in a v0 transaction with +one-byte indices. That makes complex routes and batched flows possible, but it +also creates deployed state with its own authority and lifecycle. A careless +table change can turn otherwise valid transactions into opaque client failures. + +This skill helps an agent produce an evidence-backed plan before it touches a +table. It treats ALTs as versioned dependencies rather than a bag of public +keys. + +## What it covers + +- A decision gate for legacy, v0-without-ALT, and v0-with-ALT transactions. +- Immutable address manifests, ownership and change-control design. +- Safe create, extend, activation, fetch, compile, simulate, and rollout flow. +- v0 transaction composition with explicit fallback behavior. +- Security review for mutable authorities, poisoned manifests, and stale RPC + responses. +- Metrics, alert thresholds, incident triage, deactivation, and closure. +- A dependency-free manifest validator with fixture-backed tests. + +## Install + +Review the repository before installing. The installer never overwrites an +existing skill. + +```bash +git clone https://github.com/anantgar/solana-alt-ops-skill +cd solana-alt-ops-skill +./install.sh # ~/.claude/skills/solana-alt-ops +./install.sh --project # ./.claude/skills/solana-alt-ops +``` + +For other agent runtimes, copy the `skill/` directory into that runtime's +skills folder. + +## Use it for + +```text +"Our Jupiter route now exceeds the legacy transaction size. Decide whether an +ALT is appropriate and create a safe rollout plan." + +"Review this ALT manifest before our multisig extends the production table." + +"A v0 transaction started failing after an ALT extension. Triage it without +asking users to re-sign blindly." + +"We need to deprecate an ALT and recover its rent without breaking queued +transactions." +``` + +## Validate an ALT manifest + +The validator catches inexpensive mistakes before a proposal reaches a signer: +invalid public keys, duplicates, capacity overflows, unsafe mode/authority +combinations, and missing operational ownership metadata. + +```bash +npm test +node scripts/validate-alt-manifest.mjs fixtures/valid-manifest.json +node scripts/validate-alt-manifest.mjs --strict fixtures/valid-manifest.json +``` + +The validator is intentionally offline. It verifies a plan, not chain state. +Before any mainnet action, independently fetch the table account from a trusted +RPC and have the designated authority review the exact transaction bytes. + +## Repository layout + +```text +skill/ + SKILL.md Entry point and routing table + architecture.md Decision, manifest, and ownership design + operations.md Create/extend/rollout/retire runbook + transaction-composition.md v0 construction and client compatibility + security.md Authority, integrity, and signing controls + testing.md Local, devnet, and production verification + observability.md Metrics and incident triage +commands/ + plan-alt.md Discovery-to-rollout command contract + audit-alt.md Read-only review command contract +scripts/ + validate-alt-manifest.mjs Offline manifest validator +fixtures/ Good and intentionally broken examples +tests/ Node test coverage for the validator +``` + +## Non-negotiable safety rules + +1. Never put a signer in an ALT. Loaded addresses cannot sign a v0 message. +2. Do not use addresses added in the current slot; fetch the table again after + the next slot and simulate the exact v0 message first. +3. Do not silently switch an existing transaction path from legacy to v0. + Roll out behind a capability and error-rate gate. +4. A frozen table has no authority. It cannot later be extended, deactivated, + or closed to reclaim rent. Freeze only deliberately. +5. Do not promise a fixed deactivation duration. Closure becomes possible only + once the deactivation slot ages out of the `SlotHashes` sysvar. +6. Never include private keys, seed phrases, raw signing payloads, or API keys + in a manifest, issue, PR, or agent transcript. + +## Sources + +- [Solana: Address Lookup Tables](https://solana.com/developers/guides/advanced/lookup-tables) +- [Solana: Versioned Transactions](https://solana.com/docs/core/transactions/versioned-transactions) +- [Solana: RPC JSON structures](https://solana.com/docs/rpc/json-structures) +- [Official Address Lookup Table program](https://github.com/solana-program/address-lookup-table) + +## License + +[MIT](LICENSE) diff --git a/solana-alt-ops-skill/SECURITY.md b/solana-alt-ops-skill/SECURITY.md new file mode 100644 index 0000000..b3727e2 --- /dev/null +++ b/solana-alt-ops-skill/SECURITY.md @@ -0,0 +1,13 @@ +# Security policy + +This repository contains guidance and an offline manifest validator; it does +not custody keys or submit transactions. + +Do not disclose private keys, seed phrases, API keys, confidential routes, or +unreleased vulnerability details in public issues. For a security issue in the +skill itself, open a minimal private-contact request through the repository +owner's GitHub profile and include only the information needed to establish a +secure channel. + +Operational advice in this skill is not a substitute for an independent review +of the exact v0 message by the authorized signer(s). diff --git a/solana-alt-ops-skill/agents/alt-operator.md b/solana-alt-ops-skill/agents/alt-operator.md new file mode 100644 index 0000000..a18e49f --- /dev/null +++ b/solana-alt-ops-skill/agents/alt-operator.md @@ -0,0 +1,20 @@ +# ALT operator + +Use this focused agent for planning and read-only audit work around Solana +Address Lookup Tables. + +## Priorities + +1. Preserve signer and user safety over reducing transaction bytes. +2. Ask for the cluster, table address, authority process, and fallback before + recommending any write. +3. Fetch and distinguish live chain facts from an unverified manifest. +4. Produce explicit evidence requirements and an owner handoff. +5. Never request secrets or send/sign mainnet transactions. + +## Do not do + +- Treat a cached address array as a deployed table. +- Recommend a table extension for an unprovenanced or user-controlled address. +- Say a table can be closed immediately after deactivation. +- Recommend freezing without explaining the permanent loss of the close path. diff --git a/solana-alt-ops-skill/commands/audit-alt.md b/solana-alt-ops-skill/commands/audit-alt.md new file mode 100644 index 0000000..38164c6 --- /dev/null +++ b/solana-alt-ops-skill/commands/audit-alt.md @@ -0,0 +1,33 @@ +# /audit-alt + +Perform a read-only, evidence-based review of an existing Solana Address Lookup +Table and its proposed manifest diff. + +## Required inputs + +- Cluster, RPC endpoint, and table address. +- Manifest or proposed change diff. +- Intended client/repository release that consumes the table. +- Public authority/change-control details. + +## Audit checks + +1. Fetch and identify the table owner, authority/frozen state, deactivation + status, address count, last extension slot, and capacity. +2. Compare chain addresses with the manifest; report missing, extra, duplicate, + and unprovenanced addresses. +3. Check mode/authority/change-control consistency. +4. Identify signer/static-key violations in the consuming v0 message design. +5. Evaluate client, RPC, and decoder support for v0 messages. +6. Provide a canary and rollback recommendation, not a write command. + +## Output format + +```text +Verdict: ready / needs changes / blocked +Facts observed: +Manifest differences: +Risks: +Required evidence before approval: +Recommended owner actions: +``` diff --git a/solana-alt-ops-skill/commands/plan-alt.md b/solana-alt-ops-skill/commands/plan-alt.md new file mode 100644 index 0000000..e15c12c --- /dev/null +++ b/solana-alt-ops-skill/commands/plan-alt.md @@ -0,0 +1,31 @@ +# /plan-alt + +Produce a safe ALT rollout plan from a user's transaction-size or account-count +problem. This command is planning only: it must not create, extend, freeze, +deactivate, close, sign, or broadcast a table. + +## Required inputs + +- Cluster and target environment. +- Current transaction path and exact failure/sizing evidence. +- Known payer and signer roles (public identifiers only). +- Proposed recurring non-signer accounts and their provenance. +- Existing table address, if any. +- Authority/change-control process and fallback path. + +## Output + +1. Legacy/v0/ALT decision record. +2. Manifest draft compatible with `validate-alt-manifest.mjs`. +3. Step-by-step create/extend → later-slot fetch → exact compile → simulation → + canary rollout plan. +4. Wallet/RPC/indexer compatibility matrix. +5. Rollback and retirement plan. +6. List of actions reserved for the authorized operator. + +## Refuse / pause when + +- The user asks to provide or embed secrets. +- The cluster/table/authority is unknown for a requested write. +- Address provenance is absent or comes from an untrusted dynamic source. +- There is no tested fallback and the change could affect users. diff --git a/solana-alt-ops-skill/fixtures/invalid-manifest.json b/solana-alt-ops-skill/fixtures/invalid-manifest.json new file mode 100644 index 0000000..a7ea64c --- /dev/null +++ b/solana-alt-ops-skill/fixtures/invalid-manifest.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": 1, + "cluster": "mainnet-beta", + "tables": [ + { + "name": "bad", + "mode": "frozen", + "authority": "11111111111111111111111111111111", + "purpose": "too short", + "reviewedAt": "not-a-date", + "reviewedBy": "x", + "addresses": [ + "not-a-public-key", + "not-a-public-key" + ], + "staticAddresses": [] + } + ] +} diff --git a/solana-alt-ops-skill/fixtures/valid-manifest.json b/solana-alt-ops-skill/fixtures/valid-manifest.json new file mode 100644 index 0000000..118425c --- /dev/null +++ b/solana-alt-ops-skill/fixtures/valid-manifest.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": 1, + "cluster": "devnet", + "tables": [ + { + "name": "swap-route-core-v1", + "address": null, + "mode": "mutable", + "authority": "11111111111111111111111111111111", + "changeControl": "multisig proposal with independent manifest review", + "purpose": "Stable non-signer program and route accounts for the v0 route compiler.", + "reviewedAt": "2026-07-10", + "reviewedBy": "protocol-ops", + "addresses": [ + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", + "So11111111111111111111111111111111111111112" + ], + "staticAddresses": [ + "11111111111111111111111111111111" + ] + } + ] +} diff --git a/solana-alt-ops-skill/install.sh b/solana-alt-ops-skill/install.sh new file mode 100755 index 0000000..67303f7 --- /dev/null +++ b/solana-alt-ops-skill/install.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: ./install.sh [--project] + +Installs the skill without overwriting an existing destination. + default ~/.claude/skills/solana-alt-ops + --project ./.claude/skills/solana-alt-ops +EOF +} + +case "${1:-}" in + "") target_root="${HOME}/.claude/skills" ;; + --project) target_root="$(pwd)/.claude/skills" ;; + -h|--help) usage; exit 0 ;; + *) usage >&2; exit 2 ;; +esac + +source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/skill" +target="${target_root}/solana-alt-ops" + +if [[ ! -f "${source_dir}/SKILL.md" ]]; then + printf 'SKILL.md not found at %s\n' "${source_dir}" >&2 + exit 1 +fi + +if [[ -e "${target}" ]]; then + printf 'Refusing to overwrite existing skill: %s\n' "${target}" >&2 + exit 1 +fi + +mkdir -p "${target_root}" +cp -R "${source_dir}" "${target}" +printf 'Installed solana-alt-ops to %s\n' "${target}" diff --git a/solana-alt-ops-skill/package.json b/solana-alt-ops-skill/package.json new file mode 100644 index 0000000..3465d9a --- /dev/null +++ b/solana-alt-ops-skill/package.json @@ -0,0 +1,15 @@ +{ + "name": "solana-alt-ops-skill", + "version": "1.0.0", + "private": true, + "description": "Production operations skill and offline manifest validator for Solana Address Lookup Tables.", + "license": "MIT", + "type": "module", + "scripts": { + "test": "node --test tests/*.test.mjs", + "validate:fixture": "node scripts/validate-alt-manifest.mjs --strict fixtures/valid-manifest.json" + }, + "engines": { + "node": ">=20" + } +} diff --git a/solana-alt-ops-skill/rules/alt-operations.md b/solana-alt-ops-skill/rules/alt-operations.md new file mode 100644 index 0000000..4fa16e2 --- /dev/null +++ b/solana-alt-ops-skill/rules/alt-operations.md @@ -0,0 +1,10 @@ +# Solana ALT operating rules + +- Keep all signers and the payer static in a v0 message. +- Validate manifests offline, then verify actual state from a trusted RPC. +- Extend → wait for a later slot → fetch fresh → compile exact v0 → simulate → + canary. Do not compress this sequence. +- Use a versioned replacement table for material changes; ALT address lists are + append-only. +- Treat freeze as permanent loss of extension, deactivation, and closure. +- Require a tested fallback before widening production ALT traffic. diff --git a/solana-alt-ops-skill/scripts/validate-alt-manifest.mjs b/solana-alt-ops-skill/scripts/validate-alt-manifest.mjs new file mode 100644 index 0000000..c59d2d9 --- /dev/null +++ b/solana-alt-ops-skill/scripts/validate-alt-manifest.mjs @@ -0,0 +1,196 @@ +#!/usr/bin/env node + +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import process from 'node:process'; +import { pathToFileURL } from 'node:url'; + +const BASE58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; +const BASE58_INDEX = new Map([...BASE58].map((character, index) => [character, index])); +const ALLOWED_CLUSTERS = new Set(['mainnet-beta', 'devnet', 'testnet', 'localnet']); +const TABLE_NAME = /^[a-z][a-z0-9-]{2,63}$/; +const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/; + +function decodeBase58(value) { + if (typeof value !== 'string' || value.length === 0) return null; + let numericValue = 0n; + for (const character of value) { + const digit = BASE58_INDEX.get(character); + if (digit === undefined) return null; + numericValue = numericValue * 58n + BigInt(digit); + } + + const bytes = []; + while (numericValue > 0n) { + bytes.unshift(Number(numericValue & 0xffn)); + numericValue >>= 8n; + } + for (const character of value) { + if (character !== '1') break; + bytes.unshift(0); + } + return Uint8Array.from(bytes); +} + +function isPublicKey(value) { + const decoded = decodeBase58(value); + return decoded !== null && decoded.length === 32; +} + +function addIssue(collection, path, message) { + collection.push({ path, message }); +} + +function validateAddressList(value, path, errors, warnings) { + if (!Array.isArray(value)) { + addIssue(errors, path, 'must be an array'); + return []; + } + + const seen = new Set(); + for (const [index, address] of value.entries()) { + const itemPath = `${path}[${index}]`; + if (!isPublicKey(address)) addIssue(errors, itemPath, 'must be a 32-byte base58 public key'); + if (seen.has(address)) addIssue(errors, itemPath, `duplicates ${address}`); + seen.add(address); + } + if (value.length === 0) addIssue(warnings, path, 'is empty; confirm that this table is intentionally blank'); + return value; +} + +function validateTable(table, index, errors, warnings) { + const path = `tables[${index}]`; + if (table === null || typeof table !== 'object' || Array.isArray(table)) { + addIssue(errors, path, 'must be an object'); + return; + } + + if (!TABLE_NAME.test(table.name ?? '')) { + addIssue(errors, `${path}.name`, 'must use lowercase letters, digits, and hyphens (3–64 characters)'); + } + if (!['mutable', 'frozen'].includes(table.mode)) { + addIssue(errors, `${path}.mode`, 'must be "mutable" or "frozen"'); + } + if (typeof table.purpose !== 'string' || table.purpose.trim().length < 12) { + addIssue(errors, `${path}.purpose`, 'must explain the stable non-signer use case in at least 12 characters'); + } + if (!ISO_DATE.test(table.reviewedAt ?? '')) { + addIssue(errors, `${path}.reviewedAt`, 'must be an ISO calendar date (YYYY-MM-DD)'); + } + if (typeof table.reviewedBy !== 'string' || table.reviewedBy.trim().length < 3) { + addIssue(errors, `${path}.reviewedBy`, 'must identify the reviewer or review process'); + } + if (table.address !== undefined && table.address !== null && !isPublicKey(table.address)) { + addIssue(errors, `${path}.address`, 'must be null or a 32-byte base58 public key'); + } + + const addresses = validateAddressList(table.addresses, `${path}.addresses`, errors, warnings); + const staticAddresses = validateAddressList(table.staticAddresses, `${path}.staticAddresses`, errors, warnings); + if (addresses.length > 256) addIssue(errors, `${path}.addresses`, 'exceeds the ALT capacity of 256 addresses'); + + const loaded = new Set(addresses); + for (const [addressIndex, address] of staticAddresses.entries()) { + if (loaded.has(address)) { + addIssue(errors, `${path}.staticAddresses[${addressIndex}]`, 'also appears in addresses; keep signers/payer and other required static keys out of the ALT'); + } + } + + if (table.mode === 'mutable') { + if (!isPublicKey(table.authority)) { + addIssue(errors, `${path}.authority`, 'must be a 32-byte base58 public key for a mutable table'); + } + if (typeof table.changeControl !== 'string' || table.changeControl.trim().length < 3) { + addIssue(errors, `${path}.changeControl`, 'must name the review or governance control for extensions'); + } + } + + if (table.mode === 'frozen') { + if (table.authority !== null) { + addIssue(errors, `${path}.authority`, 'must be null for a frozen table; freezing removes the on-chain authority'); + } + if (table.changeControl !== undefined && table.changeControl !== null) { + addIssue(warnings, `${path}.changeControl`, 'is ignored for a frozen table; record the pre-freeze control in release notes instead'); + } + } + + if (table.address && loaded.has(table.address)) { + addIssue(warnings, `${path}.address`, 'also appears in its loaded address set; confirm this is intentionally required'); + } + if (table.mode === 'mutable' && table.authority && loaded.has(table.authority)) { + addIssue(warnings, `${path}.authority`, 'also appears in its loaded address set; an authority is commonly a signer and should normally remain static'); + } +} + +export function validateManifest(manifest) { + const errors = []; + const warnings = []; + if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) { + addIssue(errors, 'root', 'must be an object'); + return { errors, warnings, tableCount: 0 }; + } + if (manifest.schemaVersion !== 1) addIssue(errors, 'schemaVersion', 'must equal 1'); + if (!ALLOWED_CLUSTERS.has(manifest.cluster)) { + addIssue(errors, 'cluster', `must be one of: ${[...ALLOWED_CLUSTERS].join(', ')}`); + } + if (!Array.isArray(manifest.tables) || manifest.tables.length === 0) { + addIssue(errors, 'tables', 'must contain at least one table'); + return { errors, warnings, tableCount: 0 }; + } + + const names = new Set(); + const deployedAddresses = new Set(); + manifest.tables.forEach((table, index) => { + validateTable(table, index, errors, warnings); + if (table && typeof table === 'object') { + if (names.has(table.name)) addIssue(errors, `tables[${index}].name`, `duplicates table name ${table.name}`); + names.add(table.name); + if (table.address) { + if (deployedAddresses.has(table.address)) addIssue(errors, `tables[${index}].address`, `duplicates deployed table address ${table.address}`); + deployedAddresses.add(table.address); + } + } + }); + + return { errors, warnings, tableCount: manifest.tables.length }; +} + +function formatIssue(level, issue) { + return `${level} ${issue.path}: ${issue.message}`; +} + +export async function run(argv = process.argv.slice(2)) { + const strict = argv.includes('--strict'); + const json = argv.includes('--json'); + const paths = argv.filter((argument) => !argument.startsWith('--')); + if (paths.length !== 1) { + throw new Error('Usage: node scripts/validate-alt-manifest.mjs [--strict] [--json] '); + } + + let manifest; + try { + manifest = JSON.parse(await readFile(paths[0], 'utf8')); + } catch (error) { + throw new Error(`Unable to read valid JSON from ${paths[0]}: ${error.message}`); + } + + const result = validateManifest(manifest); + if (json) { + console.log(JSON.stringify({ ...result, valid: result.errors.length === 0 && (!strict || result.warnings.length === 0) }, null, 2)); + } else { + for (const issue of result.errors) console.error(formatIssue('ERROR', issue)); + for (const issue of result.warnings) console.error(formatIssue('WARN', issue)); + if (result.errors.length === 0 && (!strict || result.warnings.length === 0)) { + console.log(`Valid ALT manifest: ${result.tableCount} table(s), ${result.warnings.length} warning(s).`); + } + } + + if (result.errors.length > 0 || (strict && result.warnings.length > 0)) process.exitCode = 1; + return result; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + run().catch((error) => { + console.error(`ERROR: ${error.message}`); + process.exitCode = 2; + }); +} diff --git a/solana-alt-ops-skill/scripts/validate.sh b/solana-alt-ops-skill/scripts/validate.sh new file mode 100755 index 0000000..da488c8 --- /dev/null +++ b/solana-alt-ops-skill/scripts/validate.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${root_dir}" + +npm test +npm run validate:fixture +bash -n install.sh + +for file in \ + skill/SKILL.md \ + skill/architecture.md \ + skill/operations.md \ + skill/transaction-composition.md \ + skill/security.md \ + skill/testing.md \ + skill/observability.md \ + commands/plan-alt.md \ + commands/audit-alt.md \ + agents/alt-operator.md \ + rules/alt-operations.md; do + test -f "${file}" +done + +printf 'Skill repository validation passed.\n' diff --git a/solana-alt-ops-skill/skill/SKILL.md b/solana-alt-ops-skill/skill/SKILL.md new file mode 100644 index 0000000..e6a2ec0 --- /dev/null +++ b/solana-alt-ops-skill/skill/SKILL.md @@ -0,0 +1,75 @@ +--- +name: solana-alt-ops +description: Design, operate, audit, and retire Solana Address Lookup Tables (ALTs) safely. Use for v0 transaction sizing, ALT manifests, authority design, table lifecycle, v0 compatibility, production rollout, deactivation, closure, and ALT-related transaction failures. +user-invocable: true +--- + +# Solana ALT Operations + +Use this skill when a Solana application needs more accounts than its current +transaction path can carry comfortably, or when an existing Address Lookup +Table needs to be changed, audited, or retired. + +An ALT is on-chain infrastructure. It compresses account references in a v0 +message; it does not make a transaction exempt from packet size, compute, +account-lock, signer, program, or client-compatibility constraints. + +## Safety boundary + +- Treat every mainnet table change as a governance action. Do not sign, submit, + freeze, deactivate, or close anything without the user naming the cluster, + table, authority process, and intended recipient for reclaimed rent. +- Never ask for or handle a seed phrase, private key, raw secret, or wallet + export. An authorized wallet or multisig must review the final transaction. +- Do not use an ALT as a reason to skip simulation or to retry a failed user + transaction blindly. +- State uncertainty plainly. Fetch the deployed table and current RPC behavior; + do not infer either from a stale manifest. + +## Fast routing + +| User intent | Read next | +| --- | --- | +| Decide whether to use an ALT | [architecture.md](architecture.md) | +| Plan a new table or extension | [operations.md](operations.md) | +| Compile or debug a v0 transaction | [transaction-composition.md](transaction-composition.md) | +| Review authority and address provenance | [security.md](security.md) | +| Build test evidence or roll out safely | [testing.md](testing.md) | +| Investigate failures or watch production health | [observability.md](observability.md) | + +## Default operating procedure + +1. **Classify the failure.** Gather the exact serialized size, instruction + account set, signer set, wallet capability, RPC endpoint, and simulation + result. Do not assume an ALT is the correct fix. +2. **Choose the least complex path.** Prefer legacy when it fits and users need + broad compatibility. Prefer v0 without an ALT when v0 features are needed + but address compression is not. Add an ALT only for a stable, repeated, + non-signer address set. +3. **Make the dependency explicit.** Create a reviewed manifest with purpose, + cluster, table version, address provenance, authority, change-control owner, + and rollback/fallback path. Validate it locally before proposing a change. +4. **Separate lifecycle actions.** Create or extend in one transaction, wait at + least one later slot, fetch the table again, compile the exact v0 message, + simulate it, then canary it. Never rely on an address extended in the same + bank slot. +5. **Roll out reversibly.** Gate by client capability, preserve the prior path, + record the table version used per transaction, and only increase traffic + after successful canary evidence. +6. **Retire deliberately.** Migrate clients away first. A table can be closed + only after deactivation ages out of `SlotHashes`; a frozen table has no + authority and cannot be deactivated or closed. + +## Deliverable contract + +For a plan or review, produce: + +1. A decision record: legacy, v0-without-ALT, or v0-with-ALT—and why. +2. A table manifest or a read-only diff, with address provenance and ownership. +3. Exact verification commands or code paths, plus expected evidence. +4. A canary, rollback, and user-impact plan. +5. An explicit list of actions the authorized operator must perform themselves. + +For a diagnosis, produce a timeline of fetched table state, slot, compiled +message version, simulation logs, wallet/RPC behavior, and a minimally risky +next action. Do not call an issue an ALT problem without that evidence. diff --git a/solana-alt-ops-skill/skill/architecture.md b/solana-alt-ops-skill/skill/architecture.md new file mode 100644 index 0000000..dcc6011 --- /dev/null +++ b/solana-alt-ops-skill/skill/architecture.md @@ -0,0 +1,97 @@ +# ALT architecture and decision record + +## When an ALT earns its complexity + +Use an ALT only when the same non-signer addresses recur across v0 transactions +and compressing them materially improves the path. Typical candidates are +stable routing accounts, protocol program accounts, oracle accounts, pool +accounts, and long-lived configuration accounts. + +Do not use an ALT merely because a transaction feels complicated. Start with +this decision tree: + +```text +Can a legacy transaction fit with required instructions, signatures, and data? + ├─ yes → Use legacy unless a verified product requirement needs v0. + └─ no + Can a v0 message fit without a lookup table? + ├─ yes → Use v0 without an ALT; avoid deployed table lifecycle. + └─ no + Are most excess accounts stable, repeated, non-signers, and governed? + ├─ yes → Plan an ALT-backed v0 path. + └─ no → Split/batch the operation, redesign the instruction set, + or use a different product flow. +``` + +An ALT does not solve high compute, account contention, stale blockhashes, +wallet capability gaps, excessive instruction data, or invalid account metas. + +## The non-signer invariant + +Loaded ALT addresses cannot sign. Keep the payer, every signer, and every +account whose signature semantics matter in the static message keys. Compile +the exact message and inspect its signer set before presenting it to a wallet. + +## Treat an ALT as a versioned dependency + +Name tables by scope and version, not by a vague project name: + +```text +swap-route-core-v1 +swap-route-core-v2 +``` + +Prefer a new immutable table over quietly mutating a widely consumed one when a +change affects routing, permissions, or client assumptions. A table version +should be discoverable in telemetry and in the application's release record. + +## Manifest contract + +Keep a public, non-secret plan alongside the client code. The bundled validator +expects this shape: + +```json +{ + "schemaVersion": 1, + "cluster": "devnet", + "tables": [ + { + "name": "swap-route-core-v1", + "mode": "mutable", + "authority": "", + "changeControl": "squads-v4 proposal", + "purpose": "Stable non-signer accounts used by the route compiler.", + "reviewedAt": "2026-07-10", + "reviewedBy": "protocol-ops", + "addresses": [""], + "staticAddresses": [""] + } + ] +} +``` + +The manifest is a review artifact, not a source of truth for chain state. A +deployment record should add the actual table address, creation signature, +table account snapshot, slot, authority verification, and an exact source hash. + +## Authority choices + +| Mode | Use when | Consequence | +| --- | --- | --- | +| Mutable, single key | Short-lived devnet experiment | Fast but weak operational control. Never default on mainnet. | +| Mutable, multisig/governance | A table legitimately changes over time | Every extension is a reviewed governance action. | +| Frozen | Stable, fully reviewed address set | No future modification. It also removes the ability to deactivate or close the table. | +| Versioned replacement | A mature table needs a material change | Safest production default: introduce v2, migrate clients, then retire v1. | + +Record the actual authority public key and the control process. “The team owns +it” is not an auditable answer. + +## Address provenance + +For each address, record one of: program ID, official protocol registry entry, +PDA derivation with seeds and program ID, config account fetched from a known +registry, or a reviewed deployment address. Do not copy accounts from an +untrusted route response into a mutable mainnet table. + +An ALT stores only addresses, not labels or intent. The manifest supplies the +human meaning that the chain account lacks. diff --git a/solana-alt-ops-skill/skill/observability.md b/solana-alt-ops-skill/skill/observability.md new file mode 100644 index 0000000..13f5b76 --- /dev/null +++ b/solana-alt-ops-skill/skill/observability.md @@ -0,0 +1,57 @@ +# ALT observability and incident response + +## Minimum metrics + +Tag all relevant metrics with `cluster`, `transaction_version`, `alt_path`, and +`alt_table_version` (or address). Track: + +- Compile success/failure, categorized by table fetch, table resolution, and + message-size failure. +- Simulation success/failure and top log/error class. +- Wallet signing failure rate by wallet/version. +- Send, processed, confirmed, and finalized rates and latency. +- Fallback selection rate. +- RPC table-fetch latency and stale/missing response rate. +- Table address count/capacity and last extension/deactivation slot. +- Decoder/indexer unresolved-loaded-address rate. + +Avoid logging sensitive user payloads. Public keys and transaction signatures +may still be personal or commercially sensitive in context; apply the product's +logging policy. + +## Alert conditions + +Alert on a material difference between an ALT path and its baseline fallback: + +```text +compile failures increase after a table version rollout +simulation or wallet signing failures increase for v0 only +table fetch becomes missing/stale on one production RPC +loaded-address decoding begins to fail in support/indexing systems +unexpected extension/deactivation or authority change is observed +``` + +Use a deployment-time baseline, not a universal threshold. Traffic patterns and +program behavior differ across applications. + +## Incident playbook + +1. **Contain:** stop new traffic to the affected ALT path with the feature gate. + Do not mutate the table under pressure. +2. **Preserve evidence:** capture application release, manifest commit, table + snapshot, RPC endpoint, slot, exact compiled message, simulation logs, and + representative transaction signatures. +3. **Classify:** missing/stale table, same-slot extension, malformed compiler + use, unsupported wallet/RPC, wrong cluster, or unrelated instruction error. +4. **Restore:** use the tested fallback path if it is safe for the user's + action. Do not re-sign an altered transaction without informed user review. +5. **Remediate:** change code or publish a replacement table version through + normal governance. Append-only tables are rarely the right emergency fix. +6. **Learn:** add a regression fixture and update the manifest/release record. + +## Retirement monitoring + +Before deactivation, confirm zero supported clients select the old table and +that queued/background jobs have migrated. Continue monitoring fallback and +old-table requests during the compatibility window. Only the authorized +operator should submit deactivation or closure transactions. diff --git a/solana-alt-ops-skill/skill/operations.md b/solana-alt-ops-skill/skill/operations.md new file mode 100644 index 0000000..86fc10f --- /dev/null +++ b/solana-alt-ops-skill/skill/operations.md @@ -0,0 +1,111 @@ +# ALT lifecycle operations + +## 1. Preflight before a create or extension + +Collect and review these facts before an authority is asked to sign: + +- Cluster and RPC endpoint, including the endpoint used by production clients. +- Current table address, owner, authority, deactivation state, and full address + list fetched directly from RPC. +- A manifest diff with address provenance and table capacity remaining. +- Exact signer/payer identities kept outside the table. +- A client compatibility plan for wallets and RPC consumers that do not support + v0 transactions. +- A canary transaction that uses the intended addresses, its expected result, + and a fallback path. + +Run the offline check first: + +```bash +node scripts/validate-alt-manifest.mjs --strict alt-manifest.json +``` + +Then compare the manifest with the on-chain table; the local validator cannot +verify authority, ownership, slot, or deployed addresses. + +## 2. Create deliberately + +The lookup-table program derives a table address from a recent slot, the +authority, and a bump. Capture the chosen recent slot and the creation +signature. Do not reuse a stale proposed transaction after long approval or +signing delays. + +Creation can use a legacy or v0 transaction. Using the stored addresses later +requires a v0 transaction. + +For a mainnet table, capture: + +```text +table address +cluster and RPC URL +creation signature and slot +expected authority and verified authority +address manifest commit SHA +rent paid +rollback owner and escalation channel +``` + +## 3. Extend without a same-slot race + +The program records `last_extended_slot`; addresses added in that slot are not +available for lookup in the current bank. Therefore: + +1. Send the extension transaction with a reviewed address chunk. +2. Confirm it at the agreed commitment level and record the extension slot. +3. Wait for a later slot. +4. Re-fetch the table from the production RPC. +5. Compile the **exact** v0 transaction with the fetched table account. +6. Simulate it and verify loaded addresses, account metas, and logs. +7. Send a low-risk canary before enabling the path for users. + +Do not hardcode a sleep duration as the activation proof. The proof is a +freshly fetched table plus a successful simulation in a later slot. + +## 4. Extension batching + +An ALT can hold at most 256 addresses. The transaction packet has its own byte +limit, so extension size must be measured rather than guessed. Use small, +reviewable chunks and preserve a manifest snapshot per extension. For a new +table, leave capacity for expected stable additions instead of filling it with +volatile route addresses. + +An extension is append-only. You cannot remove or reorder an individual entry. +For a bad or obsolete address, create a replacement table and migrate. + +## 5. Freeze only after the retirement decision + +Freezing makes the ALT immutable by clearing its authority. That makes it a +strong integrity control for a mature table, but it also means you cannot later +extend, deactivate, or close it. Make the rent-retention tradeoff explicit in +the approval record. + +## 6. Roll out and rollback + +Ship an ALT-backed path behind a capability gate: + +```text +known v0-capable wallet/RPC + healthy ALT fetch → v0 + intended table version +otherwise → existing legacy/v0 fallback +``` + +Record the selected path and table address in telemetry. On elevated compile, +simulation, signature, or landing errors, stop new ALT traffic first; retain +the prior path while investigating. Do not alter the table as an immediate +reaction unless the authority process has independently verified the cause. + +## 7. Deactivate and close + +Retirement is a two-stage process: + +1. Remove the table from every supported client path; keep the fallback alive + long enough to cover queued work and client update lag. +2. The authority submits `DeactivateLookupTable` and records the slot. +3. Wait until that slot is no longer present in the `SlotHashes` sysvar. This + is the real cooldown condition; do not promise a fixed number of slots. +4. Re-fetch state, verify the table is fully deactivated, and have the + authority submit `CloseLookupTable` to a separately verified recipient. +5. Record closure signature, recipient, recovered lamports, and the migration + completion evidence. + +The program will reject early closure. A frozen table has no authority and +cannot enter this process. diff --git a/solana-alt-ops-skill/skill/security.md b/solana-alt-ops-skill/skill/security.md new file mode 100644 index 0000000..237e337 --- /dev/null +++ b/solana-alt-ops-skill/skill/security.md @@ -0,0 +1,76 @@ +# ALT security and integrity review + +## Threat model + +An ALT changes how a v0 message resolves account keys. The important risks are +not secret-key custody inside the table—ALTs hold public keys—but integrity, +authority, and change control: + +1. **Manifest poisoning:** an unreviewed address is appended and later used by + a route compiler. +2. **Authority drift:** the recorded authority differs from the actual on-chain + authority, or a supposedly immutable table is still mutable. +3. **Environment mix-up:** a devnet table address or cached response is used in + a mainnet flow. +4. **Semantic drift:** a client selects a different table version than the + instructions and account metas were designed for. +5. **Silent compatibility loss:** unsupported wallets, indexers, or support + tooling receive v0 traffic without a fallback. + +## Review every mutable change + +For each proposed extension, require: + +- A manifest diff with every address and its provenance. +- A trusted on-chain snapshot of the current table and authority. +- The exact incremental addresses, capacity before/after, and expected slot. +- A review of the v0 transaction that will consume the table. +- A named authority process—ideally a multisig proposal—rather than an + ambiguous team wallet. +- Canary criteria, monitoring window, and rollback path. + +Do not approve an extension that says only “add the latest route accounts.” +Volatile, user-controlled, or opaque aggregator outputs belong in the normal +transaction account list unless their provenance and lifecycle are explicitly +managed. + +## Freeze decision checklist + +Freeze only if all answers are yes: + +- Is every address stable and independently reviewed? +- Is there no credible future extension need? +- Is permanently retaining the table's rent acceptable? +- Have all clients been tested against the immutable address set? +- Has the governance record acknowledged that freeze removes authority and + therefore prevents extension, deactivation, and closure? + +If any answer is no, keep the table mutable behind an auditable authority, or +publish a new version when the address set changes. + +## Safe public artifacts + +Safe to publish: table addresses, public authority addresses, manifest diffs, +program IDs, PDAs, transaction signatures, slots, non-secret simulation logs, +and public RPC URLs. + +Never publish: seed phrases, private keys, raw signing requests with secrets, +API credentials, confidential user account lists, or unremediated vulnerability +details. Redact before attaching logs to a PR or bounty submission. + +## Read-only audit output + +An ALT audit should report facts—not unsupported assurances: + +```text +cluster and RPC endpoint +table address and program owner +current authority or frozen status +deactivation status +address count and remaining capacity +manifest-vs-chain differences +last extension slot and current observed slot +consumer repositories / release versions +v0-capability and fallback status +open risks and recommended next owner action +``` diff --git a/solana-alt-ops-skill/skill/testing.md b/solana-alt-ops-skill/skill/testing.md new file mode 100644 index 0000000..9219686 --- /dev/null +++ b/solana-alt-ops-skill/skill/testing.md @@ -0,0 +1,56 @@ +# ALT testing and release evidence + +## Test the lifecycle, not just compilation + +A unit test that compiles a v0 message is useful but insufficient. A credible +release package covers the same lifecycle that production will exercise: + +| Stage | Required evidence | +| --- | --- | +| Manifest | Validator passes in strict mode; address provenance reviewed. | +| Local | v0 compiler resolves the intended table and preserves signer/account semantics. | +| Devnet | Create or extend, observe a later slot, re-fetch, simulate, and send a canary. | +| Client | Supported wallets can sign; unsupported wallets receive the documented fallback. | +| RPC/indexing | Send, simulate, fetch, and decoder paths all handle version `0`. | +| Rollback | Feature flag can stop new ALT traffic without a table mutation. | +| Retirement | A dry-run explains deactivation state, cooldown dependency, and recipient verification. | + +## Exact-message simulation + +Every test report should distinguish the following: + +```text +instruction list → compileToV0Message(fetchedTable) → serialized message +→ signer map → simulate → send/confirm → fetch/decode +``` + +If a test compiles using a hand-built address list rather than the fetched table +account, it has not tested the deployed dependency. + +## Devnet canary + +Use a harmless, idempotent flow where possible. The canary should prove: + +1. The table exists on the correct cluster. +2. Newly extended addresses are usable in a later slot. +3. The client selects the intended table version. +4. The wallet signs the v0 message without changing its semantics. +5. The RPC confirms it and the indexer/support decoder resolves loaded keys. + +Record table address, table hash/list snapshot, extension slot, simulation slot, +transaction signature, commitment result, and decoded account map. + +## Capacity and change regression tests + +Maintain fixtures for at least: + +- A valid mutable table manifest. +- Duplicate addresses. +- More than 256 addresses. +- A frozen table that incorrectly carries an authority. +- A mutable table missing authority or change control. +- A static/signer address duplicated in the loaded set. + +The repository's Node test suite covers the manifest validator. Add application +tests around the actual route/compiler that consumes the table; this skill +cannot infer an application's instruction semantics. diff --git a/solana-alt-ops-skill/skill/transaction-composition.md b/solana-alt-ops-skill/skill/transaction-composition.md new file mode 100644 index 0000000..5a81ad6 --- /dev/null +++ b/solana-alt-ops-skill/skill/transaction-composition.md @@ -0,0 +1,77 @@ +# v0 transaction composition and compatibility + +## Compile from live table state + +Use a table account fetched from the intended cluster and RPC endpoint. Do not +construct a v0 message from an address list copied from a README or cached for +an arbitrary period. + +With `@solana/web3.js`, the core pattern is: + +```ts +const lookup = await connection.getAddressLookupTable(tableAddress); +if (!lookup.value) throw new Error("ALT missing on the selected cluster/RPC"); + +const { blockhash } = await connection.getLatestBlockhash(); +const message = new TransactionMessage({ + payerKey: payer.publicKey, + recentBlockhash: blockhash, + instructions, +}).compileToV0Message([lookup.value]); + +const transaction = new VersionedTransaction(message); +transaction.sign([payer]); +``` + +This only compiles a message. It is not a waiver for simulation, wallet +compatibility checks, expiry handling, or signer review. + +## Signers stay static + +Before presenting a transaction to a wallet, inspect the compiled message: + +- The payer and every signer must be static account keys. +- Loaded addresses must match the reviewed manifest/table version. +- Writability must match the instruction design. +- The expected program IDs and PDAs must be present. + +Never “fix” a missing signer by adding it to an ALT. A lookup-loaded account +cannot satisfy a message signature requirement. + +## Client and RPC compatibility + +Versioned messages require v0-aware signing, simulation, decoding, and +transaction-history paths. A compatibility plan should cover: + +- Wallet capability and signing behavior for `VersionedTransaction`. +- RPC behavior for simulation and send. +- Indexer/explorer decoding with `maxSupportedTransactionVersion: 0` where + transaction responses may contain v0 messages. +- Support/analytics pipelines that must resolve `loadedAddresses` and + `addressTableLookups`, not only static account keys. + +When reading a v0 transaction, retain both the static keys and resolved loaded +addresses; a static-only decoder can produce a misleading account map. + +## Simulation checklist + +Simulate the exact v0 message after fetching the current table. Save: + +```text +cluster, RPC endpoint, slot, blockhash, table address, table authority/state, +compiled message version, static keys, loaded writable keys, loaded readonly +keys, log messages, units consumed, error, and simulation timestamp. +``` + +Compare the compiled account map to the pre-ALT path. An ALT should change wire +encoding, not application semantics. + +## Error triage + +| Symptom | Evidence to gather first | Safe next step | +| --- | --- | --- | +| Table not found | Cluster, RPC, table address, cache age | Re-fetch from the intended endpoint; do not fallback to an arbitrary table. | +| Address missing immediately after extension | Extension slot, fetch slot, `last_extended_slot` | Wait for a later slot, re-fetch, and recompile. | +| Wallet rejects message | Wallet/version, serialized message version, signer map | Route to a supported fallback; do not ask users to sign altered bytes. | +| Indexer shows wrong accounts | Raw response, `addressTableLookups`, `loadedAddresses` | Update decoder to resolve loaded addresses before classifying the transaction. | +| User failures after rollout | Table version/path metric and simulation evidence | Halt new ALT traffic; preserve the prior path while isolating the regression. | diff --git a/solana-alt-ops-skill/tests/validate-alt-manifest.test.mjs b/solana-alt-ops-skill/tests/validate-alt-manifest.test.mjs new file mode 100644 index 0000000..984f187 --- /dev/null +++ b/solana-alt-ops-skill/tests/validate-alt-manifest.test.mjs @@ -0,0 +1,38 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; + +import { validateManifest } from '../scripts/validate-alt-manifest.mjs'; + +async function fixture(name) { + return JSON.parse(await readFile(new URL(`../fixtures/${name}`, import.meta.url), 'utf8')); +} + +test('accepts the reviewed valid fixture without warnings', async () => { + const result = validateManifest(await fixture('valid-manifest.json')); + assert.deepEqual(result.errors, []); + assert.deepEqual(result.warnings, []); + assert.equal(result.tableCount, 1); +}); + +test('rejects malformed keys, duplicate addresses, and an unsafe frozen authority', async () => { + const result = validateManifest(await fixture('invalid-manifest.json')); + assert.ok(result.errors.some(({ path }) => path === 'tables[0].authority')); + assert.ok(result.errors.some(({ path }) => path === 'tables[0].addresses[0]')); + assert.ok(result.errors.some(({ path }) => path === 'tables[0].addresses[1]')); + assert.ok(result.errors.length >= 6); +}); + +test('rejects loaded addresses duplicated in the static/signer set', async () => { + const manifest = await fixture('valid-manifest.json'); + manifest.tables[0].staticAddresses.push(manifest.tables[0].addresses[0]); + const result = validateManifest(manifest); + assert.ok(result.errors.some(({ message }) => message.includes('keep signers/payer'))); +}); + +test('rejects tables over the on-chain address capacity', async () => { + const manifest = await fixture('valid-manifest.json'); + manifest.tables[0].addresses = Array.from({ length: 257 }, () => '11111111111111111111111111111111'); + const result = validateManifest(manifest); + assert.ok(result.errors.some(({ message }) => message.includes('exceeds the ALT capacity of 256'))); +});