Blockchain in C# - scimmiottare il Bitcoin

di Andrea Zani, in Blockchain,

Un po' di teoria

Sinceramente al mio primo approccio con il mondo delle Blockchain e cryptovalute l'ostacolo più grande fu il comprendere che cosa fosse l'entità Bitcoin in sé. Prima che questo post possa venire frainteso, questo mio interesse fu prettamente tecnico e non speculativo, e sia in questo post che nei prossimi non darò nessuna dritta o spiegherò nessun trucco per diventare milionari - anche perché se li sapessi, di certo, non li scriverei in un blog né tantomeno li venderei a boccaloni alla ricerca di facile fortuna.

Innanzitutto non si deve pensare che la Blockchain e il Bitcoin (o le innumerevoli altre cryptovalute) siano la stessa cosa. Bitcoin si basa sulla Blockchain, e tale tecnologia, anzi, per meglio dire, questo insieme di tecnologie della blockchain si basano su questi punti:

  • Decentralizzazione: distribuzione delle informazioni su più nodi.
  • Tracciabilità: ogni voce registrata nella Blockchain è tracciabile e si può risalire ad ogni dato inserito.
  • Disintermediazione: esclusione di intermediari.
  • Verificabilità e trasparenza: tutto è visibile in chiaro e verificabile.
  • Immutabilità: una volta inserito un dato nella Blockchain NON può essere modificato.
  • Programmabilità: possibilità di programmare determinate azioni (Smart Contract).

Prima di andare nel dettaglio della Blockchain, che è quella che mi interessa, ritorno al mio quesito iniziale su come viene rappresentato una cryptovaluta, il Bitcoin nel mio caso. Inoltre, come poteva essere un dato digitale univoco e assegnato ad una sola persona visto che qualsiasi sequenza di byte può essere copiato e condiviso? La risposta fu semplice quando capii che lo scopo della Blockchain era mantenere la tracciabilità delle transazioni eseguite, quindi il possesso di un Bitcoin non è altro una transazione in cui viene memorizzato, in modo definitivo, che l'entità A ha data all'entità B un Bitcoin. Leggendo tutti le transazioni e i blocchi della Blockchain risulterà che l'utente B possiede un Bitcoin.

La tecnologia Blockchain è nata grazie proprio al Bitcoin e al suo fantomatico creatore Satoshi Nakamoto. In sé il Bitcoin, nel momento ufficiale della sua nascita - 2009 -  è riuscita a fare coesistere alla perfezione idee che in passato avevano avuto altre cryptovalute, come B-money nel 1998, Bit Gold che ebbe l'idea della Secure Digital Transaction, e DigiCash per la crittografia, e non ultima Hashcash per l'utilizzo delle Hash per la sicurezza nella metà egli anni '90.

A livello strutturale la Blockchain è in verità semplice: è una lunga sequenza di blocchi concatenati, senza ramificazioni e con semplici regole: i blocchi possono essere solo aggiunti, i blocchi non possono essere modificati, i blocchi non possono essere cancellati (anche se in passato si è dovuto andare contro a queste regole per la scoperta di pericolosi bug - qui e qui). Inoltre, ogni blocco ha riferimenti al blocco precedente per evitare qualsiasi modifica. Prima di vedere come vengono creati questi blocchi e perché attira moltissimi alla diffusa pratica del mining, è il momento di capire cosa c'è in ogni blocco.

Il contenuto, o per meglio dire, la sezione del blocco contenente le informazioni importanti - Transaction - vengono sottoposte ad Hashing (nel caso del Bitcoin viene usata una doppia operazione di Hash256). Di base, nella sezione delle Transaction sono presenti tre tipologie di operazioni:

  • Coinbase: transazione di criptovaluta che sarà assegnato al miner che ha creato il blocco.
  • transazione: transazione di criptovalute tra un portafoglio ad un altro portafoglio.
  • Fee: piccola percentuale che va ancora ai minatori.

La tipica transazione di criptovaluta si basa sulle due entità coinvolte, di cui una cede un determinato quantitativo ad un'altra entità. L'identificativo di queste due entità è detto Address. L'Address in sé è una stringa alfanumerica la cui creazione - i dettagli tecnici li mostrerò a breve - è l'ultimo step nell'operazione di creazione di un proprio Wallet. Questa operazione si basa sulla classica accoppiata di chiave pubblica e chiave privata. Semplificando il tutto si ottengono questi tre dati:

Private key -> Public key -> Address

Queste operazioni sono ovviamente a senso unico (da un Address non sarà possibile risalire alla Private Key). Per la creazione di questo Wallet ci si può affidare a tool appositi - come Exodus per Bitcoin o MetaMask per Ethereum - che offrono anche aiuti per un eventuale recupero della Private Key in caso venisse persa o dimenticata. Il mezzo più semplice utilizzato è dato dalla creazione di una sequenza di parole (di solito una dozzina) che permettono di recuperare la Private Key perché questa è creata proprio dalla combinazione di quelle parole. Per non correre rischi di perdere la propria Private Key è sufficiente stampare l'elenco delle parole e metterle al sicuro!? oppure utilizzare hardware apposito (penne USB create per questo scopo).

Importante dettaglio che l'Address/Wallet non è nominativo: chiunque venisse in possesso della chiave privata avrebbe pieno potere sul contenuto. Come scritto all'inizio di questo post, la Blockchain fornisce le informazioni di possesso di criptovaluta grazie alle transazioni, nel quale da un Address viene ceduta valuta ad un altro Address. La conseguenza è semplice: entrare in possesso della chiave privata ci rende anche i proprietari di qualsiasi valuta assegnata all'Address collegato. Naturale conseguenza, anche perdere una chiave privata rende impossibile recuperare qualsiasi valuta al suo interno. Qualcuno ha calcolato che sono 4.000.000 i Bitcoin persi per sempre per questo motivo, quantitativo notevole sapendo che il limite massimo di Bitcoin che potranno essere creati è 21.000.000.

