All articles
solanasecurityverified-buildsanchorrustinfrastructureauditing

Solana verified program builds: reproducible bytecode and the on-chain registry

How solana-verify and OtterSec's on-chain registry let anyone confirm that the program deployed at a given address matches a public GitHub repo — and why this matters for audits, users, and multisig upgrade flows.

Share

Every Solana program you interact with is a .so binary sitting in an account. The RPC returns bytecode. The program ID is a public key. Nothing in that pipeline tells you whether the code you're running matches the open-source repo the team pointed you at during their audit announcement.

This gap is not hypothetical. Auditors review source code. What gets deployed is a compiled binary. Unless someone verifies that the binary produced from that source matches what's on-chain, the audit gives you a security assessment of code that may or may not be the code users are actually executing.

solana-verifiable-build (CLI: solana-verify) closes that gap. It uses a Docker container to pin the exact Rust, Anchor, and Solana toolchain versions, produce a deterministic.so, and compare its hash to the hash of the on-chain program data. If they match, the verification result is written to a PDA on Solana that anyone can read — and Solana Explorer surfaces a "verified" badge on the program page.

Why reproducible builds are hard without tooling

Rust compilation is not deterministic by default across machines or toolchain versions. The same source can produce different binaries when:

  • The Rust toolchain version differs (nightly vs stable, different nightly dates)
  • The Solana BPF target version differs
  • Build metadata (timestamps, paths) is embedded
  • Dependency resolution picks a different minor version

The Docker-based approach in solana-verify eliminates all of these variables. The container image is pinned to a specific Solana tools version. The build runs inside it with a fixed environment. Anyone who runs the same command against the same source gets the same output.

Installing solana-verify

The CLI is distributed as a Rust crate. You also need Docker running locally — the build step executes inside a container.

bash
# Install the CLI
cargo install solana-verify

# Confirm Docker is available
docker info

# Check the CLI version
solana-verify --version

The CLI fetches build images from Docker Hub on first use. The image tag corresponds to the Solana tools version — e.g. ellipsislabs/solana:1.18.22.

Repo requirements

Two files must be present and committed in your repo for verification to work reliably:

  • Cargo.lock — locks all transitive dependency versions. Without it, the build inside Docker may resolve different dep versions than your local build. Rust workspaces sometimes .gitignore the lockfile; remove that exclusion.
  • build.toml (optional but recommended) — explicitly pins the Solana tools image and the library name so verification commands don't need extra flags.
toml
# build.toml — place at repo root
[build]
# The Docker image used for the reproducible build.
# Must match the Solana tools version used when the program was deployed.
solana_tools_version = "v1.18.22"

# Name of the .so output (without lib prefix or extension).
# Corresponds to your Cargo package name with hyphens replaced by underscores.
library_name = "my_program"

# Optional: if the program's Cargo.toml isn't at repo root
base_image = "ellipsislabs/solana:1.18.22"

If you don't have a build.toml, you pass these values as CLI flags instead. The file just saves you from repeating them.

Building reproducibly

The core command is solana-verify build. It launches the Docker container, mounts your repo, runs cargo build-sbf inside, and writes the output to ./target/deploy/.

bash
# From your repo root
solana-verify build

# If you haven't committed build.toml, pass flags explicitly:
solana-verify build \
  --library-name my_program \
  --solana-tools-version v1.18.22

# The output .so lands at the standard path:
# ./target/deploy/my_program.so

This takes the same time as a normal release build — Docker adds a layer of isolation but not significant overhead once the image is cached locally.

Hashing: local build vs on-chain program

Once you have a verified local build, you need to compare its hash to the hash of the program deployed on-chain. Two commands handle this:

bash
# Hash the .so you just built locally
solana-verify get-build-hash ./target/deploy/my_program.so

# Hash the program data from on-chain (fetches via RPC)
solana-verify get-executable-hash \
  --program-id PROGRAM_ID_HERE \
  --url mainnet-beta

# If both hashes match, your local source produced exactly what's deployed.

The hash is a SHA256 of the program data section of the account — not the raw account data, which includes a loader header. The CLI strips the header before hashing so both sides are comparable.

End-to-end: verify-from-repo

The most useful command for independent verifiers is verify-from-repo. It takes a GitHub URL and a program ID, clones the repo, builds inside Docker, hashes both sides, and reports whether they match — all in one step.

