ZKPerp 3/4: wat zero-knowledge niet kan
Deel 3 van een vierdelige serie over ZKPerp. Elk privaat protocol heeft ergens een trust-aanname. De meeste vertellen je niet waar.
Er bestaat een genre protocoldocumentatie dat een systeem volledig beschrijft in termen van wat het voorkomt. Niemand kan dit vervalsen. Niemand kan dat stelen. De lijst wordt lang, de lezer wordt gerustgesteld, en de interessante vraag — wat is er nog waar als één specifieke partij vijandig wordt? — wordt nooit hardop gesteld.
Ik wil hier het tegenovergestelde doen, aan de hand van een systeem dat ik zelf gebouwd heb.
ZKPerp is een perpetual-futures-DEX op Aleo waar position size, entry price, collateral, leverage en PnL versleuteld in records staan in plaats van gepost op een publiek orderboek. Die eigenschap is echt en wordt door de chain afgedwongen. Ze heeft ook een prijs, en die prijs is geen afrondingsfout. Hij is structureel, hij volgt uit de manier waarop zero-knowledge-uitvoering werkt, en hij duikt ook in jouw systeem op als je er een bouwt.
Aan het eind van deze post heb je de volledige lijst: wie wordt vertrouwd, waarvoor, en waar in elk geval de grens ligt. Onderaan staat een tabel als je de redenering liever overslaat.
De beperking in één zin
Private witnesses kunnen niet doorstromen naar publieke state.
Dat is het. Alles hieronder is een gevolg.
In het model van Aleo kan de body van een transition de private inputs van een trader lezen, maar geen on-chain mappings. Een finalize-blok draait on-chain tegen publieke mapping-state — en elk argument dat aan finalize wordt meegegeven, verschijnt in plaintext on-chain. Op het moment dat je wilt dat een private waarde gedeelde state beïnvloedt, moet je hem dus publiceren.
Dit is geen eigenaardigheid van Aleo. Elk ZK-uitvoeringsmodel waarin de verifier alleen leert dát een berekening correct was, en niet waarover hij rekende, heeft dezelfde vorm.
Waar het ons beet: open interest
Een perp-pool moet zijn geaggregeerde exposure kennen. Long open interest en short open interest bepalen hoeveel liquiditeit LP's veilig kunnen opnemen — je kunt LP's geen pool laten leegtrekken die de tegenpartij is van $5 mln aan live posities.
De natuurlijke implementatie is om OI atomair bij te werken in de finalize van open_position. Positie opent, exposure gaat omhoog, één transactie, geen vertrouwen nodig.
We hebben het geprobeerd. Dit is wat de chain opslaat:
active_position_ids— welke position-ID's bestaan. Hashes. Geen sizes.position_commits— commitment-hashes van positieparameters. Ondoorzichtig by design.pool_state— wat er als laatste is weggeschreven.
Er is geen manier om size_usdc over actieve posities op te tellen, omdat sizes nooit on-chain zijn opgeslagen. Ze zitten versleuteld in PositionSlot-records. Om long_open_interest binnen finalize bij te werken, moeten de size en de richting als publieke finalize-inputs worden gedeclareerd.
We hebben dit empirisch vastgesteld in plaats van erover te redeneren. Toen we OI-updates in finalize opnamen, verschenen trade-sizes en -richtingen als u64.public en boolean.public in elke transactie op de explorer. Perfecte atomaire boekhouding, nul privacy.
Drie opties, en een vierde die er bijna komt
OI bijwerken in finalize. Atomair en on-chain, geen vertrouwen nodig — en sizes en richtingen lekken publiek weg. Dit is de optie die het hele doel ondermijnt.
De orchestrator schrijft de aggregaten. OI is on-chain zichtbaar, posities blijven privé voor het publiek, en je hebt een trust-aanname geïntroduceerd.
Off-chain berekenen, alleen tonen. Geen vertrouwen nodig, en het OI-cijfer bereikt de chain nooit — dus niets on-chain kan ervan afhangen, inclusief de LP-opnamebeveiliging.
We kozen de middelste. Een off-chain orchestrator heeft een view key over de positiedata, berekent de aggregaten en schrijft ze on-chain weg via update_pool_state.
De vierde optie verdient een beschrijving, want het is degene die een cryptograaf zal aandragen en hij komt dichterbij dan de andere drie. Publiceer een Pedersen-commitment op +size als publieke finalize-input en accumuleer die in een mapping van het type group. Groepsoptelling werkt in finalize, dus de chain zou een lopend commitment op de totale OI bijhouden zonder dat een individuele size ooit in plaintext verschijnt. Dat is strikt beter dan optie één op privacy en strikt beter dan optie twee op integriteit — de orchestrator zou dan alleen nog het werkelijke aggregaat kunnen openen.
Toch dicht het het gat niet, om twee redenen. De LP-opnamebeveiliging heeft een ongelijkheid tegen OI nodig, en finalize kan niet vergelijken met een verborgen, vastgelegde waarde zonder dat iemand een opening aanlevert — waarmee je terug bent bij het vertrouwen van diegene, nu met meer machinerie. En niets in dat schema weerhoudt een trader ervan zich vast te leggen op een negatieve of absurde size, dus zou je bij elke opening een in-circuit range proof nodig hebben om de accumulator überhaupt betekenis te geven.
De eerlijke versie is dus: de optieruimte is niet uitgeput, maar elke kandidaat publiceert ofwel de sizes, ofwel eindigt bij een off-chain partij die een getal beweert dat de chain niet kan controleren. Ik heb geen vijfde gevonden, en ik zou er oprecht graag een aangewezen krijgen.
Wat de orchestrator kan zien
Eerst wat hij kan lezen, vóór wat hij kan doen, want dat is het deel dat de meeste disclosures overslaan.
Om sizes op te tellen, moet de orchestrator ze ontsleutelen. Hij heeft een view key over de positiedata en ziet daarom voor elke open positie: size, richting, entry price en collateral — waarmee leverage en live PnL af te leiden zijn, aangezien de oracle-prijs publiek is. Posities zijn privé voor andere traders, voor MEV-bots, voor de frontend en voor iedereen die de explorer leest. Ze zijn niet privé voor de orchestrator.
Dat is de prijs van optie twee, en het is een privacykost en geen integriteitskost, dus hij komt nergens voor in de lijst hieronder van "wat een kwaadwillende operator kan doen". Hij hoort hoe dan ook in de disclosure.
De eerlijke versie van het trust-oppervlak
update_pool_state overschrijft de volledige pool-state vanuit ongeverifieerde inputs — niet alleen de twee OI-cijfers, maar ook total_liquidity, total_lp_tokens en accumulated_fees. De enige controle is dat de aanroeper het orchestrator-adres in roles[1u8] is.
Het voor de hand liggende gevolg is denial of service. total_liquidity naar beneden bijstellen blokkeert opnames, maar het blokkeert ook close_position, liquidate, execute_take_profit en execute_stop_loss — alle vier asserten ze dat de pool de uitbetaling dekt voordat ze draaien. Een orchestrator die een voldoende klein getal wegschrijft, bevriest elk exit-pad in het protocol, en onderwater staande posities blijven open terwijl de markt doorbeweegt.
Het niet voor de hand liggende gevolg zit in het opnamepad van LP's. remove_liquidity prijst LP-tokens vanuit muteerbare pool-state:
let max_usdc: u64 = (amount_to_burn * state.total_liquidity)
/ (state.total_lp_tokens + 1u64);
assert(expected_usdc <= max_usdc as u128);
LPSlot heeft geen commitment-mapping. Posities van traders worden commitment-geverifieerd tegen een BHP256-hash die bij het openen is weggeschreven; LP-opnames zijn een verhouding over getallen die de orchestrator schrijft. De orchestrator bepaalt dus de inwisselprijs per token, en de solvabiliteitsbeveiliging op opnames waar hij daarnaast langs moet, wordt berekend uit OI-cijfers die hij eveneens zelf schrijft.
Wat betekent: blaas total_liquidity op, zet de open interest op nul, stel total_lp_tokens naar beneden bij, en één LP-opname veegt het echte saldo van de pool leeg. add_liquidity heeft een minimum van 100 USDCx, dus de orchestrator kan die LP zijn. Eén key, geen samenspanning nodig.
Het is verleidelijk om dit weg te schrijven als "de orchestrator kan niet meer bewegen dan het echte saldo van de pool — het USDCx-tokencontract dwingt dat onafhankelijk af". Dat is waar en het is misleidend. Het tokencontract begrenst de diefstal tot het echte saldo; het voorkomt de diefstal niet. De pool is overgeleverd aan de orchestrator, en anders beweren is precies de fout waartegen deze post bestaat.
Wat de orchestrator werkelijk niet kan:
- De uitbetaling op de positie van een individuele trader opblazen. Close, take-profit, stop-loss en liquidatie herberekenen de positie elk vanuit het on-chain BHP256-commitment. Een uitbetalingsbedrag dat door wie dan ook wordt aangeleverd — trader, keeper of orchestrator — wordt getoetst aan een hash die bij het openen is vastgelegd. Dit is de bescherming die LP-slots niet hebben, en die asymmetrie is de bug, niet het ontwerp.
- Oracle-prijzen zetten. Die worden apart bestuurd, wat niet hetzelfde is als veilig — zie verderop.
De fix
total_liquidity en total_lp_tokens worden al correct on-chain bijgehouden door add_liquidity en remove_liquidity. Ze hoefden nooit schrijfbaar te zijn voor de orchestrator. update_pool_state neemt nu twee argumenten in plaats van vijf:
fn update_pool_state(
public new_long_oi: u64,
public new_short_oi: u64,
) -> Final {
waarbij de resterende velden uit de huidige mapping worden gelezen en ongewijzigd worden teruggeschreven. De orchestrator-bot las die drie waarden off-chain toch al en echode ze rechtstreeks terug, dus het eerlijke pad verliest niets; alleen het oneerlijke sluit.
Daarmee blijft de orchestrator vertrouwd voor open interest en netto PnL — de twee cijfers die de chain werkelijk niet kan afleiden, om de reden bovenaan deze post — en voor het eerlijk vuren van TP/SL-triggers. Die gaan hierna achter een k-of-n-quorum, hetzelfde on-chain patroon dat de prijs-oracle al gebruikt. Dat brengt het vertrouwen terug van één key naar een threshold. Het is defense-in-depth, geen verificatie. De aggregaten worden nog steeds off-chain berekend, omdat het niet anders kan.
Samenvattend, als je dit beoordeelt: de orchestrator wordt vertrouwd voor open interest en netto PnL, en kan elke open positie lezen. Uitbetalingen op posities van traders zijn commitment-beschermd. Vóór de fix hierboven waren LP-opnames dat niet, en was het saldo van de pool bereikbaar met één key.
De keepers kunnen je positie ook lezen
Deel 2 beschreef het keeper-auth-patroon: open_position() mint drie LiquidationAuth-records in dezelfde atomaire transition, één aan elke keeper in de on-chain liquidator_set, elk met een kopie van entry_price, size_usdc, collateral_usdc, is_long en position_id.
Ik heb dat als integriteitsvraag geframed — wat weerhoudt een keeper ervan die velden aan te passen — en beantwoord met het commitment-schema. De vertrouwelijkheidsvraag verdiende evenveel aandacht en kreeg die niet. In Aleo ontsleutelt de eigenaar van een record dat record. De keepers zijn eigenaar van die records. Elke keeper kent dus de volledige parameters van elke positie vanaf het moment dat die opent, en de auths worden gemint in dezelfde transition als het slot van de trader, waardoor de koppeling met een openingstransactie eenvoudig te correleren is.
De grens: keepers kunnen buiten de vijf gates niets doen met wat ze weten. Ze kunnen geen solvente positie liquideren, geen prijs verzinnen, geen beloning opblazen en het slot van een trader niet consumeren. Wat ze hebben is informatie, geen macht.
Maar informatie is precies wat dit protocol hoort te beschermen, dus de accurate claim is smaller dan die ik in deel 2 maakte. De posities van ZKPerp zijn privé voor het publiek, voor andere traders en voor MEV-bots. Ze zijn niet privé voor de operator-set. Dat is een wezenlijk zwakkere eigenschap dan "alleen de eigenaar kan ontsleutelen", en het is de zin die ik een gebruiker zou willen laten lezen vóór hij stort.
Of dat gat te dichten is, is hetzelfde open probleem als permissionless liquidatie. De keeper moet een margin-conditie evalueren; die evaluatie vereist de parameters; een proof of liquidatability die alleen de boolean onthult is onderzoek, geen roadmap.
De oracle is een trust-aanname, geen waarborg
"Kan geen oracle-prijzen zetten" staat in de kan-niet-kolom van de orchestrator, en het daarbij laten zou precies het soort framing zijn waartegen deze serie bestaat.
Prijzen komen van een 2-of-3-quorum in een apart on-chain programma. Twee samenspannende ondertekenaars zetten de prijs. Gate 4 van het liquidatiepad berekent de equity uit die prijs, en Gate 2 vereist dat de exitprijs eraan gelijk is. Een gecompromitteerd oracle-quorum kan gezonde posities dus als onderwater markeren en liquideren — voor 10% van de collateral per stuk, over elke open positie, op een moment naar eigen keuze.
Dat is een strikt grotere macht dan wat dan ook op de lijst van de orchestrator. Hij is begrensd — het quorum is on-chain en auditeerbaar in plaats van één off-chain key, prijzen worden na 150 blocks afgewezen, en de ondertekenaars kunnen geen middelen omleiden, geen tokens minten en de LP-boekhouding niet aanraken. De opbrengst van een liquidatie loopt via hetzelfde commitment-geverifieerde uitbetalingspad als al het andere. Maar "apart programma, 2-of-3" is een mitigatie, geen vrijstelling, en n verhogen is de voor de hand liggende hardening-stap.
Wat te repareren is en wat niet
De fix hierboven was het makkelijke geval: een fout, dus die kon gewoon gecorrigeerd worden. Wat overblijft, kan dat niet.
Open interest is architectuurgebonden. Sizes zijn private witnesses. Er bestaat geen versie hiervan waarin de chain OI afleidt uit private positiedata zonder die data publiek te maken. Niet met meer engineering, niet met een beter circuit. Het volgt uit de beperking bovenaan deze post, en voor zover ik weet doet geen enkele privacy-behoudende perpetual-DEX op welke chain dan ook het wél — de beperking verklaart waarom.
Zichtbaarheid voor operators is óók architectuurgebonden. Zowel de keeper-set als de orchestrator hebben parameters in plaintext nodig om hun werk te doen. Dat terugbrengen tot een threshold verkleint het aantal partijen dat alles ziet; het verwijdert de categorie niet.
Het onderscheid dat ik aan het eind van de liquidatiepost aanstipte, hoort hier ook thuis. De keeper-set lijkt oppervlakkig op het orchestrator-quorum — meerdere partijen, één actie — maar ze lossen verschillende problemen op. De chain kan een liquidatie volledig verifiëren, dus de keeper-set hoeft alleen te garanderen dat er iemand komt opdagen. De chain kan een OI-cijfer niet verifiëren, dus die key verdelen is de enige beschikbare verdediging. Meerdere partijen zijn een surrogaat voor verificatie wanneer verificatie niet mogelijk is, en een liveness-mechanisme wanneer dat wel zo is. Het is de moeite waard om te weten waar je naar kijkt.
Dezelfde vorm, nog drie keer
Zodra je weet waar je op moet letten, keert het patroon door het hele systeem terug.
De dark-pool-operator ziet je order. ZKDarkPool settelt sealed-bid batch-veilingen tegen een uniforme clearingprijs, en niets van een pending order is zichtbaar voor andere traders of MEV-bots — vóór noch na settlement. Dat is de anti-front-running-eigenschap, en dat is degene die telt voor uitvoeringskwaliteit. Maar om off-chain een clearingprijs te berekenen, moet de operator de orders ontsleutelen. submit_order geeft een OrderAuth-record uit dat eigendom is van de operator, dus de operator ziet size, limietprijs, richting, asset en het adres van de trader.
De grens heeft dezelfde vorm als die van de orchestrator: de operator kan niet buiten de ondertekende limietprijzen settelen, kan niet meer bewegen dan het bedrag in escrow, en kan geen fill fabriceren tegen een geconsumeerde of verlopen order — allemaal ZK-afgedwongen bij settlement. Zijn resterende macht is censuur: weigeren te matchen. Dat wordt gemitigeerd door order-expiry en annulering door de gebruiker zelf, niet geëlimineerd. Ook de inhoud van orders afschermen voor de operator zou in-circuit matching over vastgelegde orders vereisen, en dat is open onderzoek.
De AMM kan helemaal niet privé zijn. De concentrated-liquidity-AMM houdt LP-posities als private records, maar de pool-state — prijs, tick, liquiditeit — is publiek, en swap-bedragen verschijnen als finalize-argumenten. Dit is geen kortere weg. Prijsvorming vereist publieke state; een marktprijs die niemand kan lezen is geen prijs. Volledige AMM-privacy is architectonisch onmogelijk, en precies daarom bestaat de dark pool als de plek voor omvang.
TP/SL-uitvoering vertrouwt op de trigger. De richting van de trigger wordt on-chain gevalideerd wanneer de order wordt geplaatst, en de uitvoeringsprijs moet gelijk zijn aan de verse oracle-prijs. Maar bij uitvoering wordt niet opnieuw gecontroleerd of de oracle de trigger daadwerkelijk gepasseerd is — de orchestrator wordt vertrouwd om alleen te vuren wanneer dat zo is. Klein, begrensd, en het staat in de beperkingentabel in plaats van weggemoffeld.
Het volledige trust-oppervlak in één tabel
| Partij | Vertrouwd voor | Ziet | Kan niet |
|---|---|---|---|
| Orchestrator — 1 key | Open interest, netto PnL, TP/SL-triggers | Elke positie: size, richting, entry, collateral | De uitbetaling van een trader opblazen; liquiditeits- of LP-tokencijfers schrijven |
| Keeper-set — 3, permissioned | Alleen liveness | Elke positie, vanaf het moment dat die opent | Een solvente positie liquideren; een beloning opblazen |
| Oracle-ondertekenaars — 2-of-3, on-chain | Prijswaarheid | Alleen publieke prijzen | Middelen bewegen; LP-boekhouding aanraken |
| Dark-pool-operator | Eerlijk matchen; niet censureren | Order-size, limiet, richting, asset, adres | Buiten ondertekende limieten settelen; escrow overschrijden |
| AMM | Niets — publiek by design | — | — |
Deze terugbrengen: de resterende writes van de orchestrator gaan hierna achter een k-of-n-quorum. n verhogen op de oracle is rechttoe rechtaan. Zichtbaarheid voor operators — zowel voor de keepers als voor de dark pool — is open onderzoek, geen roadmap-item.
Lees de tweede en derde kolom samen. Met het ingeperkte schrijfoppervlak is de integriteit van dit systeem redelijk op orde: geen enkele partij op die lijst kan het geld van een trader of van een LP afpakken. De vertrouwelijkheid is smaller dan de kop suggereert, en de operators zijn de reden.
Waarom dit publiceren
Twee redenen, één principieel en één uit eigenbelang.
De principiële: een trust-aanname die je niet hebt benoemd, is een trust-aanname die je gebruikers dragen zonder hem te beprijzen. Elk privaat systeem heeft er ergens één — het alternatief voor de jouwe openbaren is niet dat je er geen hebt, maar dat je er een hebt die niet openbaar is. De protocollen die dit fout doen liegen meestal niet; ze beschrijven de cryptografie accuraat en laten lezers daaruit generaliseren naar het hele systeem.
Die uit eigenbelang: dit is de post die de andere geloofwaardig maakt. Wanneer ik beweer dat een keeper een gezonde positie niet kan liquideren, of dat een trader een uitbetaling niet kan opblazen, zijn die claims meer waard van iemand die je ook vertelde dat diezelfde keepers elke positie die je opent kunnen lezen, en dat de orchestrator op dit moment total_lp_tokens kan overschrijven. Iedereen kan opsommen wat zijn systeem voorkomt. De disclosure die iets kost, is degene die informatie draagt.
Ik corrigeer mijn eigen privacyclaim liever in het openbaar dan dat iemand anders het na een audit doet.
Waar dit staat
ZKPerp draait op Aleo-testnet, is niet formeel geaudit en is niet bedoeld voor echt geld. De volledige trust-analyse staat in §4 en §13 van de technische whitepaper; de complete beperkingentabel is §14.
Deze serie
- Wat ZKPerp is — het overzicht: wat versleuteld is, wat live is en wat niet
- Hoe liquideer je een positie die je niet kunt zien? — het keeper-auth-patroon, vijf on-chain gates, en het deel dat onopgelost blijft
- Wat zero-knowledge niet kan — deze post
- Vier buildathons en een regelgevingsmuur — wat brak, wat ik verkeerd deed, en waarom ik dit niet zelf kan draaien
Technische whitepaper: ZKPerp-whitepaper (PDF). Contracten en broncode: github.com/hwdeboer1977/ZKPerp. Netwerk: Aleo-testnet. MIT-gelicentieerd.
Werk je aan privacy-behoudende markten, ZK-uitvoeringsmodellen, of exploiteer je een gelicentieerde venue en klinkt de zin "settlement-laag waar order flow niet zichtbaar is voor de markt" als een product in plaats van een slogan — dan hoor ik graag van je.