Il passaggio di valuta da un Address ad un altro avviene grazie alle transazioni. I due metodi più utilizzati per registrare queste informazioni è l'UTXO (Unspent Transaction Output) per il Bitcoin e l'Account/Balance Model usato, per esempio, da Ethereum. Di seguito descriverò solo l'UTXO che si basa su un concetto molto semplice che è quello che si utilizza abitualmente con soldi reali che ognuno di noi ha nel portafoglio. Se vogliamo cedere ad un altro utente cinque euro, prendiamo una banconota di quella valuta e la cediamo al destinatario. Ma se nel portafoglio c'è disponibile solo una banconota da dieci euro, ecco che la passiamo al destinatatio che di conseguenza ci restituirà la differenza.

Così avviene anche con i Bitcoin. Ogni quantitativo di Bitcoin in nostro possesso è figlia di una transazione. Se, ripetendo l'esempio qui sopra, volessi passare cinque Bitcoin ad un altro utente, dovrei prima controllare quali banconote ho nel mio Wallet. Ipotizzando di aver ricevuto una transazione da dieci Bitcoin precedentemente, a livello tecnico io inserirò nella transazione i dieci Bitcoin i quali saranno divisi: cinque saranno assegnati al destinatario e cinque ritorneranno a me. A livello tecnico la mia transazione sarà simile a questa:

TransactionId 0x000001a
Input TransactionId Precedente (10 Bitcoin)
Output Address Destinatario 5 Bitcoin
Output Address Mittente 5 Bitcoin

A sua volta, se il destinatario volesse girare 2 Bitcoin ad un terzo:

TransactionId 0x000001b 
Input 0x000001a (5 Bitcoin, transazione precedente)
Output Address Destinatario Terzo 2 Bitcoin
Output Address Destinatario 3 Bitcoin

Questo comporta che se la cessione fosse maggiore a qualsiasi transazioni ricevuta, io dovrei includere in input il numero di transazioni necessarie. Dovendo dare 15 Bitcoin ad un utente:

TransactionId 0x000001c
Input 0x000003 (3 Bitcoin)
Input 0x000004 (3 Bitcoin)
Input 0x000005 (3 Bitcoin)
Input 0x000006 (1 Bitcoin)
Input 0x00001a (Row 2) (5 Bitcoin)
Output Address Destinatario (15 Bitcoin)

Si può notare che nell'ultima transazione in input ho aggiunto anche il numero di riga della transazione 0x00001a perché, in questo caso, la prima riga era la cessione di Bitcoin ad un altro utente, mentre la seconda riga erano i cinque Bitcoin di resto che avevo ricevuto.

Questa metodologia di implementazione ha permesso di evitare fin da subito uno dei più gravi problemi per la gestione della moneta digitale, il double-spending, cioè l'utilizzo della stessa moneta per più transazioni. Dovendo utilizzare obbligatoriamente l'output di una transazione precedente qualsiasi controllo farebbe nascere immediatamente una qualsiasi anomalia di cessione di valuta già ceduta o non posseduta.

A livello implementativo la cosa è un po' più complessa ovviamente, perché nel caso di Bitcoin il corretto funzionamento dell'UTXO è garantito dal Bitcoin Script, un semplice linguaggio di programmazione che descrive la transazione. Senza andare nel dettaglio sono essenzialmente due i tipi di script più utilizzati in Bitcoin: P2PKH (pay to public key) e P2PSH (pay to hashkey). La più interessante che prenderò in considerazione a breve, con una blanda imitazione, è la pay to public key, che si basa sulla signature della transazione (grazie alla chiave privata) e alla condivisione della chiave pubblica per verificare che non ha avuto alterazioni. Infatti, alla fine della creazione, le transazioni sono inviate alla rete della Blochchain (mempool) per essere validate e inserite nel prossimo blocco. La signature permette questa sicurezza aggiuntiva visto che, grazie anche alla presenza della chiave pubblica ed al controllo eseguibile da chiunque, le informazioni presenti siano corrette.

Ma chi verifica e chi inserisce questi blocchi nella Blockchain?

Un passo indietro: la Blockchain può essere vista come un libro mastro (ledger) dove vengono registrate e informazioni delle transazioni in modo indelebile - ad oggi la dimensione della Blockchain di Bitcoin è di circa 392 GB). A livello tecnico, questo libro mastro, viene replicato in tutti i nodi (macchine) che partecipano alla Blockchain, e chiunque ne può fare parte. Lo fanno gratis perché generosi e altruisti? Assolutamente no, ovviamente la partecipazione alla Blockchain ha sì l'onere di condividere questo libro mastro, ma partecipando alla validazione e creazione di questi blocchi si avrà come ricompensa criptovaluta. Qui si entra nel mondo del mining. Il suo scopo è prendere dalla Mempool (dove vengono memorizzate tutte le richieste di transazione) le singole transazioni e creare un blocco, verificando anche che ogni transazione sia valida e scartarla in caso non lo fosse (cessione di valuta che, in verità, non si possiede come il double-spending prima citato).

Quando un Miner ha a disposizione un blocco lo invia agli altri nodi della rete perché questo blocco sia a sua volta validato, e quando la maggioranza dei nodi ha accettato il blocco, esso viene inserito in coda alla Blockchain e si ricomincia la creazione di un nuovo blocco. Se la cosa è così semplice, perché i Miner comprano decine di schede grafiche per minare e si accusa Bitcoin del consumo di energia elettrica superiore di certi Stati sovrani?

La colpa è data dalla metodologia scelta per la creazione/validazione/conferma dei blocchi: il Proof-of-Work. Anzi, per meglio definirla si tratta di un algoritormo di consenso (consensus algorithms). Il Proof-of-Work è stato il primo adottato nelle Blockchain e inventato insieme al Bitcoin, ma con il tempo sono state introdotte altre, come il Proof-of-Stake, il Proof-of-Burn, il Proof-of-Authority, il Proof-of-Weight, etc... Anche se vedrò nel dettaglio poi il Proof-of-Work, questo si basa sulla risoluzione di un problema possibile grazie solo alla potenza di calcolo dei computer coinvolti. Banalmente si tratta di calcolare un Hash per il blocco in questione che abbia dei requisiti ben precisi. Il secondo algoritmo più utilizzato è il Proof-of-Stake, che elimina la necessità della potenza di calcolo dei nodi coinvolti, e le macchine sono scelte casualmente per la creazione e la validazione dei blocchi (rimando a pagine con maggiori dettagli, per esempio qui e qui visto che non tratterò questo algoritmo in questo post).