bash
solana-verify verify-from-repo \
  --program-id PROGRAM_ID_HERE \
  --url mainnet-beta \
  --repo-url https://github.com/your-org/your-program \
  --commit-hash abc123def456 \  # pin to exact commit
  --library-name my_program

# Output if successful:
# Verified build hash: 8f3a...
# On-chain hash:       8f3a...
# ✓ Hashes match. Program is verified.

The --commit-hash flag is important: it pins verification to the exact commit that was deployed, not whatever is on main today. If your team tags releases, use the tag ref instead.

Writing to the on-chain registry

Once you've confirmed locally that hashes match, you can submit the verification to the on-chain registry. OtterSec maintains this registry — the result is written to a PDA derived from the program ID, and Solana Explorer reads it to show the verified badge.

bash
# Upload verified status to the on-chain registry.
# This requires a keypair with enough SOL to pay for the PDA rent.
solana-verify upload-program \
  --program-id PROGRAM_ID_HERE \
  --url mainnet-beta \
  --repo-url https://github.com/your-org/your-program \
  --commit-hash abc123def456 \
  --library-name my_program \
  --keypair ~/.config/solana/id.json

The registry PDA stores: the program ID, repo URL, commit hash, on-chain hash, and a timestamp. This is public and permissionless — anyone can read it, and the program's upgrade authority doesn't need to be involved in registry writes (though it's best practice for the deployer to do this themselves).

Reading verification status on-chain

The registry program ID is verifycLy8mB96wd9wqq3WkUXjgqdiCBRZXrZSRbHZTe. The PDA is derived from the seeds ["verified_build", program_id_bytes]. You can read it from any client:

typescript
import { Connection, PublicKey } from "@solana/web3.js";

const REGISTRY_PROGRAM_ID = new PublicKey(
  "verifycLy8mB96wd9wqq3WkUXjgqdiCBRZXrZSRbHZTe"
);

async function getVerificationStatus(programId: string) {
  const connection = new Connection("https://api.mainnet-beta.solana.com");

  const [verificationPda] = PublicKey.findProgramAddressSync(
    [Buffer.from("verified_build"), new PublicKey(programId).toBuffer()],
    REGISTRY_PROGRAM_ID
  );

  const accountInfo = await connection.getAccountInfo(verificationPda);
  if (!accountInfo) {
    return { verified: false };
  }

  // The account data is Borsh-encoded. In practice, use the registry SDK
  // or just check account existence as a proxy for verified status.
  return {
    verified: true,
    data: accountInfo.data,
  };
}

// Usage
const status = await getVerificationStatus("YOUR_PROGRAM_ID");
console.log("Verified:", status.verified);

Solana Explorer, SolanaFM, and OtterSec's own verify.osec.io all read from this same PDA. Checking account existence is sufficient for a simple "is this program verified?" guard in your UI.

CI integration: verify on every release

The right time to run verification is immediately after a deployment, as part of your release workflow. Here's a GitHub Actions step that builds reproducibly, verifies against the on-chain program, and uploads to the registry:

yaml
# .github/workflows/verify.yml
name: Verify program build

on:
  workflow_dispatch:
    inputs:
      program_id:
        description: "Program ID to verify"
        required: true
      commit_hash:
        description: "Exact commit hash that was deployed"
        required: true

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.commit_hash }}

      - name: Install solana-verify
        run: cargo install solana-verify

      - name: Verify from repo
        run: |
          solana-verify verify-from-repo \
            --program-id ${{ inputs.program_id }} \
            --url mainnet-beta \
            --repo-url https://github.com/${{ github.repository }} \
            --commit-hash ${{ inputs.commit_hash }} \
            --library-name my_program

      - name: Upload to registry
        env:
          DEPLOYER_KEYPAIR: ${{ secrets.DEPLOYER_KEYPAIR }}
        run: |
          echo "$DEPLOYER_KEYPAIR" > /tmp/keypair.json
          solana-verify upload-program \
            --program-id ${{ inputs.program_id }} \
            --url mainnet-beta \
            --repo-url https://github.com/${{ github.repository }} \
            --commit-hash ${{ inputs.commit_hash }} \
            --library-name my_program \
            --keypair /tmp/keypair.json
          rm /tmp/keypair.json

