Architecture

Three layers. Two clusters. One verifiable settlement.

A condensed tour of how Nyx keeps order intent private without giving up on-chain auditability. Source-of-truth lives in docs/ARCHITECTURE.md ,this page is the visual map.

01 · System overview

Three trust boundaries.

01L1

Custody, Merkle tree, ZK verifier

Anchor 0.32 vault program holds the depth-20 incremental Poseidon Merkle tree, 32-root ring buffer, TEE pubkey, and protocol-fee config. Withdrawals go through an on-chain Groth16 verifier (alt_bn128 syscall). The matching engine program owns the per-market PDAs.

vaultmatching_enginegroth16-solana
02ER

Hidden order intent + matching

MagicBlock Ephemeral Rollup hosts the delegated PendingOrder PDAs. submit_order is signed by the user's seed-derived trading key and lives only in the rollup. run_batch executes uniform-clearing-price match with a Pyth circuit breaker and writes BatchResults.

PendingOrdersubmit_orderrun_batch
03Client

Key derivation, proofs, ix builders

@nyx/sdk hand-codes every Anchor instruction (no IDL parser at runtime). snarkjs runs in a Web Worker for VALID_WALLET_CREATE and VALID_SPEND. Key chain: Phantom signature → master seed → spending / viewing / trading keys.

@nyx/sdksnarkjsdarkpool-crypto
02 · Privacy boundary

What stays hidden, what surfaces.

Nyx hides individual order intent. Aggregate match data (clearing price, total matched volume, the two consumed note commitments) is public — that's by design, not an oversight.

ObjectL1 visible?Notes
Order side / price / amountHiddenStays in the ER until run_batch matches
Order's collateral note commitmentHiddenSame — only inside the ER
User's trading-key signature on submit_orderHiddenThe whole submit tx lives in the ER
note_commitment of the deposit notePublicPublic on vault::deposit (always was)
Deposit amount / mintPublicSPL transfer is on L1
Match clearing price + matched volumePublicSurfaces in BatchResults after commit
Settlement note commitments (note_c, note_d, note_fee)PublicTEE appends them in tee_forced_settle
Withdrawal amount + recipient ATAPublicSPL transfer-out is on L1
03 · End-to-end flow

One trade, ten transactions.

The hot-path tx for users is step 5 — submit_order on the ER. The other steps are mostly one-time per user / per market.

#ClusterInstructionSignerPrivacy property
1L1vault::create_wallet (VALID_WALLET_CREATE)user payerlinks user_commitment to a Solana payer; identity-only
2L1vault::deposituser payerreveals deposit amount + mint (SPL transfer)
3aL1matching_engine::init_pending_order_slotuser trading_keyempty PDA, zero order intent
3bL1matching_engine::delegate_pending_orderfunder + user trading_keyhand slot to ER validator
5ERmatching_engine::submit_orderuser trading_keyHIDDEN — order intent never on L1
6ERmatching_engine::run_batchTEE / operatormatch all delegated slots in the rollup
7ERmatching_engine::undelegate_marketTEE / operatorcommits BatchResults back to L1
9aL1vault::lock_note(note_a) + lock_note(note_b)TEEreferences commitments already public from deposit
9bL1Ed25519 precompile + vault::tee_forced_settleTEEatomic note_a/b consume + note_c/d/fee append
10L1vault::withdraw (VALID_SPEND)recipientspends a note, reveals amount + mint + recipient ATA
04 · Cryptographic primitives

Every choice is boring on purpose.

Standard, well-audited primitives only. The on-chain Groth16 verifier is groth16-solana v0.2.0; the Poseidon implementation is light-protocol's reference (BN254 Fr).

Curve
BN254 (alt_bn128)
Groth16 verifier on-chain · snarkjs prover off-chain
Hash · in-circuit
Poseidon2 / BN254 Fr
Note commitments · nullifiers · Merkle · user commitments
Hash · ambient
SHA-256 · SHA3
Inclusion commitment · key derivation · payload hash
Signature
Ed25519 (Solana precompile)
TEE attestation in tee_forced_settle
ZK proof system
Groth16
VALID_WALLET_CREATE · VALID_SPEND
Merkle tree
Incremental Poseidon · depth 20
vault::merkle.rs · 32-root ring buffer
05 · Security model

What the system protects against.

  • Front-running of unmatched orders
    Order intent lives in the ER, never on L1.
  • Replay of TEE-signed settlements
    consumed_note PDAs lock both legs; a second identical settle collides at PDA allocation.
  • Withdrawals without ownership
    VALID_SPEND requires the spending key; nullifier PDAs prevent double-spend.
  • Conservation violations
    tee_forced_settle enforces note.amount = trade + change + fee exactly before any state mutation.
  • Mismatched canonical hashes
    The Ed25519 precompile message must equal canonical_payload_hash(payload); a TEE that signs a different message is rejected.
06 · Roadmap

What is not yet shipped.

  • 01
    Real TDX/SEV TEE + remote attestation
    Today the TEE is a software Ed25519 keypair. Production deploys must pin the key inside an attested enclave.
  • 02
    Browser prover replacing snarkjs shell-out
    WebProverSuite is wired in for VALID_WALLET_CREATE + VALID_SPEND in the dapp; SDK-level prover still shells out for tests.
  • 03
    Off-chain indexer for shielded notes
    Without one, the dapp reconstructs the Merkle tree from RPC history every time. See apps/demo/ARCHITECTURE.md §2 for the eleven workarounds.
  • 04
    Continuous ER ↔ L1 commit scheduler inside the TEE
    Production wants commit_market_state every N slots so settlement can pick up matches without a full undelegate cycle.
  • 05
    Real protocol-owner keypair for fee withdrawal
    Fee notes accumulate but can't be spent until a real protocol-owner key is wired in.
  • 06
    PER JWT session manager wired into the ER trade-flow test
    Network-side anonymity-set requires JWT-gated ingress to be effective.

Try it on devnet.
Every step is verifiable.

Connect a Phantom wallet on Solana devnet and run the full flow — identity derivation, shielded deposit, ER-private order, TEE settlement, and proof-backed withdraw. Every receipt is an explorer link.