Decisões de arquitetura que não podem mudar depois de tomadas. Cada ADR documenta o que foi decidido, por quê, e quais alternativas foram rejeitadas.
Regra: decidir na Semana 1 e não revisar sem motivo crítico. Mudar qualquer um desses itens depois quebra a arquitetura inteira.
Status: Aceito
O Solana tem syscalls nativas para operações em curvas elípticas específicas. A escolha da curva define toda a compatibilidade do sistema de verificação on-chain.
Usar BN254 (Barreto-Naehrig 254-bit) via syscalls alt_bn128 do Solana.
BN254 é a única curva com syscalls nativas no Solana (alt_bn128_pairing, alt_bn128_addition, alt_bn128_multiplication), tornando verificação Groth16 extremamente barata — menos de 250K compute units por proof (ver ADR-022 para decomposição pós-ADR-020). Sem as syscalls, a verificação exigiria milhões de CUs, tornando o produto economicamente inviável.
- BLS12-381: Não tem syscalls no Solana. Verificação custaria ordens de magnitude mais.
- Ristretto255: Solana tem syscalls, mas não há tooling ZK maduro para esta curva.
Circuits Noir devem ser compilados com target BN254. Todos os provers e verifiers devem suportar BN254.
Status: Aceito
Múltiplos proof systems existem (Groth16, PLONK, halo2, STARKs). A escolha define custo de verificação on-chain, necessidade de trusted setup, e compatibilidade com tooling.
Groth16 compilado via Sunspot — compilador mantido pela Reilabs com suporte oficial da Solana Foundation.
Groth16 produz proofs de tamanho fixo e pequeno (256 bytes) com verificação O(1) — ideal para on-chain. Os alt_bn128 syscalls do Solana foram projetados especificamente para Groth16 sobre BN254. Sunspot é o único compilador Groth16 com suporte oficial da Solana Foundation e exemplos funcionais em solana-foundation/noir-examples.
- halo2: Sem trusted setup, mas proof size maior e sem syscalls dedicadas no Solana.
- STARKs: Proof size muito maior (~200KB vs 256 bytes), verificação mais cara on-chain.
- PLONK: Tooling menos maduro para Solana no momento.
Requer uma trusted setup ceremony (Powers of Tau). Para o hackathon, usar a ceremony pública do Hermez. Produção requer ceremony própria com múltiplos participantes.
Status: Aceito
ZK circuits podem ser escritos em circom, Noir, ou DSLs específicas como halo2 em Rust puro. A escolha define DX, velocidade de desenvolvimento, e compatibilidade com o ecossistema Solana.
Noir — linguagem Rust-like da Aztec, compilada via Sunspot para Groth16 BN254.
Noir é a stack oficialmente suportada pela Solana Foundation. O repo solana-foundation/noir-examples contém exemplos funcionais de Merkle membership, ECDSA verification, e Sparse Merkle Tree exclusion — exatamente os primitivos necessários. DX superior ao circom (tipagem forte, funções, módulos). O backend Barretenberg compila para WASM, habilitando proof generation diretamente no browser sem servidor intermediário.
- circom: DX inferior, sintaxe mais verbosa, tooling menos integrado com Solana.
- halo2 em Rust puro: Curva de aprendizado extremamente alta, sem exemplos Solana, impraticável em 5 semanas.
Proof generation no browser via @noir-lang/backend_barretenberg + @noir-lang/noir_js. Time precisa aprender Noir na Semana 1.
Status: Aceito
Merkle trees, commitments, e nullifiers dentro de circuits ZK precisam de funções hash. A escolha afeta diretamente o número de constraints e a performance do circuit.
Poseidon2 hash — ZK-friendly, usado via poseidon2_permutation no Noir. O circuit implementa um sponge sobre a permutação pública (std::hash::poseidon2_permutation) porque Poseidon2::hash é pub(crate) no Noir 1.0.0-beta.18.
Poseidon foi projetado especificamente para ser eficiente em arithmetic circuits. Um hash Poseidon custa ~220 constraints vs ~25.000 constraints para SHA-256 dentro de um circuit ZK. Para uma Merkle tree de profundidade 20, isso significa a diferença entre ~4.400 e ~500.000 constraints — impacto direto no tempo de proof generation no browser.
- SHA-256: ~100× mais constraints por operação dentro do circuit. Proof generation > 60 segundos no browser.
- Keccak: Implementação complexa em circuits, compatibilidade ruim com Noir.
A Merkle tree off-chain (TypeScript) também deve usar Poseidon para manter compatibilidade com os circuits. Usar @iden3/js-crypto ou implementação custom com Poseidon.
Status: Aceito
Solana tem dois token standards: SPL Token (legacy) e Token Extensions (Token-2022). Compliance hooks precisam interceptar transferências atomicamente e de forma não-bypassável.
Token Extensions com Transfer Hooks.
Transfer Hooks são executados atomicamente com a transferência — impossível de contornar via chamada direta ao SPL Token legacy. Um wrapper contract externo pode ser ignorado por wallets que chamam SPL diretamente. Para compliance regulatório, atomicidade é não-negociável. Token Extensions é o padrão recomendado pela Solana Foundation para tokens com lógica avançada.
- SPL Token legacy + wrapper: Não atomicamente seguro. Pode ser bypassado por qualquer wallet que chame o programa SPL diretamente.
- Programa proxy: Complexidade adicional sem garantia de atomicidade, não é padrão reconhecido.
Token de teste deve ser criado com Token-2022. Todos os clients (wallets, fintechs) precisam suportar Token Extensions — verificar compatibilidade com Phantom, Backpack, e ferramentas usadas no demo.
Status: Aceito
ComplianceAttestation accounts e o nullifier set acumulam on-chain com cada transação verificada. Sem compressão, custo de rent escala linearmente e inviabiliza o produto em produção.
Light Protocol ZK Compression para nullifier set e attestation accounts.
Light Protocol reduz custo de state em 200–5.000×. Um nullifier account normal custa ~0.002 SOL. Com ZK Compression: ~0.000001 SOL. Para um protocolo processando milhões de transações, a diferença é economicamente decisiva. Light Protocol está em produção, é auditado, e tem SDK TypeScript mature com exemplos de integração com Anchor.
- Bitmap on-chain: Simples mas inflexível para nullifiers de 256 bits, não escala.
- Implementação manual de compressed accounts: Reinventar a roda sem auditoria de segurança.
- Ignorar compressão no MVP: Economicamente inviável em produção. Melhor já usar a abstração certa desde o início.
Integrar light-sdk no Anchor program. Operações de nullifier check usam verify_compressed_cpi. Adicionar dependência na Semana 2.
Status: Aceito
Uma ZK proof válida poderia ser reutilizada em múltiplas transações se não houver mecanismo de anti-replay. Isso quebraria completamente o modelo de segurança do sistema.
nullifier = Poseidon(private_key, context_hash). Armazenado como compressed account via Light Protocol. Verificado atomicamente no Transfer Hook.
Nullifier é o mecanismo padrão de anti-replay em protocolos ZK (Zcash, Tornado Cash, Aztec). Derivado deterministicamente da chave privada do usuário e do contexto, é único por transação e matematicamente impossível de reutilizar sem conhecer a chave privada. Compressão via Light Protocol torna o custo de armazenamento desprezível.
- Timestamp-based expiry only: Não previne replay dentro da janela de validade. Atacante pode reutilizar proof enquanto ainda válida.
- Centralized nullifier registry: Single point of failure, derrota o propósito trustless do sistema.
- Sem anti-replay: Vulnerabilidade crítica. Não aceitável.
O circuit Noir deve computar e expor o nullifier como public input. O Transfer Hook verifica que o nullifier não está no compressed account set antes de autorizar a transferência.
Status: Aceito
Múltiplos modelos de monetização são possíveis: subscription, protocol fee on-chain com token próprio, enterprise license, ou pay-per-usage.
Pay-per-proof ($0.05/proof) com tiers mensais por volume. Sem token próprio no MVP.
Pay-per-proof alinha o custo ao uso real — fintechs pagam proporcionalmente ao volume gerado. Sem token elimina pressão de tokenomics, especulação de mercado, e complexidade regulatória desnecessária. Tiers mensais criam MRR previsível para pitch de investidores. É o modelo mais simples de implementar, explicar, e vender.
- Token próprio: Distrai do produto, cria obrigações regulatórias adicionais, complica o pitch com VCs.
- Subscription flat: Não escala para fintechs com volume muito variável mês a mês.
- Protocol fee on-chain: Requer token para distribuir fees, cria dependência de liquidez.
Billing via Stripe ou similar off-chain inicialmente. On-chain billing via x402 pode ser adicionado como feature premium no futuro sem alterar a arquitetura core.
| ADR | Decisão | Alternativa principal rejeitada |
|---|---|---|
| ADR-001 | Curva BN254 | BLS12-381 (sem syscalls) |
| ADR-002 | Groth16 via Sunspot | halo2 (sem syscalls dedicadas) |
| ADR-003 | Noir para circuits | circom (DX inferior) |
| ADR-004 | Poseidon2 hash | SHA-256 (100× mais constraints) |
| ADR-005 | Token Extensions + Transfer Hooks | SPL legacy + wrapper (bypassável) |
| ADR-006 | Light Protocol compression | Manual compressed accounts |
| ADR-007 | Nullifier on-chain | Timestamp expiry only (inseguro) |
| ADR-008 | Pay-per-proof sem token | Token próprio (complexidade desnecessária) |
Decisões derivadas das propostas de melhoria no PRD §15. Status Proposto — precisam de review do time e não bloqueiam o MVP atual. Referências PRD §15.x apontam para a descrição completa da feature no PRD.
Status: Proposto (PRD §15.1)
Verificação individual custa ~200K CU por proof. Fintechs com alto volume pagam linearmente. Múltiplas proofs no mesmo bloco poderiam compartilhar custo de pairing.
Adicionar instrução verify_proof_batch(proofs, public_inputs) que agrega N proofs em um pairing check amortizado (random linear combination) ou via recursive Groth16.
Pairing é o dominador de custo. Batching amortiza para ~50K CU/proof em N=10. Reduz custo efetivo para fintechs de alto volume, diferencial direto no pitch de economia.
- Recursive SNARK per-user: complexidade alta, setup custoso no MVP.
- Off-chain aggregator: perde atomicidade com Transfer Hook.
SDK expõe zksettle.proveBatch(). Transfer Hook precisa suportar batch mode opcional. Implementação pós-MVP — requer benchmarking de CU real.
Status: Proposto (PRD §15.2)
Credentials emitidas com schema vN invalidam quando issuer migra para schema vN+1. Sem versionamento, toda atualização quebra base de users.
Circuit expõe schema_version: u32 como public input. Program mantém registry de versões aceitas por issuer. Deprecation com grace period configurável.
Circuit recebe input adicional (1 field). Issuer account ganha campo accepted_versions: Vec<u32>. Custo negligível em constraints.
Status: Proposto (PRD §15.3)
Revogar 1 credential exige republicar Merkle root da árvore de users inteira — O(n) por revogação. Inviável para sanctions updates diários (OFAC).
Manter SMT de revogação separada da árvore de membership. Circuit prova (a) membership na tree de users E (b) non-membership na SMT de revogados. Issuer publica apenas delta.
- Rebuild da árvore principal: O(n) por revogação, inaceitável.
- Expiry curto sem revogação: força re-emissão massiva, ruim pra UX.
Circuit ganha ~2× constraints do current membership check. Issuer publica 2 roots por update: users + revogados.
Status: Proposto (PRD §15.4)
1 proof por tx é caro em UX. User pagando 3 tx seguidas gera 3 proofs = 30s de espera.
Circuit emite session_commitment = Poseidon(sk, mint, epoch, max_uses). Hook aceita N tx sob mesma session até expiry. Nullifier escopado à session, não à tx.
Anti-replay preservado via nullifier por session. Requer UI no SDK para gerenciar sessions ativas. Trade-off: leak de pattern (N tx da mesma session são linkáveis).
Status: Decidido / implementado (circuit + issuer account; PRD §15.5)
jurisdiction_set_hash como public input invalida todas proofs antigas quando issuer muda conjunto. Adicionar país = força re-prove global.
Substituir jurisdiction_set_hash por Merkle root do conjunto permitido. Circuit prova membership da jurisdiction do user na tree. Issuer adiciona país = publica root novo, proofs antigas continuam válidas se jurisdiction ainda permitida.
Circuit ganha Merkle path check adicional (~pequenos constraints com Poseidon). Issuer account armazena root de jurisdiction em vez de hash.
Status: Proposto (PRD §15.6)
Hook binário (aceita/rejeita) não escala para casos além de travel rule. Accredited investor gating, RWA compliance e multi-jurisdiction precisam de lógica configurável.
Attestation carrega payload rico: {jurisdiction, risk_tier, amount_cap, accredited_flag, ...}. Cada mint tem PolicyAccount associada com regras. Hook avalia policy vs attestation e decide.
1 program core serve múltiplos produtos (travel rule, accredited, RWA). Complexidade de policy DSL — iniciar com enum simples (Allow/Deny por campo), evoluir depois.
Status: Proposto (PRD §15.7)
Prover regenera witness completo a cada prove(). Para um user que faz múltiplas tx/dia, desperdício.
SDK cacheia witness + partial proof em IndexedDB indexado por credential hash. Re-prove recomputa apenas gates dependentes do novo contexto (nullifier, recipient, amount).
Puro DX win, zero impacto on-chain. Precisa invalidação do cache quando credential expira ou issuer root muda.
Status: Proposto (PRD §15.8, contingente)
Circuit único (membership + sanctions + jurisdiction + expiry + nullifier) pode estourar constraint budget ou exceder 10s no browser. Ativar apenas se checkpoint S1 indicar >10s.
Split em (a) proof de membership + jurisdiction, (b) proof de sanctions + nullifier. Hook verifica ambos no mesmo tx via 2 pairing checks (<200K CU combinado). Browser paraleliza em 2 web workers.
Hook mais complexo. Proof payload 2× maior. Trade-off aceito se proof time cair ~40%.
Status: Proposto (PRD §15.9)
ComplianceAttestation account por tx cresce linearmente com volume. Inviável em produção sem compressão agressiva.
Program acumula Merkle root de attestations por epoch (24h) em state único. Full attestations armazenadas off-chain (Helius webhook → S3/Arweave). Auditor pede prova de inclusion off-chain.
- Light Protocol compressed per-attestation (ADR-006): ainda O(N) storage, só comprimido.
- Drop attestations: perde audit trail, não-negociável.
On-chain state O(1) por epoch. Requer serviço de indexação confiável off-chain. Compatível com ADR-006 (pode usar Light para o root history).
Status: Proposto (PRD §15.10)
Schema custom de credential obriga issuers a adotar stack proprietária. W3C VC é padrão indústria com suporte de Jumio, Onfido, Persona.
Credentials emitidos como W3C VC JSON-LD com signature BBS+ (selective disclosure nativa). Circuit recebe BBS+ signature verification como input, prova posse + disclosure seletiva.
Issuers reusam stack existente. Credentials portáveis entre protocolos. Circuit ganha complexidade de BBS+ verification (~vs Poseidon-native custom format). Trade-off: padrão indústria vale DX loss.
Status: Decidido / implementado (suplementa Light compressed attestation; PRD §15.11)
check_attestation via CPI continua disponível, mas wallets e integradores esperam ativos comprimidos endereçáveis por DAS (Helius, SolanaFM, etc.). Uma cNFT Bubblegum dá UX de “NFT no wallet” sem substituir o nullifier + CompressedAttestation na Light state tree (ADR-006 / ADR-007).
- Instrução
init_attestation_tree: cria o concurrent Merkle tree (SPL account-compression) +TreeConfigvia CPI ao Metaplex Bubblegum; persisteBubblegumTreeRegistryPDA (sementebubblegum-registry) com endereço da árvore e bump do delegate programa. - Após Light CPI bem-sucedido em
verify_proof,settle_hooketransfer_hook(tail TLV), o program emite BubblegumMintV1pararecipient(leaf owner), na mesma transação (rollback atômico se o mint falhar). - Metadados URI (
programs/zksettle/src/instructions/bubblegum_mint.rs): template estávelhttps://zksettle.dev/meta/v1/{content_id}ondecontent_idderiva dehashv(issuer ‖ nullifier ‖ merkle_root ‖ slot ‖ expiry_slot)(44 chars);name=ZKS-{slot}. Expiry canônica =slot + MAX_ROOT_AGE_SLOTS(432_000 slots, alinhado acheck_attestation/ ADR-021). JSON off-chain completo pode ser resolvido pelocontent_id; binding on-chain permanece nos hashes Light + prova. - Contas Bubblegum em
verify_proof/settle_hooksão campos nomeados; no hook Token-2022, metas TLV extras para Bubblegum ficam após as contas Light para preservar índices emStagedLightArgs.StagedLightArgs.bubblegum_tail=0ou9(contagemMintV1).
- Leitura por outros programs on-chain: não é “ler um SPL token account”; consumidores usam prova Merkle / DAS + política off-chain. O caminho CPI
check_attestationpermanece quando acoplamento on-chain é aceitável. - CU / limites de tx: Groth16 + Light + Bubblegum na mesma instrução pode exigir
SetComputeUnitLimitacima do default; medição: compilar com--features hook-cu-probee inspecionar logscu-probe pre/post-bubblegum-mint(e estágios Light) no mesmo harness ADR-022. - Compatibilidade: IDs Bubblegum / account-compression são de cluster; o program usa constantes Metaplex/SPL atuais para mainnet/devnet.
Status: Decidido / implementado (refina ADR-007, PRD §15.12)
ADR-007 define nullifier = Poseidon(sk, context_hash) mas deixa context sem especificação formal. Risco de implementações inconsistentes.
Especificação formal:
nullifier = Poseidon(sk, mint_pubkey, epoch_index, recipient, amount)
epoch_index = floor(unix_timestamp / 86400)
Bind a recipient + amount previne front-running (ver threat model). epoch_index permite 1 proof/dia/token (UX) mantendo anti-replay.
Circuit recebe mint, epoch, recipient, amount como public inputs. Hook valida esses campos vs tx corrente. Fix de segurança crítico além da melhoria de UX.
- Pubkeys são split em dois limbs de 128 bits (
*_lo,*_hi) para caber no scalar field BN254 (~254 bits).pubkey_to_limbsgarante correspondência byte-a-byte entre circuit witness e ix args. epocheamountviajam comou64(pad BE em 32 bytes) viau64_to_field_bytes.verify_proofvalida frescor deepoch:EpochInFutureseepoch > current_epoch,EpochStalesecurrent_epoch - epoch > MAX_EPOCH_LAG(hoje1— ontem vale, amanhã não).- Sem Transfer Hook, a vinculação é apenas tão forte quanto os args que o chamador passa. Com Transfer Hook (
settle_hook/transfer_hook), os campos são rebindados contra a tx corrente viavalidate_settlement_guards.
Status: Decidido (implementado)
Issuer.root_slot é gravado em register_issuer e update_issuer_root mas até então não era consultado por verify_proof. Uma root antiga (issuer offline, chave comprometida sem rotação, ou qualquer janela onde a árvore off-chain diverge da root on-chain) permanecia válida indefinidamente, alargando a janela de ataque para provas obsoletas.
verify_proof rejeita com ZkSettleError::RootStale quando
current_slot - issuer.root_slot > MAX_ROOT_AGE_SLOTS,
com MAX_ROOT_AGE_SLOTS = 432_000 (~48h a 400ms/slot). O issuer é forçado a republicar a root via update_issuer_root em cadência de no máximo 48h; falha em republicar pausa verificações sem exigir upgrade on-chain.
- Issuer service precisa de job de rotação de root com SLA < 48h.
- Zero impacto em testes existentes — o fixture roda imediatamente após
register_issuer. - Ajustável: configuração futura pode expor
MAX_ROOT_AGE_SLOTSpor issuer caso diferentes verticais peçam janelas distintas.
Status: Decidido
ADR-001 fixou o custo de verificação em <200K CU. ADR-020 expandiu os public inputs de 2 → 8 (mint limbs, epoch, recipient limbs, amount), cada um exigindo preparação de MSM adicional durante a verificação Groth16. Medição pós-implementação: 219.767 CU (cargo test --test verify valid_proof_passes).
Novo ceiling operacional: <250K CU por proof. A transação envelopa com SetComputeUnitLimit(600_000) para margem de segurança e overhead da budget instruction. O valor de 600K é o safety ceiling nos testes, não o custo esperado.
- Custo SOL/proof sobe proporcionalmente (ainda <$0.001 a preços atuais de CU).
- Referências a <200K em ADR-001, README e PRD agora apontam para ADR-022 como fonte canônica.
- Redução futura possível via (a) batch verification (ADR-009), (b) Poseidon on-chain do tuple para comprimir pub-inputs em um único field element.
O mesmo verify_bundle roda sob o hook do Token-2022, mas o path adiciona: (a) parse + check de extensões do source_token (TransferHookAccount.transferring), (b) Light-CPI emitindo nullifier + attestation comprimidos na mesma tx, (c) Bubblegum MintV1 quando bubblegum_tail / contas nomeadas estão presentes (ADR-019). O ceiling de 250K cobre apenas o Groth16. Hook-path + mint total ainda não foi medido de ponta a ponta; a feature hook-cu-probe emite sol_log_compute_units em pre/post-verify_bundle, post-light-cpi, e pre/post-bubblegum-mint (também em verify_proof). TODO: registrar números aqui após o primeiro run do harness com fixture gnark + mint Token-2022 + árvore Bubblegum inicializada.
| ADR | Feature | Prioridade |
|---|---|---|
| ADR-009 | Batch verification | Alta (pitch) |
| ADR-010 | Schema versioning | Alta (produção) |
| ADR-011 | Revocation SMT | Crítica (sanctions) |
| ADR-012 | Session proofs | Média (UX) |
| ADR-013 | Jurisdiction Merkle | Decidido (implementado) |
| ADR-014 | Policy engine | Alta (pitch) |
| ADR-015 | Witness caching | Baixa (DX) |
| ADR-016 | Circuit split | Contingente (S1 bench) |
| ADR-017 | Audit epoch merkleizado | Alta (produção) |
| ADR-018 | W3C VC + BBS+ | Alta (pitch) |
| ADR-019 | cNFT attestation | Decidido (implementado) |
| ADR-020 | Nullifier context explícito | Decidido (security) |
| ADR-021 | Janela de frescor da Merkle root | Decidido (security) |
| ADR-022 | Orçamento de CU pós-ADR-020 | Decidido (perf) |