Chiunque può essere un Miner nella Proof-of-Work di Bitcoin, ma ormai è diventato antieconomico ed è diventato pressoché impossibile battere nella creazione e validazione dei blocchi le mega farm di macchine presenti nel mondo. Infatti ormai tutti i Miner minori si sono spostati su altre valute come Ethereum fino a quando sarà possibile farlo visto che è prevista a breve la migrazione di questa cryptovaluta all'algoritmo a Proof-of-Stake. Anche allora si potrà essere un Miner, ma per poterlo fare si dovrà disporre di una base di 32 Ethereum (al cambio attuale circa $80.000) per poter inserire una propria macchina tra i valdatori della Blockchain (e ottenerne anche i relativi guadagni) e seguire le ferree regole per non essere buttati fuori o vedersi prosciugare il capitale investito.

Ritornando al Proof-of-Work di Bitcoin, come scritto prima, grande importanza nella validazione dei blocchi e per la loro sicurezza è l'Hashing del contenuto. Lo scopo del Miner è quello di eseguire l'Hash del blocco fino a quando viene trovato un valore inferiore a quello deciso. Per spiegare semplicemente queste cosa, l'Hash256 crea una sequenza di byte non prevedibile, unica e consistente. Per esempio, la parola ciao crea l'Hash256:

b133a0c0e9bee3be20163d2ad31d6248db292aa6dcb1ee087a2aa50e0fc75ae2

E tale parola darà sempre questo valore, che è un numero scritto in esadecimale che può avere un range di valori da 0 a 2^256. Ai miner viene dato questo semplice compito: trova un Hash che rappresenti un valore inferiore al seguente:

0x00000000ffff0000000000000000000000000000000000000000000000000000

Il valore qui sopra non è un caso, perché è stato il primo Target Value della storia del Bitcoin. Per ottenere un valore esadecimale con l'Hash256 con la parola sopra - ciao - aggiungo un codice numerico incrementale fino a quando trovo il valore voluto. Scrivo questo codice in C#:

const string msg = "ciao";
using SHA256 sha256Hash = SHA256.Create();
Console.WriteLine($"{msg}: {sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(msg)).ToHex()}");
for (uint i = 0; i < 4_294_967_295; i++)
{
    string text = $"{msg} {i}";
    byte[] bytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(text));

    if (bytes.ToHex().StartsWith("00000000"))
    {
        Console.WriteLine($"{text} = {bytes.ToHex()}");
        break;
    }
}

public static string ToHex(this byte[] data) => string.Concat(data.Select(x => x.ToString("x2")));

Ed ecco il risultato:

ciao 1669172161 = 00000000bb11cbdca1317d25ab5824dab0386cdc6ff06cd6dddab851f519a6c2

Ottenuto in circa 50 minuti su una CPU non proprio recente usando un solo Thread. Per trovare l'Hash desiderato ha dovuto compiere 1 miliardo e 600 milioni di cicli. Ovviamente il blocco in una Blockchain ha un'altra struttura ed ha un campo apposito - Nonce - per questo scopo, e se non bastasse (questo campo è un numero 2^32) è possibile aggiungere informazioni in altri campi. In questo sito è possibile vedere la difficoltà richiesta per la generazione del blocco in Bitcoin e la potenza attuale di calcolo degli Hash.

La domanda conseguente potrebbe essere del perché dell'uso di questa "difficoltà" per la generazione dell'Hash. Il motivo è semplice: si era deciso che ogni blocco fosse disponibile ogni 10 minuti e per ottenere queste tempistiche dipendentemente dalle macchine collegate per il mining si è deciso di inserire questa difficoltà in modo da rimanere sempre in quella tempistica. La difficoltà attuale al momento della scrittura di questo post è 26,690,525,287,405 (preso dal sito il cui url ho postato nel paragrafo sopra). Questo valore viene calcolato con il valore di Target iniziale (visto prima nell'esempio della creazione in C# dell'Hash) e il valore preso nella colonna Bits, in questo caso 0x170a8bb4. Per essere convertito in un numero esadecimale a 256 bit, si deve prendere il primo byte - 0x17 = 23 - che indica la dimensione in byte del numero, mentre 0a8bb4 è la prima parte del numero. Semplificando: scrivo un numero di 23 byte contenenti zero:

0000000000000000000000000000000000000000000000

0a8bb4 è composto da 3 byte che sostituiranno i primi 3 byte:

0a8bb40000000000000000000000000000000000000000

Quindi lo trasformo in un numero a 256 bit aggiungendo gli zero iniziali in modo che sia composto alla fine da 32 byte:

0000000000000000000a8bb40000000000000000000000000000000000000000

Questo è il valore di Target attuale e i miner dovranno ottenere con l'Hash un numero inferiore a questo (ci vorrebbero anni se utilizzassi il codice in C# mostrato prima con le giuste modifiche). La difficoltà si calcola dividendo il Target iniziale con questo valore. Io mi sono scritto questo semplice programma in C# per calcolarlo:

using System;
using System.Linq;
using System.Numerics;

var target = "00000000FFFF0000000000000000000000000000000000000000000000000000".StringToByteArray();
var current_value = "0000000000000000000a8bb40000000000000000000000000000000000000000".StringToByteArray();
target = target.Reverse().ToArray();
current_target = current_target.Reverse().ToArray();

BigInteger t = new BigInteger(target);
BigInteger c = new BigInteger(current_target);
BigInteger value = t / c;
Console.WriteLine($"{value:#,##0}");
...
public static byte[] StringToByteArray(this string hex) => Enumerable.Range(0, hex.Length)
                     .Where(t => t % 2 == 0)
                     .Select(t => Convert.ToByte(hex.Substring(t, 2), 16))
                     .ToArray();

