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 nineteen 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.