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.
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`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 charsStep 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"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 amountsAudit 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_IDIn 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
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
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
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
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
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.