Che visualizza la difficoltà 26,690,525,287,405 come da quella pagina. La colonna Average Hashrate mostra la potenza di calcolo di tutte le macchine connesse, nel mio caso 191.04 EH/s, cioè 191 hexa hash al secondo.

Ogni 14 giorni (2016 blocchi) viene ricalcolata la difficoltà controllando le tempistiche dei 14 giorni passati. La ricompensa per i minatori sono i coinbase visti prima e inclusi nella lista delle transazioni; altra voce di transazione e il Fee che andrà anch'essa ai Miner (la differenza è che i coinbase sono nuova valuta, mentre i Fee sono detratti da tutte le transazioni incluse nel blocco). E quanta nuova valuta è donata al Miner con il Coinbase? Nel 2009 erano 50 Bitcoin. Quindi ogni 210.000 blocchi (all'incirca ogni 4 anni) tale valore viene dimezzato, attualmente il valore è 6.25 e si andrà avanti così fino alla creazione di 21 milioni di Bitcoin, cosa che si concluderà nel 2140.

Questo sistema di generazione blocchi ha dei limiti dovuti sia alla capacità dei Miner di creare blocchi sia alla grandezza massima degli stessi. Attualmente la dimensione di un blocco non può essere più grande di un megabyte e, data questa dimensione, è possibile stipare una media di 3.000 transazioni. Quando le transazioni in attesa superano questo limite, il Miner seleziona quelle con maggiore Fee per il proprio tornaconto; questo è possibile perché nell'inserimento della transazione è possibile risalire la coda della Mempool grazie all'assegnazione di un maggiore Fee ai Miner. Ma questo Fee è obbligatorio? In realtà no. Nei primi anni il Miner veniva pagato solo con i Bitcoin inclusi nella Coinbase, ma con il tempo, e con una maggiore richiesta di transazioni, si è deciso di permettere questo ulteriore premio ai Miner che ha permesso  di velocizzare certe transazioni con un alto Fee. Se si fa un rapido calcolo, infatti, la capacità di creare transazioni per il Bitcoin è molto bassa, si parla di circa 6/7 transazioni al secondo quando i circuiti di carte di credito ne reggono migliaia. Questo può già rispondere ad un primo dubbio: potrà il Bitcoin sostituire i metodi di pagamento attuali? Ovviamente no, se non si vuole attendere trenta minuti alla cassa di un supermercato aspettando che un Miner inserisca la mia transazione per la spesa nel prossimo blocco nella Blockchain. Per questo motivo, nel momento dell'inserimento di una transazione è buona regola controllare sempre il Fee che le sarà assegnato, perché potrebbe portare ad esborsi molto alti in percentuale alla valuta immessa, e questa regola vale per qualsiasi criptovaluta.  Ma su questo scriverò ancora alla fine di questo post.

A questo punto dell'evoluzione della Blockchain molte altre informazioni si possono basare su di essa oltre le crytpovalute. La base si scambio su di essa si può basare sui Token, che è una base nominale assegnabile a qualsiasi cosa e, ovviamente, identificabile e assegnabile sempre grazie ad una transazione. I Token si possono suddividere in due categorie:

  • Fungible Token
  • Non fungible Token

Per Fungible Token si può definire qualsiasi moneta o oggetto di scambio equiparabile ad essa. Se io ricervo 5 Bitcoin, li cedo ad un terzo, e ne ricevo altri 5, io sarò sempre in possesso di quel quantitativo di Token, perché come i soldi reali in cui una banconota da 10 euro vale sempre quel valore e posso scambiarla con un'altra banconota simile senza che ci siano variazioni di valore.

Per i Non Fungible Token basta usare le iniziali per capire immediatamente il loro scopo visto che sono diventati molto di moda: NFT. In questo caso il Token rappresenta qualcosa di unico come può essere una qualsiasi opera digitale, sia che essa sia l'immagine di un gatto volante o fotografie che conosciamo tutti usati nei meme, oppure attrezzature e vestiario da utilizzare nei videogiochi etc... E cedibili a chiunque dietro una normale compravendita con criptovaluta.

Lascio a questo post un profilo tecnico, perché interessante è anche la storia del Bitcoin e dell'evoluzione del mondo delle criptovalute. Ma io non sarei la persona adatta per parlarne perché ho solo un'infarinatura sull'argomento. In rete sono disponibili moltissimi documenti in merito e anche nel mondo editoriale ci sono dei libri ben fatti sull'argomento - uno di essi da cui ho tratto informazioni lo metterò come riferimento in coda a questo post.

Dimenticavo: ci sarebbe, prima di andare alla pratica, l'ultima domanda che molti si pongono a riguardo del Bitcoin: ma da cosa è dovuto il suo valore? Pura speculazione indotta dalla domanda e dall'offerta?

Scimmiottare il Bitcoin

Dopo questa lunga spiegazione (incomprensibile, lo so) sulla teoria di base del Bitcoin, perché non scimmiottarne il comportamento creando una piccola Blockchain, in C# naturalmente, in modo molto più semplice e didattico?

Come scritto sopra, la parte importante è la gestione del Wallet che si basa sull'Address e sulla coppia chiave pubblica e chiave privata. Prendo come riferimento questa documentazione che ne spiega il funzionamento con la versione 1 della gestione degli Address in Bitcoin. Innanzitutto è necessario lo stesso algoritmo per la creazione delle chiavi usate da Bitcoin: secp256k1, che usa le curve ellittiche per la gestione della crittografia a chiave pubblica. Non è presente di default in Net Core, ma è possibile utilizzarla con la libreria BouncyCastle. Creo quindi un progetto in Net6 e aggiungo tale libreria. Quindi prendo la chiave private da quella pagina e seguo la documentazione per ricreare lo stesso esempio:

PublicKey("18e14a7b6a307f426a94f8114701e7c8e774e7f9a47e2c2035db29a206321725".StringToByteArray());

Che richiama la mia funzione:

var curve = SecNamedCurves.GetByName("secp256k1");
var domain = new ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H);

var d = new BigInteger(privateKey);
ECPoint ec = domain.G.Multiply(d).Normalize();
Console.WriteLine($"X cord: {ec.XCoord.ToBigInteger().ToByteArrayUnsigned().ToHex()}");

