Skip to main content

Sepolia demo

Run the whole network on Sepolia from your computer, with one phone on USB. The phone runs three apps: the User wallet and two member apps, Member A and Member B.

note

The demo currently supports Android phones only.

  1. Create keys and a network IDMade on the phone. The keys never leave it.
    Where: User wallet
  2. Register through a memberMember A puts the ID on chain. The paymaster pays the gas.
    Where: Member A
  3. Issue a credentialMember A signs the credential. The User wallet keeps it.
    Where: Member A
  4. Check eligibilityMember B learns you qualify, not who you are.
    Where: Member B
  5. Revoke a credential, then reuse the IDThe dashboard revokes it and the next check fails. Member B finds the ID: you count once.
    Where: DashboardMember B

The User wallet and the member apps run on the phone, and the dashboard in your browser. An optional SDK run after the demo shows token delivery to a stealth account. The web wallet and the web member app are not part of this demo.

Watch a run​

A narrated, captioned recording of one run, three and a half minutes long. It follows Run the demo, except that Member A runs the first eligibility check and the recording skips step 7.

If the player does not load, open the recording on Google Drive.

The recorded run's contracts

The recording used a deployment made with this guide. Its contracts on Sepolia:

ContractAddress
Governance Safe0xf25001D964D37C2a3Ebf3076C5C40d37d5E2fb81
NetworkMemberRegistry0x1Dcf10789d3dF7d8dCab0BB52fc505BD2FBFfe72
NetworkMemberGroup0x14D3dBAa0d2e9Af730823A769F112eDAf4240Bf4
NetworkMemberGovernance0x62836fE015cFf89BaAd09AcE2C86fE5Cc4Cf31F4
ApprovedIssuerSet0x501e825d3B17d193ea27a9Fa4C708D4A437F1Ead
IdentityRegistry0xaA2E44C61EB6CF2e08058229e3B65609FB639fF3
SemaphorePaymaster0x9334C58c7ec5713675152a847b66BA156b260dAd
CredentialRevocationRegistry0xF9ebA2A12b5157944129DD333768Fcc045F6e8B6

Before you start​

You need:

  • Node 22.18 or later, Yarn 4, Foundry 1.8.5 and jq.
  • Docker, for the optional SDK run.
  • For the phone apps: JDK 17, SDK platform 36, NDK 27.1.12297006 and adb.
  • An arm64 phone with USB debugging on.
  • Accounts with a Sepolia RPC provider, Etherscan and, for the SDK run, Pimlico. The next section says why.

Four terminals are enough: the first runs everything except the phone apps, including the stack, and the other three each run one phone app.

Install and build from the repository root:

yarn
yarn setup:foundry
yarn build:libraries && yarn build:contracts

Create the secrets​

Every key lives in .stack.env at the repository root, which Git ignores. Three values come from outside services:

ValueWhat it is for
SEPOLIA_RPC_URLYour Sepolia RPC provider. Forge, the stack and, through the stack, the phones reach the chain with it. It must allow eth_getLogs over 10,000 blocks. Alchemy's free tier allows 10, so it fails. The recorded run used the free https://ethereum-sepolia-rpc.publicnode.com.
ETHERSCAN_API_KEYThe deploy script uploads the contracts' source code to Etherscan, so anyone can read and check it. Nothing else uses the key.
BUNDLER_URLA Pimlico Sepolia bundler, from dashboard.pimlico.io. Only the optional SDK run uses it, to send its sponsored transfer. The phones use the stack's own bundler.

The rest are generated:

ValueWhat it is for
DEPLOYERSends the contract deployments and holds no role afterwards.
OWNEROwns the governance Safe and the paymaster, and signs for genesis and the running stack.
STACK_BUNDLER_PRIVATE_KEYPays gas for the stack's bundler, which sends the phones' sponsored transactions.
STACK_MEMBER_SEEDSSeeds for Member A, Member B and Member C, a candidate this demo does not admit.
MAX_COST_PER_OP, PAYMASTER_*Paymaster defaults: the most it pays per operation, its deposit, its stake and unlock delay.

This creates fresh keys and writes the file:

