Privacy pool estilo Tornado en Soroban: depósito fijo de 10 XLM, commitment en un Merkle tree Poseidon
(16 niveles), retiro con prueba Groth16 (BN254) + nullifier. Ver PRD.md y CLAUDE.md.
Página de demo: https://veil.agentposhq.com (alias: https://veil-lime-alpha.vercel.app) · código: https://github.com/NovaCorpAI/veil
| Qué | Valor |
|---|---|
| Contract ID | CA4D3V4DB6FPXM4K7WAOG4LHOI5L7Q6R2EI4CENKZKQGER5VUG3CL6QQ · stellar.expert |
| Token | SAC XLM nativo CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC |
| init tx | 528f08c2… |
| deposit (alice, 10 XLM) | 4b1a171b… |
| withdraw (carol → dave, prueba ZK) | 188ac2f9… |
| corrida 2 (con OpenRouter): deposit / withdraw | b54d05bb… / b5297ecc… · asesor IA: "riesgo medio, esperar más depósitos" |
| segundo withdraw con la misma nota | rechazado en simulación: Error(Contract, #5) = NullifierSpent |
Un observador ve: alice → pool (10 XLM) y pool → dave (10 XLM) firmado por carol. Nada vincula las dos txs:
el retiro solo expone root, nullifierHash y recipient, y la prueba Groth16 demuestra que existe un
commitment en el árbol sin revelar cuál.
- alice (depositante):
GAJJUTIVOWTTHL73PFPSBNSNMJJZY3EFTTLW7VLVP4LDVA4KXGKYRMVX - carol (firma el retiro):
GACFTWXM6RWGB5PDVFRCTBC5T3K6MS7NHKXMLGO3JPD6YH7IN6KZ3RLD - dave (destinatario):
GDN5MJPTUSOEAMXMFD3LTYIG7JP4Z7UI746YQHLPK4QATSHZPDKGO2GA
Nivel 1 · sin instalar nada (2 min). Abrir el contrato en stellar.expert: hay 2 deposit y 2 withdraw. En cada withdraw, los args son la prueba (A, B, C), la raíz, el nullifierHash y el destinatario; la tx solo tuvo éxito porque el pairing_check BN254 pasó dentro del contrato. Las cuentas que firman (alice deposita, carol retira) no tienen relación on-chain. https://veil.agentposhq.com muestra lo mismo en vivo.
Nivel 2 · tests del contrato (5 min, solo Rust). cd contracts && cargo test. Cuatro tests: Poseidon(1,2) == circomlib, recipient→Fr igual que el cliente, una prueba real de snarkjs (src/test_fixture.rs) verifica con el pairing BN254 y una alterada no, y el flujo completo deposit → withdraw → doble gasto rechazado con un SAC en el env de pruebas.
Nivel 3 · demo contra el contrato desplegado (10 min). Necesita Node 20+, la Stellar CLI y cuentas de testnet; no necesita Rust ni circom porque el wasm, el zkey y la VK ya están en el repo.
npm i
stellar keys generate alice --network testnet --fund
stellar keys generate carol --network testnet --fund
stellar keys generate dave --network testnet --fund
cp .env.example .env # opcional: OPENROUTER_API_KEY; sin key el paso de IA usa un stub
STELLAR=$(which stellar) bash scripts/demo.shEso hace un depósito nuevo desde tu alice, retira con tu carol hacia tu dave con una prueba generada en tu máquina, e intenta el doble retiro. deploy.json apunta al contrato ya desplegado. Para desplegar uno propio: bash scripts/deploy.sh (requiere Rust + target wasm32v1-none).
| Fase | Estado |
|---|---|
| 1 · Circuito | ✅ circuits/withdraw.circom (4396 restricciones no lineales), setup Groth16, prueba verificada con snarkjs |
| 2 · Contrato | ✅ contracts/veil_pool — 4 tests: Poseidon(1,2) == circomlib, recipient→Fr, prueba real de snarkjs verifica on-chain (pairing BN254), flujo completo deposit/withdraw/doble-gasto |
| 3 · Cliente | ✅ client/deposit.js, client/withdraw.js (eventos getEvents → árbol → snarkjs → invoke) |
| 4 · Demo | ✅ scripts/demo.sh |
| IA (requisito) | ✅ client/openrouter.js: asesor de privacidad vía OpenRouter antes de cada retiro (key en .env, ver .env.example) |
- Poseidon on-chain = circomlib: crate
soroban-poseidon28.0.0 (poseidon_hash::<3, Bn254Fr>); test vectorPoseidon(1,2) = 0x115cc0f5e7d690413df64c6b9662e9cf2a3617f2743245519e19607a4417189a✅ en ambos lados. - G2 (snarkjs → host functions): cada coordenada Fp2 va
be32(c1) || be32(c0)(formato EIP-197). snarkjs entrega[c0, c1]. Implementado enclient/encode.js; el pairing pasó al primer intento. - Verificador:
pairing_check([-A, alpha, vk_x, C], [B, beta, gamma, delta])conenv.crypto().bn254()(soroban-sdk 28, sin feature extra). VK pegada como constantes ensrc/vk.rs(generado porclient/export-rs.js). - recipient como Fr:
sha256(XDR del ScVal Address)[0..31](31 bytes para no exceder el campo), idéntico enclient/addr.jsy enaddr_to_frdel contrato. - Árbol:
zeros[0] = 0,zeros[i] = H(zeros[i-1], zeros[i-1]),filled_subtrees[16], ring de 30 raíces. Misma convención enclient/tree.js(pathIndices[i]=0 ⇒ hoja a la izquierda).
npm i
bash scripts/build-circuit.sh # circom + setup Groth16 + prueba de ejemplo + verify
node client/export-rs.js # regenera vk.rs y test_fixture.rs del contrato
(cd contracts && cargo test) # 4 tests
bash scripts/deploy.sh # build wasm, deploy, init → deploy.json
bash scripts/demo.sh # deposit (alice) → nota → withdraw (carol→dave) → doble retiro rechazadoUso manual:
node client/deposit.js alice # imprime la nota veil-<nullifier>-<secret>
node client/withdraw.js <nota> <G...dest> carol # genera prueba y retiratools/circom.exe2.2.3,tools/stellar.exe28.1.0 (no están en PATH; los scripts los invocan por ruta).- Rust: toolchain
stable-x86_64-pc-windows-gnu(el MSVC no está instalado; el GNU trae linker propio) +wasm32v1-none. - soroban-sdk 28.0.0, soroban-poseidon 28.0.0. Testnet corre protocolo 29.
- Powers of tau: los mirrors públicos del
.ptau2^16 devuelven 403, así queptau/pot14_final.ptause generó localmente con snarkjs (2^14 basta: < 2^13 restricciones). Solo demo; no es una ceremonia real.
Entrega y checklist de unlinkability: SOLUTION.md.
Association sets, relayer de fees, montos variables, web UI, mainnet.