Shopping Cart
0

Il codice di Bitcoin: spiegazione approfondita

Introduzione

Immagina di conoscere Bitcoin come un enorme libro mastro pubblico – questo probabilmente lo sai già molto bene – ma ora vogliamo sfogliare le sue pagine di codice per capire come funziona tutto “sotto il cofano”. In altre parole, passeremo dalla teoria alla pratica, esplorando il software di Bitcoin e i suoi componenti chiave. Bitcoin Core (il programma principale di Bitcoin) è scritto in linguaggio C++ per garantire efficienza e velocità, mentre alcuni strumenti e wallet, come Electrum, sono scritti in Python, più facili da leggere e modificare. Partiremo dalle basi degli indirizzi Bitcoin (e dei loro formati come Base58 e Bech32), poi vedremo come funzionano gli script di spesa (il linguaggio interno delle transazioni), parleremo di Miniscript (un modo semplificato per scrivere condizioni di spesa avanzate) e infine esploreremo come un wallet leggero come Electrum utilizza un approccio diverso (SPV) rispetto a un nodo completo. Il tutto verrà introdotto in modo graduale, utilizzando metafore e paragoni per rendere i concetti tecnici più comprensibili, quasi fosse un racconto. Preparati quindi a un viaggio “narrato” nel codice di Bitcoin, passo dopo passo!

Indirizzi Bitcoin: da Base58 a Bech32

Quando vedi o condividi un indirizzo Bitcoin – quella stringa di lettere e numeri che inizia tipicamente con 1, 3 o bc1 – stai maneggiando una forma codificata di informazioni binarie. In termini semplici, un indirizzo Bitcoin non è altro che la rappresentazione testuale di una chiave (o di uno script) pubblica, arricchita con qualche dato di controllo per evitare errori. Storicamente Bitcoin ha usato un formato di codifica chiamato Base58Check per gli indirizzi “legacy” (i primi formati), mentre più di recente è stato introdotto il formato Bech32 per gli indirizzi SegWit (le versioni più moderne). Vediamo cosa significa tutto ciò e perché esistono due formati diversi.

Base58Check e gli indirizzi legacy: Il formato Base58Check è stato scelto da Satoshi Nakamoto per rendere gli indirizzi più maneggevoli rispetto a una sequenza esadecimale pura. Come suggerisce il nome, Base58 utilizza un alfabeto di 58 simboli (cifre e lettere) invece dei 64 del Base64, escludendo caratteri che possono confondersi visivamente tra loro, come 0 (zero), O (O maiuscola), I (i maiuscola) e l (L minuscola) . Questo fa sì che se detti o copi un indirizzo a voce o su carta, è meno probabile confondere, ad esempio, uno zero con una O. Inoltre c’è l’aggiunta del “Check”: ogni indirizzo include un checksum (controllo di integrità) di 4 byte calcolato dalla doppia hash SHA-256 dei dati dell’indirizzo . In pratica, il software calcola questo checksum e lo accoda prima di codificare in Base58. Quando tu o un wallet inserite un indirizzo, il programma ricontrolla il checksum: se non corrisponde, significa che c’è un errore di battitura, e l’indirizzo viene dichiarato non valido. Questo meccanismo ha salvato molti utenti dall’inviare Bitcoin a indirizzi digitati male!

Come si genera in pratica un indirizzo in Base58Check? Ecco i passaggi in parole semplici: si parte dall’hash della chiave pubblica. Ad esempio, per un indirizzo classico P2PKH (“Pay to PubKey Hash”), si prende la chiave pubblica ECDSA, la si sottopone a SHA-256 e poi a RIPEMD-160 per ottenere un hash di 160 bit (il famoso pubkey hash). A questo hash il software antepone un byte di versione che indica la rete (per Bitcoin mainnet è 0x00, che identifica gli indirizzi che iniziano per 1) . A questo punto si calcola il checksum (4 byte) come descritto sopra e lo si aggiunge in coda. L’insieme di [versione || hash160 || checksum] – che in binario sono byte – viene infine codificato in Base58, cioè convertito in una stringa di caratteri Base58. Il risultato è l’indirizzo leggibile che conosciamo. Ad esempio, un indirizzo P2PKH potrebbe apparire come 1BoatSLRHtKNngkdXEeobR76b53LETtpyT (sto inventando). Tutti gli indirizzi che iniziano con “1” sono di questo tipo e infatti usano la codifica Base58 . Analogamente, gli indirizzi che iniziano con “3” sono anch’essi codificati in Base58 ma rappresentano un altro tipo (P2SH, “Pay to Script Hash”), con un diverso byte di versione (0x05) introdotto nel 2012 col BIP 13 . Quindi, sia 1… che 3… sono formati legacy Base58Check, solo che indicano tipi di destinatario diversi (chiave pubblica hash vs. script hash).