var publicKeyXInt = ec.XCoord.ToBigInteger();
var publicKeyYInt = ec.YCoord.ToBigInteger();
var publicKeyTemp = publicKeyXInt.ToByteArrayUnsigned();
var publicKey = new byte[publicKeyTemp.Length + 1];
publicKey[0] = (byte)(publicKeyYInt.LongValue % 2 == 0 ? 0x02: 0x03);
for (int i = 0; i < publicKeyTemp.Length; i++)
{
    publicKey[i + 1] = publicKeyTemp[i];
}

In questa prima parte del codice ottengo la chiave pubblica estraendo le coordinate X e Y in cui userò la X per la chiave pubblica e controllerò se in Y è presente un valore pari o dispari per inserire il coretto valore (0x2 o 0x3) come primo byte della chiave pubblica. Quindi ho tutto e posso creare l'Address:

var hashPublicKey = publicKey.HashBytes();
Console.WriteLine($"Hashpubkey: {hashPublicKey.ToHex()}");

var ripemd160 = hashPublicKey.Ripemd160();
Console.WriteLine($"Ripemd-160: {ripemd160.ToHex()}");

var extendedRipemd160 = new byte[ripemd160.Length + 1];
extendedRipemd160[0] = 0x00;
for (int i = 0; i < ripemd160.Length; i++)
{
    extendedRipemd160[i + 1] = ripemd160[i];
}

Console.WriteLine($"Ripemd-ext: {extendedRipemd160.ToHex()}");

var hashExtended = extendedRipemd160.HashBytes();
Console.WriteLine($"Exten hash: {hashExtended.ToHex()}");

var doubleHashExtended = hashExtended.HashBytes();
Console.WriteLine($"doublehash: {doubleHashExtended.ToHex()}");

var extendedChecksum = new byte[extendedRipemd160.Length + 4];
for (int i = 0; i < extendedRipemd160.Length; i++)
{
    extendedChecksum[i] = extendedRipemd160[i];
}

for (int i = 0; i < 4; i++)
{
    extendedChecksum[extendedRipemd160.Length + i] = doubleHashExtended[i];
}

Console.WriteLine($"   Address: {extendedChecksum.ToHex()}");

string address = Base58.Bitcoin.Encode(extendedChecksum);
Console.WriteLine($"    Base58: {address}");

Per controllarne l'esecuzione ho messo molte istruzioni per la visualizzazione parziale dell'elaborazione che, una volta avviato, darà:

Hashpubkey: 0b7c28c9b7290c98d7438e70b3d3f7c848fbd7d1dc194ff83f4f7cc9b1378e98
Ripemd-160: f54a5851e9372b87810a8e60cdd2e7cfd80b6e31
Ripemd-ext: 00f54a5851e9372b87810a8e60cdd2e7cfd80b6e31
Exten hash: ad3c854da227c7e99c4abfad4ea41d71311160df2e415e713318c70d67c6b41c
doublehash: c7f18fe8fcbed6396741e58ad259b5cb16b7fd7f041904147ba1dcffabf747fd
   Address: 00f54a5851e9372b87810a8e60cdd2e7cfd80b6e31c7f18fe8
    Base58: 1PMycacnJaSqwwJqjawXBErnLsZ7RkXUAs

Ok, tutto corrisponde. E' la posso usare anche per firmare e per verificarla poi con solo la chiave pubblica:

string message = "ciao";
var curve = SecNamedCurves.GetByName("secp256k1");
var domain = new ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H);

var keyParameters = new ECPrivateKeyParameters(new BigInteger(privateKey), domain);

ISigner signer = SignerUtilities.GetSigner("SHA-256withECDSA");
signer.Init(true, keyParameters);
signer.BlockUpdate(Encoding.ASCII.GetBytes(message), 0, message.Length);
var signature = signer.GenerateSignature();
Console.WriteLine($"Signature: {signature.ToHex()}");

Che visualizzerà:

Signature: 3046022100f46b0ac22c976aefa70d57b6b650d24a4aa92d761d045fbce657ed26576c607402210094397d7c7e0e2f0bb99beff1574e1df877ded3f90abaed073c6db6c9c61f7c70

E il codice che verifica questa firma:

var curve = SecNamedCurves.GetByName("secp256k1");
var domain = new ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H);

var q = curve.Curve.DecodePoint(publicKeyBytes);

var keyParameters = new ECPublicKeyParameters(q, domain);

ISigner signer = SignerUtilities.GetSigner("SHA-256withECDSA");
signer.Init(false, keyParameters);
signer.BlockUpdate(Encoding.ASCII.GetBytes(message), 0, message.Length);
Console.Writeline(signer.VerifySignature(signature));

Che visualizzerà un banale True.

Per i miei scopi voglio che sia il mio codice a creare una chiave privata randomica, ed è possibile con questo codice:

var curve = ECNamedCurveTable.GetByName("secp256k1");
var domainParams = new ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H, curve.GetSeed());

var secureRandom = new SecureRandom();
var keyParams = new ECKeyGenerationParameters(domainParams, secureRandom);

var generator = new ECKeyPairGenerator("ECDSA");
generator.Init(keyParams);
var keyPair = generator.GenerateKeyPair();

var privateKey = keyPair.Private as ECPrivateKeyParameters;
var publicKey = keyPair.Public as ECPublicKeyParameters;

var privateKeyBytes = privateKey.D.ToByteArray();
var publicKeyBytes = publicKey.Q.GetEncoded();

Ora posso finalmente iniziare a creare la mia Blockchain. Primo passo creo quattro Address casuali che utilizzerò nelle mie transazioni. Nel mio codice di esempio, come primo passo dopo la creazione, visualizzo queste informazioni a schermo:

==========================================================================================
                                     Create users...
==========================================================================================
Address         1N1vALdSS9wxxbP7QxCbnsyumKfkLGUR6L
Private Key     1b5a0d95f8ae1033638fdc83f74f96b2ef4ae3c8119fd56c84b9810dba0bf369
Public Key      04a956cefe0183cd73816be95ea229f9e6a8..28ccbfba9daf139f089967c87def385e9457

