L'ultimo aggiornamento di Starknet (v0.13.2) denominato Bolt apporta due cambiamenti importanti: esecuzione parallela e block-packing . Sebbene indipendenti l'una dall'altra, entrambe le funzionalità supportano la spinta verso l'obiettivo di uno spazio di blocco veloce ed economico, crittograficamente protetto da Ethereum.
L'esecuzione parallela consente alle transazioni non contenziose (ovvero, transazioni che non toccano lo stesso stato) di essere eseguite contemporaneamente. Implementando l'esecuzione parallela, L2 come Starknet possono ridurre il tempo di esecuzione senza aumentare l'utilizzo delle risorse. Ciò significa commissioni di transazione inferiori per gli utenti e tempi di conferma delle transazioni notevolmente migliorati.
Il block packing ottimizza l'utilizzo di Starknet del blobspace su Ethereum L1: con il block packing, i sequencer possono generare una prova per verificare più blocchi Starknet L2 contemporaneamente. Ciò separa l'utilizzo del blobspace dalla frequenza di produzione dei blocchi L2 e ammortizza i costi di verifica della prova. Entrambi riducono i costi operativi per il sequencer Starknet, il che significa che gli utenti pagano meno per transazione.
Come abbiamo detto, Bolt rende Starknet più economico e veloce! Questo report fornirà un'analisi dettagliata dell'aggiornamento di Bolt, concentrandosi sull'esecuzione parallela e sul block packing, ed esplorerà le implicazioni per le prestazioni di Starknet.
I rollup sono soluzioni di scaling di livello due (L2) che mirano a scalare la blockchain di livello uno (L1) sottostante spostando il calcolo fuori dalla catena. Spostando l'esecuzione fuori dalla catena, i rollup possono ottimizzare la scalabilità (transazioni economiche e veloci), mentre L1 fornisce sicurezza per le transazioni L2.
Si dice spesso che i rollup "ereditino la sicurezza dall'L1". Ciò significa essenzialmente che ereditano il consenso e le garanzie di disponibilità dei dati fornite dall'L1. Oltre a ciò, l'L1 fornisce anche una garanzia di sicurezza sotto forma di bridging sicuro tra sé e il rollup.
Quando i sequencer pubblicano blocchi L2 su L1, L1 fornisce garanzie di disponibilità e ordinamento di queste informazioni. Da qui, i nodi L2 possono calcolare in modo affidabile la catena L2 canonica con queste informazioni insieme alle regole del rollup sulla derivazione della catena e sulla transizione di stato descritte dall'implementazione del nodo.
Per facilitare il bridging sicuro tra L1 e L2, L1 richiede la prova che la catena L2 che sta seguendo al momento sia corretta e non includa modifiche di stato illegali (ad esempio doppia spesa). Questa necessità di rollup per dimostrare la validità delle modifiche di stato, assicura che L1 non autorizzi prelievi dal rollup in base allo stato illegale.
I rollup differiscono in base al modo in cui dimostrano la validità delle modifiche di stato alla L1:
I rollup forniscono inoltre al livello di base dati sufficienti per le parti interessate a ricostruire lo stato L2. Mentre i rollup ottimistici devono pubblicare dati completi sulle transazioni per consentire ai contestatori di calcolare le prove di frode, i rollup di validità non hanno tali requisiti (le prove di validità garantiscono l'esecuzione corretta). Tuttavia, pubblicare dati completi sulle transazioni su L1 è comunque utile da una prospettiva di minimizzazione della fiducia (ricostruzione dello stato senza fiducia e prelievi senza autorizzazione).
Starknet è un rollup di validità che usa S calable, T rasparent AR gument of K nowledge (STARK) per dimostrare la validità delle modifiche di stato. L'ultimo aggiornamento di Starknet, nome in codice Bolt, aggiunge l'esecuzione parallela e il block packing. Nelle sezioni successive, spiegheremo come funzionano le due funzionalità e quali miglioramenti apportano agli utenti di Starknet.
Ad alto livello, l'aggiornamento di Bolt ha modificato i meccanismi di esecuzione, verifica e disponibilità dei dati di Starknet.
Prima dell'aggiornamento di Bolt, le transazioni Starknet venivano eseguite in sequenza, una dopo l'altra, dal sequencer. L'esecuzione sequenziale è semplice ma anche molto inefficiente. È inefficiente perché non sfrutta le molteplici unità di elaborazione indipendenti che i computer moderni offrono e la parallelizzabilità di un set di transazioni.
La parallelizzabilità è una misura di quanto siano indipendenti le transazioni in un dato set. Ad esempio, considera il set di tre transazioni qui sotto:
Transazione 1: Alice vuole inviare a Bob 1 STRK
Transazione 2: Caitlyn vuole inviare a Danny 100 ETH
Transazione 3: Caitlyn vuole inviare a Ella 100 ETH
La transazione 1 è completamente indipendente dalle transazioni 2 e 3, perché accede a una parte diversa dello stato (il saldo di Alice) e può essere eseguita contemporaneamente. Tuttavia, le transazioni 2 e 3 sono in conflitto perché vogliono accedere allo stesso stato, il saldo ETH di Caitlyn. Queste transazioni non possono essere eseguite contemporaneamente o finiremo con risultati conflittuali.
Per fare un esempio:
Evitare questi tipi di conflitti (e la natura complessa dei meccanismi di mitigazione) è il motivo per cui Ethereum ha scelto l'esecuzione sequenziale. Tuttavia, mentre l'esecuzione sequenziale riduce la complessità e migliora la sicurezza, si traduce in un uso inefficiente dell'hardware. Peggio ancora, la tendenza della progettazione hardware suggerisce che l'esecuzione sequenziale diventerà sempre più inefficiente nei prossimi anni.
La figura 4 mostra l'andamento della progettazione hardware negli ultimi 50 anni. La conclusione rilevante? Le prestazioni single-thread (cerchi viola) hanno raggiunto un plateau dalla metà degli anni 2000, mentre il numero di core logici è aumentato più o meno nello stesso periodo. Possiamo trarre due conclusioni sulla base di questi dati:
I progettisti hardware stanno ampliando i chip dei computer aggiungendo più unità di elaborazione indipendenti anziché migliorare le prestazioni di una singola unità.
Qualsiasi sistema che continui a fare affidamento sulle prestazioni migliorate di una singola unità di elaborazione subirà una battuta d'arresto nei guadagni di prestazioni, anche con hardware più recente.
Negli ultimi anni sono comparsi algoritmi sofisticati per gestire i conflitti di transazione e garantire la correttezza dell'esecuzione parallela. Block-STM (basato su un articolo di Fikunmi et al*) è uno di questi algoritmi e costituisce la parte centrale del nuovo motore di esecuzione parallela di Starknet. Analizzeremo l'algoritmo Block-STM nelle sezioni successive.
SHARP (abbreviazione di Shared Prover) di Starknet ha sempre sfruttato le dimostrazioni ricorsive per ridurre il più possibile i costi di verifica. Una dimostrazione ricorsiva è essenzialmente una "prova di dimostrazione" in cui una dimostrazione verifica che una o più dimostrazioni siano corrette. Di seguito è riportato uno schizzo di come SHARP genera una dimostrazione ricorsiva:
Il sistema SHARP prende un set di programmi da eseguire (un "lavoro") come input e genera una prova di esecuzione per il lavoro. Questi "programmi" sono blocchi L2 e la prova attesta la correttezza delle transazioni.
La prova viene inviata a un altro programma che verifica la prova e converte il programma di verifica della prova in un lavoro. SHARP prende il nuovo lavoro come input e genera un'altra prova (questa prova conferma la validità della prova precedente).
Il processo (bozza → lavoro → bozza) si riavvia e continua fino al raggiungimento di un obiettivo, a quel punto la bozza finale (che ora è una versione altamente compressa della bozza originale) viene pubblicata su L1
Questo design ammortizza notevolmente i costi per due motivi principali:
Sebbene il sistema di verifica fosse valido, sono state perse delle occasioni per risparmiare ulteriormente sui costi. Ad esempio, ogni lavoro era un singolo blocco Starknet e ognuno di questi blocchi era progettato per occupare un blob sulla L1. Ciò ha comportato alcune inefficienze come descritto di seguito:
Il block packing affronta questi problemi utilizzando un albero binario di dimostrazioni ricorsive. Discuteremo del block packing in una sezione successiva dell'articolo.
Come discusso in precedenza, l'esecuzione sequenziale è inefficiente (e diventerà sempre più inefficiente con il passare del tempo) e l'esecuzione parallela ingenua produce risultati non validi. Tuttavia, i motori di esecuzione parallela di produzione si prendono cura di evitare risultati incoerenti.
Esistono due approcci per affrontare l'esecuzione parallela: Pessimistic Concurrency Control (PCC) e Optimistic Concurrency Control (OCC) . PCC e OCC sono unità di elaborazione delle transazioni (TPU). Di seguito è riportata una definizione di unità di elaborazione delle transazioni da Block-STM vs. SVM: un confronto di motori di esecuzione parallela:
La TPU è solitamente accoppiata con, ma distinta dalla Virtual Machine (VM). Le VM Blockchain come EVM, SVM e MoveVM sono VM di linguaggio di alto livello... La TPU, che è solitamente l'oggetto di interesse, subordina la VM. È incaricata della gestione dell'intera pipeline di esecuzione delle transazioni, inclusa la creazione e la gestione di istanze della VM.
Il controllo della concorrenza pessimistica è progettato in base al presupposto che molte delle transazioni all'interno del set di transazioni da eseguire saranno in conflitto, ovvero toccheranno lo stesso stato. PCC impedisce questi conflitti.
Per evitare conflitti, PCC richiede che una transazione dichiari in anticipo a quali parti dello stato accederà durante le operazioni di lettura/scrittura. L'unità di elaborazione delle transazioni può utilizzare queste informazioni per pianificare le transazioni in modo da garantire che le transazioni in conflitto vengano eseguite in sequenza (anziché contemporaneamente). Alcune TPU utilizzano anche blocchi per imporre questo comportamento (un blocco (noto anche come mutex) è un meccanismo utilizzato per impedire l'accesso concorrente a una posizione di memoria).
Detto questo, l'esecuzione basata su PCC comporta alcuni compromessi. Innanzitutto, il requisito di fornire elenchi di accesso (che identificano lo slot di memoria toccato da una transazione) degrada l'esperienza dello sviluppatore e riduce la gamma di possibili applicazioni. In secondo luogo, la pianificazione delle transazioni può comportare un overhead non necessario, soprattutto quando non ci sono conflitti.
Il controllo della concorrenza ottimistica è progettato con l'ipotesi che molte delle transazioni all'interno del set dato non saranno in conflitto, ovvero non scriveranno nello stesso stato. Pertanto, le TPU OCC eseguono il set di transazioni con tutte le risorse disponibili e provano solo a rilevare i conflitti. Se viene rilevato un conflitto, le transazioni in conflitto vengono eseguite e verificate di nuovo finché l'intero set non passa e può essere eseguito il commit.
Le TPU OCC non subiscono overhead dalla pianificazione, quindi tendono a funzionare meglio quando ci sono pochi conflitti. Le unità di elaborazione delle transazioni basate su OCC hanno anche una migliore esperienza di sviluppo e una gamma più ampia di casi d'uso perché le dipendenze di stato non devono essere note in anticipo.
Tuttavia, quando l'insieme delle transazioni è altamente controverso, OCC funziona peggio di PCC. Trattiamo i progetti TPU (in modo molto più dettagliato) e confrontiamo gli approcci OCC e PCC nel nostro articolo sull'esecuzione parallela.
La nuova TPU di Starknet utilizza l'approccio OCC. Più specificamente, è un'implementazione dell'algoritmo Block-STM. Block-STM esegue le transazioni in modo ottimistico con tutte le risorse disponibili, supponendo che nessuna di esse entri in conflitto e verifica dopo l'esecuzione che nessuna transazione in conflitto venga eseguita contemporaneamente. Prima di immergerci nella nuova architettura di Starknet, è importante esaminare alcune definizioni chiave:
txj
si dice dipendente da (o una dipendenza di) una transazione txi
se e solo se entrambe le transazioni scrivono nella stessa posizione di memoria e txj
viene dopo txi
nell'ordinamento seriale. Se txi
venisse dopo txj
, txi
sarebbe dipendente da txj
.Dopo aver chiarito le definizioni, possiamo passare a spiegare il funzionamento di Block-STM.
L'input per Block-STM è una coda (un elenco ordinato) di transazioni, questo elenco è spesso chiamato BLOCK. Questo elenco può essere ordinato in qualsiasi modo; l'unico requisito è che ci sia un ordine chiaramente definito. Quindi, dato un set di transazioni T contenente transazioni {t0…tn}
, le transazioni sono ordinate in modo tale che {t0 > t1 > t2 … > tn}
(letto come t0
ha una priorità maggiore di t1
, t1
ha una priorità maggiore di t2
ecc.)
All'inizio del processo di esecuzione, vengono creati due set: un set di esecuzione E e un set di convalida V. E tiene traccia delle transazioni che devono ancora essere eseguite, mentre V tiene traccia delle transazioni che sono state eseguite ma devono ancora essere convalidate. Ogni transazione è anche associata a un numero di incarnazione n per tenere traccia di quante volte è stata eseguita (e rieseguita). Lo stato iniziale dei set è che E contiene tutte le transazioni e V è vuoto, ovvero E = {t0,1 > t1,1 > t2,1 > … > tn,1}
e V = {}
.
Con questi insiemi ordinati di transazioni, ogni thread utilizzato per l'esecuzione esegue un ciclo in tre fasi:
Durante questa fase, il thread controlla sia V che E. Se entrambi sono vuoti e non viene eseguita alcuna transazione, il batch corrente di transazioni è stato eseguito completamente e i risultati possono essere salvati nell'archivio.
Se V o E contengono transazioni, Block-STM seleziona la transazione con l'indice più basso (non il numero di incarnazione) da entrambi i set di transazioni, ovvero se E contiene {t1,3 , t3,1 and t5,2}
e V contiene {t0,1, t2,4, t4,3}
, l'attività di convalida per la transazione t0
verrà selezionata come attività successiva.
Una volta identificato e selezionato il task successivo, questo viene eseguito. Alla fine di questo passaggio, l'algoritmo torna a Check Done. Questo processo continua finché entrambi i set di transazioni non sono vuoti.
Diamo un'occhiata a cosa succede durante l'esecuzione e la convalida:
Durante l'esecuzione di una transazione, l'algoritmo Block-STM popola due set (per transazione); un set di lettura ( Ri,n
) e un set di scrittura ( Wn,i
). Il set di lettura contiene tutte le posizioni di memoria da cui una transazione ha letto durante la sua esecuzione, mentre il set di scrittura contiene tutte le posizioni di memoria in cui ha scritto. Durante l'esecuzione, le transazioni applicano le loro scritture alla struttura dati multi-versione, ma la lettura è un po' sfumata.
In Block-STM quando una transazione vuole leggere dalla struttura dati, controlla il valore scritto dalla transazione con priorità più bassa che ha priorità più alta. Ad esempio, se tx1
, tx2
e tx7
hanno scritto tutti in una posizione di memoria e tx5
vuole leggere da questa posizione, legge la versione della struttura dati corrispondente a tx2
.
Questo viene fatto per imporre la serializzabilità; poiché tx5
dovrebbe essere eseguito dopo tx2
e prima di tx7
, dovrebbe usare i valori scritti da tx2
non tx7
. In questo esempio, tx7
dovrà essere rieseguito perché avrebbe dovuto leggere i valori scritti da tx5
, non da tx2
o da transazioni con priorità più alta. Se fosse stata utilizzata una struttura dati a versione singola, il valore scritto da tx2
non sarebbe disponibile e si verificherebbe sicuramente un conflitto.
Per un'attività di convalida, il set di lettura della transazione viene confrontato con i valori correnti nelle posizioni di memoria da cui legge durante l'esecuzione. Ad esempio, se tx2
legge l'account B durante l'esecuzione, durante la convalida, la posizione di memoria per l'account B viene letta (tenendo a mente la definizione di lettura che abbiamo stabilito in precedenza). Se i due valori sono uguali, significa che nessuna transazione con priorità più alta (ad esempio tx0
o tx1
) ha scritto in quella posizione durante l'esecuzione di tx2
. Ciò comporta che tx2
venga contrassegnato come convalidato ma non sicuro per il commit.
La transazione non è considerata sicura da eseguire perché una transazione con priorità inferiore potrebbe, per un numero qualsiasi di ragioni, essere eseguita dopo che la transazione è stata convalidata. Nel nostro esempio in esecuzione, se tx1
tocca l'account B e lo tocca solo dopo, tx2
supera la convalida, quindi tx2
deve essere rieseguito.
Per provvedere a ciò, ogni volta che una transazione completa l'esecuzione, tutte le transazioni di priorità inferiore che hanno superato la convalida vengono riconvalidate per garantire che non siano in conflitto con la transazione. Ad esempio, se tx1
, tx3
e tx4
sono stati convalidati e tx2
completa l'esecuzione, tx3
e tx4
devono essere riconvalidati per garantire che non siano in conflitto con (e quindi non siano dipendenze di) tx2
.
Se una transazione non supera la convalida, ovvero se contemporaneamente è stata eseguita una transazione con priorità più alta che scrive nello stesso stato, le scritture effettuate dalla transazione risultano sporche (perché i valori sono errati). Tuttavia, anziché eliminare completamente questi valori dal database, vengono contrassegnati come ESTIMATE.
Il flag ESTIMATE indica a qualsiasi transazione che legge quella posizione di memoria che i valori non sono corretti e le transazioni mettono in pausa la loro esecuzione. Questo avviene al posto dell'eliminazione perché la riesecuzione della transazione che non ha superato la convalida probabilmente comporterebbe la scrittura nelle stesse posizioni di memoria dell'esecuzione precedente.
Contrassegnando la posizione di memoria come stima anziché eliminarla, le dipendenze (della transazione che non ha superato la convalida) possono essere intercettate anche prima della riesecuzione, impedendo ri-esecuzioni non necessarie. Questa euristica riduce notevolmente lo spreco di lavoro.
Una panoramica completa di come Block-STM affronta la parallelizzazione può essere riassunta come:
BLOCK
di transazioni inizia come un elenco ordinato con un ordine seriale chiaramente definito. Queste transazioni vengono eseguite su tutte le risorse disponibili in ordine di priorità.
Di seguito è riportato un esempio:
Questa è una panoramica del funzionamento di Block-STM; maggiori dettagli sono disponibili nel nostro rapporto qui e nel documento originale di Block-STM qui .
Per quantificare l'importanza dell'aggiunta di Block-STM, abbiamo eseguito alcuni benchmark per valutare i miglioramenti delle prestazioni che offre rispetto all'esecuzione sequenziale; i risultati sono riportati di seguito.
I risultati mostrano che all'aumentare del numero di thread (analoghi alle unità di elaborazione indipendenti) utilizzati, aumentano anche le prestazioni. I miglioramenti sono più pronunciati con blocchi più grandi che offrono miglioramenti delle prestazioni fino a 9 volte superiori rispetto all'esecuzione sequenziale con soli 16 thread. Abbiamo scoperto che i risultati sono più pronunciati con blocchi più grandi.
I nostri test dimostrano che le prestazioni di Block-STM si degradano in modo significativo sotto carichi altamente controversi, ma la prassi standard del settore è quella di ricorrere all'esecuzione sequenziale durante tali periodi. Raccomandiamo la stessa meccanica a Starknet per preservare la produttività sotto carichi di lavoro altamente controversi. Ma, in generale, l'aggiunta di Block-STM migliorerà in modo significativo e renderà Starknet a prova di futuro.
La seconda modifica importante inclusa nell'aggiornamento v0.13.2 è il block packing, di cui parleremo più avanti.
Come discusso in precedenza, prima di Bolt, ogni blocco Starknet era un lavoro a sé stante, con un conseguente costo fisso per blocco per ogni blocco. Inoltre, il sistema era progettato in modo che ogni blocco richiedesse il proprio blob indipendentemente dalla quantità di dati effettivamente consumata dal blocco.
In un mondo in cui la domanda è sempre stata alta, questo non sarebbe un problema, ma Starknet attualmente offre più blockspace di quanto ci sia domanda e quindi ci sono molte risorse sprecate che potrebbero causare la perdita di centinaia di ETH nel corso di un mese. Per contestualizzare ulteriormente la gravità della situazione prima di Bolt, questi sono i costi associati alla pubblicazione di un blocco su L1:
Ciò equivale a 215k gas per blocco e questo costo è fisso, ovvero è lo stesso indipendentemente dalla quantità di dati contenuta in ogni blocco e correlato al numero di blocchi da $Cost = num blocks * 215000$. La soluzione ideale a questo problema sarebbe che i costi fossero correlati alla quantità di dati pubblicati anziché alla quantità di blocchi. Ed è esattamente ciò che il block packing ottiene tramite alberi SNAR.
Gli alberi Starknet Applicative Recursive (SNAR) sono un nuovo tipo di albero binario introdotto in Bolt per risolvere i problemi evidenziati sopra. Un albero SNAR ha la seguente struttura: ogni foglia è un blocco Starknet e i nodi in tutti gli altri livelli sono prove ricorsive dei loro figli. Il nodo radice che è una prova ricorsiva di tutte le altre prove è il lavoro finale che viene inviato allo SHARed Prover (SHARP).
Il vantaggio principale di SNAR Tree è che anziché pubblicare un blocco per prova, molti blocchi Starknet possono essere ammortizzati nello stesso aggiornamento L1. Le radici dell'albero SNAR vengono ora pubblicate su L1 solo quando viene raggiunto uno dei due limiti configurabili: il limite DA (6 blob di dati) o dopo che un certo numero di foglie sono state aggiunte all'albero (dove una foglia è un blocco).
Questo design disaccoppia il costo delle transazioni dal numero di blocchi. Ora, c'è ancora un costo fisso per ogni blocco che deriva dall'esecuzione di StarkNet OS (SNOS) in ogni blocco, ma in generale i costi sono disaccoppiati. Questi sono i numeri ora:
Il grafico nella Figura 11 sottostante mostra come i costi del gas variano in base al numero di blocchi nel progetto precedente e ora (sotto Bolt):
È abbastanza ovvio che il block packing riduce notevolmente i costi di verifica sulla L1, il che si tradurrà senza dubbio in prezzi del gas più bassi per gli utenti Starknet.
L'effetto delle modifiche apportate in Bolt: esecuzione parallela ottimistica tramite Block-STM e block-packing tramite SNAR proprietario può essere riassunto come: più veloce ed economico.
L'esecuzione parallela riduce i tempi di esecuzione e, per estensione, la congestione, il che ridurrà le tariffe del gas durante i periodi di traffico elevato, mentre gli alberi SNAR affrontano i costi DA e di verifica associati. È interessante notare che questo aggiornamento rende Starknet il primo L2 con esecuzione parallela e lo prepara a essere un contendente importante nello spazio L2. È importante notare che è improbabile che l'effetto di queste modifiche sarà immediatamente evidente, in particolare quello dell'esecuzione parallela, ma sono fondamentali per rendere Starknet e l'intero ecosistema Ethereum a prova di futuro.
Nota dell'autore: una versione di questo articolo è stata precedentemente pubblicata qui .