La codifica Base58Check, dal punto di vista del codice C++, è implementata con un algoritmo di conversione di base numerica. I dati binari vengono interpretati come un numero intero molto grande e poi convertiti in “base 58” attraverso divisioni successive, un po’ come si converte un numero decimale in formato binario o viceversa. Nel codice Bitcoin Core troviamo funzioni come EncodeBase58() e EncodeBase58Check() che eseguono proprio questo: effettuano divisioni per 58 e mappano i resti ai caratteri dell’alfabeto Base58 . Ogni byte 0x00 iniziale nei dati produce il carattere 1 iniziale nell’indirizzo (ecco perché l’identificativo di rete 0x00 genera indirizzi che cominciano per 1). In Python, lo stesso processo si può ottenere più concisamente usando gli interi arbitrariamente grandi: ad esempio convertendo il valore binario in un numero e poi facendo modulo e divisione per 58 iterativamente. Anche se i dettagli matematici precisi possono sembrare complessi, l’idea chiave è che Base58 fornisce un modo user-friendly di rappresentare un identificatore binario, evitando caratteri ambigui e includendo un controllo d’errore.

Limiti di Base58 e nascita di Bech32: Col passare degli anni, la comunità Bitcoin ha individuato alcuni miglioramenti possibili al formato degli indirizzi. In particolare, con l’introduzione di SegWit (le witness segregate, BIP-141) si è deciso di adottare un nuovo formato chiamato Bech32 (definito nel BIP-173 da Pieter Wuille e altri). Bech32 è pensato per essere ancora più amichevole per gli utenti e più efficiente . La differenza più visibile è che gli indirizzi Bech32 cominciano con bc1 (per Bitcoin mainnet) invece del classico 1 o 3, e sono tipicamente tutti in minuscolo. Infatti Bech32 usa un alfabeto “base32” composto solo da lettere minuscole (oltre alle cifre) e non distingue tra maiuscole e minuscole  . Questo significa che non c’è più il problema di specificare “maiuscola” o “minuscola” quando si legge un indirizzo al telefono, ad esempio – ogni carattere è univoco senza ambiguità di caso. È un sollievo pratico: “b” sarà sempre “bi” e non può essere scambiata per un 8 o per una maiuscola, perché quelle possibilità sono eliminate in partenza. In effetti, nel definire l’alfabeto Bech32 si sono rimossi quattro caratteri confondibili (tra cui il numero 1 e le lettere b, i, o) per ridurre al minimo gli errori di lettura  . Il risultato è un insieme di 32 simboli chiari.

Ma il vantaggio maggiore di Bech32 sta nel suo algoritmo di checksum avanzato. Gli indirizzi Bech32 incorporano un codice di controllo basato su un particolare schema di error-correcting code (chiamato BCH code). Questo checksum non solo rileva se c’è un errore nell’indirizzo, ma riesce perfino a individuare la posizione dell’errore e suggerire correzioni in molti casi  . In pratica, se sbagli di poco a copiare un indirizzo Bech32, alcuni wallet possono dirti “Ehi, controlla il quarto carattere, potrebbe essere diverso”, rendendo più facile correggere. È per questo che si dice che il checksum di Bech32 è “più intelligente” di quello precedente – non è solo un controllo pass/fail, ma aiuta attivamente a evitare pasticci.

Dal punto di vista della codifica, un indirizzo Bech32 è costruito in modo differente rispetto a Base58Check. Qui non usiamo più l’alfabeto a 58 simboli con maiuscole/minuscole, ma un alfabeto di 32 simboli case-insensitive. L’indirizzo è formato da una parte umanamente leggibile (HRP, human-readable part) seguita da 1 come separatore, e da una parte dati codificata in base32. Per Bitcoin mainnet la HRP è fissata a “bc” (da cui il prefisso iniziale bc1…), mentre ad esempio per la testnet è “tb”. Nella parte dati, Bech32 include la versione della witness (per SegWit v0 è 0, per Taproot/SegWit v1 è 1, ecc.) e il programma di locking script (per P2WPKH è l’hash di chiave pubblica di 20 byte, per P2WSH l’hash di script di 32 byte, e così via). Tutto questo viene convertito in serie di numeri a 5 bit e codificato con l’alfabeto da 32 caratteri. Infine, viene calcolato il checksum BCH su tutta la stringa e aggiunto in coda. Può sembrare complicato, ma chi utilizza un wallet non vede nulla di tutto ciò: vede solo un indirizzo che tipicamente è lungo 42 caratteri se è un segwit v0 (P2WPKH) oppure 62 caratteri se è Taproot (P2TR segwit v1), sempre iniziando per bc1  .

Bech32 porta con sé anche benefici di efficienza: essendo tutto minuscolo e senza caratteri speciali, gli indirizzi possono essere comodamente inseriti nei codici QR in modalità alfanumerica compatta, risultando in QR code leggermente più piccoli e veloci da scansionare  . Inoltre, dal punto di vista computazionale, calcolare il checksum Bech32 è più veloce che eseguire due volte SHA-256 come avviene per Base58Check . Anche la decodifica risulta più snella perché mappa direttamente i caratteri in valori, invece di dover effettuare calcoli di modulo su grandi numeri . Non che Base58 fosse lentissimo (parliamo di millisecondi), ma su larga scala e dispositivi a bassa potenza, ogni ottimizzazione aiuta.