Address         1izABsicCxuneFzcXkMG4ffgTa1CVjixz
Private Key     0086963416f9d3bb86c6f6c8c148d258fe9a615d61679efb2b68c46b9df8705498
Public Key      040a7da95c68e975bba1b2c989004198ae38..7c356c6eadc417bfc15a687c0caf3cb36c60

Address         1PZua6og4wUgNKLjc6ZqBD6ueQMfFyYi9Z
Private Key     5ef1201b0110459eb15faa3328cf1c78ecd6e905dd69efc2dbe80365fcda61dc
Public Key      04dd1ea49ddd3fe7640c91f0eeddc2344665..34f0437feef728adb3305523e335532053e8

Address         1BW1NqmFcLdT2AmB5oxkoEA13UKJdbHASk
Private Key     00fe6d7f3c531f99d7f37c965d30a9fd3e6bcc5acb33b8e023398416ca40d5fe2a
Public Key      04943da2f23586df5b3a726d82fbdfd9d652..1b4c95c9e5e954976a53f9b3797d3d1d711f

Visibili l'Address calcolato come sopra, quindi le due chiavi (privata e pubblica visualizzate in formato ridotto solo per mia comodità). A questo punto i quattro utenti non hanno nessuna valuta da scambiarsi, ma, come spiegato sopra, per crearla è sufficiente che qualcuno crei il primo blocco (genesis block) con il mining (in questo caso il primo utente):

// Create genesis block. 100 to user 1
{
    var block = users[0].MinerInstance.CreateBlock(BlockChain.Heigth, DIFFICULTY, BlockChain.LastHash, null);
    if (block is not null)
    {
        try
        {
            BlockChain.VerifyAndInsert(block, MAXCOINBASE);
            ConsoleHelper.ShowInfoBlock(block, "Create genesis block. 100 to user 1");
            ConsoleHelper.ShowAmountForUsers(users);
        }
        catch (BlockChainVerifyException e)
        {
            Console.WriteLine(e.Message);
        }
    }
}

Che eseguito visualizzerà le informazioni sul blocco e sulle transazioni:

==========================================================================================
                       Block 0 - Create genesis block. 100 to user 1
==========================================================================================
Hash                      0000b44924626758199028ebf68851eb7f44a0e936abbb6de108b0647ae14f0f
Time                      2022-02-22 19:53:48
Nonce                     149992
Bits                      0x1effffff
Hash previous block
Hash Merkle root          20b7615e1a662d22358a3f2d980c18593326a6dc13cba41b496233f139a33acd
Time to mining            1117
------------------------------------------------------------------------------------------
                                       Transactions
------------------------------------------------------------------------------------------
Numero                    1
1 - Txid                  20b7615e1a662d22358a3f2d980c18593326a6dc13cba41b496233f139a33acd
1 - Public Key
1 - Signature
Input
-

Output
Amount                    100
Recipient Address         1N1vALdSS9wxxbP7QxCbnsyumKfkLGUR6L
------------------------------------------------------------------------------------------

Show amount for users
1 1N1vALdSS9wxxbP7QxCbnsyumKfkLGUR6L = 100
2 1izABsicCxuneFzcXkMG4ffgTa1CVjixz = 0
3 1PZua6og4wUgNKLjc6ZqBD6ueQMfFyYi9Z = 0
4 1BW1NqmFcLdT2AmB5oxkoEA13UKJdbHASk = 0

C'è solo una transazione che è quella di Coinbase che deposita il valore di 100 "monete" al minatore. Alla fine riassumo anche il quantitativo di monete possedute da tutti gli User, e infatti l'utente numero 1 possiede il quantitativo corretto. Ora è possibile fare sul serio con transazioni da quell'User ad altri:

// Add block with transaction signed: 15 da user 0 to user 1
{
    var block = users[0].MinerInstance.CreateBlock(BlockChain.Heigth, DIFFICULTY, BlockChain.LastHash, new[]
    {
        TransactionHelper.CreateTransaction(users[0].PrivateKey, users[0].PublicKey, users[0].Address, users[1].Address, 15),
    });

    if (block is not null)
    {
        try
        {
            BlockChain.VerifyAndInsert(block, MAXCOINBASE);
            ConsoleHelper.ShowInfoBlock(block, "Add block with transaction signed: 15 da user 0 to user 1");
            ConsoleHelper.ShowAmountForUsers(users);
        }
        catch (BlockChainVerifyException e)
        {
            Console.WriteLine(e.Message);
        }
    }
}

Ora l'User numero 1 ha ceduto 15 monete all'User 2 ed essendo ancora lui il minatore, ha ottenuto ulteriori 100 monete:

==========================================================================================
           Block 1 - Add block with transaction signed: 15 da user 0 to user 1
==========================================================================================
Hash                      0000da905b5757c519adc7b105e4c13fe41cc92b974d5caafc9c96772a4b7fbd
Time                      2022-02-22 19:57:57
Nonce                     91879
Bits                      0x1effffff
Hash previous block       0000b44924626758199028ebf68851eb7f44a0e936abbb6de108b0647ae14f0f
Hash Merkle root          f8007493fc06a0df278eb17508656e7a0e9c874974f4c221f76e306b7222f011
Time to mining            481
------------------------------------------------------------------------------------------
                                       Transactions
------------------------------------------------------------------------------------------
Numero                    2
1 - Txid                  3f218cdbba17c2aa3496d536cdac427a2c30ea7bafcb4a16315f38616c779078
1 - Public Key            04a956cefe0183cd73816be95ea229f..fba9daf139f089967c87def385e9457
1 - Signature             304502210097e0f0cd3fcffda1284ae..82d09094eca52507216f4f1fceb0e6b
Input
Position                  0
Txid                      20b7615e1a662d22358a3f2d980c18593326a6dc13cba41b496233f139a33acd

Output
Amount                    15
Recipient Address         1izABsicCxuneFzcXkMG4ffgTa1CVjixz
Amount                    85
Recipient Address         1N1vALdSS9wxxbP7QxCbnsyumKfkLGUR6L
------------------------------------------------------------------------------------------
2 - Txid                  2bff48dc21b835da397c1b885c9179422f08375538fb9d27c766c26dfd89f095
2 - Public Key
2 - Signature
Input
-

