logo
XCM Parte III: Esecuzione e gestione degli errori

XCM Parte III: Esecuzione e gestione degli errori

Nei primi due articoli (Parte I e Parte II) sull’XCM, abbiamo introdotto le basi del suo design e come le sue versioni vengono gestite. In questo articolo, approfondiremo le nostre conoscenze su XCVM e la sua architettura, e di conseguenza comprenderemo meglio la struttura di base e il modello di esecuzione di XCM.

XCVM è una macchina virtuale di livello molto alto, non Turing-complete. È basata su registri (anziché su stack) e dispone di numerosi registri speciali, la maggior parte dei quali contiene dati altamente strutturati. A differenza dei processori generici, i registri di XCVM non sono liberi di essere impostati su valori arbitrari, ma hanno una meccanica rigorosa che ne regola la modifica/gestione. Al di là di alcuni mezzi per interagire con lo stato locale della chain (come le funzioni WithdrawAsset e DepositAsset, che abbiamo già visto) non c'è alcuna "memoria" aggiuntiva. Non c'è possibilità di looping e non ci sono istruzioni esplicite di tipo branch.

Abbiamo già introdotto due dei registri: l'Holding Register, che è in grado di gestire temporaneamente uno o più asset e può essere aggiornato da prelievi di un asset dalla chain locale, oppure dalla ricezione di un asset da una origine esterna fidata (ad esempio un'altra chain); e l'Origin Register, che contiene la posizione del sistema di consenso da cui ha avuto origine l'esecuzione corrente dell'XCM, e può essere solo cambiato in una posizione interna o cancellato del tutto.

Degli altri registri, tre riguardano la gestione delle eccezioni/errori e due il monitoraggio della peso dell'esecuzione. Li conosceremo tutti in questo articolo.

🎬 Modello di esecuzione

La mancanza di istruzioni condizionali esplicite o di primitive di looping, che impediscono alla stessa linea di codice di essere eseguita più di una volta, rende semplice predeterminare il flusso di controllo di un programma in XCVM. Questa proprietà è fondamentale per calcolare il possibile tempo di esecuzione (noto come weight in Substrate/Polkadot) di un messaggio XCM ancora prima di essere inviato.

Per evitare che le blockchain si blocchino, esse devono garantire che i singoli blocchi non impieghino un tempo di elaborazioni troppo lungo. Allo stesso tempo, per prevenire interruzioni durante l’elaborazione o inutili carichi di lavoro, esse devono anche prevedere che il pagamento della commissione per il caso peggiore di un’elaborazione sia pagato in completo a priori. Per queste ragioni, ci aspettiamo che la maggior parte delle piattaforme di consenso che utilizzeranno XCM siano in grado di determinare il tempo di esecuzione più sfavorevole prima dell'inizio dell'esecuzione.
Nei sistemi che consentono l'uso di linguaggi Turing-completi (come ad esempio Ethereum) invece la determinazione di questo weight avviene solo durante la vera e propria elaborazione del programma e, per questo, richiedono all’utente di stimare l’utilizzo delle risorse (o più precisamente, definire un limite massimo al pagamento della commissioni) per l’esecuzione del programma. Nel caso in cui, questo limite viene superato (a volte le cose cambiano prima dell’esecuzione di una transazione e di conseguenza il peso risulta errato) allora essi interrompono l’esecuzione del programma. Fortunatamente, in macchine virtuali come XCVM, tutto questo non succede.

🏋️ Weight (Peso)

Il weight è tipicamente rappresentato come il numero intero di picosecondi che una risorsa hardware impiegherebbe per eseguire una operazione. Come abbiamo visto con l'istruzione BuyExecution, la XCVM include questo concetto di tempo di esecuzione/peso quando gestisce determinate istruzioni.

È possibile prevedere con precisione il peso di esecuzione per la maggior parte delle istruzioni, ma a volte il programma può richiedere un tempo inferiore a quello stimato per il caso peggiore. solo al momento dell’esecuzione del programma è possibile sapere di quanto la previsione era stata sovrastimata. Il registro chiamato Surplus Weight Register (Registro del peso in eccesso) viene utilizzato per tenere traccia di tutte le quantità stimate in eccesso e consente alla chain di ottimizzare la contingenza di tempo dell’esecuzione di un blocco.

