Skip to content

How to anchor a certificate SHA-256 hash on Stellar with Soroban

A developer walkthrough of how Stellaroid Earn binds a certificate's SHA-256 hash to a Stellar wallet with a Soroban smart contract: register_certificate to anchor it, verify_certificate to trust it, and link_payment to pay the graduate — every step auditable on stellar.expert.

For Developers and technical issuers

This guide walks through how Stellaroid Earn anchors a certificate's SHA-256 hash on Stellar using a Soroban smart contract, and how the same short flow turns a verified credential into an instant on-chain payout. Every function named here maps to a real call in the deployed testnet contract.

Stellaroid Earn is an early-access pilot running entirely on Stellar testnet with test XLM. It is not a mainnet or production financial product. The sub-five-second settlement and fraction-of-a-cent fees referenced below are real testnet characteristics.

Why the hash goes on-chain, not the document

You never put the certificate PDF on-chain. Instead you store its SHA-256 hash — a 32-byte fingerprint written as 64 hexadecimal characters. That single digest is enough to prove a document is the exact one that was registered, without ever exposing the document itself.

  • Deterministic: hashing the same file always yields the same 64-character digest, so a document can be re-checked against the on-chain record at any time.
  • One-way: the digest reveals nothing about the document's contents, so private certificate data stays off-chain.
  • Tamper-evident: change a single byte of the file and the hash changes completely, breaking the match.
  • Cheap and small: a fixed 32 bytes fits on-chain regardless of how large the original document is.

This is also how the contract enforces uniqueness. Because the hash is the credential's on-chain key, registering the same hash twice is rejected — a credential cannot be silently overwritten.

The contract surface: three writes do the work

Stellaroid's contract exposes twelve public functions, but the credential lifecycle rests on three writes. register_certificate anchors the hash to a student's wallet. verify_certificate marks it trusted. link_payment pays the verified wallet. Along the way a credential moves through explicit statuses: issued, verified, suspended, revoked, and expired.

// Illustrative signatures showing the shape of the flow.
// The contract crate on GitHub is the canonical source.
// cert_hash is the 32-byte SHA-256 digest; amount is an i128 (stroops).

pub fn register_certificate(
    env: Env,
    issuer: Address,
    student: Address,
    cert_hash: BytesN<32>,
    title: String,
    cohort: String,
    metadata_uri: String,
) -> Result<(), Error>;   // stores the record, emits `cert_reg`, rejects duplicates

pub fn verify_certificate(
    env: Env,
    verifier: Address,       // must be an approved issuer or the admin
    cert_hash: BytesN<32>,
) -> Result<(), Error>;   // sets status = Verified, emits `cert_ver`

pub fn link_payment(
    env: Env,
    employer: Address,
    student: Address,
    cert_hash: BytesN<32>,   // must resolve to a Verified credential
    amount: i128,
) -> Result<(), Error>;   // transfers XLM via the native SAC, emits `payment`
The signatures above are illustrative — they show the argument shape, not the exact source. Read the contract crate on GitHub for the authoritative definitions and the tests that cover the full flow.

Step 1 — Hash the certificate

Compute the SHA-256 digest of the document on your own machine. The file itself never leaves your side; only the 64-character hex string is sent on-chain.

# Hash the certificate locally — the file never leaves your machine.
# Linux:
sha256sum diploma.pdf
# macOS:
shasum -a 256 diploma.pdf
# -> 9f2c8b...e41d   (64 hex characters = the 32-byte digest that goes on-chain)

Stellaroid's web app does exactly the same thing in the browser with the Web Crypto API, so a school can drop in a PDF and the digest is computed client-side before anything is signed.

// The browser computes the same digest with Web Crypto,
// so only the 32-byte hash — never the document — is sent on-chain.
const bytes = new Uint8Array(await file.arrayBuffer());
const digest = await crypto.subtle.digest("SHA-256", bytes);
const hash = [...new Uint8Array(digest)]
  .map((b) => b.toString(16).padStart(2, "0"))
  .join(""); // 64 hex chars

Step 2 — Anchor it with register_certificate

As an approved issuer, sign a register_certificate call that binds the hash to the student's Stellar wallet along with minimal proof metadata. The contract stores the record and emits a cert_reg event.

stellar contract invoke \
  --id "$CONTRACT_ID" \
  --source issuer-key \
  --network testnet \
  -- \
  register_certificate \
  --issuer "$ISSUER_ADDRESS" \
  --student "$STUDENT_ADDRESS" \
  --cert_hash 9f2c8b...e41d \
  --title "Full-Stack Web3 Bootcamp" \
  --cohort "2026-Q2" \
  --metadata_uri "ipfs://bafy.../proof.json"