Output
Amount                    100
Recipient Address         1N1vALdSS9wxxbP7QxCbnsyumKfkLGUR6L
------------------------------------------------------------------------------------------

Show amount for users
1 1N1vALdSS9wxxbP7QxCbnsyumKfkLGUR6L = 185
2 1izABsicCxuneFzcXkMG4ffgTa1CVjixz = 15
3 1PZua6og4wUgNKLjc6ZqBD6ueQMfFyYi9Z = 0
4 1BW1NqmFcLdT2AmB5oxkoEA13UKJdbHASk = 0

Ora sono presenti due transazioni (l'identificativo di ogni transazione è il Txid, che è l'Hash del contenuto della transazione). Nella prima in Input ora è possibile vedere una voce con il Txid uguale al valore della transazione creata con il primo blocco alla posizione zero (c'è solo un'operazione in output per quella transazione). E interessante è la sezione di Output, dove di può vedere che da quella transazione del primo blocco vengono date 15 monete all'Address dell'User numero 2 e, immediatamente, 85 vengono rimandati all'utente numero 1 (nella prima transazione, si ricorderà, la transazione era di 100 monete, e qualsiasi nuova transazione deve sempre coinvolgere il numero di monete pari alla singola riga in input della transazione precedente).

Il codice sorgente è possibile scaricarlo dal link a fine post, ed esegue altre transazioni simili a quelle già mostrate. Mi ripeto perché è importante, se si prende il codice di transazione del Block 1, 3f218cdbba17c2aa3496d536cdac427a2c30ea7bafcb4a16315f38616c779078 ho come input 100 monete dalla transazione precedente, e due di uscita:

Amount                    15
Recipient Address         1izABsicCxuneFzcXkMG4ffgTa1CVjixz
Amount                    85
Recipient Address         1N1vALdSS9wxxbP7QxCbnsyumKfkLGUR6L

Alla transazione del Block 2 ho:

Input
Position                  0
Txid                      3f218cdbba17c2aa3496d536cdac427a2c30ea7bafcb4a16315f38616c779078

Output
Amount                    5
Recipient Address         1PZua6og4wUgNKLjc6ZqBD6ueQMfFyYi9Z
Amount                    10
Recipient Address         1izABsicCxuneFzcXkMG4ffgTa1CVjixz

In input la transazione vista prima e in position ho il valore zero, cioè devo prendere il primo output precedente, che ha come valore in uscita di 15, che vengono suddivise ancora e la prima parte inviata al destinatario e la seconda viene ridata al mittente come resto. E la transazione precedente in posizione zero può essere definita come chiusa e non più utilizzabile. Non è così per l'output della seconda riga che ritrovo nel Block 4:

Input
Position                  1
Txid                      3f218cdbba17c2aa3496d536cdac427a2c30ea7bafcb4a16315f38616c779078

Output
Amount                    15
Recipient Address         1PZua6og4wUgNKLjc6ZqBD6ueQMfFyYi9Z
Amount                    70
Recipient Address         1N1vALdSS9wxxbP7QxCbnsyumKfkLGUR6L

La seconda riga di output aveva come quantitativo 85 monete, e in output, infatti sono suddivisi in due valori che fanno sempre come somma 85 e viene ripetuto il giro di cessione al destinatario e dato il resto al mittente. Ora la transazione vista precedente può definirsi chiusa definitivamente e non dev'essere più coinvolta in nessun altro blocco/transazione.

La funzione in C# vista prima: 

var block = users[0].MinerInstance.CreateBlock(BlockChain.Heigth, DIFFICULTY, BlockChain.LastHash, new[]
{
    TransactionHelper.CreateTransaction(users[1].PrivateKey, users[1].PublicKey, users[1].Address, users[2].Address, 5),
});

Richiama la funzione CreateBlock che richiede le transazioni utilizzabili dall'address del mittente e, senza fare ottimizzazioni varie, prende le prime disponibili per la creazione di una nuova transazione verso il destinatario simulando il P2PKH visto prima, inserendo la Signature con la Private Key del mittente sulla transazione. Quindi se non ci sono stati problemi nella creazione del blocco (viene eseguito solo un controllo sul quantitativo di moneta disponibile che dev'essere sufficiente per la transazione), viene richiamata la funzione:

BlockChain.VerifyAndInsert(block, MAXCOINBASE);

Qui vengono eseguite le operazioni di verifica di base per inserimento del blocco nella Blockchain, come il controllo della Signature grazie alla Public Key, quindi il controllo del quantitativo di moneta per il coinbase del mining che dev'essere uguale al valore numerico passato come parametro, quindi riverifica che il quantitativo di monete utilizzate per lo scambio sia disponibile. Solo per mia pigrizia ho inserito anche il controllo che un utente non può fare più di una transazione per blocco, ma è solo una mia aggiunta dettata, come scritto, dalla pigrizia implementativa perché nel mondo delle Blockchain è una cosa consentita.

Una volta che il blocco è creato viene eseguito l'Hashing. Innanzitutto, per una maggiore sicurezza il Bitcoin esegue un doppia operazione di Hash, e io scimmiotterò anche questa cosa nel mio codice. Innanzitutto ecco la definizione del blocco come classe in C#:

public class Block
{
    public Block(string minerAddress, long height, BlockHeader blockHeader, Transaction[] transactions)
    {
        ...
    }

    public string MinerAddress { get; init; }
    public long Height { get; init; }
    public byte[]? HashBlock { get; set; } = null;
    public BlockHeader BlockHeader { get; set; }
    public int TransactionCounter { get; init; }
    public Transaction[] Transactions { get; init; }
    public long TimeToMine { get; set; }
} set; }
}

Che cerca di imitare come struttura ancora il Bitcoin anche se ho aggiunto property custom, come TimeToMine in cui inserirò il tempo in millisecondi per la ricerca dell'Hash. Inoltre nella mia struttura ho inserito la property HashBlock in cui inserirò l'Hash calcolato, ma nella definizione del blocco nella documentazione non è presente. Questo perché quando un blocco è ricevuto e confermato dalla Blockchain, l'Hash viene calcolato da ogni singolo nodo della rete e salvato come metadata in un database separato.