In sintesi, gli indirizzi Bitcoin moderni (bech32, quelli che iniziano per bc1) sono stati un upgrade significativo rispetto ai vecchi indirizzi Base58: niente più confusioni di maiuscole/minuscole, identificazione rapida del tipo di indirizzo dal prefisso (bc1q indica P2WPKH segwit nativo, bc1p indica Taproot ecc.), maggiore resistenza agli errori e una leggera efficienza in più  . I tuoi Bitcoin però non “cambiano” in base al tipo di indirizzo: questi formati sono solo modi diversi di rappresentare gli script di blocco (locking script) sottostanti. Infatti, ancora oggi convivono più formati: se invii da un exchange potresti ricevere monete su un indirizzo legacy (1… o 3…) perché magari non supporta segwit nativo; altri wallet invece preferiscono sempre segwit nativo (bc1…). Fortunatamente, Bitcoin è retrocompatibile: un indirizzo P2PKH o P2SH funziona sempre, anche se meno efficiente, mentre per beneficiare delle commissioni minori di SegWit conviene usare Bech32. L’importante è capire che dietro ogni formato c’è sempre un hash (di una chiave pubblica o di uno script) e delle regole di spesa associate. E questo ci porta al prossimo argomento: il linguaggio di script e le condizioni di spesa.

Bitcoin Script e condizioni di spesa

Una transazione Bitcoin può essere vista come una cassaforte digitale che si può aprire solo soddisfacendo certe condizioni. Queste condizioni sono espresse tramite un piccolo linguaggio di programmazione chiamato Bitcoin Script. Se finora hai pensato agli output delle transazioni solo come “indirizzi che ricevono monete”, tecnicamente ogni output contiene uno script di blocco (locking script) che stabilisce i requisiti per poter spendere quei bitcoin in futuro . Quando qualcuno vuole spendere quel valore, deve fornire uno script di sblocco (unlocking script, storicamente parte dell’input, oggi spesso nella witness) che, concatenato allo script di blocco originale, soddisfi tutte le condizioni e sblocchi la cassaforte. In pratica, l’unione di script di sblocco + script di blocco forma un programma Script completo che viene eseguito dai nodi: se l’esecuzione termina con successo (cioè con un risultato True sullo stack e senza errori), allora le condizioni sono rispettate e la transazione è valida .

Bitcoin Script è un linguaggio semplice, di basso livello, simile al Forth, basato su stack e intenzionalmente non Turing-complete  . “Basato su stack” significa che opera mettendo e togliendo valori da una pila (stack) in memoria: ogni operazione (opcode) prende alcuni elementi dalla cima dello stack e ne aggiunge eventualmente altri come risultato. “Non Turing-complete” vuol dire che è deliberatamente limitato: non ha cicli o funzioni arbitrarie, il che impedisce di scrivere programmi infiniti o troppo complessi. Questo è voluto per ragioni di sicurezza e prevedibilità: i nodi devono poter verificare rapidamente uno script senza rischiare loop infiniti o elaborazioni esagerate. Pensalo come un lucchetto combinatorio con istruzioni finite: ogni script è una combinazione deterministica di mosse per aprire un lucchetto, non un programma che può fare qualunque cosa.

Vediamo l’esempio più comune di script, quello che regola la stragrande maggioranza delle transazioni fino ad oggi: P2PKH (Pay to PubKey Hash). In questo caso, lo script di blocco (locking script) di un output è generalmente:

OP_DUP OP_HASH160 <20-byte pubkey hash> OP_EQUALVERIFY OP_CHECKSIG

Se lo leggiamo a parole: “Duplica la chiave pubblica fornita (sarà nello script di sblocco), fai l’hash160 (SHA-256 + RIPEMD-160) di essa e confrontalo con il valore hash di 20 byte specificato nello script; verifica che siano uguali, poi verifica la firma crittografica.” Cosa deve fornire chi spende? Lo script di sblocco tipicamente contiene . Quindi quando il nodo verifica la transazione, costruisce lo stack e segue le istruzioni: prima mettiamo dallo script di sblocco, poi eseguiamo l’OP_DUP (duplica la chiave pubblica, ora abbiamo due copie sullo stack), OP_HASH160 prende la copia e la trasforma nel relativo hash160, poi c’è quel valore costante nello script (che è l’hash dell’indirizzo a cui sono stati inviati i fondi) messo sullo stack, OP_EQUALVERIFY confronta l’hash calcolato con quello atteso (fallisce se non combaciano, ad esempio se la chiave pubblica fornita non corrisponde all’indirizzo di destinazione), e infine OP_CHECKSIG verifica che la firma data sia valida per quella chiave pubblica e per questa transazione . Se tutto fila liscio, lo script termina con successo e la transazione può spendere l’output. In sintesi, la condizione di spesa per un P2PKH è: “dimostra di conoscere la chiave privata corrispondente a questo hash di chiave pubblica, fornendo una firma valida”. Questo garantisce che solo il legittimo proprietario (chi ha la private key) possa sbloccare i fondi.

Bitcoin Script però permette condizioni ben più complesse di “una firma e via”. Ad esempio, grazie a opcodes come OP_CHECKMULTISIG, è possibile richiedere multiple firme. Puoi avere uno script di blocco che dice: “servono almeno 2 firme su queste 3 chiavi per spendere” (questo è un classico 2-of-3 multisig). In Script sarebbe qualcosa tipo: 3 OP_CHECKMULTISIG (semplificando un po’), e lo script di sblocco dovrà fornire due firme corrispondenti a due di quelle tre chiavi. Il risultato è che nessuno da solo può spendere quei bitcoin: serve la collaborazione (cioè le firme) di almeno due parti su tre. Questa è la base dei portafogli multifirma e di certe soluzioni di custodia.

