XCM Parte II: Versioni e compatibilità

Nel primo articolo sull’XCM* abbiamo introdotto la sua architettura base, i suoi obiettivi e il suo utilizzo per alcuni semplici casi d’uso. In questa seconda parte invece, esamineremo come l’XCM può evolvere nel tempo senza provocare disservizi alle reti che interconnette.*

Un linguaggio comune è alla base della comunicazione tra individui. Permette di lavorare insieme, di risolvere conflitti e molto altro. In un mondo in continua evoluzione questo non è però sufficiente, se una lingua non rinnova e adatta il proprio repertorio concettuale a questi cambiamenti perde la sua utilità primaria, non è più in grado di esprimere i concetti come in precedenza e rischia di diventare obsoleta e inutile.

Sfortunatamente, improvvisi stravolgimenti in una lingua possono compromettere il suo scopo principale: facilitare la comunicazione tra persone. I cambiamenti vanno gestiti e le nuove espressioni devono essere rese comprensibili da tutti. Per ovviare a questo problema è stato inventato il dizionario, uno strumento che aiuta a documentare e archiviare nel tempo gli elementi concettuali di una lingua, consentendo così alle nuove generazioni di comprendere i testi storici. Una certa edizione di un dizionario, potrebbe essere considerata come una “versione” formalizzata di una lingua.

Come spiegato nell’articolo precedente, XCM è un linguaggio, anche se molto specializzato, e di conseguenza si possono riscontrare le stesse problematiche. Dato che le esigenze dell’XCM evolvono allo stesso passo dell’industria crittografica, e in particolare dell’ecosistema Polkadot è essenziale assicurarsi che questi cambiamenti non compromettano il suo obiettivo originale: l’interoperabilità. Ora dobbiamo risolvere non solo l'interoperabilità nello spazio del consenso, ma anche nel suo tempo.

🔮 Controllo delle versioni

Poiché ci aspettiamo che il linguaggio dell’XCM cambi nel tempo, una precauzione molto semplice da prendere è quella di assicurarsi di identificare con quale versione dell’XCM stiamo comunicando, prima del contenuto effettivo del messaggio. Per farlo, utilizziamo una serie di tipi version-wrapper, così chiamati perché contengono la versione del messaggio XCM o di un suo componente. In Rust, questo sembra molto semplice:

pub enum VersionedXcm {
    V0(v0::Xcm),
    V1(v1::Xcm),
    V2(v2::Xcm),
}

Un messaggio XCM inviato all’interno di questo contenitore, dotato di versioni, garantisce che i sistemi troppo datati lo possano ricevere in modo sicuro e riconoscere che quel formato non è supportato, e consente ai sistemi più recenti di interpretare anche i messaggi più vecchi.

Anche MultiLocation, MultiAsset e i tipi associati sono provvisti di versioni; queste versioni evitano di interpretare una vecchia MultiLocationcome una nuova e scoprire solo successivamente che è incomprensibile (o peggio, comprensibile ma con un significato diverso da quello originale).

💬 Compatibilità & Trasformazioni

Il controllo delle versioni è il primo passo per identificare l’edizione del linguaggio che stiamo utilizzando. Questo però non ci dà la sicurezza di poterlo interpretare correttamente e non ci garantisce che questa èla versione che desideriamo usare. A questo punto potete immaginare come la capacità di continuare a interpretare ed esprimerci in versioni diverse dell’XCM assume un ruolo centrale, soprattutto quando desideriamo comunicare con altre reti che utilizzano versioni diverse tra loro. Questa importante proprietà è chiamata “compatibilità”.

Esistono due tipi di compatibilità: la compatibilità all'indietro è la capacità di un sistema aggiornato di continuare a funzionare in un mondo preesistente (con versioni precedenti); mentre la compatibilità in avanti è la capacità di un sistema legacy, non aggiornato, di funzionare in un ambiente in cui i sistemi eseguono versioni successive.
La soluzione perfetta prevederebbe l’utilizzo di entrambe, ma purtroppo esistono limiti pratici difficili da superare: quando vengono introdotte nuove funzionalità, non è realistico aspettarsi che dei sistemi che utilizzano versioni precedenti siano in grado di interpretare questi nuovi messaggi XCM. Sarebbe un po’ come aspettarsi che Giulio Cesare comprendesse subito il significato del termine moderno “social media” tradotto in latino. Alcuni concetti non possono essere espressi in un contesto legacy.