Questo hash viene calcolato dal contenuto dall'oggetto 9;oggetto BlockHeader:

public class BlockHeader
{
    public BlockHeader(string bits, long time, uint nonce, byte[]? previousBlock)
    {
        Bits = bits;
        Time = time;
        Nonce = nonce;
        HashPrevBlock = previousBlock;
    }

    public byte[]? HashPrevBlock { get; init; }
    public byte[]? HashMerkleRoot { get; set; }
    public long Time { get; set; }
    public string Bits { get; init; }
    public uint Nonce { get; set; }
}

E' questa la struttura che sarà utilizzata per il doppio hashing. Notare l'HashMerkleRoot che è utilizzato per l'hashing di tutte le transazioni. Qui un link descrittivo di cosa si tratta, da cui prendo l'immagine che descrive l'operazione fatta:

L1...L4 sono le transazioni presenti all'interno del blocco a cui viene calcolato l'Hash, quindi risalendo l'albero come nell'immagine, vengono concatenati i risultato e rifatto l'Hash fino a giungere ad un unico valore di Hash che sarà inserito nella property prima vista. Io da codice:

public static void SetMerkleRoot(this Block block)
{
    ...
    var coll = block.Transactions.Where(t=>t.Txid is not null).Select(t => t.Txid!).ToList();
    while (coll.Count > 1)
    {
        List<byte[]> buffer = new();
        for (int i = 0; i < coll.Count; i += 2)
        {
            var item1 = coll[i];
            var item2 = coll[i + (i + 1 == coll.Count ? 0 : 1)];
            if (item1 is not null && item2 is not null)
            {
                using var ms = new MemoryStream();
                ms.Write(item1.HashBytes());
                ms.Write(item2.HashBytes());
                buffer.Add(ms.ToArray().HashBytes());
            }
        }

        coll = buffer;
    }

    block.BlockHeader.HashMerkleRoot = coll[0];
}

Ora ho tutto il necessario per calcolare l'Hash con la Difficulty prima spiegata. Io utilizzerò un valore molto facile per questione tempistiche, 0x1effffff bits:

0x0000ffffff000000000000000000000000000000000000000000000000000000

Con il codice:

public static void MineHash(this Block block, string difficulty)
{
    Stopwatch sw = new();
    bool found = false;
    sw.Start();

    var blockHeader = block.BlockHeader;

    while (true)
    {
        for (uint i = 0; i <= 4_294_967_295; i++)
        {
            // Set hash for the block
            blockHeader.Nonce = i;
            var hash = blockHeader.GetHashBlock();
            var hashString = hash.ToHex();
            if (hashString.StartsWith(difficulty))
            {
                found = true;
                block.HashBlock = hash;
                break;
            }
        }

        if (found) break;
        blockHeader.Time = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
    }

    sw.Stop();
    block.TimeToMine = sw.ElapsedMilliseconds;
}

Qui ho tutto molto semplificato, ma ora il codice dell'Hash sarà usato per collegare il blocco successivo e sarà usato anche al suo interno per cementificare il collegamento tra di essi. Con questo semplice stratagemma sarà difficile poter modificare qualsiasi campo all'interno dei blocchi perché comporterebbe un cambiamento a cascata di tutti i blocchi successivi.

In ogni caso il link del codice lo posterò a breve per chi volesse guardarne il funzionamento. Metto già la mani avanti affermando che il codice è molto semplice e manca di qualsiasi forma di ottimizzazione per ottenere maggiori prestazioni. Inoltre presenta dei limiti come ho già scritto, e i controlli sono minimali. Era anche mia idea creare una simulazione più decente per il consensus che avrei simulato con l'avvio di più processi per il calcolo in parallelo del Hash e conseguente distribuzione, ma il problema è sempre uno: il tempo libero è quello che è... Inoltre l'argomento è così vasto e ramificato (e la mia conoscenza base) che è meglio che mi fermo qui...

Qualche opinione personale per chiudere

Di seguito alcune note personali... PERSONALI! Io non posseggo verità assolute. Al Bitcoin va tutto il merito per la creazione e la diffusione della Blockchain. E' tuttora la criptovaluta più utilizzata e scambiata, ma se si osserva il dettaglio tecnico di com'è ora, molte altre criptovalute gli sono superiori. I limiti del Bitcoin sono sotto gli occhi di tutti. Come ho scritto sopra la velocità e il numero di transazioni che è in grado di elaborare (6-7 al secondo) sono completamente inutilizzabili in un contesto d'uso capillare. Si è pensato di migliorare questo aspetto e ci sono state proposte in merito, come aumentare la dimensione di ogni blocco oltre a un megabyte... quando saranno adottate? Inoltre il Proof-of-Work sta diventando sempre meno sostenibile per il consumo di risorse per tenere in piedi il tutto oltre a vari altri problemi. La seconda cryptovaluta più usata al mondo, Ethereum, abbandonerà a breve questo algoritmo sposando il Proof-of-Stake, con la versione di Ethereum 2 che dovrebbe avvenire a Giugno di quest'anno (ma la cosa potrebbe ancora essere ritardata). Inoltre Ethereum 2 passerà alla distribuzioni dei nodi e delle transazioni grazie allo Sharding, e si parla di una capacità di decine di migliaia di transazioni al secondo.

Lasciando perdere future promesse che nel mondo delle criptovalue equivalgono a quelle da marinaio, prendo come riferimento cryptovalute già esistenti come Solana, che fin da subito ha adottato il Proof-of-History e supporta attualmente fino a 60.000 transazioni al secondo. Inoltre, come Ethereum, permette l'uso avanzato degli Smart Contract che sono dei piccoli programmi in grado di girare e interagire direttamente con la Blockchain, base dell'incompreso Web 3.0.

Smart contract e Web3... E' un argomento interessante. E si può anche interagire con C# e .Net... Potrebbe essere l'argomento del prossimo post.

Qualche info utile:

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