Ci sono anche operatori condizionali: Bitcoin Script, nelle versioni aggiornate post-SegWit, supporta OP_IF/OP_ELSE/OP_ENDIF per costruire logiche if-then-else basilari. Ciò significa che puoi avere uno script che prevede percorsi di spesa alternativi. Ad esempio: “se viene fornita la firma di Alice, ok, altrimenti devono esserci sia la firma di Bob che Carol” – questo potrebbe essere codificato con un OP_IF (controlla presenza di un elemento che potrebbe essere una firma valida di Alice) e in caso falso un OP_ELSE che verifica un’altra combinazione di firme. Un altro potente meccanismo è l’uso dei timelock: opcodes come OP_CHECKLOCKTIMEVERIFY e OP_CHECKSEQUENCEVERIFY permettono di richiedere che una certa transazione sia spendibile solo dopo un certo tempo (espresso in altezza di blocco o timestamp) oppure che tra creazione e spenditura intercorra un certo numero di blocchi. Ad esempio, potrei bloccare dei fondi con uno script che dice: “o firma Alice dopo il blocco X, oppure prima del blocco X servono firme sia di Alice che di Bob”. Ciò implementa un elemento di time-lock (dopo una certa data, basta Alice da sola, prima di allora serve anche Bob). Queste condizioni di tempo sono utili per costruire sicurezza (chiavi di backup che si attivano dopo un po’), eredità (sblocco dopo N anni), o contratti come gli HTLC (Hash Time-Locked Contracts) usati nei canali Lightning, dove c’è anche l’uso di hash lock: un’altra condizione possibile è “presenta il preimage (segreto) che corrisponde a questo hash SHA-256”, grazie all’opcode OP_HASH160/OP_EQUAL (o OP_SHA256/OP_EQUAL). In breve, Bitcoin Script è un mini kit di costruzione di “smart contract” semplici: hai firme, hash e timelock come mattoni fondamentali, e li puoi combinare con and/or/if per esprimere molte condizioni di spesa logiche.

Tuttavia, c’è un problema: scrivere e verificare mentalmente script complessi è difficile e soggetto a errori. Bitcoin Script è notoriamente poco intuitivo – è facile sbagliare l’ordine delle operazioni o la struttura logica e magari creare uno script che non fa esattamente quello che pensavi. Inoltre, fino a qualche anno fa non c’erano strumenti standard per comporre condizioni di spesa elaborate: ogni volta che si voleva fare qualcosa di particolare (multisig con clausole speciali, ecc.), bisognava sviluppare software ad-hoc e assicurarsi che tutti i partecipanti usassero lo stesso schema, altrimenti non si riuscivano a spendere i fondi. Ad esempio, esistevano wallet che supportavano solo 2-of-3 multisig statici e nulla di più – se tu creavi uno script diverso, quei wallet non lo “capivano” e non potevano aiutarti a spendere. Insomma, lo script c’era, ma l’ecosistema non lo sfruttava appieno perché mancavano astrazioni di alto livello. Ed è qui che entra in scena Miniscript.

Miniscript: script complessi resi semplici (come mattoncini Lego)

Verso la fine del 2019, due sviluppatori di Bitcoin di alto profilo, Pieter Wuille e Andrew Poelstra, hanno presentato Miniscript, un linguaggio intermedio pensato per rendere più accessibile e sicuro l’uso di Bitcoin Script complesso  . Miniscript in sostanza è un sottoinsieme di Bitcoin Script – ciò significa che ogni policy espressa in Miniscript può essere tradotta in uno script Bitcoin valido al 100%, senza bisogno di modifiche al protocollo (dunque si può usare subito, senza hard fork o soft fork per abilitarlo) . L’idea di Miniscript è di fornire una sintassi strutturata e leggibile per descrivere politiche di spesa avanzate, permettendo ai software di fare analisi automatiche su queste politiche e ai programmatori di combinarle come dei blocchi componibili.

Pensa a Miniscript come a un set di mattoncini LEGO con cui costruire il tuo lucchetto di condizioni . Invece di scrivere direttamente il complicato Script “in assembly” con OP_DUP, OP_IF, OP_CHECKSIG, ecc., puoi descrivere la tua condizione in modo quasi matematico o ad alto livello, e lasciare che un compiler la traduca nell’incastro ottimale di opcodes. Ad esempio, riprendiamo la situazione di prima: “Alice da sola può spendere, oppure se aspetti 144 blocchi allora può spendere Bob” – è uno scenario con due percorsi: o firma A subito, oppure firma B ma solo dopo un certo tempo (circa un giorno, dato 144 blocchi). In Script puro risultava una sequenza non ovvia di opcodes . Miniscript la esprime in maniera molto più chiara e concisa. Usando la sintassi testuale di Miniscript (che è come una funzione), potresti scriverla così:

or_d(c:pk(A), and_v(vc:pk_h(B), older(144)))