Duplicate hashes are rejected on-chain: a second register_certificate for the same hash fails with AlreadyExists (error #4), so a credential can never be silently overwritten.

Step 3 — Verify, then pay

verify_certificate can only be called by an approved issuer or the admin wallet — an arbitrary wallet is rejected with Unauthorized (error #3), while a registered-but-unapproved issuer fails with IssuerNotApproved (#8) and a suspended issuer with IssuerSuspended (#9). A successful call flips the credential's status to Verified and emits a cert_ver event. Only then does payment unlock — link_payment transfers XLM through the native Stellar Asset Contract straight to the student's verified wallet, emits a payment event, and on testnet settles in typically under five seconds for a fraction of a cent.

# 2 — an approved issuer or the admin verifies the credential
stellar contract invoke --id "$CONTRACT_ID" --source approved-issuer-key --network testnet \
  -- verify_certificate \
  --verifier "$VERIFIER_ADDRESS" \
  --cert_hash 9f2c8b...e41d

# 3 — once Verified, the employer pays the wallet directly
stellar contract invoke --id "$CONTRACT_ID" --source employer-key --network testnet \
  -- link_payment \
  --employer "$EMPLOYER_ADDRESS" \
  --student "$STUDENT_ADDRESS" \
  --cert_hash 9f2c8b...e41d \
  --amount 1000000000        # stroops (100 XLM); the native SAC uses 7-decimal i128 amounts

Audit every step on stellar.expert

Reads are public and require no signing. You can query the record directly, or simply open the contract's event stream in a browser — no wallet, no login.

# Public read (simulation only — no signing, no fee)
stellar contract invoke --id "$CONTRACT_ID" --source any-key --network testnet \
  -- get_certificate --cert_hash 9f2c8b...e41d

# Or open the contract's event stream in a browser:
# https://stellar.expert/explorer/testnet/contract/$CONTRACT_ID

In the explorer you can trace the credential's whole life:

  • The cert_reg event, emitted when register_certificate anchors the hash.
  • The cert_ver event, emitted when an approved issuer or the admin verifies it.
  • The payment event, emitted when link_payment sends XLM to the student's wallet.
  • The full transaction history — every write is a public Stellar transaction anyone can inspect.

Wire it into a dApp

In the browser, Stellaroid builds these calls with @stellar/stellar-sdk. Read-only calls like get_certificate run through simulateTransaction using a public read address — no signing needed — which is why proof pages resolve a hash without a wallet. Writes are signed with Freighter or Albedo and submitted over Soroban RPC. Wallet components are marked "use client" because the wallet APIs are browser-only.

Ready to try it end to end? Open the Stellaroid app to hash a file and sign register_certificate against the live testnet contract, or clone the repository to read the contract crate and run its tests.

Step-by-step checklist

  1. 1

    Hash the certificate

    Compute the document's SHA-256 digest with sha256sum (Linux), shasum -a 256 (macOS), or the Web Crypto API in the browser. The result is 64 hex characters; the document itself never goes on-chain.

  2. 2

    Anchor it with register_certificate

    As an approved issuer, sign a register_certificate call that binds the hash to the student's Stellar wallet along with its title and cohort. The contract stores the record, emits a cert_reg event, and rejects any duplicate hash.

  3. 3

    Verify with verify_certificate

    An approved issuer or the admin wallet calls verify_certificate with the hash. The contract sets the credential's status to Verified and emits a cert_ver event that anyone can audit.

  4. 4

    Pay with link_payment

    Once the credential is Verified, the employer calls link_payment to send XLM through the native Stellar Asset Contract straight to the student's wallet. Testnet settlement is typically under five seconds.

  5. 5

    Audit on stellar.expert

    Open the contract on stellar.expert and inspect its event stream to confirm the cert_reg, cert_ver, and payment events. Reads are public, so no wallet is required.

Frequently asked questions

Why anchor a hash instead of the certificate itself?
Only the SHA-256 digest goes on-chain, never the document. The 32-byte hash is a tamper-evident fingerprint: re-hashing the same file always produces the same value, so anyone can confirm a document matches the on-chain record while the file itself stays private and off-chain.
Which Soroban function anchors the hash?
register_certificate binds a certificate's SHA-256 hash to a student's Stellar wallet along with minimal metadata (title, cohort, and a metadata URI) and emits a cert_reg event. Duplicate hashes are rejected on-chain with an AlreadyExists error, so the same credential cannot be registered twice.
How do I compute the SHA-256 hash?
On the command line run sha256sum file.pdf on Linux, or shasum -a 256 file.pdf on macOS. Stellaroid's web app does the same in the browser using the Web Crypto API, so only the 64-character hex digest is ever sent on-chain.
How can anyone audit the anchored hash?
Every write is a public Stellar transaction. Open the contract on stellar.expert and inspect its events — cert_reg from registration, cert_ver from verification, and payment from payout — without a wallet or login.
Do you need a wallet to read a credential?
No. Reads such as get_certificate are read-only and public, so Stellaroid's proof pages resolve a hash without any wallet or login. A wallet is only needed to sign the write transactions: registering, verifying, or paying.
Is this running on Stellar mainnet?
No. Stellaroid Earn is an early-access pilot running entirely on Stellar testnet with test XLM. Settlement is typically under five seconds for a fraction of a cent in network fees, but it is not a production or regulated financial product.

See it work on a live testnet proof

Stellaroid Earn is in early access on Stellar testnet — open a public proof page or run the flow yourself.

Anchor a Certificate Hash on Stellar with Soroban | Stellaroid Earn