Smart contract con Ethereum e Dapp in C#

di Andrea Zani, in Blockchain,

Smart contract con Ethereum e Dapp in C#

Ecco la seconda parte dei post dedicati alla Blockchain e alle criptovalute. Dopo la teoria e una semplicissima implementazione in C# di una Blockchain, è arrivato il momento di dedicarsi a qualcosa di più utile. Da qualche anno si parla del Web3. Dopo che negli anni '90, con il Web 1 fatto di pagine statiche e poca iterazione con gli utenti siamo arrivati all'attuale Web 2, fatto di social e multimedialità, che sia arrivato il momento del Web 3? Ma in cosa consiste questo nuovo web? Il Web 3 cerca di eliminare quello che viene considerato il problema attuale del Web 2, e portare le regole della Blockchain nel mondo Web che conosciamo tutti. Qual è il problema, se si può definire come tale, dell'attuale Web 2? Pochissimi siti fanno la maggioranza del traffico mondiale e questi sono in mano a poche società. Ecco, il Web 3 vuole eliminare questa centralizzazione dei dati e, grazie alla Blockchain, decentralizzare dati, applicazioni e quant'altro. Conseguenza? Togliere tutti questi dati dalle mani dei pochi e fare in modo che chiunque possa partecipare ed essere parte attiva non solo creando cazz... ops, contenuti, ma pure contribuendo pure alle decisioni e scelte future di una piattaforma. Questo vuol dire togliere potere alle grosse aziende, cloud, etc... Anche le applicazioni a cui siamo abituati dovrebbero diventare DApp - Decentralized Apps - perché anch'esse baseranno il loro funzionamento su una Blockchain, ma si è pronti a tutto ciò?

Nel mondo informatico la decentralizzazione non è una novità. Prendendo l'esempio di Ipfs (InterPlanetary File System) dove non esiste un server centrale, chiunque può fare parte della rete per la condivisione di file. Ma anche senza scomodare questi servizi, anche le reti P2P, usate in modo legale o meno, come BitTorrent, sono decentralizzate. Il Web 3 vuole trasformare tutto il web in un enorme rete P2P dove i database sono su una Blockchain e tutti i dati in mano a tutti i nodi della rete? Mera illusione o è il futuro alle porte? Ne parlerò poi.

Se si è digiuni sull'argomento e pure il mio post precedente non ha aiutato, il mondo della Blockchain non è solo criptovaluta. Così come in una transazione è possibile scambiare la valuta corrente di quella Blockchain è possibile anche inserire degli Smart Contract, così come è pure possibile richiamare funzioni di quello Smart Contract. In modo semplicistico questi sono programmi che possono eseguire operazioni nella Blockchain e usarla per la memorizzazione di dati, e altresì scambiare criptovaluta in modo intelligente al succedersi di determinati eventi.

Uno dei primi esempi che si legge quando si cerca di capire che cosa è uno Smart Contract in un contesto reale è quello per il biglietto di un treno o vari tipi di assicurazioni. Un viaggiatore lo acquista e versa il corrispettivo valore che rimane memorizzato all'interno dello Smart Contract. Se l'arrivo del treno a destinazione viene in modo regolare il valore del biglietto viene ceduto alla società dei trasporti, ma se viene accusato un forte ritardo che dà diritto alla restituzione del prezzo dei biglietto, o di una sua parte, lo Smart Contract restituirà quanto versato al viaggiatore in modo del tutto autonomo, senza che questo debba andare allo sportello preposto, compilare documenti e aspettare settimane per il rimborso.

Banale come esempio ma rende l'idea. Ma quale rete Blockchain mette a disposizione questi Smart Contract? Pressoché tutte le cryptovalute hanno questo supporto più o meno evoluto, pure Bitcoin che lo utilizza, come scritto nel post precedente, per gestire le transazioni UTXO. Ethereum ha fatto la sua fortuna offrendo una gestione avanzata di questo tipo di contratti, così come Solana e altre. Parere del tutto personale, per ora mi sembra che il sistema più maturo sia quello di Ethereum... sì, perché ad un certo punto si deve valutare qualche rete Blockchain usare perché non tutte le criptovalute sono compatibili con le altre - imparare il linguaggio di programmazione e la sua architettura per Ethereum (Solidity) non servirà a nulla nella rete di Solana visto ha il suo supporto ad altri linguaggi di programmazione e ha ad altre API per accedere alla Blockchain.

Eh sì, in questo post, tratterò solo Ethereum e Solidity.

Ma come funzionano gli Smart Contract? Come già scritto, non sono altro che programmi memorizzati nella Blockchain che espongono esternamente funzioni, memorizzano dati e sono salvati nella Blockchain con un specifico Address. Inoltre li si può equiparare ad un Wallet/Account, perché in essi è possibile inserire la criptovaluta della Blockchain a cui è legata, e qualunque utente può interagire con esso dietro a determinate role ovviamente, e al succedersi di eventi può inviare criptovaluta ad altri Smart Contract o Wallet/Account come nell'esempio del biglietto del treno.