Non lasciarti spaventare dalla notazione criptica: leggiamola insieme. C’è un or_d(X, Y) che significa “o condizione X oppure condizione Y (d, meaning disjoint)”. La condizione X è c:pk(A), ovvero check la firma di A (il prefisso c: indica che dopo la firma va controllato che la condizione sia completa). La condizione Y è and_v( vc:pk_h(B), older(144) ). Questo significa “and concatenato in modo verificato di (firma di B corrispondente a un hash di chiave pubblica, vc sta per verify check) E (older(144), cioè un timelock di 144 blocchi)”. In pratica la Y richiede sia la firma di B sia che siano passati almeno 144 blocchi. L’or_d(X,Y) esterno combina le due: o X (A firma) oppure Y (B firma dopo attesa). Miniscript ci permette di leggere chiaramente l’intenzione: “per spendere, serve o la firma di A, oppure la firma di B ma solo dopo 144 blocchi”. Molto più trasparente rispetto al lungo script in opcode di prima, no? Inoltre Miniscript può automaticamente capire, ad esempio, che A è una chiave che se firma sblocca immediatamente, B è una chiave che però richiede un tempo, e così via. Può ottimizzare l’ordine degli opcodes per minimizzare la dimensione finale (ha routine che trovano la forma di script più compatta equivalente alla policy richiesta)  . E può segnalarti se certe combinazioni non sono sicure o possibili.

Una delle caratteristiche principali di Miniscript è proprio questa analizzabilità automatica. Dal Miniscript, un wallet o software può capire facilmente quali combinazioni di firme e condizioni saranno necessarie per spendere in ogni scenario . Può ad esempio dirti: “Ok, questo output potrà essere speso se ho la firma di A, oppure se ho la firma di B ma non prima di X tempo”. Questo elimina l’ambiguità e la necessità di programmare a mano casi speciali per ogni tipo di script. Con Miniscript, in teoria, wallet diversi potrebbero tutti supportare politiche arbitrarie: finché parlano la “stessa lingua” Miniscript, sanno generare l’indirizzo corrispondente e sanno creare la transazione di spesa quando le condizioni sono soddisfatte, senza dover prevedere quel caso specifico in anticipo. È un po’ come avere un linguaggio standard dei contratti: invece di dire “supporto solo multisig 2-di-3 e timelock fisso”, un wallet Miniscript-enabled potrebbe supportare ogni combinazione di AND/OR firme e locktime, perché riesce a interpretarla e gestirla genericamente.

Un altro vantaggio è la sicurezza: Miniscript è concepito in modo da evitare le trappole di Script. Ad esempio, Bitcoin Script crudo ha stranezze come il bug degli OP_CHECKMULTISIG che consumano un item extra dallo stack (il famoso bug del OP_CHECKMULTISIG che salta un elemento, per cui si mette un OP_0 in più nello script di sblocco). Miniscript astrae questi dettagli: se scrivi una policy, il codice generato sarà corretto, senza bisogno che tu sappia di questo bug o di come evitarlo. Inoltre Miniscript può fare analisi di sicurezza – ad esempio determinare se uno script è malleabile, o se c’è il rischio che qualcuno possa malleare una transazione non confermata e rubare fondi (un problema in passato con script personalizzati). Insomma, costruisce attorno a Bitcoin Script delle regole di buona condotta e automatizza il lavoro noioso e suscettibile di errore umano  .

Per utilizzare Miniscript oggi, esistono librerie e tool sviluppati dalla community. C’è una libreria in Rust molto completa (rust-miniscript) e anche implementazioni in C++ e Python. Ad esempio, Blockstream (l’azienda per cui lavorano Wuille e Poelstra) ha messo online un compilatore Miniscript: tu inserisci la policy che vuoi (in una sintassi di policy ancora più amichevole, tipo or( pk(A), and(pk(B), older(144)) ) con percentuali di probabilità se vuoi aiutare l’ottimizzazione) e lui ti sputa fuori lo script Miniscript ottimizzato  . Ci sono anche wallet come Liana (progetto di Antoine Poinsot) che usano Miniscript sotto al cofano per offrire funzionalità di eredità o custodia complessa, oppure il Bitcoin Dev Kit che permette a sviluppatori di giocare con Miniscript per creare wallet custom. È probabile che col tempo vedremo sempre più adozione di Miniscript nell’ecosistema, perché consente una flessibilità incredibile senza richiedere modifiche al protocollo Bitcoin. Come nota storica, vale la pena ricordare che Miniscript ha persino aiutato a migliorare cose come le transazioni Lightning: in test interni, script ottimizzati con Miniscript erano più efficienti di quelli specificati inizialmente nel protocollo Lightning (BOLT3) . Questo per dire che anche per chi progetta protocolli sopra Bitcoin, avere uno strumento per trovare lo script più snello che soddisfa una certa condizione è utilissimo.

In conclusione, se Bitcoin Script è un linguaggio di basso livello che assomiglia un po’ all’assemblatore (istruzioni macchina semplici e non molto intuitive), Miniscript è come un linguaggio di alto livello o un schema logico che permette di esprimere chiaramente cosa vuoi fare. Si tratta di un ottimo esempio di come l’ecosistema Bitcoin evolva non solo con nuove opcodes o funzionalità on-chain, ma anche con tool di sviluppo che rendono più facile usare al massimo ciò che c’è già. Miniscript in definitiva ti dà la possibilità di narrrare le condizioni di spesa in maniera comprensibile e far sì che il codice sottostante le realizzi in modo corretto ed efficiente.

Il wallet Electrum e la verifica SPV (Python vs C++)