Il Surplus Weight Register è quindi utile per la gestione del tempo di esecuzione del blocco, ma non garantisce che l'importo pagato della commissione sia più alto di quello dovuto. Occorre un'istruzione complementare a BuyExecution, che rimborsi il peso pagato in eccesso. Questa istruzione si chiama RefundSurplus e sfrutta un secondo registro, chiamato Refunded Weight Register (Registro dei pesi rimborsati), per accertarsi che i surplus vengano rimborsati una e una sola volta.

😱 Controllo del flusso ed eccezioni

Finora due importanti registri sono stati trascurati nella nostra presentazione di XCVM: il primo è il Programme Register (Registro del programma), che memorizza il programma XCVM in esecuzione; il secondo è il Program Counter, che memorizza l'indice dell'istruzione in esecuzione. Quest’ultimo viene azzerato quando il Program Counter viene modificato e viene incrementato di uno a ogni istruzione eseguita con successo.

La gestione di possibili "eccezioni" è fondamentale per scrivere codice robusto. Quando su un sistema remoto accade qualcosa che non ci si aspettava (o che non si poteva prevedere), è necessario occuparsene anche se si tratta semplicemente di inviare una segnalazione alla sorgente.

XCVM dispone di una struttura generale per la gestione delle eccezioni integrata nel suo modello di esecuzione che include altri due registri di codice, ognuno dei quali contiene un programma XCVM come il Programme Register. Questi registri sono chiamati Appendix Register (Registro delle appendici) e Error Handler Register (Registro dei gestori di errori). Quello che seguirà potrebbe ricordarvi molto la gestione delle eccezioni try/catch/finally di altri linguaggi comuni.

Tutte le istruzioni di un programma XCVM vengono eseguite una dopo l’altra e durante la loro esecuzione si possono verificare due situazioni: o il programma terminerà con successo, o verrà interrotto a causa di un errore. Nel primo caso, il programma corrente terminato con successo viene rimosso, il gestore degli errori inizializzato e il programma di appendice avviato, se ne esiste uno. Più nel dettaglio: il Error Handler Register viene svuotato e il suo “peso” aggiunto al Surplus Weight Register, infine il contenuto del Appendix Register viene aggiunto nel Programme Register e poi svuotato. Se il Programme Register rimane vuoto, ci si ferma; altrimenti, il Program Counter viene azzerato.

In caso di errore, il programma corrente viene eliminato e viene avviata l’esecuzione prevista per la gestione degli errori. Nello specifico, il tempo di esecuzione delle istruzioni non eseguite viene aggiunto al Surplus Weight Register; il contenuto del Error Handler Register viene aggiunto nel Programme Register e poi svuotato, il Program Counter azzerato. Se l’Appendix Register non viene cancellato dalla gestione degli errori, allora il programma in esso contenuto verrà eseguito una volta terminata la gestione degli errori.

Grazie alla sua struttura componibile, la gestione degli errori e le appendici possono essere annidate. Ciò significa che a loro volta possono contenere al loro interno moduli per la gestione di errori e appendici.

Esistono due istruzioni che consentono la manipolazione dei dati in questi registri: SetAppendix e SetErrorHandler. Il peso di ciascuno di essi è di poco superiore al peso del relativo parametro. Tuttavia, al momento dell'esecuzione, il tempo di esecuzione del messaggio XCM nel registro che verrà sostituito viene aggiunto al Surplus Weight Register, consentendo di recuperare il tempo previsto per le appendici o la gestione degli errori non eseguiti.

☄️ Lanciare gli errori

A volte può essere utile assicurarsi che si verifichi un errore e personalizzarne alcuni aspetti. Nella XCVM queste avviene tramite l'istruzione Trap, la quale genera sempre un errore di tipo Trap. Sia la funzione che l'errore contengono un argomento intero che consente di passare una forma di informazione tra chi lancia l'errore e un osservatore esterno.

Ecco un semplice esempio:

WithdrawAsset((Here, 10_000_000_000).into()),
BuyExecution {
    fees: (Here, 10_000_000_000).into(),
    weight: Unlimited,
},
SetErrorHandler(Xcm(vec![
    RefundSurplus,
    DepositAsset {
        assets: All.into(),
        max_assets: 1,
        beneficiary: Parachain(2000).into(),
    },
])),
Trap(0),
DepositAsset {
    assets: All.into(),
    max_assets: 1,
    beneficiary: Parachain(3000).into(),
},

Se si è verificato un errore Trap, allora il rimanente codice, nel nostro caso solo la funzione DepositAsset finale, viene bypassato e si passa all’esecuzione del codice all’interno del gestore degli errori. Qui, grazie all’istruzione DepositAsset, 1 DOT (meno il costo di esecuzione) viene depositato nella parachain 2000. Questa parte di codice viene eseguita solamente quando si verifica un errore, e comporta un tempo di esecuzione del programma inferiore a quanto previsto inizialmente. Per questo motivo l’istruzione RefundSurplus sarà sempre presente all'inizio del codice di gestore degli errori.

🗞 Segnalazione degli errori

La possibilità di gestire gli errori è molto utile, ma ancora più importante è riportare in maniera comprensibile l’esito di un messaggio XCM al mittente originale. Nell'articolo precedente abbiamo spiegato come l'istruzione QueryResponse consente a un sistema di consenso di ritornare informazioni a un altro sistema; ciò che resta da fare è inserire l'esito dell'XCM in questa QueryResponse e inviarla a chi si aspetta di ricevere il risultato.

Per svolgere questo esiste l’istruzione ReportError, che utilizza il Error Registrer (Registro degli errori). Il Registro degli errori è di tipo opzionale e se attivato, memorizza due informazioni: un indice numerico e un tipo di errore XCM. Viene abilitato ogni volta che un'istruzione produce un errore; l'indice numerico viene impostato sul valore del Programme Counter Register e il tipo di errore XCM ovviamente impostato sul tipo di errore. Può essere cancellato solo eseguendo l'istruzione ClearError. Questa istruzione è una delle istruzioni infallibili: non può mai dare luogo a un errore. Il funzionamento del Error Registrer e’ molto basilare: viene alimentato quando si verifica un errore e viene cancellato quando si esegue l'istruzione appropriata.

L'istruzione ReportError non è altro che l'istruzione QueryResponse che invia ad una particolare destinazione il contenuto del Registro degli errori. Per garantire l’esecuzione di questa istruzione indipendentemente dal fatto che un errore avvenga nel codice del programma principale, è opportuno collocare la chiamata alla ReportError all’interno dell’Appendix. Il codice nel Appendix Register viene infatti sempre eseguito successivamente al codice memorizzato nel Error Handler Register.

Vediamo un semplice esempio: teletrasporteremo un asset (1 DOT) dalla Relay Chain a Statemint (parachain 1000), acquisteremo del tempo di esecuzione lì e poi, usando Statemint come riserva, depositeremo l'asset sulla parachain 2000. Il messaggio originale (senza segnalazione di errori) sarebbe stato il seguente:

WithdrawAsset((Here, 10_000_000_000).into()),
InitiateTeleport {
    assets: All.into(),
    dest: Parachain(1000).into(),
    xcm: Xcm(vec![
        BuyExecution {
            fees: (Parent, 10_000_000_000).into(),
            weight: Unlimited,
        },
        DepositReserveAsset {
            assets: All.into(),
            max_assets: 1,
            dest: ParentThen(Parachain(2000)).into(),
            xcm: Xcm(vec![
                BuyExecution {
                    fees: (Parent, 10_000_000_000).into(),
                    weight: Unlimited,
                },
                DepositAsset {
                    assets: All.into(),
                    max_assets: 1,
                    beneficiary: Parent.into(),
                },
            ]),
        },
    ]),
}

Con una segnalazione degli errori di base, invece, il codice sarebbe il seguente:

WithdrawAsset((Here, 10_000_000_000).into()),
InitiateTeleport {
    assets: All.into(),
    dest: Parachain(1000).into(),
    xcm: Xcm(vec![
        BuyExecution {
            fees: (Parent, 10_000_000_000).into(),
            weight: Unlimited,
        },
        SetAppendix(Xcm(vec![
            ReportError {
                query_id: 42,
                dest: Parent.into(),
                max_response_weight: 10_000_000,
            },
        ])),
        DepositReserveAsset {
            assets: All.into(),
            max_assets: 1,
            dest: ParentThen(Parachain(2000)).into(),
            xcm: Xcm(vec![
                BuyExecution {
                    fees: (Parent, 10_000_000_000).into(),
                    weight: Unlimited,
                },
                SetAppendix(Xcm(vec![
                    ReportError {
                        query_id: 42,
                        dest: Parent.into(),
                        max_response_weight: 10_000_000,
                    },
                ])),
                DepositAsset {
                    assets: All.into(),
                    max_assets: 1,
                    beneficiary: ParentThen(Parachain(2000)).into(),
                },
            ]),
        },
    ]),
}

Qui sono state introdotte due istruzioni SetAppendix che garantiscono che la Relay chani sia informata se un errore si è verificato o no all’interno di Statemint e della parachain 2000. Questo presuppone che la Relay Chain sia in grado di riconoscere e gestire i messaggi QueryResponse provenienti da Statemint e dalla parachain 2000 con query ID 42 e un limite di weight di dieci milioni. Ovviamente, questa opzione è ben supportata da Substrate, ma al momento non rientra nell'ambito di applicazione nel nostro esempio.

🪤 La trappola degli asset

La gestione degli errori può diventare molto problematica quando si verificano errori in programmi che trattano asset (come la maggior parte di essi, dato che spesso devono pagare per la loro esecuzione con BuyExecution). Può capitare che l'istruzione BuyExecution stessa generi un errore, per esempio perché il limite del tempo di esecuzione non era corretto o gli asset utilizzati per il pagamento erano insufficienti. O forse un asset viene inviato a una chain che non può gestirlo. In questi casi, e in molti altri, l'esecuzione del messaggio XCVM termina con gli asset nell’Holding Register, che essendo un registro temporaneo come gli altri ci si aspetta che venga trascurato.

I team e i loro utenti saranno felici di sapere che XCM di Substrate permette alle chain di scongiurare completamente questa perdita 🎉. Questo meccanismo ha due fasi. La prima parte è chiamata Asset Trap e prevede che gli asset memorizzati nell’Holding Register al momento della sua cancellazione non siano completamente dimenticati. Quando la XCVM si ferma, viene emesso un evento contenente le seguenti tre informazioni: il valore dell'Holding Register, il valore originale dell'Origin Register e l'hash di queste due informazioni. Questo hash infine viene salvato in memoria dal sistema XCM di Substrate.

🎟 Il sistema di rimborso

Il secondo passo del meccanismo è la possibilità di recuperare alcuni degli asset contenuti nell'Holding Register. Questo avviene attraverso un'istruzione generica, chiamata ClaimAsset. Ecco come viene dichiarata in Rust:

pub enum Instruction {
    /* snip */
    ClaimAsset { assets: MultiAssets, ticket: MultiLocation },
    /* snip */
}