Essendo memorizzato all'interno di una Blockchain ne segue pedissequamente le regole, quindi uno Smart Contract memorizzato in una Blockchain non potrà essere modificato o cancellato (ci sono dei trucchi appositi per eseguire aggiornamenti, ma questo argomento non lo tratterò in questo post) e sì, come intuibile, questo potrebbe essere un problema molto grave in caso della presenza di gravi bug nel codice.

Lo Smart Contract può memorizzare il suo stato all'interno della Blockchain come si vedranno negli esempi successivi, e questi rimarranno legati a quello Smart Contract. Questo è un dettaglio importante, perché qualsiasi modifica del codice creerà un nuovo Smart Contract nella Blockchain che NON condividerà alcun dato con la versione precedente. Inoltre per fare in modo che un Smart Contract possa essere fidato (trust) deve inserire nella Blockchain anche il source code in modo che possa essere esaminato da chiunque, per esempio:

https://etherscan.io/address/0xaf1610d242c7cdd30c546844af75c147c12e94f9#code

Cerco di dare una definizione finale: lo Smart Contract è un servizio di memorizzazioni dati e di interscambio di criptovaluta o Token su una Blockchain in grado di eseguire operazioni su di essa all'avverarsi di determinati eventi.

Solidity, un po' di teoria

Senza andare nel dettaglio del linguaggio (anche perché è presente sul sito ufficiale una documentazione completa) Solidity è nato come linguaggio di programmazione per gli Smart Contract in Ethereum anche se è usato anche da altre reti. Viene definito come un linguaggio di script tra il Javascript, il Python e il C++. Infatti la sintassi ricorda molto quella di Javascript ma in più le variabili sono tipizzate e c'è il supporto alla programmazione ad oggetti - le classi tipiche di questo tipo di linguaggio sono Contract, e come le classi permettono l'ereditarietà e il tipo di visibilità dei membri al suo interno. Classico esempio di Smart Contract in Solidity che mostra le basi del linguaggio:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract TestContract {
    string[] private _messages;

    function SendMessage(string calldata message) external {
        _messages.push(message);
    }

    function GetMessages() external view returns(string[] memory) {
        return _messages;
    }
}

La prima riga è solo un commento consigliato per la tipologia di licenza utilizzato per lo script. La seconda riga informa il compilatore quale versione di Solidity vogliamo sia usata. Quindi inizia il contratto vero e proprio grazie alla parola chiave Contract. Così come il C#/C++ ho inserito poi un membro privato di un array di stringhe. Ho inserito la clausola private solo per pignoleria visto che di base tali oggetti sono internal. Quindi ho inserito i due metodi, SendMessage e GetMessages il cui codice, per chi è avvezzo alla programmazione, è molto semplice (nel primo caso il parametro passato viene inserito nell'array di stringhe, nel secondo viene ritornato l'intero array). La loro visibilità viene definita alla fine della signature è può essere:

  • private: solo visibile all'interno del contratto.
  • internal: visibile all'interno del contratto o dai contract che derivano da essa
  • public: visibile da chiunque
  • external: non può essere chiamata internamente, ma solo esternamente. Questa tipologia serve a esporre le funzioni pubbliche nella Blockchain.

Se non si specifica altri parametri la funzione è di tipo void (senza valori di ritorno), altrimenti con la parola chiave returns si può specificare uno o più parametri di ritorno a 'mo di tuple.