Facciamo ora un piccolo cambio di prospettiva: passiamo dal codice di Bitcoin Core (il nodo completo in C++ che verifica ogni blocco e transazione) al mondo dei wallet Bitcoin leggeri, in particolare Electrum. Electrum è uno dei wallet “storici” di Bitcoin (nato nel 2011) ed è interessante perché il suo funzionamento è molto diverso da quello di un full node e perché il suo codice è scritto in Python, un linguaggio ad alto livello più facile da leggere per i non programmatori. Con Electrum possiamo vedere come certi compiti – come la gestione degli indirizzi, delle chiavi e delle transazioni – vengano svolti in Python, e confrontarli mentalmente con come li gestirebbe un nodo in C++.

Come funziona Electrum? Electrum adotta la filosofia di essere veloce e snello, delegando ai server remoti il grosso del lavoro pesante . Quando avvii Electrum, non deve scaricare l’intera blockchain (che è enorme, centinaia di gigabyte ormai), né verificare tutti i blocchi dall’inizio della rete. Invece, Electrum si collega ad un server Electrum (ci sono molti server pubblici, oppure puoi gestirne uno tuo per maggiore privacy) e gli chiede le informazioni che gli servono. Ad esempio: “Dammi la lista delle transazioni legate a questo indirizzo” o “Avvisami quando arrivano nuove transazioni per questi indirizzi”. Questo protocollo client-server è specifico di Electrum e altamente ottimizzato: i server mantengono indicizzati gli indirizzi, così possono rispondere rapidamente alle query del tipo “fammi sapere se l’indirizzo X appare in qualche transazione”. Il risultato è che l’avvio è immediato e l’uso richiede poche risorse, perché stai sfruttando la potenza e i dati di server sempre online .

Tutto ciò però senza cedere completamente il controllo: Electrum non consegna mai le tue chiavi private al server. Le firme delle transazioni vengono fatte localmente sul tuo dispositivo, proprio come farebbe Bitcoin Core: il wallet crea la transazione, poi la firma con le chiavi che conserva (criptate con la tua password) nel tuo computer o telefono . Questo significa che, anche se ti appoggi a un server per sapere il saldo o per inviare la transazione in rete, il server non può spendere i tuoi fondi, perché non possiede le tue chiavi (e la transazione gli arriva già firmata). In più, Electrum cifra il file wallet con una password (se impostata) e ti permette di fare backup deterministici: quando crei il wallet ti dà un seed mnemonico (una serie di 12 parole) con cui potrai rigenerare tutte le tue chiavi in caso di perdita del file originale. Nota che Electrum usa un suo formato di seed, diverso dallo standard BIP-39, ma comunque 12 parole inglesi che assicurano la rigenerabilità delle chiavi . Questo per dire che sul fronte sicurezza utente, Electrum è piuttosto attento: supporta anche multi-firma, integrazione con hardware wallet, e così via .

La magia dietro Electrum è il concetto di SPV (Simple Payment Verification), originariamente descritto nel whitepaper di Satoshi. SPV è un modo per verificare le transazioni senza dover verificare tutto il blockchain. Come fa? Invece di scaricare tutti i blocchi completi, un client SPV come Electrum scarica solo le intestazioni dei blocchi (block headers), che sono piccolissime (80 byte ciascuna). Le intestazioni includono, tra le altre cose, il Merkle root di tutte le transazioni contenute nel blocco e il riferimento al blocco precedente, nonché la prova di lavoro (nonce, target). Il client SPV si assicura di avere la catena di intestazioni più lunga (ossia con più lavoro accumulato) per fidarsi che sia la catena valida di Bitcoin. Ora, quando vuole verificare una transazione, non ha l’intero blocco, quindi chiede al server: “Ehi, mi mandi la Merkle path (ramificazione di Merkle) per questa transazione nel blocco tal dei tali?”. Il server gli fornisce l’elenco di hash necessari che, combinati con l’hash della transazione in questione, ricostruiscono il Merkle root del blocco. Siccome il client SPV ha l’intestazione e dentro c’è il Merkle root ufficiale firmato dalla proof-of-work, può confrontare: se il Merkle root calcolato combacia con quello nell’intestazione, significa che la transazione effettivamente era inclusa in quel blocco. E se quel blocco fa parte della catena di maggior lavoro, la transazione è confermata. Tutto questo senza aver mai scaricato l’intero blocco! In breve, SPV verifica che una transazione sia inclusa in un blocco valido, usando solo un percorso di Merkle e l’intestazione del blocco . Il server non può inventarsi un Merkle root falso perché il client verificherebbe che non coincide con quello “firmato” dalla potenza di calcolo. Dovrebbe letteralmente trovare un blocco con proof-of-work valido per ingannare il client su una transazione fasulla, cosa computazionalmente impraticabile. In questo senso l’SPV è trust-minimized: non devo fidarmi che il server mi dia blocchi validi, perché controllo la PoW, devo solo fidarmi che mi dia tutte le info giuste che chiedo.