Analogamente, modifiche significative potrebbero comportare la rimozione di alcune funzionalità al modello concettuale dell’XCM. Questo coincide con il problema di tradurre alcuni termini arcaici in termini equivalenti moderni; essi potrebbero assumere significati alternativi e quindi essere interpretati e usati in modo diverso. Tutto questo per sottolineare il fatto che le nuove versioni dell’XCM sono progettate per essere per lo più compatibili con tutte le versioni (anche precedenti), ma in genere ci saranno dei messaggi XCM che semplicemente non avranno valore nel contesto alternativo e non saranno traducibili.

🗣 Comunicazione concreta

Come illustrato precedentemente, tutti i messaggi inviati tra sistemi o conservati in memoria, includono un identificatore della propria versione. Le versioni degli altri messaggi, locazioni e asset o altri dati che esistono perché parte di altri oggetti, invece, possono essere dedotte direttamente dal loro contesto e non necessitano di nient’altro.

L'identificazione della versione, la compatibilità e la trasformazione sono fondamentali per consentire alla rete ricevente di interpretare in maniera corretta un messaggio XCM. È responsabilità della rete mittente assicurarsi tutto questo, perché una rete legacy non contiene la logica per interpretare e tradurre un messaggio XCM se inviato da una rete aggiornata. Tale logica esiste solo nel lato mittente, dove risiede il codice di trasformazione in grado di reimplementare il nuovo messaggio in termini legacy. In pratica, la versione XCM utilizzata per il messaggio deve essere meno recente della versione XCM supportata dalla rete ricevente.

Per questo motivo, le Relay chain Polkadot e Kusama, Statemint, Statemine, Shell e qualsiasi altra chain basata su Substrate/Frame e il suo motore XCM, mantengono un registro delle versioni XCM supportate dalle chain remote. Ogni volta che devono inviare un messaggio XCM, esse traducono il messaggio XCM in base alla più recente versione supportata dal mittente e dal destinatario, identificata consultando il proprio registro. Per le chain aggiornate, nella maggior parte dei casi si tratta della stessa e ultima versione rilasciata, che rende disponibile tutto il set di funzionalità XCM.

Soprattutto quando il numero potenziale di destinazioni aumenta, l’aggiornamento di questo registro tramite i processi di governance diventa pesante e complicato. Per questo motivo è stato introdotto il controllo delle versioni appena presentato.

🤝 Negoziazione delle versioni

Il sistema di rilevamento delle versioni è il pezzo che completa il puzzle nella gestione delle versioni dell’ XCM. Questo processo avviene autonomamente e on-chain; la sua funzione è quella di eliminare qualsiasi processo off-chain o di governance per rilevare la versione XCM delle potenziali chain di destinazione.

In sostanza, ogni rete può utilizzare l’XCM per chiedere a un’altra rete l'ultima versione XCM che supporta, e di essere aggiornata ogni volta che questa cambia. Le risposte di ritorno permettono alla rete iniziale di alimentare e mantenere il proprio registro delle versioni, garantendo che tutti i messaggi saranno inviati utilizzando l’ultima versione XCM compatibile con ogni rete destinataria.

Esistono tre fondamentali istruzioni nell’XCM: SubscribeVersion, che consente a una rete di chiedere a un'altra di notificare la sua versione XCM ora e quando cambia; UnsubscribeVersion, per annullare la richiesta; e QueryResponse, un mezzo generale per riportare alcune informazioni dalla rete rispondente alla rete iniziante. Ecco come si rappresentano suRust:

enum Instruction {
    SubscribeVersion {
        query_id: QueryId,
        max_response_weight: u64,
    },
    UnsubscribeVersion,
    /* snip */
}