Questa istruzione come le altre istruzioni di "funding", come WithdrawAsset e ReceiveTeleportedAsset, prova a depositare gli asset (indicati dall'argomento assets) nell’Holding Register. A differenza di WithdrawAsset, che riduce il saldo degli asset di un conto sulla chain; ClaimAsset cerca una richiesta valida per questi assets disponibili nell’Origin Register grazie alle informazioni fornite tramite l'argomento ticket. Se viene trovata una richiesta di risarcimento valida, questa viene cancellata dalla chain e gli asset vengono aggiunti nell’Holding Register.

Ora, ciò che costituisce esattamente una richiesta di risarcimento dipende interamente dalla chain stessa. Chain diverse possono supportare diversi tipi di rimborsi e Substrate permette di crearle facilmente. Ma, come si può intuire, un tipo particolare di richiesta, già pronta per l’uso, è quella dei contenuti precedentemente abbandonati nell’Holding Register.

In pratica, supponiamo che la parachain 2000 del nostro utente invii un messaggio a Statemint in cui preleva 0,01 DOT dal suo conto sovrano per pagare le commissioni e gli notifica anche un trasferimento di 100 unità del suo token nativo da inserire nel suo conto sovrano su Statemint. Potrebbe avere un aspetto simile a questo:

WithdrawAsset((Parent, 100_000_000).into()),
BuyExecution {
    fees: (Parent, 100_000_000).into(),
    weight: Unlimited,
},
SetAppendix(Xcm(vec![
    ReportError {
        query_id: 42,
        dest: ParentThen(Parachain(2000)).into(),
        max_response_weight: 10_000_000,
    },
    RefundSurplus,
])),
ReserveAssetDeposited((ParentThen(Parachain(2000)), 100).into()),
DepositAsset {
    assets: All.into(),
    max_assets: 2,
    beneficiary: ParentThen(Parachain(2000)).into(),
}

Supponendo che 0,01 DOT sia una tariffa sufficiente e che Statemint supporti i depositi on-chain dell'asset nativo della parachain 2000 (così come l'utilizzo di parachain 2000 come riserva), allora tutto dovrebbe filare liscio. Tuttavia, se Statemint non può ancora riconoscere l'asset nativo della parachain 2000, il DepositAsset lancerà un errore. Dopo l'esecuzione dell'appendice che notificherà alla parachain 2000 questo errore, rimarranno le 100 unità di asset nativi di parachain 2000 e potenzialmente alcuni DOT nell’Holding Register. Supponiamo che le tasse siano solo a 0,005 DOT, lasciando 0,005 DOT rimanenti.

Il successivo evento registrato dal pallet XCM di Statemint relativo a questi nuovi asset reclamabili sarebbe qualcosa come segue:

Event::AssetsTrapped(
    /* snipped hash */,
    ParentThen(Parachain(2000)),
    vec![
        (Parent, 50_000_000).into(),
        (ParentThen(Parachain(2000)), 100),
    ].into(),
)

Alla parachain 2000 verrebbe inviato un messaggio del tipo:

QueryResponse {
    query_id: 42,
    response: ExecutionResult(Err((4, AssetNotFound))),
    max_weight: 10_000_000,
}

La parachain 2000 sarebbe in grado, in una fase successiva (una volta stabilito che Statemint è in grado di accettare depositi del suo asset nativo), di recuperare le 100 unità con un'operazione piuttosto semplice:

ClaimAsset {
    assets: vec![
        (Parent, 50_000_000).into(),
        (ParentThen(Parachain(2000)), 100),
    ].into(),
    ticket: Here,
}
BuyExecution {
    fees: (Parent, 50_000_000).into(),
    weight: Unlimited,
},
DepositAsset {
    assets: All.into(),
    max_assets: 2,
    beneficiary: ParentThen(Parachain(2000)).into(),
}

In questo caso, il valore passato nell’argomento ticket non fornisce informazioni particolari per aiutare a localizzare la richiesta di rimborso. Anche se potrebbe essere utile per altri tipi di richieste, questa soluzione funziona per le richieste di tipo Asset Trap.

🏁 Conclusione

Per ora è tutto: spero che questi articoli siano stati utili per farvi capire meglio come la macchina virtuale sottostante a XCM può aiutarvi a gestire e recuperare situazioni inaspettate. I prossimi articoli di questa serie tratteranno le direzioni future di XCM e i miglioramenti suggeriti al formato, nonché’ un approfondimento sull'implementazione XCM Rust di Substrate e come possiamo usarla per fornire a una chain la capacità di interpretare facilmente XCM.

SLPx Pallet - A further step into the Omni-chain Liquid Staking

SuperDupont

What sets Bifrost apart from other LSD protocols?

What sets Bifrost apart from other LSD protocols?

New players have appeared amidst the LSD frenzy sparked by the Ethereum Shanghai upgrade. While the seasoned players vie for the highest minting volum

SuperDupont

July 2023 In Review

July 2023 In Review

July was a very quiet month at Subsocial, as we were heads down and building after the excitement of June. Let’s quickly run through last month’s high

Yung Beef 4.2

Monthly Report | vETH 2.0 Release & Gauge Proposal Live on Balancer

Monthly Report | vETH 2.0 Release & Gauge Proposal Live on Balancer

Developments Dapp 1.8.1 Launch vETH 2.0 Launch vETH Farming The cross-chain transaction structure is upgraded to XCM v3 v0.9.72 XCM dependency u

SuperDupont