Tuttavia, qui sta un punto cruciale: un client SPV deve fidarsi del server per la completezza delle informazioni. Cioè, se il server decide di omettere qualcosa, il client non ha modo di saperlo. Ad esempio, se qualcuno ti invia dei bitcoin e il server non ti avvisa (perché compromesso o malfunzionante), il client SPV rimane ignaro di quella transazione. In gergo si dice che “il server può mentire per omissione” . Inoltre, le transazioni non confermate (mempool) non possono essere verificate via SPV, perché non appartengono ancora a un blocco: su quelle il client deve fidarsi del server (questione mitigata dal fatto che importi minori non confermati sono meno critici, ma rimane un atto di fede verso il server) . C’è anche un tema di privacy: per sapere quali transazioni ti riguardano, devi rivelare al server i tuoi indirizzi o almeno degli hash dei tuoi script. Un singolo server, vedendo tutte le richieste correlate ai tuoi indirizzi, può dedurre che appartengono alla stessa persona . Electrum attenua un po’ questo rischio permettendo all’utente di connettersi a server diversi o di utilizzare Tor, e come detto hai sempre la possibilità di far girare un tuo server Electrum personale così da non rivelare a estranei le tue informazioni. In ogni caso, Electrum sacrifica un po’ di fiducia e privacy in cambio di velocità e leggerezza. Non c’è un archivio locale di tutti i dati: ti fidi che i server (che comunque sono diversi, open source e decentralizzati) lavorino bene. Se c’è un problema di sicurezza grosso (tipo server malevoli dirottano utenti), di solito la comunità se ne accorge. Ma per somme ingenti, gli stessi sviluppatori consigliano un full node per la massima sicurezza .

Dal punto di vista del codice, usare Python per Electrum ha diversi pro e contro rispetto al C++ di Bitcoin Core. Il pro maggiore è la semplicità di sviluppo e lettura: Python è un linguaggio ad alto livello, con sintassi molto più vicina all’inglese. Ad esempio, Electrum può utilizzare direttamente la libreria di grandi numeri di Python o funzioni di hashing di librerie come OpenSSL senza doversi preoccupare di dettagli di memoria o di tipi a lunghezza fissa come farebbe C++. Una funzionalità come la generazione di un indirizzo Bitcoin può essere scritta in Python in poche righe: prendere una chiave pubblica, fare l’hash SHA256 e RIPEMD160 (ci sono funzioni già pronte), aggiungere un byte e un checksum, e usare un’implementazione Base58 per ottenere la stringa finale. Infatti, nel codice di Electrum esiste una funzione per codificare in Base58Check molto simile a quella di Bitcoin Core, solo espressa in Python. E grazie al supporto di Python per i numeri interi arbitrariamente grandi, l’algoritmo di conversione Base58 risulta breve: si può convertire l’intera stringa di byte in un numero intero e poi fare divisioni/modulo per 58 per costruire l’indirizzo, come accennavamo prima. Dove in C++ c’è magari un loop con gestione di array di byte, in Python puoi scriverlo in maniera più diretta. Questo vale in generale: il codice Python di Electrum è più conciso e leggibile, tant’è che il sito ufficiale vanta che “il codice è corto e facile da rivedere” . Se sei curioso di capire come Electrum genera un nuovo indirizzo o firma una transazione, puoi aprire i file .py corrispondenti e troverai funzioni abbastanza autoesplicative. Ad esempio, c’è del codice per generare nuove chiavi a partire dal seed (Electrum implementa il suo metodo deterministico per derivare le chiavi), codice per costruire le transazioni (crea l’oggetto Tx con input e output e calcola i campi), e così via, tutto in Python puro.

D’altra parte, Python è più lento di C++ e non adatto a eseguire verifiche intensive come quelle di un full node. Un nodo Bitcoin deve verificare blocchi con migliaia di transazioni ogni 10 minuti, eseguire miriadi di operazioni crittografiche e di controllo script – insomma un lavoro gravoso che C++ svolge molto più velocemente e con controllo fine sulla memoria. Electrum, delegando le verifiche pesanti ai server, può permettersi di essere in Python perché localmente fa poche operazioni (gestione interfaccia, calcoli per le proprie transazioni, ecc.). Se provassimo a implementare un full node in pure Python, probabilmente sarebbe irrimediabilmente lento e non riuscirebbe a stare al passo con la rete. Per questo i ruoli sono distinti: Bitcoin Core in C++ fa da motore ad alte prestazioni e wallet come Electrum in Python fungono da comodi veicoli leggeri per interagire col motore.

Un utile confronto è pensare a come i due gestiscono la rete: Bitcoin Core parla il protocollo Bitcoin P2P, riceve blocchi grezzi, controlla ogni firma, mantiene l’intera UTXO set in RAM/disk. Electrum invece parla col protocollo Electrum server (che a sua volta è un software, spesso chiamato ElectrumX, che gira accanto a un full node e indicizza gli indirizzi). Quindi Electrum non conosce neanche il formato binario esatto dei blocchi interi: richiede informazioni già pronte al server in formati più semplici (ad esempio “lista delle transazioni dell’indirizzo X” viene fornita come elenco già filtrato). Questo API thinking rende il codice Electrum più simile a quello di un’applicazione che chiama servizi web, piuttosto che a quello di un nodo che macina dati raw. Se apri il codice Python di Electrum troverai classi per il wallet, la sincronizzazione col server (via socket o HTTP), la costruzione di transazioni e l’interfaccia utente. In C++, invece, aprendo Bitcoin Core trovi file dedicati a gestire blocchi (validation.cpp), script (interpreter.cpp), networking a basso livello (net.cpp), database (per la mempool, wallet.dat, ecc.), cryptography (ecc). Due mondi differenti in termini di implementazione, ma che cooperano: i server Electrum stessi dipendono da un full node per avere i dati validati.

