Traduzione italiana di Challenges for Cardano Developers
Traduzione italiana a cura di Lordwotton di RIOT Stake Pools. Se apprezzi queste traduzioni, per favore valuta di supportare il mio lavoro delegando i tuoi ada a RIOT entra nel nostro gruppo Telegram
Sfide per gli sviluppatori di Cardano
Per gli sviluppatori può essere difficile sfruttare appieno il modello UTxO perché devono tenere conto della parallelizzazione. Cardano non consente di mantenere un unico stato globale dell’applicazione nella parte on-chain dello smart contract. Ogni UTxO può rappresentare un pezzo dello stato dell’applicazione e può essere elaborato indipendentemente e in parallelo. Ciò consente teoricamente un elevato throughput e scalabilità, ma l’applicazione deve affrontare le complessità legate alla gestione delle transazioni concorrenti. Quali sono le sfide che gli sviluppatori devono affrontare su Cardano?
Quando la parallelizzazione è facile
Nel modello Extended Unspent Transaction Output (eUTxO), ogni UTxO può essere elaborato indipendentemente e in parallelo. La spesa di UTxO non dipende da nessuno stato globale di Cardano. Se le condizioni di spesa sono soddisfatte, uno o più UTxO di uscita saranno creati attraverso la transazione a partire dall’UTxO di ingresso (o da più UTxO di ingresso). Gli UTxO di input devono essere completamente consumati. Gli UTxO di uscita devono avere un valore uguale (o inferiore) a quello degli UTxO di ingresso.
Se Alice invia 100 ADA a Bob, la spesa di UTxO dipende solo dal Testimone di Alice. Se cento altri utenti inviano una transazione simile, vengono utilizzati altri 100 UTxO di ingresso unici. Ogni mittente di transazione è il proprietario dell’UTxO di ingresso. Non c’è dipendenza tra transazioni e UTxO di input.
Non c’è dipendenza tra transazioni e UTxO di input perché tutti i mittenti sono indipendenti l’uno dall’altro. Hanno deciso autonomamente di inviare 100 ADA. Tutte le 100 transazioni possono essere inserite nello stesso blocco e saranno valutate come valide.
Una transazione consuma uno o più UTxO come input e produce uno o più nuovi UTxO come output. Questa semplice regola si applica sia al trasferimento di valore tra Alice e Bob, sia nel caso delle applicazioni, come si vedrà più avanti.
Nell’immagine si vedono 3 transazioni identiche. Con 100 transazioni l’aspetto sarebbe lo stesso. Non lasciatevi confondere dal fatto che il mittente è sempre Alice e il destinatario è Bob. Ogni volta si tratta di un’Alice e di un Bob diversi. La figura vuole dimostrare che non è necessaria alcuna sincronizzazione tra i mittenti delle transazioni. Se le transazioni sono valide, non possono fallire e vengono tutte inserite nella blockchain.
Il significato della figura potrà essere compreso in seguito, dopo aver spiegato il funzionamento di un DEX.
La rete Cardano può convalidare le transazioni in qualsiasi ordine, poiché le transazioni sono indipendenti l’una dall’altra. Questo è un vantaggio dal punto di vista del throughput della rete, poiché il consenso non dipende dall’elaborazione sequenziale.
Come creare un’applicazione parallela?
Idealmente, gli sviluppatori di applicazioni dovrebbero creare una logica di smart contract che si comporti in termini di parallelizzazione in modo simile a quando 100 mittenti (Alices) inviano una transazione. Tuttavia, questo è quasi impossibile.
Chiariamo questo punto con un esempio di DEX che utilizza pool di liquidità.
Un pool di liquidità viene riempito da più transazioni che producono UTxO in uscita. Questi UTxO rappresentano i token del pool di liquidità. Ad ogni nuovo blocco aggiunto al libro mastro, la composizione degli UTxO nel pool di liquidità può cambiare.
Nella figura, si vede un pool di liquidità con una coppia di token X e Y. Nel pool di liquidità ci sono diversi UTxO con i token X e Y. Nel blocco successivo, Alice, Bob e Carol inseriscono nel pool di liquidità nuovi UTxO con il token X, mentre Dave, Eve e Frank inseriscono alcuni UTxO con il token Y. Poiché non si è verificato alcuno scambio, nessun UTxO è stato rimosso dal pool.
Per effettuare uno swap, il DEX deve consumare UTxO (o più UTxO) dal pool di liquidità come input per ogni tipo di token. Questi UTxO devono essere spesi completamente. Se lo swap non richiede tutti i token dell’UTxO, i token rimanenti devono essere restituiti al pool di liquidità come nuovi UTxO.
Nel caso di DEX, molte operazioni sono in corso contemporaneamente. Gli utenti inviano token (fornitori di liquidità) ai pool di liquidità. I fornitori sono i mittenti e DEX è il destinatario. Allo stesso tempo, altri utenti possono inviare richieste di swap. Sono mittenti che danno X token a DEX e vogliono ottenere Y token. O viceversa.
Più partecipanti possono inviare una richiesta di swap in un breve periodo di tempo. Questi swap possono riguardare un unico pool di liquidità. Pertanto, esiste un’interdipendenza tra i partecipanti.
È anche una buona idea chiarire cosa sia esattamente il DEX. I contratti intelligenti complessi in Cardano (come DEX) sono costituiti da due parti: la logica on-chain che viene eseguita sulla blockchain e la logica off-chain che viene eseguita sui server (o nei portafogli locali).
Nell’immagine si può vedere un DEX composto da logica on-chain e off-chain.
Mentre l’esecuzione della logica on-chain è naturalmente decentralizzata in quanto avviene nella rete Cardano, il team è responsabile della decentralizzazione della logica off-chain del DEX. La parte off-chain del DEX non è (non dovrebbe essere) composta da un solo agente, ma da diversi agenti.
Nel caso del DEX, questi agenti sono chiamati batcher. Sono responsabili dell’esecuzione degli swap. I batcher creano transazioni che soddisfano le condizioni di spesa degli UTxO nel pool di liquidità e trasferiscono attività nel rapporto richiesto da entrambi i partecipanti allo swap.
Cardano non consente di mantenere uno stato applicativo globale uniforme nella parte on-chain dello smart contract. Tuttavia, è tecnicamente possibile.
Se gli sviluppatori dovessero memorizzare l’intero stato di una DApp in un singolo UTXO, creerebbero essenzialmente uno stato globale simile a quello che esiste nel modello basato sugli account di Ethereum. Questo potrebbe limitare la concurrency e il throughput della DApp. Questo approccio non sfrutterebbe appieno i vantaggi del modello EUTxO.
Nella parte on-chain del DEX, gli UTxO e i Datum associati rappresentano lo stato dell’applicazione. Lo stato è quindi distribuito tra gli UTxO. Se il DEX deve avere uno stato applicativo globale uniforme, questo deve essere mantenuto fuori dalla catena attraverso i batcher.
Nell’immagine si vede un pool di liquidità con i token X e Y e 3 batcher. Lo stato globale dell’applicazione e la sincronizzazione dello stato tra i batcher sono indicati in blu. Lo stato dell’applicazione è costituito da dati on-chain che sono Datum associati agli UTxO e dallo stato dell’applicazione off-chain mantenuto dai batcher (agenti). I batcher comunicano tra loro per sincronizzare uno stato dell’applicazione globale uniforme.
Questo è necessario perché i batcher (agenti) accedono alla stessa risorsa, ovvero il pool di liquidità. Devono utilizzare gli UTxO in ingresso dal pool di liquidità (per eseguire lo swap) e può accadere che due (o più) agenti vogliano utilizzare lo stesso UTxO. Potrebbe verificarsi un problema di contesa.
Ora è il momento di ricordare la prima immagine dell’articolo. Quando 100 mittenti inviano una transazione, non possono competere tra loro per lo stesso UTxO, poiché ogni mittente utilizza il proprio UTxO. Gli UTxO nel pool di liquidità sono una risorsa condivisa, cioè una risorsa a cui accedono più agenti.
In generale, la contesa si riferisce allo scenario in cui più thread o processi (nel nostro caso, agenti) cercano di accedere alla stessa risorsa in modo tale che almeno uno di essi funzioni più lentamente di quanto farebbe se gli altri non fossero in esecuzione.
Nel nostro caso, c’è il rischio che due agenti costruiscano una transazione in cui utilizzano lo stesso UTxO in ingresso dal pool di liquidità. In questo caso, solo una transazione sarà accettata da Cardano. La seconda fallisce.
Nell’immagine, si può vedere che il batcher 1 e il batcher 3 stanno cercando di utilizzare lo stesso UTxO con il token X. Si è verificata una contesa. I batcher sono apparentemente poco sincronizzati e non conoscono l’intenzione dell’altro di utilizzare questo particolare UTxO. Se vengono create due transazioni di swap, una avrà successo e l’altra fallirà.
L’obiettivo di DEX è quello di consentire l’esecuzione di swap in modo concorrente, cioè in modo che i singoli agenti possano costruire transazioni simultaneamente e che non si verifichino contese quando scelgono gli UTxO.
Per evitare che le transazioni falliscano, deve esserci una comunicazione fuori catena tra gli agenti o qualche altra forma di sincronizzazione. In altre parole, gli agenti devono mantenere uno stato globale coerente del DEX.
I singoli agenti devono in qualche modo riservare gli UTxO nel pool, in modo che lo stesso UTxO non venga utilizzato da un altro agente. In alternativa, si può lavorare in modo che in ogni blocco successivo (20 secondi) tutte le transazioni siano costruite da un singolo agente (scelto a caso). Sebbene questo approccio sia decentralizzato, è meno concorrente.
Nell’immagine si può notare che il batcher 1 e il batcher 3 hanno scelto UTxO con i token X e Y in modo esclusivo per gli swap, quindi non c’è stata contesa. Gli swap 1 e 2 vengono eseguiti in modo concorrente. Non c’era contesa perché tutti i batcher sincronizzavano lo stato globale tra loro.
Si noti che il token X ha un valore di mercato esattamente doppio rispetto al token Y e che, per coincidenza, nel pool di liquidità erano presenti UTxO adatti all’accoppiamento. Lo swap 1 consuma 100 gettoni X e 50 gettoni Y. Lo swap 2 consuma 200 token X e 100 token Y. Se non ci fosse un UTxO con 100 X token nel pool, il secondo più adatto sarebbe un UTxO con 114 X token. Ciò significa che 14 gettoni X dovrebbero essere restituiti al pool di liquidità come nuovo UTxO.
Un’altra sfida, che non verrà discussa ulteriormente nell’articolo, è la selezione appropriata degli UTxO per gli swap. Con Ethereum, questo non è un problema, poiché fondamentalmente vengono aggiornati solo i saldi dei conti.
È possibile affrontare il problema in modo completamente diverso dall’utilizzo di un pool di liquidità. Invece di mettere gli UTxO in un unico pool, è possibile collegare i singoli candidati allo swap. Tuttavia, per semplicità, in questo articolo ci atteniamo ai pool di liquidità.
Progettare un DEX su Cardano in grado di elaborare gli UTxO in parallelo mantenendo la decentralizzazione significa affrontare il problema della concorrenza. Questa è una delle sfide per gli sviluppatori.
Nell’articolo abbiamo mostrato una delle possibili soluzioni, ovvero l’uso di componenti off-chain e on-chain. Il componente off-chain può essere usato per formare correttamente le transazioni per interagire con il codice on-chain. La correttezza è garantita dalla comunicazione off-chain che consente la sincronizzazione dello stato globale dell’applicazione.
Uno degli altri approcci possibili è quello di creare un algoritmo che dia agli utenti accesso esclusivo all’invio dell’azione desiderata. L’algoritmo può successivamente unire tutte le azioni rispettando i tempi e l’equità.
Gli sviluppatori possono mantenere un unico stato nella parte on-chain dell’applicazione o suddividerlo tra più UTxO. Avere un unico stato sulla catena è facile perché è più semplice mantenere la coerenza. Tutte le parti dell’applicazione lavorano con gli stessi dati. La suddivisione dello stato on-chain su più UTxoS può aumentare la concurrency, ma comporta diverse sfide. La gestione di più UTxO aggiunge complessità alla logica dei contratti smart. È necessario garantire una forma di sincronizzazione che assicuri la correttezza (evitando la contesa).
Il nocciolo del problema sta nel realizzare la parallelizzazione in un ambiente decentralizzato.
La logica applicativa è sempre collegata agli UTxO. Ogni UTxO rappresenta un pezzo di stato indipendente che può essere elaborato in parallelo. Come abbiamo spiegato nell’articolo, questo è possibile solo se viene implementata una forma affidabile di sincronizzazione.
Se ci fosse un solo batcher o agente off-chain, potrebbe gestire lo stato del DEX e preparare le transazioni senza doversi preoccupare dei problemi di concorrenza. Questo potrebbe potenzialmente portare a un’elaborazione più veloce delle transazioni e a un maggiore throughput.
Tuttavia, questo approccio centralizzerebbe essenzialmente la parte off-chain del DEX, il che va contro il principio della decentralizzazione. La sfida, quindi, è quella di ottenere la decentralizzazione off-chain mantenendo alte le prestazioni ed evitando i problemi di concorrenza.
Anche gli sviluppatori di Ethereum devono affrontare delle sfide
Ethereum utilizza un modello basato sugli account e gli smart contract hanno uno stato globale che viene aggiornato dalle transazioni. Lo stato globale è costituito da codice (funzioni) e dati (stato) che risiedono a un indirizzo specifico sulla blockchain di Ethereum.
Uno stato globale di DEX potrebbe rappresentare lo stato attuale del portafoglio ordini, compresi tutti gli ordini di acquisto e di vendita aperti. Quando viene presentata una nuova transazione swap, essa rappresenta una potenziale modifica di questo stato globale. Tuttavia, questa modifica viene accettata solo quando la transazione viene inclusa in un blocco e convalidata dalla rete.
Le transazioni in Ethereum vengono elaborate in modo sequenziale, una alla volta. Ciò significa che nel mondo di Ethereum non c’è concurrency e quindi non ci sono problemi di concurrency. L’elaborazione sequenziale rende più semplice la progettazione di DEX su Ethereum per quanto riguarda la concorrenza e il parallelismo, perché gli sviluppatori non devono affrontare le complessità della gestione delle transazioni concorrenti.
Tuttavia, questa validazione sequenziale non riesce a sfruttare la concorrenza. L’esecuzione di transazioni in parallelo non sarebbe sicura, perché potrebbero esserci dipendenze tra i contratti. Se un contratto dipende dai risultati di un altro, questi contratti devono essere eseguiti nello stesso ordine da ogni validatore.
L’ordine delle transazioni all’interno di un blocco è determinato dai validatori. Essi possono scegliere di ordinare le transazioni in base a fattori quali il prezzo del GAS, il nonce e il tempo di prima visione. Pertanto, sebbene un DEX possa creare una coda di molti swap, non può essere certo dell’ordine in cui questi swap verranno eseguiti.
Questa incertezza può portare a situazioni in cui le transazioni falliscono a causa di condizioni di gara, in cui diverse transazioni competono per consumare la stessa liquidità. Per gestire questo problema, alcuni DEX implementano meccanismi come la tolleranza allo slittamento e le scadenze delle transazioni per aumentare la probabilità che le transazioni vengano eseguite con successo.
Una condizione di gara può verificarsi quando più utenti cercano di scambiare token nello stesso momento. Ad esempio, supponiamo che due utenti vogliano entrambi scambiare ETH con USDT e che nel pool di liquidità vi sia solo una quantità di USDT sufficiente a far andare a buon fine uno degli scambi. Entrambi gli utenti inviano le loro transazioni di swap più o meno nello stesso momento. I validatori di Ethereum decideranno l’ordine in cui queste transazioni saranno incluse in un blocco.
Se la transazione dell’utente A viene inclusa per prima, il suo swap andrà a buon fine e non rimarrà abbastanza USDT nel pool per lo swap dell’utente B. Quando la rete Ethereum cercherà di elaborare la transazione dell’utente B, fallirà perché non potrà soddisfare lo swap.
Il risultato dipende dalla tempistica relativa di due o più operazioni (swap). Anche se Ethereum elabora le transazioni in modo sequenziale, le condizioni di gara possono verificarsi quando più transazioni dipendono da una risorsa condivisa (come un pool di liquidità in un DEX) e vengono inviate nello stesso momento.
Si noti che una race condition può verificarsi anche se il pool di liquidità è gestito da un solo DEX. Questo perché la race condition non è causata dal DEX stesso, ma dalla natura dell’elaborazione delle transazioni.
In altre parole, le applicazioni su Ethereum possono preferire un certo ordine di elaborazione delle transazioni, ma questo non è sotto il loro controllo. Nel caso di Cardano, le transazioni vengono elaborate in base al principio “primo arrivato, primo servito”. Tuttavia, è necessario ricordare che i pool non sono obbligati a seguire questa regola e non esiste un meccanismo che costringa il pool a selezionare correttamente le transazioni dal mem-pool.
Confrontiamo brevemente le sfide per gli sviluppatori su entrambe le piattaforme.
Quando si costruisce un DEX o qualsiasi altra applicazione decentralizzata su Cardano, una delle sfide è la gestione e la sincronizzazione di pezzi di stato tra più UTXO e agenti. Ciò consente un throughput e una scalabilità maggiori, ma introduce anche ulteriori complessità legate alla gestione delle transazioni concorrenti.
L’applicazione può gestire e aggiornare il proprio stato al di fuori della catena, e quindi impegnare periodicamente lo stato sulla blockchain. Questo approccio può contribuire a ridurre il carico sulla blockchain e ad aumentare la velocità delle transazioni. Inoltre, la sincronizzazione off-chain può anche facilitare l’esecuzione parallela delle transazioni on-chain. DEX può preparare più transazioni in parallelo e poi inviarle alla blockchain per l’esecuzione.
La rapida sincronizzazione off-chain può potenzialmente portare a un’elevata scalabilità e parallelismo on-chain. Gli sviluppatori cercano di ottenere la massima parallelizzazione possibile attraverso la sincronizzazione.
Questo è diverso dal modello basato sugli account di Ethereum, in cui lo stato dell’intera applicazione è memorizzato in un unico stato globale. Lo stato globale può contenere tutti i dati necessari per il DEX e cambia attraverso le transazioni. Poiché Ethereum elabora le transazioni (chiamate) in modo sequenziale, gli sviluppatori non devono assolutamente preoccuparsi della concorrenza.
Tuttavia, il problema è che una forma di parallelismo è rappresentata dal comportamento degli utenti che possono cercare di consumare la stessa liquidità nel DEX su Ethereum. In pratica stanno lottando per un’unica risorsa, come i batcher che potrebbero voler consumare lo stesso UTxO.
Gli sviluppatori di Ethereum DEX non possono evitare completamente le condizioni di gara legate a più utenti che tentano di consumare la stessa liquidità. Lo stato del pool di liquidità (cioè quanta liquidità è disponibile) è determinato dalle transazioni che sono state inserite nella blockchain. Gli utenti non sanno in tempo reale se le loro transazioni andranno a buon fine perché Ethereum elabora le transazioni in modo sequenziale e l’ordine è deciso dai validatori.
Gli sviluppatori possono cercare di creare un meccanismo che impedisca agli utenti di inviare una transazione con un’alta probabilità di fallimento. Tuttavia, si tratta di un compito molto difficile.
Si noti che Cardano si comporta in modo deterministico, a differenza di Ethereum.
Conclusione
Per sfruttare appieno il modello UTxO e la parallelizzazione, è necessario migliorare la Ouroboros Proof-of-Stake. La rete Cardano deve essere in grado di convalidare e pre-approvare un gran numero di transazioni alla volta. Se diverse decine di transazioni venissero inserite nel blocco una volta ogni 20 secondi, non importa che sia possibile elaborarle in parallelo. La scalabilità sarebbe comunque relativamente bassa. Gli Endorser in ingresso apporteranno il necessario miglioramento del consenso PoS in cui il modello UTxO brillerà ancora di più.