Run this as a workflow_dispatch immediately after deploying. If the deploy is automated, chain the verify job after the deploy step with the program ID and commit hash as outputs.

Program upgrades and re-verification

Verification is tied to a specific bytecode hash. When you upgrade a program — solana program deploy --program-id PROGRAM_ID — the on-chain hash changes. The existing registry entry is now stale. The verified badge disappears on Explorer until you re-run the upload with the new commit.

This is intentional, not a flaw. A stale verification is worse than no verification — it would falsely assure users that new code they haven't seen was reviewed. The registry entry expiring on upgrade forces the team to consciously re-verify each new deployment.

For teams using Squads multisig for upgrades, the flow is:

  1. Create a Squads proposal for the program upgrade
  2. Signers review the linked commit hash in the proposal
  3. Proposal executes → program is upgraded on-chain
  4. Deploy the verify CI workflow against the new program ID + commit hash to re-establish verification

The two-step nature — multisig approval then independent verification upload — gives users a clean audit trail: here's the governance action that changed the code, and here's the cryptographic proof that the code on-chain matches the PR that was reviewed.

Squads multisig + verified builds: the full governance loop

Most serious Solana protocols combine Squads (multi-party upgrade authority) with verified builds (cryptographic source matching). Together they answer two separate questions:

  • Squads: who approved this change? (M-of-N governance)
  • Verified builds: does the deployed binary match the source that was reviewed?

Neither alone is sufficient. Squads without verified builds means M-of-N approved a deployment but users can't confirm what was deployed. Verified builds without Squads means the bytecode matches source, but a single compromised key could have pushed it.

bash
# Example: the upgrade + re-verify sequence
# 1. Build the new version reproducibly
solana-verify build --library-name my_program

# 2. Get the local build hash to include in the Squads proposal description
solana-verify get-build-hash ./target/deploy/my_program.so
# → 8f3acd...

# 3. Create a Squads proposal referencing commit abc123 and hash 8f3acd
# (done via Squads UI or SDK — out of scope here)

# 4. After proposal executes, verify on-chain
solana-verify verify-from-repo \
  --program-id PROGRAM_ID \
  --url mainnet-beta \
  --repo-url https://github.com/your-org/your-program \
  --commit-hash abc123

# 5. Upload to registry
solana-verify upload-program \
  --program-id PROGRAM_ID \
  --url mainnet-beta \
  --repo-url https://github.com/your-org/your-program \
  --commit-hash abc123 \
  --keypair ~/.config/solana/id.json

Common failure modes

A few things that cause verification to fail even when source and binary look the same:

  • Missing Cargo.lock: the Docker build resolves different patch versions. Always commit your lockfile.
  • Wrong solana-tools-version: the build.toml version must match the version used during deployment. Check your original deploy environment's solana --version output.
  • Multiple programs in a workspace: use --library-nameto specify which .so to hash. Without it the CLI may pick the wrong one.
  • Build features differ: if the deployed program was built with --features some-feature, pass --cargo-args to match.
  • IDL embed: Anchor programs embed an IDL into the binary. If the IDL changed between commits (even just field descriptions), the binary hash changes. Pin to the exact commit deployed.

What verification doesn't cover

Verified builds confirm bytecode-source equivalence. They don't:

  • Audit the source code for vulnerabilities — that's a separate engagement. Verification tells you the deployed code is the audited code; it says nothing about whether the audited code is safe.
  • Verify the program's data accounts or state — only the executable code.
  • Protect against a compromised upgrade authority acting before the multisig is in place. Lock down upgrade authority to a multisig before any significant TVL touches the protocol.

The state of ecosystem adoption

As of mid-2026, verified builds have become a baseline expectation for any protocol handling meaningful TVL. OtterSec maintains the registry and audits programs; they also require verification as part of their audit workflow. Solana Explorer, SolanaFM, and Solscan all surface the verified badge. The Solana Foundation highlights verification as a recommended practice in their security guidance.

If you're deploying a program that handles user funds and you haven't set up verified builds, that's the first thing to fix before your next upgrade. The tooling is mature, the workflow is well-documented, and the cost is one CI step and a few lamports for the registry PDA.

Keep reading

Get new articles in your inbox

Technical deep-dives on Solana tooling, infrastructure, and ecosystem. No noise.

Solana verified program builds: reproducible bytecode and the on-chain registry | devrels.xyz