(
set -e -o pipefail -o noclobber
umask 077
key() { cast wallet new --json | jq -er '.data[0].private_key'; }
deployer=$(key); owner=$(key); bundler=$(key); seed_a=$(key); seed_b=$(key); seed_c=$(key)
cat > .stack.env <<EOF
SEPOLIA_RPC_URL="<your Sepolia RPC URL>"
RPC_URL="\$SEPOLIA_RPC_URL"
ETHERSCAN_API_KEY="<your Etherscan API key>"
BUNDLER_URL="<your Pimlico bundler URL>"
DEPLOYER_PRIVATE_KEY=$deployer
DEPLOYER=$(cast wallet address --private-key $deployer)
OWNER_PRIVATE_KEY=$owner
OWNER=$(cast wallet address --private-key $owner)
STACK_BUNDLER_PRIVATE_KEY=$bundler
STACK_MEMBER_SEEDS=$seed_a,$seed_b,$seed_c
MAX_COST_PER_OP=100000000000000000
PAYMASTER_DEPOSIT=100000000000000000
PAYMASTER_STAKE=1000000000000000000
PAYMASTER_UNSTAKE_DELAY=86400
EOF
)

Running it again refuses to overwrite the file. Replace the three values in angle brackets and keep the quotes. Skip the bundler URL if you skip the SDK run.

Then fund two Sepolia addresses: OWNER with about 1.8 ETH for the setup, and DEPLOYER with about 0.1 ETH. Print them:

set -a; . ./.stack.env; set +a
echo "OWNER=$OWNER DEPLOYER=$DEPLOYER"

The paymaster stake and deposit come back when you clean up. In the recorded run, the deploy and genesis took about 1.55 ETH from OWNER, almost all of it the stake, the deposit and the account top-ups. Gas cost next to nothing.

danger

Anyone with .stack.env controls the network, and the three seeds cannot be recovered. Never commit or share the file, and back it up.

Each later step loads the file with set -a; . ./.stack.env; set +a. Run it again in every new terminal.

Rehearse locally​

Optional and free. This starts a local chain, deploys, and plays onboarding, issuing, proving, revoking and admission over HTTP:

yarn stack --check

It ends with stack check passed.

warning

Running TESTBED_MODE=testnet yarn workspace integration-testbed serve --check or --check-admission against your Sepolia deployment sponsors registrations, revokes a credential and leaves rows in the dashboard. --check-admission also admits Member C for good. Use yarn stack --check instead.

Deploy the contracts​

This deploys the contracts, sets up governance, funds the paymaster and admits the founding member. It overwrites the committed packages/contracts/deployments/11155111.json, which the stack reads. Do not commit it or the packages/contracts/broadcast/ folder.

cd packages/contracts
set -a; . ../../.stack.env; set +a
yarn deployAndConfigureAll:sepolia \
--private-keys "$DEPLOYER_PRIVATE_KEY" \
--private-keys "$OWNER_PRIVATE_KEY" \
--sender "$DEPLOYER"
cd ../..

It prints ONCHAIN EXECUTION COMPLETE & SUCCESSFUL., verifies the contracts on Etherscan, and ends with All (7) contracts were verified!. A warning that no bytecode matched the governance Safe is expected: the Safe is a standard Safe proxy that the script does not compile.

warning

A rerun after genesis breaks the stack. The salts are fixed, so the same OWNER reuses the contracts but rewrites the deployment file's start block, and the stack reads logs from that block. Deploy once. For a fresh network later, first withdraw the stake (Clean up), then move the old .stack.env aside and run the secrets step again. The old OWNER key is the only key that can withdraw the stake and deposit.

The script also writes packages/contracts/network-member.env with the founding member's secret. Back it up with .stack.env: a later deploy reuses the file if it exists.

Start the network​

warning

The pairing QR codes carry each member's server token, so a recording can leak them. Keep the stack terminal, .stack.env, network-member.env and stack.secrets.json off camera, and open the dashboard link before you record so it never asks for the key on camera.

Genesis admits Member A and Member B through the Safe, publishes their issuer keys, and funds every account and the bundler from OWNER. It prints <label>: sent <hash> for each transaction:

set -a; . ./.stack.env; set +a
yarn stack:genesis