Vale la pena menzionare anche che Electrum, essendo estensibile (supporta plugin) e scritto in Python, è spesso usato dagli sviluppatori per prototipare funzionalità. Ad esempio, c’è un plugin che integra LN (Lightning Network) su Electrum, sfruttando la facilità con cui Python può interagire con librerie LN scritte magari in altre lingue. Bitcoin Core invece implementa solo il protocollo on-chain e di recente anche un’implementazione base di Lightning (LND o c-lightning sono separati). Dunque, come utente non programmatore, devi vedere Bitcoin Core e Electrum come due strumenti complementari: Bitcoin Core è il guardiano rigoroso della validità, Electrum è l’assistente agile che ti dà un accesso rapido, con in più un’interfaccia grafica semplice. Molti utenti avanzati infatti usano Electrum come interfaccia, ma lo puntano verso il loro Bitcoin Core (usando Electrum Personal Server o simili), così da avere il meglio di entrambi i mondi: la sicurezza e privacy del proprio nodo e la comodità di Electrum.

In sintesi, l’esperienza di Electrum mette in luce il contrasto tra il codice C++ di Bitcoin (performante, complesso, “tuttofare”) e il codice Python di un wallet (snello, focalizzato sull’esperienza utente, delegante). Entrambi sono fondamentali nell’ecosistema Bitcoin: il primo assicura che le regole siano rispettate e che la rete rimanga onesta, il secondo rende l’uso quotidiano più accessibile a tutti. E per chi vuole imparare, leggere il codice Python di Electrum può essere un ottimo trampolino per poi capire meglio anche il codice C++ di Bitcoin Core, perché i concetti sono gli stessi (chiavi, firme, transazioni, indirizzi) ma presentati in modo più leggibile. Del resto, Electrum stesso è open source e “chiunque può verificare il codice” , il che incarna uno dei valori cardine di Bitcoin: “don’t trust, verify”. Anche se magari non leggerai ogni riga, sapere che sia Bitcoin Core sia Electrum hanno codice aperto significa che tutta la comunità può (e ha) controllato per bug, backdoor, ecc., dandoci fiducia nel loro funzionamento.

Conclusione

Siamo partiti da zero (o quasi) e abbiamo esplorato con dettaglio molti aspetti del codice di Bitcoin, mantenendo uno stile discorsivo, quasi da audiolibro. Abbiamo visto come gli indirizzi Bitcoin non siano stringhe magiche ma il risultato di algoritmi di codifica (Base58Check e Bech32) progettati per essere sicuri ed efficienti  . Abbiamo aperto la scatola nera delle transazioni per scoprire Bitcoin Script, questo semplice ma potente linguaggio interno che consente di impostare condizioni di spesa personalizzate  . Poi abbiamo incontrato Miniscript, che rende quelle condizioni complesse più maneggevoli – come usare dei Lego logici per costruire contratti sicuri e interoperabili  . Infine, ci siamo spostati dal nodo core in C++ al mondo dei wallet leggeri in Python, esaminando Electrum e la magia di SPV che permette di verificare pagamenti con fiducia minima, evidenziando differenze e trade-off rispetto a un nodo completo  .

Sebbene tu abbia detto di sapere “quasi tutto” di Bitcoin a livello teorico, spero che questa spiegazione narrata ti abbia aiutato a collegare quei concetti con la realtà pratica del codice. Abbiamo usato metafore (lucchetti, casseforti, mattoncini) per tradurre termini tecnici in immagini più familiari, e messo a confronto pezzi di codice C++ e Python per darti un’idea tangibile di come le stesse logiche si implementano in modi diversi.

Ovviamente, ogni sezione che abbiamo toccato potrebbe essere approfondita ancora di più: il codice di Bitcoin Core è vasto e in continua evoluzione (oggi include anche Taproot, la firma Schnorr, vari miglioramenti di performance ecc.), e l’ecosistema dei wallet è ricco di implementazioni differenti (ce ne sono in Java, JavaScript, Rust…). Ma il filo conduttore rimane: Bitcoin in fondo è un insieme di idee semplici, realizzate con codice che deve essere estremamente affidabile. Capire il codice di Bitcoin significa apprezzare come questi principi – sicurezza, decentralizzazione, immutabilità – vengano tradotti in strutture dati, algoritmi crittografici e protocolli di rete. È un po’ come ascoltare la voce dietro la musica: ora che conosci meglio il “dietro le quinte”, quando userai Bitcoin potrai immaginare cosa succede realmente quando scansioni un QR, firmi una transazione o controlli un saldo.

Spero che questo viaggio dettagliato ti sia stato utile e ti abbia dato nuove intuizioni. Bitcoin unisce criptografia, teoria dei giochi e programmazione: esplorarlo è un percorso affascinante che non finisce mai, ma passo dopo passo – proprio come abbiamo fatto qui – tutto diventa comprensibile. Buon proseguimento nello studio e, la prossima volta che sarai in palestra ad ascoltare spiegazioni come questa, potrai magari addentrarti ancora di più in qualche aspetto specifico sapendo di avere già delle basi solide su cui costruire. Buon ascolto e buona sperimentazione con il codice di Bitcoin!

Fonti utilizzate: Bitcoin Wiki, documentazione e blog tecnici su Bitcoin (formati di indirizzo, script, Miniscript), codice e documentazione di Electrum, e risorse comunitarie    , tra gli altri. All’occorrenza, sono stati citati estratti rilevanti per confermare e approfondire i concetti presentati. Buono studio!