XCM: il formato dei messaggi a consenso incrociato
Mentre si avvicina la release finale di Polkadot 1.0, completa di Parachain, il formato Cross-Consensus Messaging, in breve XCM, si sta preparando alla sua prima release pronta per la produzione. Questa è un'introduzione al formato, ai suoi obiettivi, a come funziona e a come può essere usato per realizzare i tipici compiti della cross-chain.
Un fatto divertente per cominciare... XCM è il formato di messaggistica "cross-consensus", piuttosto che solo "cross-chain". Questa differenza è sostanziale ed è segno degli obiettivi di questo formato. Esso infatti è stato progettato non solo per comunicare tra chain, ma anche tra smart-contract e pallet, sia su bridge ed enclave sharded come Polkadot SPREE.
🤟 Un formato, non un protocollo
Per comprendere meglio XCM, è importante capire i suoi limiti e dove si inserisce nello stack tecnologico di Polkadot. XCM non è un protocollo, ma formato di messaggistica. La sua utilità consiste solo nell'esprimere ciò che deve essere fatto dal destinatario e non può essere utilizzato per "inviare" messaggi tra sistemi.
Escludendo bridge e pallet, Polkadot è dotato di tre sistemi distinti per comunicare i messaggi XCM tra le chain che lo compongono: UMP, DMP e XCMP. UMP (Upward Message Passing) consente alle parachain di inviare messaggi alla propria Relay chain. DMP (Downward Message Passing) permette alla Relay chain di passare messaggi verso una delle proprie chain. XCMP, forse il più noto, consente alle parachain di inviare messaggi tra loro. XCM può essere utilizzato per esprimere il significato dei messaggi su ciascuno di questi tre canali di comunicazione.
Oltre all'invio di messaggi tra chain, XCM è utile anche per effettuare transazioni con una chain di cui non si conosce necessariamente il formato delle transazioni con largo anticipo. Nelle chain la cui logica di business cambia poco (ad esempio Bitcoin), il formato delle transazioni, o quello utilizzato dai wallet per inviare istruzioni alla chain, tende a rimanere esattamente lo stesso all'infinito. Nelle chain basate su un metaprotocollo altamente evolvibile, come Polkadot e le parachain che la compongono, la configurazione logica può essere aggiornata in tutta la rete con una singola transazione. Questi aggiornamenti possono cambiare qualsiasi cosa, introducendo un potenziale problema per chi mantiene i wallet, specialmente quelli offline (come Parity Signer). Dal momento che, XCM ha un controllo delle versioni ben revisionato, astratto e generale, esso può essere utilizzato come formato di transazione destinato a durare nel tempo e che i wallet possono utilizzare per creare molte transazioni comuni.
🥅 Obiettivi
XCM vuole essere un linguaggio per la comunicazione di idee tra sistemi di consenso. Dovrebbe essere abbastanza generale da poter essere adeguatamente utile in un ecosistema in crescita. Dovrebbe essere estensibile e future-proof, dato che l'estensibilità implicherà inevitabilmente dei cambiamenti esso non deve diventare obsoleto, e forwards-compatible (compatibile con le nuove versioni). Infine, dovrebbe essere abbastanza efficiente da poter essere eseguito on-chain e in un ambiente a pagamento.
Come tutti i linguaggi, alcuni individui tenderanno a utilizzare alcuni elementi più di altri. XCM non è progettato in modo tale che ogni sistema che supporta XCM sia in grado di interpretare ogni possibile messaggio XCM. Alcuni messaggi non saranno interpretati da alcuni sistemi. Altri potrebbero essere accettabili, ma comunque intenzionalmente non supportati dall'interprete a causa di vincoli di risorse o perché lo stesso contenuto può essere espresso in modo più chiaro e canonico. I sistemi supportano inevitabilmente solo un sottoinsieme di messaggi possibili e i sistemi soggetti a vincoli delle risorse (come gli smart contract) rischiano di supportare solo un "dialetto" molto limitato.
Questa generalità si estende anche a concetti come il pagamento di commissioni per l'esecuzione dei messaggi XCM. Nel nostro caso, vogliamo che XCM venga utilizzato su sistemi eterogenei (dalle piattaforme di smart contract alimentate a gas, alle parachain della community, fino alle interazioni trust tra parachain e la loro Relay chain) e per questo non sono incorporati elementi come il pagamento delle commissioni.
😬 Perché non usare il formato nativo dei messaggi?
L’utilizzo del formato nativo dei messaggi e delle transazioni di una chain o di uno smart contract può essere utile in alcune circostanze, ma presenta alcuni grossi svantaggi che lo rendono meno utile per gli obiettivi di XCM. In primo luogo, manca la compatibilità tra le chain, quindi un sistema che desidera inviare messaggi a più di una destinazione dovrà comporre diversi messaggi per ciascuna di esse. Per di più, anche una singola destinazione può modificare il proprio formato nativo nel corso del tempo, gli smart contract potrebbero essere aggiornati, le blockchain potrebbero cambiare il formato delle transazioni e dei messaggi.
In secondo luogo, i casi d'uso comuni delle chain non si adattano facilmente a una singola transazione; possono essere necessari accorgimenti particolari per prelevare fondi, scambiarli e depositare il risultato all'interno di una singola transazione. Le notifiche di trasferimento, necessarie per un quadro coerente di riserve e attività, non sono presenti in chain che non conoscono le altre.
In terzo luogo, operazioni come il pagamento delle tariffe non si adattano facilmente ad un modello che presuppone che queste siano già state negoziate negli smart contract. Le transazioni forniscono un sistema per il pagamento dell'elaborazione, ma sono anche generalmente concepite per contenere una firma, cosa che non ha senso quando si comunica tra sistemi di consenso.
🎬 Alcuni casi d'uso iniziali
Se l'obiettivo di XCM è quello di essere generale, flessibile e a future-proof, esso deve soddisfare alcune esigenze pratiche: il trasferimento di token tra chain; il pagamento facoltativo di commissioni; un'interfaccia generale per la gestione di un servizio di scambio comune a tutto il mondo della DeFi; infine, dovrebbe utilizzare il linguaggio XCM per condurre alcune azioni specifiche della piattaforma, come per esempio, inviare una chiamata remota ad uno dei pallet all’interno di una cahin Substrate per accedere ad una funzionalità di nicchia.
In aggiunta, ci sono molti modelli per il trasferimento dei token che vorremmo supportare:
- Remote Transfer (Trasferimento Remoto): potremmo voler controllare un conto su una chain remota, consentendo alla chain locale di avere un indirizzo sulla chain remota in grado di ricevere fondi ed eventualmente trasferire i fondi controllati in altri conti sulla chain remota.
- Teleport (Teletrasporto): potremmo avere due sistemi di consenso, entrambi nativi per un particolare token. Immaginate un token, come USDT o USDC, che ha istanze perfettamente fungibili su diverse chain. Dovrebbe essere possibile rimuovendo un token su una chain e mintare il token corrispondente su un'altra chain supportata. Nel linguaggio di XCM, chiamiamo questo teletrasportoper l'idea che l'apparente spostamento di un asset avviene in realtà distruggendolo da una parte e creando un clone dall'altra.
- Reserve-Based Transfer (Trasferimento basato sulle riserve): ci possono essere due chain che vogliono nominare una terza chain, e quella in cui un asset potrebbe essere considerato nativo da usare come riserva per quell'asset. La forma derivata dell'asset su ciascuna di queste chain sarebbe pienamente supportata, consentendo all'asset derivato di essere scambiato con l'asset sottostante sulla chain di riserva che lo supporta. Questo potrebbe essere il caso in cui le due chain non si fidano necessariamente l'una dell'altra, ma (almeno per quanto riguarda l’asset in questione) sono disposte a fidarsi della chain nativa dell’asset. Un esempio potrebbe essere quello di diverse parachain che vorrebbero scambiare DOT tra loro. Ognuna di esse ha una forma locale di DOT supportata interamente dal DOT controllato dalla parachain sulla chain Statemint (un hub nativo per il DOT). Quando la forma locale di DOT viene inviata tra le chain, in background il "vero" DOT si muove tra i conti delle parachain su Statemint.
Anche questo livello apparentemente modesto di funzionalità presenta un numero relativamente elevato di configurazioni il cui utilizzo potrebbe essere auspicabile e richiede una progettazione interessante per evitare l'overfitting.
🫀 L'anatomia del XCM
Al centro del formato XCM si trova la XCVM, Cross-Consensus Virtual Machine. Si tratta di un computer di altissimo livello non Turing completo, le cui istruzioni sono progettate per essere approssimativamente allo stesso livello delle transazioni.
Un "messaggio" in XCM è solo un programma eseguito sulla XCVM. Si tratta di una o più istruzioniXCM. Il programma viene eseguito finché non arriva alla fine o non si verifica un errore; a quel punto termina (per il momento lo lascio intenzionalmente senza spiegazioni) e si arresta.
La XCVM include una serie di registri e l'accesso allo stato generale del sistema di consenso che la ospita. Le istruzioni possono modificare un registro, modificare lo stato del sistema di consenso o entrambi.
Un esempio di tale istruzione è TransferAsset, utilizzata per trasferire un asset a un altro indirizzo nel sistema remoto. È necessario dire quali asset trasferire e a chi/dove deve essere trasferito. In Rust, viene dichiarata in questo modo:
enum Instruction {
TransferAsset {
assets: MultiAssets,
beneficiary: MultiLocation,
}
/* snip */
}
Come si può intuire, assets è il parametro che esprime quali risorse devono essere trasferite e beneficiary indica a chi/dove devono essere destinate. Manca ovviamente un'altra informazione, ovvero da chi/dove devono essere prelevati questi asset. Questo viene automaticamente derivato dall’Origin Register (Registro delle Origini).Quando il programma inizia, questo registro viene generalmente impostato in base al sistema di trasporto (bridge, XCMP o altro) per riflettere la provenienza del messaggio, ed è dello stesso tipo di informazione del beneficiary. Il registro di origine funziona come un registro protetto: il programma non può impostarlo arbitrariamente, anche se esistono due istruzioni che possono essere utilizzate per modificarlo in determinati modi.
I tipi, Type, utilizzati sono idee fondamentali in XCM: gli asset, rappresentati da MultiAsset e i luoghi all'interno del consenso, rappresentati da MultiLocation. Il registro delle origini è un MultiLocation opzionale (opzionale, perché può essere cancellato del tutto, se lo si desidera).
📍 Location in XCM
Il tipo MultiLocation identifica qualsiasi singola posizioneesistente nel mondo del consenso. Si tratta di un'idea piuttosto astratta e può rappresentare ogni sorta di cosa esistente all'interno del consenso, da una blockchain scalabile multi-shard come Polkadot fino a un semplice conto di asset ERC-20 su una parachain. In termini informatici, si tratta semplicemente di una struttura dati globale singleton, indipendentemente dalla sua dimensione o complessità.
MultiLocation esprime sempre una posizione relativa alla posizione corrente. Si può pensare che sia un po' come il percorso del file system, ma non c'è modo di esprimere direttamente la "radice" dell'albero del file system. Questo per un semplice motivo: nel mondo di Polkadot, le blockchain possono essere unite e separate da altre blockchain. Una blockchain può iniziare la sua vita da sola e alla fine essere elevata a parachain all'interno di un consenso più ampio. Se lo facesse, il significato di "radice" cambierebbe da un giorno all'altro e questo potrebbe portare al caos per i messaggi XCM e per tutto ciò che utilizza MultiLocation. Per mantenere le cose semplici, questa possibilità è stata scartata.
Le Location in XCM sono gerarchiche; alcune Location del consenso sono interamente incapsulate all'interno di altre Location del consenso. Una parachain di Polkadot esiste interamente all'interno del consenso complessivo di Polkadot e la chiamiamo luogo interno. In termini più rigorosi, possiamo dire che ogni volta che esiste un sistema di consenso il cui cambiamento implica un cambiamento in un altro sistema di consenso, allora il primo sistema è internoal secondo. Ad esempio, uno smart contract Canvas è interno al pallet di contratti che lo ospita. Un UTXO in Bitcoin è interno alla blockchain Bitcoin.
Ciò significa che l'XCM non distingue tra il "chi?" e il "dove?". Dal punto di vista di qualcosa di abbastanza astratto come l'XCM, la differenza non è davvero importante: le due domande si confondono e diventano essenzialmente la stessa cosa.
Come vedremo, MultiLocations viene utilizzato per identificare i luoghi in cui inviare i messaggi XCM, i luoghi che possono ricevere asset e persino aiutare a descrivere il tipo di asset stesso. Sono cose molto utili.
Quando vengono scritti in un testo come questo articolo, sono espressi come un certo numero di .. (o "parent", il sistema di consenso incapsulante) seguiti da un certo numero di giunzioniseparate da /. (Questo non è ciò che generalmente accade quando li esprimiamo in un linguaggio come Rust, ma ha senso per iscritto, poiché è molto simile ai percorsi delle directory di uso comune). Le giunzioniidentificano una posizione interna al sistema di consenso incapsulante. Se non ci sono genitori/giunzioni, allora diciamo semplicemente che la posizione è Qui.
Alcuni esempi:
-
../Parachain(1000): Valutato all'interno di una parachain, identifica la nostra parachain sorella di indice 1000. (In Rust scriveremmo ParentThen(Parachain(1000)).into()).
-
../AccountId32(0x1234...cdef): Valutato all'interno di una parachain, identifica l'account a 32 byte 0x1234...cdef sulla relay chain.
-
Parachain(42)/AccountKey20(0x1234...abcd): Valutato su una relay chain, questo identificherebbe il conto a 20 byte 0x1234...abcd sulla parachain numero 42 (presumibilmente qualcosa come Moonbeam che ospita conti compatibili con Ethereum).
Esistono diversi tipi di giunzioniper identificare i luoghi che si possono trovare sulla chain in tutti i modi, come key (chiavi), indici, blob binari e descrizioni di pluralità.
💰 Asset in XCM
Quando si lavora in XCM è spesso necessario fare riferimento a un asset di qualche tipo. Questo perché praticamente tutte le blockchain pubbliche esistenti si basano su un asset digitale nativo per fornire la spina dorsale dell'economia interna e del meccanismo di sicurezza. Per le blockchain Proof-of-Work, come Bitcoin, l'asset nativo (BTC) viene utilizzato per ricompensare i miner che fanno crescere la blockchain e prevengono il double-spending (doppi pagamenti). Per le blockchain Proof-of-Stake, come Polkadot, l'asset nativo (DOT) è usato come collaterale, dove i custodi della rete (noti come stakers) devono metterlo a rischio per generare blocchi validi ed essere ricompensati.
Alcune blockchain gestiscono più asset, ad esempio il framework ERC-20 di Ethereum permette di gestire molti asset diversi sulla chain. Alcune gestiscono asset che non sono fungibili, come ETH di Ethereum, ma piuttosto non fungibili, istanze uniche nel loro genere; Crypto-kitties è stato un primo esempio di token non fungibili o NFT.
XCM è stato progettato per essere in grado di gestire tutti questi asset senza problemi. A questo scopo esiste il tipo di dato MultiAsset insieme ai tipi associati MultiAssets, WildMultiAsset e MultiAssetFilter. Vediamo il MultiAsset in Rust:
struct MultiAsset {
id: AssetId,
fun: Fungibility,
}
Ci sono due campi che definiscono la nostra risorsa: id e fun, questo è abbastanza indicativo di come XCM si approccia alle risorse. In primo luogo, è necessario fornire un'identità complessiva dell'asset. Per gli asset fungibili questo identifica semplicemente l'asset. Per gli NFT identifica la "classe" complessiva dell'asset - diverse istanze dell'asset possono rientrare in questa classe.
enum AssetId {
Concrete(MultiLocation),
Abstract(BinaryBlob),
}
L'identità dell’asset è espressa in due modi: concretoo astratto. Abstract non è molto utilizzato, ma consente di specificare gli ID delle risorse per nome. Questo è comodo, ma dipende dal fatto che il destinatario interpreti il nome nel modo in cui il mittente si aspetta, il che potrebbe non essere sempre così facile. Concreto è di uso generale e utilizza una locationper identificare un asset senza ambiguità. Per gli asset nativi (come il DOT), l'asset tende ad essere identificato come la chain che lo minta (la Relay Chain Polkadot in questo caso, che sarebbe la posizione .. di uno delle sue parachain). Gli asset gestiti all'interno del pallet di una chain possono essere identificati da una posizione che include il loro indice all'interno del pallet. Per esempio, la parachain Karura potrebbe riferirsi ad un asset sulla parachain Statemine con la posizione ../Parachain(1000)/PalletInstance(50)/GeneralIndex(42).
enum Fungibilità {
Fungible(NonZeroAmount),
NonFungible(AssetInstance),
}
In secondo luogo, devono essere fungibili o non fungibili. Se sono fungibili, deve esserci un importo associato non nullo. Se non sono fungibili, invece di un importo, deve esserci un'indicazione di quale istanza si tratta. Questo viene comunemente espresso con un indice, ma XCM consente di utilizzare anche altri tipi di dati, come array e blob binari.
Questo riguarda i MultiAsset, ma ci sono altri tre tipi associati che talvolta utilizziamo. MultiAssets è uno di questi e significa semplicemente un insieme di elementi MultiAsset. C'è poi WildMultiAsset, una wildcard (o carattere jolly) che può essere usato per confrontare uno o più elementi MultiAsset. In realtà sono supportati solo due tipi di wildcard: All (che corrisponde a tutti gli asset) e AllOf che corrisponde a tutti gli asset di una particolare identità (AssetId) e fungibilità. In particolare, per quest'ultima, non è necessario specificare l'importo (nel caso dei fungibili) o l'istanza (o le istanze) (per i non fungibili) e tutti vengono selezionati.
Infine, c'è MultiAssetFilter. Questo è quello usato più spesso ed è in realtà una combinazione di MultiAsset e WildMultiAsset, che consente di specificare una wildcard o un elenco di asset definiti(cioè non jolly).
Nell'API XCM di Rust, forniamo molte conversioni per rendere il lavoro con questi tipi di dati il più indolore possibile. Ad esempio, per specificare il MultiAsset fungibile che equivale a 100 unità indivisibili di DOT (Planck, per chi se ne intende) quando ci troviamo nella Relay Chain Polkadot, dovremmo usare (Here, 100).into().
👉 L’Holding Register (Il registro di detenzione)
Vediamo un'altra istruzione XCM: WithdrawAsset. A prima vista, è un po' come la prima metà di TransferAsset: preleva alcuni asset dal conto del luogo specificato nel Registro delle origini. Ma cosa ne fa? Se non vengono depositati da nessuna parte, è sicuramente un'operazione inutile. Diamo un'occhiata alla sua dichiarazione Rust:
WithdrawAsset (MultiAsset),
Questa volta c'è un solo parametro (di tipo MultiAssets e che indica quali asset devono essere ritirati dalla gestione del Registro delle origini), ma non viene specificata la posizione in cui collocare le attività.
Gli asset prelevati e non spesi sono temporaneamente conservati nel cosiddetto Holding Register("holding" perché si trovano in una posizione temporanea che non può persistere a tempo indeterminato). Esistono diverse istruzioni che operano sull'Holding Register. Una molto semplice è l'istruzione DepositAsset. Vediamola:
enum Instruction {
DepositAsset {
assets: MultiAssetFilter,
max_assets: u32,
beneficiary: MultiLocation,
},
/* snip */
}
Aha! Il lettore attento noterà che questa sembra la metà mancante dell'istruzione TransferAsset. Abbiamo il parametro assets, che specifica quali asset devono essere rimossi dall'Holding Register per essere depositati sulla chain. max_assets consente all'autore dell'XCM di informare il destinatario sul numero di asset unici che si intende depositare. (Questo è utile quando si calcolano le tariffe prima di conoscere il contenuto dell'Holding Register, poiché depositare un asset può essere un'operazione costosa). Infine c'è il beneficiary, che è lo stesso parametro incontrato in precedenza nell'operazione TransferAsset.
Esistono molte istruzioni che esprimono le azioni da compiere sul Registro di partecipazione, e DepositAsset è una delle più semplici. Altre sono piuttosto sofisticate 😬.
🤑 Pagamento delle commissioni in XCM
Il pagamento delle commissioni in XCM è un caso d'uso piuttosto importante. La maggior parte delle parachain della comunità Polkadot richiederanno ai propri interlocutori di pagare ogni operazione che desiderano effettuare; questo per evitare di esporsi allo “transaction spam” (“spam di transazioni”) ed a un attacco denial-of-service. Esistono eccezioni a questo principio quando le chain hanno buone ragioni per credere che il loro interlocutore si comporterà bene, come nel caso in cui la Relay Chain Polkadot collabori con Statemint, la common-good chain di Polkador. Tuttavia, nel caso generale, le tariffe sono un buon modo per garantire che i messaggi XCM e i loro protocolli di trasporto non vengano utilizzati in modo eccessivo. Vediamo come si possono pagare le tariffe quando i messaggi XCM arrivano a Polkadot.
XCM non include l'idea delle commissioni e dei pagamenti e grazie alle astrazioni a costo zero di Rust il loro design avviene senza troppi grattacapi. Questa è una sensibile differenza rispetto al modello di transazione di Ethereum, dove il costo delle commissioni è integrato nel protocollo ed i casi d'uso che non ne hanno bisogno devono aggirarlo in modo evidente.
Tuttavia, per i sistemi che richiedono il pagamento di commissioni, XCM offre la possibilità di acquistare risorse di esecuzione con asset. In linea di massima, questa operazione consiste in tre parti:
-
In primo luogo, è necessario fornire alcuni asset.
-
In secondo luogo, si deve negoziare lo scambio di asset per il tempo di calcolo (o weight, nel linguaggio di Substrate).
-
Infine, le operazioni XCM saranno eseguite come da istruzioni.
La prima parte è gestita da una delle numerose istruzioni XCM che forniscono asset. Una di queste la conosciamo già (WithdrawAsset), ma ce ne sono molte altre che vedremo in seguito. Gli asset contenuti nell'Holding Register saranno utilizzati per pagare le commissioni associate all'esecuzione dell'XCM. Tutti gli asset non utilizzati per pagare le commissioni verranno depositati in un conto di destinazione. Per esempio, assumeremo che l'XCM avvenga sulla Relay Chain Polkadot e che sia per 1 DOT (ovvero 10.000.000.000 di unità indivisibili).
Finora l'istruzione XCM si presenta come segue:
WithdrawAsset((Here, 10_000_000_000).into()),
Questo ci porta alla seconda parte: scambiare (alcune di) questi asset con tempo di calcolo utile a pagare il nostro XCM. Per questo abbiamo l'istruzione XCM BuyExecution. Vediamola:
enum Instruction {
/* snip */
BuyExecution {
fees: MultiAsset,
weight: u64,
},
}
Il prima elemento fees è l'importo che deve essere prelevato dall’Holding Registered utilizzato per il pagamento delle commissioni. Tecnicamente è solo il massimo, qualsiasi importo non utilizzato viene immediatamente restituito.
L'importo speso è determinato dal sistema di interpretazione, le fees lo limitano solo e se il sistema di interpretazione rileva che deve essere pagato di più per l'esecuzione desiderata, l'istruzione BuyExecution terminerà con un errore. Il secondo elemento weight specifica la quantità di tempo di esecuzione da acquistare. In genere questo non dovrebbe essere inferiore al peso totale del programma XCM.
Nel nostro esempio assumeremo che tutte le istruzioni XCM richiedano un milione di peso, quindi due milioni per i due elementi finora utilizzati (WithdrawAsset e BuyExecution) e un altro per quello che verrà. Utilizzeremo tutti i DOT che abbiamo per pagare queste commissioni (il che è una buona idea solo se ci fidiamo che la chain di destinazione non abbia commissioni assurde, assumiamo che sia così). Diamo un'occhiata al nostro XCM fino ad ora:
WithdrawAsset((Here, 10_000_000_000).into()),
BuyExecution {
fees: (Here, 10_000_000_000).into(),
weight: 3_000_000,
},
La terza parte del nostro XCM consiste nel depositare i fondi rimasti nell'Holding Register. Per questo useremo l'istruzione DepositAsset. In realtà non sappiamo quanto rimane nell'Holding Register, ma questo non importa, perché possiamo specificare una wildcar per gli asset che devono essere depositati. Li depositeremo nel conto di Statemint (identificato come Parachain(1000)).
L'istruzione finale di XCM si presenta quindi come segue:
WithdrawAsset((Here, 10_000_000_000).into()),
BuyExecution {
fees: (Here, 10_000_000_000).into(),
weight: 3_000_000,
},
DepositAsset {
assets: All.into(),
max_assets: 1,
beneficiary: Parachain(1000).into(),
},
⛓ Spostamento degli asset tra chain in XCM
L'invio di un asset su di un'altra chain è probabilmente il caso d'uso più comune della messaggistica interchain. Permettere ad una chain di maneggiare l'asset nativo di un'altra chain consente ogni tipo di utilizzo derivato (senza giochi di parole), il più semplice dei quali è uno scambio decentralizzato, ma generalmente raggruppato come finanza decentralizzata o DeFi.
In generale, gli asset si muovono tra chain in due modi, a seconda se le chain si fidino o meno della sicurezza e della logica reciproca.
✨Teleport (Teletrasporto)
Per le chain che si fidano l'una dell'altra (ad esempio, shard omogenei sotto lo stesso ombrello di consenso e sicurezza) possiamo utilizzare un framework che Polkadot chiama teletrasporto, che in pratica significa semplicemente distruggere un asset sul lato di invio e crearlo sul lato di ricezione. Si tratta di un'operazione semplice ed efficiente, che richiede solo il coordinamento delle due chain e comporta una sola azione da entrambe le parti. Sfortunatamente, se la chain ricevente non può fidarsi al 100% che la chain mittente distrugga effettivamente l'asset che sta creando (e che non crei asset al di fuori delle regole concordate per l'asset), allora la chain mittente non ha alcuna base per ricreare l'asset a seguito del messaggio di ritorno.
Vediamo come apparirebbe l'XCM che ha teletrasportato (la maggior parte) 1 DOT dalla Relay Chain Polkadot al suo conto sovrano su Statemint. Assumiamo che le commissioni siano già state pagate dal lato Polkadot.
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: 3_000_000,
},
DepositAsset {
assets: All.into(),
max_assets: 1,
beneficiary: Parent.into(),
},
]),
}
Come si può vedere, questo esempio è abbastanza simile a quello di prelievo-acquisto-deposito che abbiamo visto sopra. La differenza è l'istruzione InitiateTeleport, inserita intorno alle ultime due istruzioni (BuyExecution e DepositAsset). La chain mittente (Polkadot Relay) crea un messaggio completamente nuovo quando esegue l'istruzione InitiateTeleport; prende il campo xcm e lo inserisce in un nuovo XCM, ReceiveTeleportedAsset, e lo invia alla chain destinataria (Statemint). Statemint si fida del fatto che la Relay Chain Polkadot abbia distrutto l'1 DOT dalla sua parte prima di inviare il messaggio. (Lo fa!)
Il beneficiary è indicato come Parent.into() e un lettore esperto potrebbe chiedersi a cosa possa riferirsi nel contesto della Relay Chain Polkadot. La risposta sarebbe "niente", ma non c'è alcun errore. Tutto ciò che è contenuto nel parametro xcm è scritto nella prospettiva e nel contesto del ricevente. Nonostante faccia parte del XCM introdotto nella Relay Chain Polkadot, esso viene eseguitosolo su Statemint.
Il messaggio ricevuto da Statemint, si presenterà così:
ReceiveTeleportedAsset((Parent, 10_000_000_000).into()),
BuyExecution {
fees: (Parent, 10_000_000_000).into(),
weight: 3_000_000,
},
DepositAsset {
assets: All.into(),
max_assets: 1,
beneficiary: Parent.into(),
},
Si potrebbe notare che questa operazione è simile alla precedente WithdrawAsset XCM. L'unica differenza è che, invece di finanziare le commissioni e il deposito attraverso un prelievo da un conto locale, l’asset viene "magicamente" creato confidando nel fatto che il DOT sia stato distrutto nella chain mittente (Relay Chain Polkadot) e rispettando il messaggio ReceiveTeleportedAsset.
In particolare, l'identificatore dell’asset di 1 DOT inviato sulla Relay Chain Polkadot (Here, riferendosi alla Relay Chain stessa, la sede nativa del DOT) è stato automaticamente mutato nella sua rappresentazione su Statemint: Parent.into(), che è la location della Relay Chain nel contesto di Statemint.
Il beneficiary è indicato come la Relay Chain Polkadot e quindi il suo conto sovrano (su Statemint) viene accreditato con 1 DOT di nuova creazione meno le commissioni. L'XCM avrebbe potuto semplicemente nominare un conto o un altro luogo per il beneficiary. Così com'è, un successivo TransferAsset inviato dalla Relay Chain potrebbe essere usato per spostare questo 1 DOT.
🏦 Riserve
Il modo alternativo per trasferire gli asset tra chain è leggermente più complicato. Si utilizza una terza parte, nota come riserva. Il nome deriva dall'attività bancaria di riserva, in cui gli asset sono tenuti "in riserva" per dare credibilità all'idea che una promessa emessa abbia valore. Ad esempio, se possiamo ragionevolmente credere che esattamente 1 DOT "reale" (ad esempio Statemint o Relay Chain) sia riscattabile per ogni DOT "derivato" emesso su una parachain indipendente, allora possiamo trattare il DOT della parachain economicamente equivalente al DOT reale (la maggior parte delle banche fa una cosa chiamata "banca a riserva frazionaria", il che significa che tengono in riserva meno del valore nominale). Questo funziona bene fino a quando troppe persone desiderano riscattare, a quel punto tutto può andare storto molto velocemente.
Quindi, la riserva è il luogo in cui sono conservati gli asset "reali" e, ai fini del trasferimento, la cui logica e sicurezza sono affidate sia dal mittente che dal destinatario. Tutti gli asset corrispondenti dal lato del mittente e del destinatario sarebbero quindi derivati, ma sostenuti al 100% dall'asset di riserva "reale". Supponendo che la parachain si comporti bene (cioè che sia priva di bug e che la sua governance non decida di scappare con la riserva), questo consentirebbe al DOT derivato di avere più o meno lo stesso valore del DOT di riserva. Gli asset di riserva sono conservate nel conto sovranodel mittente/destinatario (cioè il conto controllabile dalla chain del mittente o del destinatario) sulla chain delle riserve, quindi, a meno che qualcosa non vada storto con la parachain, c'è una buona ragione per pensare che siano ben custoditi.
Tornando al meccanismo di trasferimento, il mittente darebbe istruzioni alla riserva di spostare gli asset che il mittente possiede (e che usa come riserva per la propria versione della stessa attività) nel conto sovrano del destinatario, e la riserva - *non il mittente! -*informa il destinatario del suo nuovo credito. Ciò significa che il mittente e il destinatario non devono fidarsi della logica o della sicurezza dell'altro, ma solo di quella della chain utilizzata come riserva. Ciò implica che le tre parti devono coordinarsi, con il conseguente aumento del costo complessivo, dell tempo e della complessità delle operazioni da eseguire.
Esaminiamo l'XCM richiesto. Questa volta invieremo 1 DOT dalla parachain 2000 alla parachain 2001, usando la parachain 1000 come riserva per il proprio DOT. Anche in questo caso, assumeremo che le comissioni siano già state pagate dal lato del mittente.
WithdrawAsset((Parent, 10_000_000_000).into()),
InitiateReserveWithdraw {
assets: All.into(),
dest: ParentThen(Parachain(1000)).into(),
xcm: Xcm(vec![
BuyExecution {
fees: (Parent, 10_000_000_000).into(),
weight: 3_000_000,
},
DepositReserveAsset {
assets: All.into(),
max_assets: 1,
dest: ParentThen(Parachain(2001)).into(),
xcm: Xcm(vec![
BuyExecution {
fees: (Parent, 10_000_000_000).into(),
weight: 3_000_000,
},
DepositAsset {
assets: All.into(),
max_assets: 1,
beneficiary: ParentThen(Parachain(2000)).into(),
},
]),
},
]),
},
Come promesso, questo è un po' più complesso. La parte esterna si occupa di estrarre 1 DOT dal lato mittente (parachain 2000) e di prelevare il corrispondente 1 DOT custodito da Statemint (parachain 1000). A questo scopo utilizza InitiateReserveWithdraw.
WithdrawAsset((Parent, 10_000_000_000).into()),
InitiateReserveWithdraw {
assets: All.into(),
dest: ParentThen(Parachain(1000)).into(),
xcm: /* snip */
}
Ora abbiamo 1 DOT nell'Holding Register di Statemint. Prima di poter fare qualsiasi altra cosa, dobbiamo acquistare del tempo di esecuzione su Statemint:
/*snip*/
xcm: Xcm(vec![
BuyExecution {
fees: (Parent, 10_000_000_000).into(),
weight: 3_000_000,
},
DepositReserveAsset {
assets: All.into(),
max_assets: 1,
dest: ParentThen(Parachain(2001)).into(),
xcm: /* snip */
},
]),
/*snip*/
Paghiamo le commissioni con il nostro 1 DOT e ipotizziamo un milione per ogni operazione XCM. Una volta pagata l'operazione, depositiamo l'1 DOT (meno le commissioni, più semplicemente All.into()) nel conto sovrano per la parachain 2001, ma lo facciamo come asset di riserva, il che significa che richiediamo anche a Statemint di inviare una notifica XCM alla chain ricevente per informarla del trasferimento insieme ad alcune istruzioni da eseguire sugli asset derivati finali. Le istruzioni DepositReserveAsset non hanno sempre molto senso; perché abbiano senso, la dest deve essere una location che possa contenere fondi nella chain di riserva, ma anche in cui la chain di riserva possa inviare un XCM. Si dà il caso che le parachain “sorelle” si adattino perfettamente a questa situazione.
/*snip*/
xcm: Xcm(vec![
BuyExecution {
fees: (Parent, 10_000_000_000).into(),
weight: 3_000_000,
},
DepositAsset {
assets: All.into(),
max_assets: 1,
beneficiary: ParentThen(Parachain(2000)).into(),
},
]),
/*snip*/
La parte finale definisce una parte del messaggio che arriva alla parachain 2001. Come per l'avvio di un'operazione di teletrasporto, DepositReserveAsset compone e invia un nuovo messaggio, in questo caso ReserveAssetDeposited. È questo messaggio, contenente il programma XCM che abbiamo definito, che arriva alla parachain ricevente. Avrà il seguente aspetto:
ReserveAssetDeposited((Parent, 10_000_000_000).into()),
BuyExecution {
fees: (Parent, 10_000_000_000).into(),
weight: 3_000_000,
},
DepositAsset {
assets: All.into(),
max_assets: 1,
beneficiary: ParentThen(Parachain(2000)).into(),
},
(Questo presuppone che non siano state prelevate commissioni su Statemint e che l'intero 1 DOT sia stato trasferito. Ciò non è realistico, per cui la linea degli assets avrà probabilmente un numero inferiore).
L'unica differenza con il messaggio ReceiveTeleportedAsset precedente è l'istruzione ReserveAssetDeposited. Questa ha uno scopo simile, ma invece di indicare che "la chain mittente ha bruciato gli asset e dare il via libera alla creazione degli asset equivalenti nella chain di destinazione", significa "la chain mittente ha ricevuto gli asset e li sta trattenendo come riserva, in questo modo permette la creazione dei derivati nella chain di destinazione ". In entrambi i casi, la chain di destinazione crea gli asset nel Holding Register e noi li depositiamo nel conto sovrano del mittente sulla chain ricevente. 🎉
🏁 Conclusione
Questo è tutto per questo articolo; spero sia stato utile per spiegare cos'è XCM e le basi del suo funzionamento. Nei prossimi articoli daremo uno sguardo più approfondito all'architettura di XCVM, al suo modello di esecuzione e alla gestione degli errori, al sistema di versioning di XCM e a come gli aggiornamenti del formato possono essere gestiti in un ecosistema interdipendente e ben collegato, nonché al suo sistema di domanda e risposta e al funzionamento di XCM in Substrate. Verranno inoltre discusse alcune delle direzioni future di XCM, le funzionalità previste e il processo di evoluzione.
Leggi la Parte II: Versioni e compatibilità