Run it once. The first run ends with Genesis: 15 step(s) done, and a second run prints Genesis: nothing to do, the network is set up. Then serve the network and leave it running in this first terminal:

yarn stack:testnet
danger

Running genesis while the stack serves makes transactions fail. Both sign as OWNER, so their nonces collide.

The stack prints the User wallet's QR, a pairing QR for each member and the path of stack.secrets.json, then Stack is up. Press Ctrl+C to stop. The secrets file exists only while the stack runs and holds the dashboard link and its key. Open the link for the dashboard on http://127.0.0.1:8080. Its Flow tab tracks the demo in five stages and marks the current one "Now":

The dashboard&#39;s Flow tab before the demo: five stages at zero, the first marked Now

Set up the phone​

The stack keeps running in the first terminal. First point the build at JDK 17 and the SDK. Find both folders:

# JDK 17 folder, if you installed it with Homebrew (brew install openjdk@17)
echo "$(brew --prefix openjdk@17)/libexec/openjdk.jdk/Contents/Home"
# JDK 17 folder, for any other installer
/usr/libexec/java_home -v 17
# SDK folder, at its default location
ls -d ~/Library/Android/sdk

The SDK folder must hold platforms/android-36 and ndk/27.1.12297006. Add these lines to your shell profile, such as ~/.zshrc or ~/.bashrc, with the two folders:

export JAVA_HOME="<your JDK 17 folder>"
export ANDROID_HOME="<your SDK folder>"
export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:$PATH"

JAVA_HOME must point at JDK 17. Newer JDKs, such as 26, fail the build.

Then open three new terminals at the repository root, one each for the User wallet, Member A and Member B. New terminals load the profile. Plug in the phone and check the tools in the second terminal:

java -version # openjdk version "17..."
adb devices # lists your phone as "device"

If the phone shows as unauthorized, accept the debugging prompt on its screen.

In the same terminal, run the script below. It copies in the circuit and setup files the User wallet bundles.

apps/user-app/scripts/sync-assets.sh

Forward the ports so the phone reaches the stack on your computer:

for p in 8081 8082 8083 8545 3001 3002 3003 4337 8080; do adb reverse tcp:$p tcp:$p; done

Install the three apps. Each command runs that app's Metro server, so keep each terminal open: the User wallet in the second terminal on port 8081, Member A in the third on 8082 and Member B in the fourth on 8083.

yarn workspace user-app android
yarn workspace network-member-app prebuild:a --clean
yarn workspace network-member-app android:a
yarn workspace network-member-app prebuild:b --clean
yarn workspace network-member-app android:b

The first build of each app takes several minutes. Without --clean, Member B replaces Member A instead of installing beside it. Both build from one native project folder, which prebuild:b --clean deletes, so wait until Member A opens on the phone before you run Member B's commands.

Open each app and allow the camera "While using the app". Then pair them with the QR codes in the stack terminal:

  1. In the User wallet, open Settings and tap "Erase everything" if you used it before.
  2. In the User wallet, open the Config tab, tap "Scan QR code" and scan its QR.
  3. Each member app opens on "Connect". Tap "Scan QR". Member A scans the first pairing QR and Member B the second.

Run the demo​

Each on-chain step takes 12 to 30 seconds on Sepolia.

StepDoYou see
1Dashboard, Network tab"Governance Safe" row: "threshold 2 of 3 owner(s)"
2User wallet, Identity tab, "Create identity""Setup complete" and a network ID
3Member A, User tab, "Register". In the User wallet, "Approve""Registered" and "Gas paid by the Semaphore paymaster"
4Member A, "Issue a credential". In the User wallet, "Approve""Credential issued". The User wallet's Credentials tab shows "Valid"
5Member B, Verify tab, "Check". In the User wallet, "Approve", then "Share""Eligible"
6Dashboard, Orgs tab, "Revoke" on Member A's credentialThe row reads "revoked"
7Member B, "Check again". In the User wallet, "Approve"The User wallet shows "Credential revoked". Tap "Done": "Not eligible"
8Member B, User tab, "Register""Already registered" and "Not counted twice"
9Member B, "Issue a credential". In the User wallet, "Approve""Credential issued"