In Solidity sono presenti delle tipologie di memorizzazione delle variabili all'interno dello Smart Contract. Questa specifica è normalmente implicita ma è consigliato specificarla nel caso di dei parametri di input e output delle variabili soprattutto nel caso di tipo Reference Type, come il tipo string o gli array. Queste tipologie sono tre:

  • memory: queste variabili sono memorizzate in memoria (come di default quelle definite all'interno dello scope di una funzione) e vengono cancellate alla fine dell'esecuzione dello scope dove sono definite.
  • storage: memorizza il contenuto della variabile all'interno della Blockchain ed è l'unica modalità disponibile per i membri del Contract.
  • calldata: sono i parametri di input delle funzioni external.

Nell'esempio qui sopra, anche se non è specificato, _messages sarà di tipo storage. Questo fa in modo che il suo contenuto venga memorizzato all'interno della Blockchain e sia disponibile alle richieste successive. Nella signature del metodo SendMessage ho usato calldata, così facendo tale variabile sarà readonly e passata per reference prima di essere copiata nell'array _messages. calldata permette solo un'ottimizzazione del codice, perché avrei potuto abche inserire memory che il tutto avrebbe funzionato senza problemi, ma del perché è consigliato usarlo lo spiegherò in seguito.

Continuando la veloce disamina del linguaggio Solidity, il tipo numerico di base è int256 (o uint256 per l'unsigned) che equivale ad un intero a 256 bit. Solidity mette a disposizione anche versioni meno esose nel consumo di byte, come int8, int16 fino a 256, ma il tipo base, in ogni caso, è quello a 256 bit. Questo dettaglio è importante per ottimizzare il codice perché ogni dato sarà ottimizzato a quel quantitativo di byte. Ipotizzando di creare due oggetti di questo tipo:

uint128 num1;
uint256 num2;

In memoria saranno memorizzati:

0x0000 - 0x007f uint128 num1
0x0080 - 0x00ff unused
0x0100 - 0x01ff uint256 num2

Quando si definiscono più variabili di diversa lunghezza è bene ottimizzarne la creazione, come in questo esempio:

uint128 num1;
uint128 num2;
uint256 num3;

Che occuperanno in modo ottimale la memoria:

0x0000 - 0x007f uint128 num1
0x0080 - 0x00ff uint num2
0x0100 - 0x01ff uint256 num3

Oltre ai classici array per gestire le liste è disponibile anche un dictionary:

magging(uint256 => string) public myDictionary;

Iniziando a scrivere vero codice andando oltre ai banali esempi in Solidity, ci si scontra con i limiti di questo linguaggio. Per esempio, proprio con il dictionary, non è presente nella versione attuale la possibilità di fare il loop di tutto il contenuto del dictionary con il classico for. Il trucco suggerito è di usare un array di appoggio dove memorizzare la key. Ecco un esempio di contract in Solidity che mostra anche altri limiti di questo linguaggio:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract TestContractDictionary {
    mapping(string => uint) collections;
    string[] keys;

    constructor() {
        collections["a"] = 1;
        collections["b"] = 2;
        collections["c"] = 3;

        keys.push("a");
        keys.push("b");
        keys.push("c");
    }

    function SearchKeyV1(string calldata keyToFind) external view returns(string memory, uint) {
        
        for (uint8 i=0; i < keys.length; i++) {
            if (StringsAreEquals(keys[i], keyToFind)) {
                uint value = collections[keyToFind];
                return (keyToFind, value);
            }
        }

        revert("Key not found");
    }

    function StringsAreEquals(string memory string1, string memory string2) private pure returns(bool) {
        return keccak256(abi.encodePacked(string1)) == keccak256(abi.encodePacked(string2));
    }
}

Constructor fa proprio quello che si intuisca faccia: è il costruttore di questo contract che sarà eseguito al momento dell'inserimento dello Smart Contract nella Blockchain. Nel mio codice popola il dictionary e l'array definiti come membri del Contract e memorizzati come tipo storage. Il metodo SearchKeyV1 cerca nel dictionary la chiave stringa passata come parametro e ritorna la tuple formata dalla stringa e dal valore numerico (nei parametri in returns non ho inserito memory per il tipo uint perché non è un reference type). Notare che non è possibile fare un confronto tra due stringhe direttamente: si ottiene un errore generico di conversione anche se hanno il tipo di storage uguale, così come il poterle concatenare. Fortunatamente ci sono trucchi utilizzati dalla community per risolvere queste banalità come mostrato nella funzione StringsAreEquals nel primo caso.

La funzione quindi prosegue ritornando i valori, se trovati, altrimenti viene eseguita la funzione Revert utilizzata per comunicare alla Blockchain che la chiamata è fallita, ed è possibile specificare anche il motivo che sarà riportato nell'errore. Questo era solo un esempio per fare il loop di tutti gli elementi di una dictionary (utile per fare altre operazioni). Se avessi voluto cercare per key un elemento la via più breve era questa:

function SearchKeyV2(string calldata keyToFind) external view returns(string memory, uint) {
    if (bytes(collections[keyToFind]).length != 0) {
        return collections[keyToFind];
    }

    revert("Key not found");
}

Nella mia esperienza recente ho approfondito anche l'uso del try/catch in Solidity, ma è quanto di più scomodo si possa immaginare nella versione attuale: è possibile fare il catch dell'errore solo per chiamate metodi external di altri contratti - non mostro esempi e rimando alla documentazione ufficiale. Nella mia esperienza con Solidity ho trovato anche altri limiti ma che non ho approfondito abbastanza e non vorrei che fossero causati più dalla mia inesperienza. Quindi mi fermo qui.

Interessanti sono le variabili globali. Queste ci forniscono importanti informazioni come il blocco della Blochain in cui gira lo Smart Contract, e informazioni sul sender (chi ha invocato lo Smart Contract). Rimando al link precedente per maggiori info anche perché negli esempi seguenti non le utilizzerò - cosa che farò invece nel prossimo post.

Gas

Ho fatto spesso riferimento alle ottimizzazioni del codice. Il motivo è semplice: ogni Smart Contract è inserito in una Blockchain grazie ad una transazione. Anche i metodi invocati che salvano dati sulla Blockchain richiedono una transazione. Arrivati a questo punto sarà chiaro che per essere inseriti in una transazione, sia che il motivo sia lo scambio di valuta, sia per l'esecuzione di uno Smart Contract, questo comporterà un esborso pecuniario, Fee, che sarà pagato ai Miner che la eseguirà. Innanzitutto la rete Blockchain di Ethereum per fare girare il nostro codice necessita dell'Ethereum Virtual Machine (EVM), che permette l'esecuzione del nostro codice scritto in Solidity e compilato nel bytecode compatibile con questa macchina virtuale. Inoltre tutti gli oggetti del contract di tipo storage devono essere salvati sempre all'interno della Blockchain e replicati in tutti i nodi, ovviamente. Il costo di queste transazioni dipende dal codice che è stato scritto (al momento dell'inserimento dello Smart Contract nella Blockchain) e del codice eseguito quando si richiamano metodi da esso. La regola è semplice: più il codice è complesso e più si paga. Più il codice accede a risorse per modifiche e più si paga. Di contro, le operazioni di sola lettura non hanno costo perché non creano transazioni. L'unità di misura per calcolare il costo dell'esecuzione del codice è il Gas. Ogni istruzione nel bytecode della EVM ha un costo di unità di Gas. Quando viene inserito uno Smart Contract nella Blockchain si deve inserire sempre il limite di Gas che il nostro codice potrà consumare per il suo funzionamento, questo permette anche di mantenere un limite al costo della transazione. Senza di esso, il codice assurdo come il seguente prosciugherebbe qualsiasi Wallet se fosse accettato:

function SetData(string calldata key) external {
    uint i=0;
    while (true) {
        i++;
    }
}
    

Di seguito quando mostrerò come effettivamente si scrive un Contract e lo si inserisce in una Blockchain di test, si vedrà il consumo di Gas per ogni operazione eseguita permettendo anche una ottimizzazione del suo consumo. Ma inserito in una rete non di test dove effettivamente si paga, oltre al Gas che sarà utilizzato si dovrà fare anche il conto del prezzo di ogni singola unità di Gas: al momento dell'inserimento della transazione reale si potrà decidere anche il costo massimo che si è disposti a pagare per questa singola unità, più è alto e prima esso sarà eseguito e inserito nella Blockchain, ovviamente. Più sarà basso e più tempo si dovrà attendere perché la transazione venga eseguita (la transazione potrebbe essere pure scartata se il prezzo inserito è troppo basso).

Dalla teoria alla pratica

Dopo tanto cianciare è il momento di passare ai fatti. Qui di seguito mostrerò gli strumenti che personalmente trovo i più semplici e produttivi (ne ho solo trovati due ad essere sincero). Il più famoso è sicuramente Truffle. Di base scarica il compilatore e altri tool che dovranno essere utilizzati per compilare il proprio codice di esempio in Solidity e quasi tutte le operazioni devono essere eseguite da terminale. Personalmente all'inizio non mi è piaciuto questo approccio ed ho preferito il più semplice e funzionale IDE Remix che si trova qui:

https://remix.ethereum.org/:

Da solo questo strumento via Web permette di scrivere i propri script, compilarli, inserirli nella Blockchain Ehereum di test già funzionante, testarli e pure fare il debug, senza installare alcunché. Anche se funziona all'interno del browser è possibile collegare l'editor al proprio file system locale grazie a Remixd. Una volta installato con npm:

npm install i -g @remix-project/remixd

E' sufficiente andare nella directory dove si vuole salvere i propri file e avviare remixd:

remixd -s .

Quindi dalla schermata di Remix qui sopra, selezionare il dropdownlist dove è presente la voce default_workspace e selezionare - connect to localhost -. Nella schermata successiva è sufficiente accettare e andare avanti per trovarsi nell'editor i file dal proprio file system. Per maggiori info consiglio la lettura del link precedente.

Il prossimo passo è opzionale se ci si ferma ai test degli Smart Contract con Solidity, ma visto che voglio poter accedere a questo mondo anche da C# consiglio l'aggiunta di un altro strumento: Ganache. Questa applicazione con interfaccia grafica crea una rete di test per Ethereum e per gli Smart Contract a cui Remix si può collegare. Funziona sia con Windows che con Linux (ho provato solo queste due piattaforme) e una volta avviato presenta questa schermata:

Vengono creati inoltre dieci Wallet su cui fare i test (io li ho limitati a quattro per mia comodità), inoltre permette il controllo dei blocchi creati così le transazioni. Per collegarlo con Remix basta prendere l'url dell'RPC server visibile nella schermata - http://127.0.0.1:7545 - e andando in Remix nella toolbar a sinistra, selezionare Deploy & run transaction e dal dropdownlist dell'Environment selezionare Web3 Provider e nella nuova finestra inserire l'url preso da Ganache:

Cliccato su Ok, ora Remix mostrerà i Wallet di Ganache ed ogni operazione nella Blockchain sarà eseguita in Ganache.

Provo il tutto

Ora posso inserire il contract di test che ho mostrato prima in Remix:

Che sarà compilato in automatico (altrimenti andare nella toolbar a sinistra e selezionare Solidity Compiler) quindi andare nel tool Deploy & run transaction:

Nella sezione di sinistra si può selezionare il Wallet desiderato e pure il Gas Limit per l'esecuzione della transazione (come spiegato prima). Inoltre si può inserire la valuta che si vuole inserire nello Smart Contract: l'esempio in questo caso non la utilizza quindi è possibile lasciarla a zero. Cliccando su Deploy si inserisce effettivamente lo Smart Contract nella Blockchain di Test di Ganache:

A destra i dettagli della transazione creata per l'inserimento dello Smart Contract (è stato consumato un quantitativo di 329,824 unità di Gas, al costo di 0.00659648 ETH). A sinistra è possibile inserire il messaggio accanto al pulsante Send Message in arancione e per avere i messaggi finora inseriti si può cliccare su GetMessages (si ricorderà sono i nomi delle due funzioni). Il colore del pulsante arancio mostra che quella funzione sarà eseguita in una transazione, quindi con un costo. Il pulsante blu indica che richiamare quella funzione non implica alcun costo. Se controllo ora Ganache trovo il blocco creato per la mia transazione:

E la transazione:

In TX DATA è presente il byte code del mio Smart Contract. Ci sarebbe da scrivere anche su questo argomento e sull'assembly creato dalla compilazione del codice scritto in Solidity, ma tralascio per non aggiungere altra confusione. Piuttosto, come ho calcolato il costo dell'inserimento di questa transazione? Prima di tutto l'Ether è l'unità di misura di base, ma sono presenti anche:

  • Wei = 1,000,000,000,000,000,000
  • Kwei= 1,000,000,000,000,000
  • Mwei = 1,000,000,000,000
  • Gwei = 1,000,000,000
  • Szabo = 1,000,000
  • Finney = 1,000
  • Ether = 1
  • Kether = 0.001
  • Mether = 0.000001
  • Gether = 0.000000001
  • Tether = 0.000000000001

L'unità di misura per il Gas è il Gwei. In valore di default in Ganache per una unità di gas è 20,000,000,000 Gwei (come si può vedere nella schermata di default di Ganache). Quindi ho moltiplicato questo valore per il Gas utilizzato per la creazione dello Smart Contract:

20,000,000,000 x 329,824 = 6,596,480,000,000,000 = 0.00659648 ETH

Al cambio attuale sono 16 euro. Ma su questo punto tornerò tra poco.

Dapp: ora tocca al C#

La definizione di Dapp è: applicazione, o parte di essa, che interagisce con la Blockchain. Quindi la Dapp potrebbe essere una web application che mostra nel browser delle informazioni salvate nella Blockchain, così come una windows application che inserisce dati su di essa, etc...

Ed è giunto quindi il momento, finalmente, di utilizzare la Blockchain direttamente da C#! Era ora. Subito la buona notizia: grazie alla libreria Nethereum il tutto si risolve con poche righe di codice. Ora il mio obbiettivo e richiamare il contratto appena creato per avere la lista dei messaggi inseriti. Innanzitutto è sufficiente creare un nuovo progetto e aggiungere la reference al pacchetto:

        Nethereum.Web3

Prima prova: avere il balance di uno dei miei Wallet inseriti in Ganache. Scrivo:

var url = "http://127.0.0.1:7545"; // <- rpc url from ganache
string address = "0x2214fD8cADBbE3edE5B95a37A88951b6Fa1D5d58"; // < - public address from ganache
var web3 = new Web3(url); // RPC
var balance = await web3.Eth.GetBalance.SendRequestAsync(address);
Console.WriteLine($"Balance in Wei: {balance.Value}");

var etherAmount = Web3.Convert.FromWei(balance.Value);
Console.WriteLine($"Balance in Ether: {etherAmount}");

Se si prova in locale è bene ricordarsi di modificare l'address di uno degli utenti creati da Ganache. L'output:

Balance in Wei: 99993403520000000000
Balance in Ether: 99.99340352

Ora provo a richiamare il metodo GetMessages dallo Smart Contract creato precedentemente. Per poterlo fare mi mancano due informazioni: il Contract Address e l'ABI (Application Binary Interface) dello Smart Contract. Il Contract Address è visibile nell'interfaccia di Remix quando viene creato, in Ganache è visibile nella transazione che ha inserito lo Smart Contract nella Blockchain, visibile anche nell'immagine qui sopra. Nel mio caso:

0xB130778f573164B8ffb8dB230A0fbCF329600530

L'Application Binary Interface è un JSON contenente le informazioni riguardanti per le funzioni pubbliche dello Smart Contract con i relativi parametri in input e output. Questa informazione la si può prendere dall'interfaccia di Remix nel tab del SolidityCompiler:

Il cui contenuto:

[
  {
    "inputs": [],
    "name": "GetMessages",
    "outputs": [
      {
        "internalType": "string[]",
        "name": "",
        "type": "string[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "message",
        "type": "string"
      }
    ],
    "name": "SendMessage",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Ho tutto e scrivo il seguente codice in C#:

internal static async Task GetMessagesAsync()
{
    var url = "http://127.0.0.1:7545"; // <- rpc url from ganache
    var web3 = new Web3(url);
    var myContractAddress = "0xB130778f573164B8ffb8dB230A0fbCF329600530"; // <- Contract Address
    string abi = GetAbi();
    var contract = web3.Eth.GetContract(abi, myContractAddress);

    var getMessageFunction = contract.GetFunction("GetMessages");
    var messages = await getMessageFunction.CallAsync<List<string>>();

    foreach (var msgObj in messages)
    {
        Console.WriteLine($"{msgObj}");
    }
}

private static string GetAbi()
{
    return File.ReadAllText(Path.Combine("ABI","GuestbookBlog.json"));
}

Da Remix o preventivamente inserito alcuni messaggi di test:

Eseguito il codice in C#:

Solidity permette anche la creazione di oggetti più complessi grazie alle Struct. Se oltre al messaggio di testo nel mio esempio avessi voluto inserire anche un counter di inserimento, avrei dovuto scrivere:

struct Message {
    uint Counter;
    string Message;
}

Message[] _messages;

function SendMessage(string memory message) external {
    _messages.push(Message(_messages.length + 1, message));
}

function GetMessages() external view returns(Message[] memory) {
    return _messages;
}

Il cui ABI:

[
  {
    "inputs": [],
    "name": "GetMessages",
    "outputs": [
      {
        "components": [
          {
            "internalType": "uint256",
            "name": "Counter",
            "type": "uint256"
          },
          {
            "internalType": "string",
            "name": "Message",
            "type": "string"
          }
        ],
        "internalType": "struct TestEvent.Message[]",
        "name": "",
        "type": "tuple[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "message",
        "type": "string"
      }
    ],
    "name": "SendMessage",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Ora da C# avrei dovuto creare anche una classe d'appoggio per i messaggi:

[FunctionOutput]
internal class MessageDTO : IFunctionOutputDTO
{
    [Parameter("uint", "Counter", 1)]
    public virtual int Counter { get; set; }

    [Parameter("string", "Message", 2)]
    public virtual string Message { get; set; } = null!;
}

Notare l'uso degli attributi per il mapping con la struct del Contract. Di seguito il codice per visualizzare i messaggi:

var getMessageFunction = contract.GetFunction("GetMessages");
var messages = await getMessageFunction.CallAsync<List<MessageDTO>>();

foreach (var msgObj in messages)
{
    Console.WriteLine($"{msgObj.Counter}: {msgObj.Message}");
}

E ora giunto il momento di inviare i messaggi, o per meglio dire, richiamare funzioni dello Smart Contract che necessitano di una transazione (e di conseguenza, anche di un esborso) per la loro esecuzione:

internal static async Task WriteMessageAsync()
{
    var url = "http://127.0.0.1:7545"; // <- rpc url from ganache
    var privateKey = "c19a8e62f637ccc3868eba2af75b6745a24dadecc01a31817a26485b79bf4225"; // <- from ganache
    var fromAddress = "0xaB7bE0c83f740176778F1833924Dd4D93ff125F8";
    var account = new Account(privateKey, new BigInteger(12345));
    var web3 = new Web3(account, url);
    web3.TransactionManager.UseLegacyAsDefault = true;
    var myContractAddress = "0x824f5FA5d310a6610762029338D973B17e011880";
    string abi = GetAbi();
    var contract = web3.Eth.GetContract(abi, myContractAddress);

    var setMessageFunction = contract.GetFunction("SendMessage");
    var gas = new HexBigInteger(1000000);
    var valueToTransfer = new HexBigInteger(0);
    await setMessageFunction.SendTransactionAndWaitForReceiptAsync(fromAddress, gas, valueToTransfer, null, "ciao 1");
}

Oltre al ContractAddress ho dovuto inserire anche l'Address dell'utente che richiama il metodo e la sua Private Key. Infine nel metodo SendTransactionAndWaitForReceiptAsync ho dovuto anche specificare il massimo quantitativo di Gas cui sono disposto a spendere e l'eventuale inserimento di Gwei all'interno dello Smart Contract (in questo caso ho lasciato zero). Ovviamente tirando in ballo la Private Key è possibile fare di tutto all'interno della Blockchain: nella documentazione ufficiale di Nethereum è disponibile anche il codice per inserire direttamente da C# il codice di un nuovo Smart Contract. Ma è un approccio corretto? Nel post precedente ho rimarcato più volte della pericolosità che lo smarrimento o la pubblicazione della Private Key può avere: chiunque ne entra in possesso diventa il padrone del Wallet a cui è legata con l'eventuale valuta inserita. Con gli Smart Contract la cosa aggiunge altra pericolosità. Anche se di questo parlerò nel prossimo post, è possibile definire delle role per l'esecuzione dei vari metodi legati all'owner del contratto. Questi metodi sono solitamente quelli che permettono il trasferimento di valuta memorizzata all'interno dello Smart Contract verso un Address esterno. Conseguenza diretta nell'avere la Private Key è poter entrare negli Smart Contract creati con questa chiave e poter fare quasi tutto quello che si vuole. Inoltre l'inserimento della Private Key all'interno del codice, anche se esso può preso da un secret service, potrebbe essere rubata da eventuali attacchi di hacker. Ancora peggio, un bug non scoperto nel momento dello sviluppo e di test, potrebbe avviare transazioni non volute che porterebbero al prosciugamento del Wallet collegato. Parere strettamente personale: qualsiasi operazioni che coinvolge operazioni onerose dovrebbero essere sempre fatte manualmente con un client che permette l'operazione dopo una richiesta in merito, e senza dovere inserire la Private Key del proprio Wallet. La realizzazione di questo lo mostrerò nel prossimo post.

Eventi

Negli Smart Contract scritti in Solidity è possibile inserire gli eventi. Piccola modifica al codice visto finora:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract TestContractEvent {
    string[] private _messages;
    event MessageInserted(uint counter, string message);

    function SendMessage(string calldata message) external {
        _messages.push(message);
        emit MessageInserted(_messages.length + 1, message);
    }

    function GetMessages() external view returns(string[] memory) {
        return _messages;
    }
}

Ho definito la signature dell'evento con due parametri (uint e string), quindi nel metodo per l'inserimento del messaggio richiamo questo evento con l'emit. L'utilità? Innanzitutto gli eventi sono salvati anch'essi all'interno della Blockchain ma non sono accessibili agli Smart Contract. Essi sono utili per strimenti esterni come potrebbe essere del nostro codice in C# che rimane in attesa di questi eventi. Innanzitutto aggiungo i due package necessari:

  • Nethereum.JsonRpc.WebSocketClient
  • Nethereum.RPC.Reactive

Creo la classe Event nel quale saranno inserite le informazioni provenienti dall'Event nella Blockchain:

[Event("MessageInserted")]
public class MyTestEvent : IEventDTO
{
    [Parameter("uint", "counter", 1)]
    public ulong Counter { get; set; }

    [Parameter("string", "message", 2)]
    public string? Message { get; set; }
}

Ed ecco il codice in C# che fa il subscribe degli eventi proveniente da quello Smart Contract:

Console.WriteLine($"Subscribe to event...");
var web3 = new Web3("http://127.0.0.1:7545");
using (var client = new StreamingWebSocketClient("ws://127.0.0.1:7545")) //wss://...
{
    var subscription = new EthLogsObservableSubscription(client);
    subscription.GetSubscriptionDataResponsesAsObservable()
                 .Subscribe(log =>
                 {
                     try
                     {
                         EventLog<MyTestEvent> decoded = Event<MyTestEvent>.DecodeEvent(log);
                         Console.WriteLine($"{decoded.Event.Counter}: {decoded.Event.Message ?? "-"}");
                     }
                     catch (Exception ex)
                     {
                         Console.WriteLine("Log Address: " + log.Address + " is not a standard transfer log:", ex.Message);
                     }
                 });

    var transferEventHandler = web3.Eth.GetEvent<MyTestEvent>("0xA8b38117d02c9F320752AFE97B9CC2181Ff85001");
    var filter = transferEventHandler.CreateFilterInput();

    await client.StartAsync();
    subscription.GetSubscribeResponseAsObservable().Subscribe(id => Console.WriteLine($"Subscribed with id: {id}"));

    await subscription.SubscribeAsync(filter);
    Console.ReadLine();

    await subscription.UnsubscribeAsync();
}

Oltre all'url utilizzato per l'oggetto Web3 è stato utilizzato un nuovo url per lo stream degli eventi: ws://127.0.0.1:7545, che solitamente è uguale all'url per l'RPC della blockchain ma con il protocollo Web Socket. Il codice seguente crea la subscription specificando inoltre come filtro l'address del mio Smart Contract. Quindi rimane in attesa dei messaggi dalla Blockchain fino alla pressione del tasto Enter. Se non si fosse specificato il filtro questo codice avrebbe ricevuto tutti gli eventi della Blockchain.

Ma quanto mi costi?

Finora ho solo fatto esempi in locale, in una Blockchain di test. Quando un giorno decidessi di postare il mio Smart Contact sulla mainnet di Ehereum che sorprese potrei avere? Come ho scritto sopra ogni operazione sulla Blockchain da parte deli Smart Contract consuma Gas. Riprendendo l'esempio iniziale, la sua creazione ha consumato 329.824 Gas Unit come è visibile dal log di Remix. L'inserimento di un messaggio come "Hello World!" ha consumato 66.847 Gas Unit. Ora non rimane che scoprire quanto in realtà costa questo Gas. Qui la risposta:

E facendo i giusti calcoli e conversioni:

50 x 329.824 = 16,491,200 Gwei
50 x 66.847 = 3,342,350 Gwei

16,491,200 Gwei = 0,0164012 ETH = ¤37,32
3,342,350 Gwei = 0,00334235 ETH = ¤7,60

E questo solo per un banale Guestbook. In ogni caso per Smart Contract con elaborazioni pesanti e memorizzazione di dati, queste cifre salgono notevolmente, ecco perché è necessaria l'ottimizzazione curata e l'uso ponderato degli oggetti di tipo storage o memory. Se in una funzione è necessario passare un oggetto di tipo storage è sconsigliato definire nella signature il parametro di tipo memory, perché questo comporterebbe una copia dell'oggetto che consuma Gas. I parametri nelle funzioni external è consigliato definirli come tipo calldata, perché se fossero di tipo memory provocherebbe la copia del contenuto e conseguente consumo di Gas inutile (i parametri calldata sono già memorizzati e sono passati per reference con notevole risparmio di Gas). Quando si esegue un ciclo for per la ricerca di elementi in una lista, è meglio valutare l'uso del mapping (dictionary) in caso sia possibile, oppure uscire dal ciclo for immediatamente quando si è trovato l'elemento cercato, e così via...

Ma come se la cavano altre Blockchain a riguardo il costo? Solana, al momento, ha il costo medio di transazione di $0,00025. Rimane la speranza che con l'avvento a Giugno di Ethereum 2 si avrà un calo del costo di transazione e di Gas. Ma non si ancora nulla di certo. E arrivati fin qui il pessimismo impera.

Conclusioni con opinioni personali

Personalmente trovo il mondo degli Smart Contract molto interessante. E' il futuro? Sicuramente, se le grosse aziende che hanno tutto il traffico e il guadagno, tutti i servizi di cloud, qualunque fornitore di spazio web e servizi attuali decidessero che è ora di mollare l'osso e perdere tutto il guadagno che hanno attualmente. Oppure, come è il presente del Bitcoin per il mining, la maggioranza dei server che attualmente creano i blocchi per quella criptovaluta sono mega web farm di società che hanno investito milioni di dollari - mi è già stato detto che dimostro troppo disprezzo verso il Proof-of-Work... Ma va? In quanto tempo società ben organizzate riuscirebbero a buttare fuori qualsiasi amatore dotato di buona volontà, ma con pochi mezzi finanziari e tecnici, se fiutassero l'affare deli Smart Contract?

Lentamente si potrebbe andare verso un mondo ibrido tra il Web 2 attuale e il Web 3. Forse i grandi servizi di cloud potrebbero fornire servizi per la Blockchain come fa AWS o come Azure fino a quando ci ha ripensato. In poche parole come sarà il futuro? Non capisco nemmeno che tempo farà tra dieci minuti guardando fuori dalla finestra, figurarsi un cambiamento così radicale nelle abitudini e nella struttura di Internet. A parte le mie visioni pessimistiche ci si potrebbe chiedere: quali sono attualmente i servizi che si utilizzano abitualmente che sarebbero papabili per il passaggio alla Blockchain e sfruttabili con gli Smart Contract che non siano gli NFT o la pura speculazione dietro alla criptovalua? Forse è proprio l'esempio degli NFT mostra come la Blockchain potrà essere utilizzata per tutta una nuova serie di servizi su Internet.

Basta, mi fermo qui. Nel prossimo post tratterò la funzionalità payable degli Smart Contract e come l'interfacciarsi via Web alla Blockchain (di Ethereum) è facile e consente di evitare le problematiche prima menzionate riguardanti la cessione e l'utilizzo di criptovaluta all'interno degli Smart Contract, e ovviamente non mancheranno critiche e pericoli che si celano dietro di essi.

Ecco il link per il codice mostrato in questo post.

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Nella stessa categoria
I più letti del mese