SubscribeVersionaccetta due parametri. Il primo, query_id di tipo QueryId, è un numero intero utilizzato per identificare e distinguere le risposte ricevute. Tutte le istruzioni XCM che prevedono una risposta, utilizzano un mezzo simile per riconoscere la risposta e di conseguenza gestirla correttamente. Il secondo parametro si chiama max_response_weight ed è un valore di tipo Weight(anch'esso intero) che indica la quantità massima di tempo computazionale che la risposta dovrebbe richiedere al suo ritorno per essere interpretata. Come il query_id, questo valore sarà inserito in tutti i messaggi di risposta generati da questa istruzione ed è necessario per garantire che i costi computazionali siano limitati a priori, evitando così costi variabili e imprevedibili. Senza questo valore non sarebbe possibile programmare l’esecuzione di un messaggio di risposta, perché non esiste un limite massimo al tempo computazionale per la sua interpretazione.

UnsubscribeVersion è un’istruzione abbastanza semplice. Dato che per una certa Location è consentito avere una sola sottoscrizione attiva, la sua cancellazione può avvenire solamente con il contenuto del suo Origin Register (Registro di Origine).

Un'illustrazione del registro delle versioni e del suo utilizzo.

Qui la chain A (XCM versione 2) negozia con la chain E (XCM versione 3) e alla fine invia un messaggio XCM versione 2, che E trasforma automaticamente nella versione 3 prima di interpretarlo.

👂 Replying

La terza istruzione da considerare è QueryResponse, un'istruzione molto generica che permette a una chain di rispondere e fornire alcune informazioni a un'altra rete. Su Rust:

enum Instruction {
    QueryResponse {
        query_id: QueryId,
        response: Response,
        max_weight: u64,
    },
    /* snip */
}

Conosciamo già due dei tre parametri, poiché ottenuti dai valori trasmessi da SubscribeVersion. Il terzo si chiama responsee contiene tutte le informazioni che ci interessano. È contenuto all’interno del tipo Response, che a sua volta è composto da diversi “types” che una rete potrebbe voler utilizzare per informare un'altra rete. Su Rust si presenta così:

pub enum Response {
    Null,
    Assets(MultiAssets),
    ExecutionResult(Result<(), (u32, XcmError)>),
    Version(XcmVersion),
}

Per i nostri scopi attuali, solo l'elemento Versionè indispensabile. Nei prossimi articoli vedremo l’utilità e l’importanza degli altri elementi in contesti diversi.

⏱ Execution time

In generale, alle istruzioni QueryResponsenon viene richiesto di acquistare il proprio tempo di esecuzione con BuyExecution poiché (supponendo che le istruzioni siano valide) è la rete che le interpreta a richiederne l'invio. Analogamente, SubscribeVersionè nell'interesse sia del mittente che del destinatario e non ci sono pretese che venga pagato. In ogni caso, data la natura asincrona e imprevedibile delle risposte generate, il pagamento sarebbe alquanto difficile da calcolare.

🤖 Automation

Sebbene le istruzioni XCM appena presentate permettano a una rete di utilizzare una logica on-chain per determinare l'ultima versione supportata dal proprio interlocutore, rimane il problema di quando definire la corretta versione. In generale, non può essere avviato alla creazione del canale per l’invio dell’XCM, in quanto la creazione del canale è di un livello concettualmente inferiore a quello dell’XCM, il quale è uno (o forse uno dei tanti) formati di dati che possono essere inviati su quel canale. Confondere le acque, in questo caso potrebbe compromettere l'indipendenza del design a strati. Peraltro, alcuni protocolli di trasporto cross-consensus non sono affatto basati sul canale, il che precluderebbe la possibilità di negoziare la versione al loro inizio.

All'interno delle chain di tipo Substrate, come la Relay Chain Polkadot e Statemint, la soluzione consiste nell'avviare questo processo di rilevamento della versione quando un messaggio deve essere impacchettato per l'invio, ma l'ultima versione della destinazione è sconosciuta. Questo approccio ha il leggero inconveniente che i primi messaggi verrebbero inviati con una versione XCM non ottimale fino alla ricezione di una risposta contenente laversione corretta. Se questo fosse un problema reale, per quella destinazione la governance potrebbe intervenire e forzare la versione iniziale dell’XCM diversa da quella predefinita (generalmente impostata sulla prima versione XCM ancora prevista in produzione).

⌨️ Compatibilità del codice all’interno dell’XCM

L'ultimo aspetto del controllo delle versioni è l'authoring (lo sviluppo) del codice. Diversamente dal formato over-the-wire dell’XCM, la compatibilità del codice riguarda ciò che deve accadere nel corso del tempo al codice base dei progetti (basati su Substrate) che utilizzano l'implementazione Rust dello stack XCM mentre esso si evolve.

È chiaro che i codici base che usano un linguaggio in evoluzione per esprimere delle idee debbano cambiare e adattarsi nel tempo. Esiste già un sistema di versionamento semantico (SemVer) che aiuta a stabilire quali modifiche possono essere apportate in determinati cambi di versione. Tuttavia, questo sistema è molto utile quando si tratta di API e ABI e meno quando si considera un formato di dati o un linguaggio generale. Fortunatamente, l’XCM è stato progettato per non avere bisogno di questo strumento.

Sappiamo che le versioni più recenti del software XCM sono in grado di tradurre i nuovi e i vecchi messaggi XCM e tutti i loro tipi di dati interni, come le location e gli asset. Tutto questo è reso possibile grazie alla presenza simultanea delle diverse versioni del linguaggio XCM nel suo codice base e alla semplicità del sistema modulare di Rust, dove una nuova versione dell’XCM corrisponde semplicemente a un nuovo modulo. Analizzando la dichiarazione Rust del tipo VersionedXcm(proprio all'inizio di questo articolo), possiamo notare che è semplicemente un’unione taggata delle versioni specifiche del tipo Xcmsottostante, ciascuna presente nel proprio modulo v0, v1, v2, eccetera.

Poiché le transazioni e le API che usano l’XCM e i suoi tipi di dati tendono a usare solo le varianti versionate e ugualmente costruibili con i vecchi e i nuovi formati, il risultato finale è che con poche modifiche il codice di base può essere aggiornato per usare il software XCM più recente (su Rust, questo è noto come crate). L'aggiornamento del crate XCM consente a una rete di interagire meglio con altre reti aggiornate in modo simile, ma l'aggiornamento di qualsiasi frammento del linguaggio XCM utilizzato dalla rete deve avvenire successivamente.

Spero che questo sia un forte incentivo per i team a mantenere aggiornati i propri crate XCM e di conseguenza mantenere tutto iterato e in rapida evoluzione.

🏁 Conclusioni

Spero che questo articolo vi abbia illustrato come il controllo delle versioni del’XMC possa essere utilizzato per mantenere una rete di sovereign chain connesse tra loro, mentre il linguaggio usato per comunicare evolve a ritmi e tempi diversi tra le reti. Tutto questo senza un significativo sovraccarico operativo per i team di sviluppatori che mantengono la loro logica.

Nel prossimo articolo, daremo uno sguardo molto più approfondito al modello di esecuzione e le capacità di gestione delle eccezioni dell’XCM.

Leggete la Parte I: XCM: il formato dei messaggi a consenso incrociato

Leggete la Parte III: Esecuzione e gestione delle eccezioni

0
Polkadot ArenaPost author

Polkadot Arena è un progetto in lingua italiana di divulgazione sull'ecosistema #Polkadot. Tutti i contenuti realizzati dai membri del collettivo.

Il progetto è stato lanciato da appassionati, ambassador, validator e collator di Polkadot e/o alcune parachain. Abbiamo pensato che unendo le forze e parlando con una unica voce avremmo potuto dare più risalto e diffondere un'informazione più completa alla community italiana. L'unione fa la forza!

Il nostro obbiettivo sarebbe quello di diventare il canale d'informazione più popolare in Italia su Polkadot.

0 comments

Polkadot Arena è un progetto in lingua italiana di divulgazione sull'ecosistema #Polkadot. Tutti i contenuti... Show More