Wait for the row in step 6 before step 7, because Member B reads the chain when it checks. The dashboard's Chain tab shows the chain ID and the paymaster deposit in collapsed panels.

What the steps look like​

These screenshots come from the recorded run in Watch a run.

Step 1, the Network tab:

Network setup: 2 of 3 orgs admitted, 2 issuer keys published, Governance Safe threshold 2 of 3

Steps 2 to 4: the User wallet after "Create identity", Member A after "Register" and after "Issue a credential", and the credential in the User wallet's Credentials tab:

User wallet Setup complete, Member A Registered, Member A Credential issued, User wallet credential marked Valid

Step 5: Member B's check, the User wallet's proof screen and the result. Before you tap "Share", the User wallet lists what the verifier learns and what stays hidden. These come from the recorded run's later check, so the User wallet proves with Member B's credential. At step 5 it proves with Member A's.

Member B Check eligibility, User wallet Ready to prove with what is shared and hidden, Member B Eligible

Step 6: the Revoke button on the Orgs tab, and the User wallet once the revocation lands:

Orgs tab with Revoke beside Member A&#39;s valid credential, and the User wallet&#39;s credential marked Revoked

Steps 8 and 9: Member B finds the existing registration, then issues its own credential:

Member B Already registered and Not counted twice, then Credential issued

The Flow tab at the end of the recorded run. Its "Revoke the credential" stage turns "Done" only after a check fails on the revoked credential, which is step 7 above. The recording skips that step, so the stage still reads "Now":

The Flow tab after the run: every stage done except Revoke the credential, still Now

Optional: show deliveries with the SDK​

Optional, and not in the recording. This shows what the phones cannot: the User wallet never scans announcements and the stack runs no indexer. It needs BUNDLER_URL and Docker.

Stop the stack first with Ctrl+C in the first terminal, because the SDK run and the stack both sign as OWNER and their nonces can collide. Run this before you clean up, because its sponsored transfer is paid from the paymaster deposit.

Deploy the test token:

cd packages/contracts
set -a; . ../../.stack.env; set +a
forge script script/test/DeployTestScopedToken.s.sol --rpc-url sepolia --broadcast --private-key "$DEPLOYER_PRIVATE_KEY"
cd ../..

Copy the address it prints after TestScopedToken, or from the == Return == block if the token already exists. Add it to the file, then load the founding member's secret:

echo "TEST_SCOPED_TOKEN=<address>" >> .stack.env
set -a; . ./.stack.env; . packages/contracts/network-member.env; set +a

Run the test from the repository root:

yarn workspace integration-testbed test:testnet

It issues Alice a credential, has Bob verify it, revokes it, then delivers 100 tokens to Alice's stealth address. Alice sends 40 to Bob, paid by the paymaster. Look for these lines:

Member -> Alice: 100 tokens; mint <hash>; announcement <hash>
Alice discovered stealth account <address> automatically; balance 100
Alice sent 40 to Bob; paymaster sponsored user operation <hash>; transaction <hash>

Clean up​

When you are done, stop the stack with Ctrl+C in the first terminal if it is still running. The paymaster holds your stake and deposit, and you take both back in two steps. First withdraw the deposit, which also starts the unstake delay:

cd packages/contracts
set -a; . ../../.stack.env; set +a
export SEMAPHORE_PAYMASTER=$(jq -r .semaphorePaymaster deployments/11155111.json)
export PAYMASTER_WITHDRAW_TO=$OWNER
forge script script/utils/WithdrawPaymasterDeposit.s.sol --rpc-url sepolia --broadcast --private-key "$OWNER_PRIVATE_KEY"
cd ../..

A day later, withdraw the stake, in a new terminal if you like. Keep deployments/11155111.json until then, because this step reads the paymaster address from it:

cd packages/contracts
set -a; . ../../.stack.env; set +a
export SEMAPHORE_PAYMASTER=$(jq -r .semaphorePaymaster deployments/11155111.json)
export PAYMASTER_WITHDRAW_TO=$OWNER
forge script script/utils/WithdrawPaymasterStake.s.sol --rpc-url sepolia --broadcast --private-key "$OWNER_PRIVATE_KEY"