Contract verification
Rebuild a Cairo contract in Starkscan's isolated verifier and obtain a portable signed exact-verification receipt.
Contract verification
Starkscan can rebuild a locked Cairo/Scarb source tree in a network-denied
gVisor sandbox and compare the produced Sierra class hash with the finalized
on-chain declaration. The workflow is public self-serve: every active
workspace API key has the independent verify scope. There is no invite list.
Use the starkscan CLI for the complete workflow. Direct HTTP is documented
below for integrations that need to own the asynchronous job lifecycle.
What exact verification means
A successful receipt proves that the uploaded source archive, the named build
target, and the pinned starkscan-exact-v25 toolchain reproduced the class hash
in a finalized Starknet declaration. It does not prove security, an audit,
official authorship, deployment configuration, or that a particular address
still uses the class. The receipt says this explicitly in notProofOf.
package-check, awaiting_upload, queued, and building are not verification
results. mismatch, failed, cancelled, and expired never issue receipts.
Only succeeded after independent controller validation and restricted receipt
admission represents verified_exact.
Prerequisites
- the current
@starkscan/clipackage; - an API key from Starkscan API keys;
- a source directory containing root
Scarb.tomlandScarb.lock; - every local/path dependency needed by the build inside that directory;
- the exact finalized class hash and unambiguous Scarb target selectors.
The initial hosted service accepts only SN_MAIN, release builds, Starknet contract
targets, and starkscan-exact-v25. The SDK rejects verification lifecycle calls for
other chain IDs locally. You can prepare and check a chain-bound
SN_SEPOLIA package locally, but hosted submission rejects it until Starkscan
operates a separately reviewed controller attached to the canonical Sepolia
data owner. Unsupported chains and toolchains fail closed; Starkscan does not
silently substitute a compiler or declaration source.
Install the public CLI and confirm that this command family is present:
npm install --global @starkscan/cli
starkscan verify --helpFor a reproducible automation image, pin the exact stable version shown on the
@starkscan/cli registry page
instead of relying on the moving latest tag.
1. Prepare a deterministic package
starkscan verify prepare \
--source ./my-contract \
--output ./my-contract.package.json \
--archive ./my-contract.source.tar.gz \
--package my_contract \
--target-name my_contract \
--artifact-id my_contract_MyContract \
--module-path my_contract::MyContract \
--profile release \
--class-hash 0x1234 \
--toolchain-policy-id starkscan-exact-v25 \
--retention-policy-id private-24h-v1 \
--source-visibility privatePreparation is local and makes no network request. Both output files are
create-only mode-0600; an existing path is never overwritten. The archive is
one deterministic gzip member containing a sorted POSIX ustar tree. User/group
IDs and modification times are normalized. Root .git, .starkscan, and
target directories are excluded. Symlinks, devices, FIFOs, sockets, nested
archives, absolute/traversing paths, ambiguous names, trailing gzip data, and
archives over 10 MiB are rejected.
Package V1 contains exactly these fields:
{
"selectedClassHash": "0x1234",
"uploadDigest": "sha256:<digest of the exact gzip bytes>",
"uploadByteLength": 12345,
"toolchainPolicyId": "starkscan-exact-v25",
"packageName": "my_contract",
"buildProfile": "release",
"targetName": "my_contract",
"targetKind": "starknet-contract",
"artifactId": "my_contract_MyContract",
"modulePath": "my_contract::MyContract",
"sourceVisibility": "private",
"retentionPolicyId": "private-24h-v1"
}For uploadByteLength, mathematically integral JSON spellings such as 12345,
12345.0, and 12345e0 normalize to one canonical unsigned integer before
validation and idempotency processing. Fractional, negative, and out-of-range
values are rejected.
chainId, local filenames, source bytes, and the idempotency key are not
Package V1 fields. The route selects the chain. The CLI sends idempotency only
in the case-sensitive Idempotency-Key HTTP header. Its value is 1–128
characters from A–Z, a–z, 0–9, ., _, :, and -; reuse the same
value only when retrying the exact same package submission.
2. Check locally
starkscan verify package-check \
--package ./my-contract.package.json \
--archive ./my-contract.source.tar.gzThis reopens both regular files safely, validates the archive envelope, and checks its exact digest and byte length. It still does not compile or submit.
3. Submit
export STARKSCAN_API_KEY='<your workspace API key>'
starkscan --chain SN_MAIN verify submit ./my-contract.package.json \
--archive ./my-contract.source.tar.gz \
--visibility privateFor the 30-day public-intake retention class, set Package V1 to public, use
--visibility public, and add --accept-public-verification-retention. This
is a local acknowledgement of the selected retention period. Package V1 does
not publish source or record a license or redistribution right; source
publication needs a separate, explicitly recorded agreement.
The CLI creates the job first, then uploads the exact gzip body. It does not
retry either mutation automatically. On an interrupted create it prints the
idempotency key required for an identical replay; after creation it prints both
the jobId and that key in any upload error. Replaying an already-committed
identical upload is safe, including two concurrent retries. Never invent a new
key until you have checked state.
4. Poll, cancel, and download
starkscan --chain SN_MAIN verify status vrf_<job-id>
starkscan --chain SN_MAIN verify cancel vrf_<job-id>
starkscan --chain SN_MAIN verify receipt vrf_<job-id> \
--output ./receipt.jsonCancellation is best effort. It wins before immutable receipt admission. If a valid receipt was admitted first, success wins the race and remains auditable.
5. Authenticate the receipt offline
Download and pin Starkscan's published receipt trust-root JSON once over your normal authenticated release channel. Then disconnect the network and run:
starkscan verify receipt-check ./receipt.json \
--trust-root ./starkscan-verification-receipt-trust-root-v1.json \
--offlineThe command checks the strict bundle shape, key ID, Ed25519 signature over
canonical JSON evidence, verified_exact outcome, and exact proof scope. It
does not contact Starkscan. A copied receipt without its separately trusted
public-key document is not authenticated.
Receipt receiptId and declaration.blockNumber accept mathematically
integral decimal or exponent spellings and normalize to canonical unsigned
integers before validation and signature verification. Receipt issuers sign
that canonical integer representation.
Job states
| State | Meaning | Retry yourself? |
|---|---|---|
awaiting_upload | Metadata accepted; source body not committed | Upload before uploadExpiresAt |
validating | Archive metadata is being committed | No |
queued | Ready for the isolated worker | Poll with backoff |
building | Networkless gVisor build in progress | No |
comparing | Independent controller comparison | No |
succeeded | Signed receipt is available | Download it |
mismatch | Rebuilt hash differed | Fix source/target/toolchain; use a new job |
failed | Typed failure | Retry only when retryable=true |
cancelled | Owner cancellation won | Use a new job if needed |
expired | Upload window elapsed | Create a new job |
The service allows one active job and five creates per UTC day per workspace,
with a bounded global queue. 429 is a workspace quota/concurrency response;
503 is temporary service or global-capacity unavailability. Neither is a
verification result.
Complete CLI reference
Global flags may appear before verify: --base-url (or
STARKSCAN_BASE_URL), --chain (or STARKSCAN_CHAIN), --timeout-ms (or
STARKSCAN_TIMEOUT_MS), --retries, --api-key (or STARKSCAN_API_KEY),
--output-format text|json, and --pretty. Keep API keys in the environment;
command arguments can be visible in shell history and process listings. The
hosted default base is https://api.starkscan.co and the default chain is
SN_MAIN.
| Command | Required inputs | Optional inputs | Network behavior |
|---|---|---|---|
verify prepare | --source, --output, --package, --target-name, --artifact-id, --module-path, --profile release, --class-hash, --toolchain-policy-id starkscan-exact-v25, --retention-policy-id, --source-visibility | --archive chooses the new archive path; otherwise the deterministic sidecar name is used | none |
verify package-check | --package | --archive; otherwise the sidecar is used | none |
verify submit | package positional path, --visibility, workspace API key | --archive, explicit --idempotency-key; the public-intake retention class also requires --accept-public-verification-retention | one metadata POST followed by one source PUT; mutations are never retried automatically |
verify status | job ID | global safe-read retry/timeout settings | safe GET; bounded transient retries |
verify cancel | job ID | none | one POST; never retried automatically |
verify receipt | job ID, --output | global safe-read retry/timeout settings | safe GET; output is create-only |
verify receipt-check | receipt path, --trust-root, --offline | none | none; any missing --offline is rejected |
JSON output is one object with a stable top-level command result. Prepare
reports both output paths, exact digest/length, and durability warnings. Submit
reports jobId, the effective idempotency key, initial state, and recommended
poll delay. Status returns the owner-visible job view. Receipt reports whether
the destination was durably committed. Receipt-check reports signature and
proof-scope validity without contacting Starkscan. Human text is for terminals;
automation should always select --output-format json and inspect the process
exit status before reading fields.
| Exit | Class | Meaning |
|---|---|---|
0 | success | The operation completed. For status, inspect the returned job state; exit 0 does not turn a pending job into verified. |
1 | runtime | Local I/O, malformed server response, or another non-HTTP runtime failure. |
2 | usage | Invalid flags, package, archive, receipt, trust root, or unsafe local path. |
3 | auth | Missing, invalid, revoked, expired, or insufficient-scope API key. |
4 | rate limited | Workspace request/job quota. Respect Retry-After; do not create a second logical job. |
5 | timeout/unavailable | Network timeout or temporary service capacity. Read the structured retry signal and recover by idempotency/status rules. |
6 | not found | The job/receipt is absent, not ready, or deliberately indistinguishable from another workspace's job. |
--retries applies only to safe reads. submit does not retry metadata creation
or source upload, and cancel does not retry its mutation. If submission is
interrupted after metadata creation, keep the job ID and run status. If the
metadata response itself was ambiguous, repeat only with the exact same
explicit idempotency key and identical Package V1. A different body with the
same key is a conflict. If the source response was ambiguous, retry the same
package and idempotency key: the matching quarantine object and ledger commit
are idempotent. Never create multiple keys to guess whether a previous mutation
committed.
Supported build matrix
The launch matrix is deliberately one immutable lane:
| Dimension | Supported | Rejected |
|---|---|---|
| hosted chain | SN_MAIN | SN_SEPOLIA and every other route value |
| policy | starkscan-exact-v25 | earlier, future, or unrecognized policies |
| build profile | release | dev, custom profiles, implicit defaults |
| target kind | starknet-contract | libraries, tests, executables, plugins |
| selection | explicit package, target name, artifact ID, and module path | null, omitted, inferred, or ambiguous selectors |
| manifest | root Scarb.toml | nested-only or missing manifest |
| lock | root immutable Scarb.lock | missing, mutable, or unresolved dependency graph |
| dependencies | complete locked material inside the submitted tree and accepted v25 policy | network fetches, branch/tag resolution, registry mutation, scripts, missing path material |
| archive | deterministic gzip plus POSIX ustar within every published bound | zip, zstd, multiple gzip members, unsafe or oversized tar content |
unsupported_policy, unsupported_toolchain, and incomplete/offline-material
failures are terminal for that job and create no receipt. Update the locked
source tree or wait for a newer published policy; do not remove the lockfile,
change the declared hash, or use a nearby compiler to force a match.
CI and automation
This bounded polling example never prints the API key and uploads only the source-free signed receipt as a CI artifact:
set -euo pipefail
export STARKSCAN_API_KEY="${STARKSCAN_API_KEY:?CI secret is required}"
starkscan --chain SN_MAIN --output-format json verify package-check \
--package contract.package.json --archive contract.source.tar.gz
submit_json="$(starkscan --chain SN_MAIN --output-format json verify submit \
contract.package.json --archive contract.source.tar.gz --visibility private)"
job_id="$(printf '%s' "$submit_json" | jq -er '.data.jobId')"
state=queued
for attempt in $(seq 1 150); do
status_json="$(starkscan --chain SN_MAIN --output-format json verify status "$job_id")"
state="$(printf '%s' "$status_json" | jq -er '.data.state')"
case "$state" in
succeeded) break ;;
mismatch|failed|cancelled|expired) exit 1 ;;
esac
sleep 2
done
test "$state" = succeeded
starkscan --chain SN_MAIN --output-format json verify receipt "$job_id" \
--output receipt.json
starkscan --output-format json verify receipt-check receipt.json \
--trust-root starkscan-verification-receipt-trust-root-v1.json --offlineUse the CI provider's masked secret store and disable command tracing around credentialed calls. Do not upload private source, API keys, raw HTTP captures, compiler working directories, or controller evidence. Cancellation belongs in an explicit interrupted-job cleanup handler; it is best effort and must never be reported as successful verification.
Direct HTTP contract
All external requests use the preferred API-host shape
https://api.starkscan.co/v1/... with X-Starkscan-Api-Key. The compatibility
/api/v1/... shape remains available, but new integrations should use /v1.
POST /v1/{chain}/verification-jobswith Package V1 andIdempotency-Key.PUT /v1/{chain}/verification-jobs/{jobId}/sourcewithContent-Type: application/gzip, noContent-Encoding, and exactContent-Length.GET /v1/{chain}/verification-jobs/{jobId}until terminal.- Optional
POST .../{jobId}/cancel. GET .../{jobId}/receiptonly aftersucceeded.
Jobs are visible only to the owning workspace. Config-only internal keys,
browser sessions, and keys without verify cannot use the routes. Error bodies
never reveal whether another workspace owns an identifier.
Privacy, retention, and isolation
Source bytes stream directly to private encrypted object storage. PostgreSQL
stores only bounded metadata, digests, opaque object keys, lifecycle events,
resource counters, and the source-free receipt. The API process does not build
source. The controller fetches one exact object and sends it to a root-owned
forced command on the isolated class-verifier host. That command has no
database, object-store, API, signing, receipt-writer, deploy, or production
credentials. Each build runs non-root in gVisor with network denied, a read-only
root filesystem, dropped capabilities, no new privileges, bounded CPU/memory/
processes/time, and deterministic pinned tools. Working copies are deleted.
Private source is deleted after the selected 24-hour or seven-day policy. The
public Package V1 class selects the 30-day public-intake retention policy; it
does not make source bytes public. Source publication or redistribution remains
a separate, explicitly recorded agreement and is not implemented by Package V1.
Deleting source never deletes the minimum append-only audit record or falsifies
an older receipt.
Failure guide
invalid_verification_package: validate all 12 fields and use v25/release.verification_upload_mismatch: do not recompress the archive after prepare.finalized_declaration_unavailable: wait for the declaration to finalize.lockfile_missing: include the rootScarb.lockand all offline material.verifier_dispatch_failed: temporary isolated-worker failure; inspectretryablebefore resubmitting.class_hash_mismatch: the selected source, target, or toolchain does not reproduce the declared class. Starkscan intentionally does not turn this into a weaker verification tier.receipt_signing_failed: no success is published; retry only after operator recovery.
For machine-readable details, use the OpenAPI contract, capability discovery, its capabilities schema, the retention and isolation contract, and the Package V1 schema. Receipt consumers should also pin the portable receipt schema, trust-root schema, and current public trust root. The TypeScript SDK guide, Privacy Policy, and support apply to the same public service.