blockchain Ethereum locale con Docker
In questo articolo costruiamo una piccola blockchain Ethereum locale, senza spendere gas reale, senza toccare la mainnet e senza affidarci a servizi cloud.
L’idea e’ semplice: invece di parlare della blockchain come se fosse una cosa lontana, la portiamo dentro il nostro computer. Creiamo una rete Ethereum privata con 5 nodi, facciamo girare transazioni tra account diversi e poi usiamo una LLM locale, tramite Ollama, per leggere il traffico e riassumere cosa sta succedendo.
Quindi non sara’ solo teoria. Alla fine avremo:
- una rete Ethereum locale avviata con Docker;
- almeno 5 nodi Ethereum che partecipano alla stessa rete;
- account con ETH finti gia’ caricati nel genesis;
- transazioni tra chiavi pubbliche diverse;
- messaggi inseriti dentro le transazioni;
- un file di log con hash, mittente, destinatario, timestamp e blocco;
- Ollama che analizza il traffico e produce un report leggibile.
Piccola nota prima di partire: quando diciamo “fork locale” in questo contesto, lo usiamo nel senso pratico da laboratorio. Non stiamo clonando la mainnet con tutto il suo stato, cosa che richiederebbe un endpoint RPC esterno e molta piu’ infrastruttura. Stiamo creando una rete Ethereum-compatible locale, cioe’ una rete che ragiona come Ethereum: account, blocchi, gas, transazioni, JSON-RPC, chiavi pubbliche e private.
E per imparare come funziona davvero, questa e’ spesso la strada migliore.
Cos’e’ una blockchain, spiegata semplice
Una blockchain e’ un registro condiviso.
Immaginiamo un quaderno. Ogni pagina del quaderno contiene una lista di operazioni: Andrea manda 1 token a Marco, Marco manda 0.5 token a Sara, Sara scrive un messaggio dentro una transazione, e cosi’ via.
La differenza e’ che questo quaderno non vive su un solo computer. Vive su tanti nodi.
Ogni nodo conserva una copia della catena e controlla che le nuove pagine, cioe’ i nuovi blocchi, rispettino le regole della rete.
Un blocco, semplificando, contiene:
- un numero progressivo;
- una lista di transazioni;
- un timestamp;
- l’hash del blocco precedente;
- altri dati tecnici usati dal protocollo.
L’hash del blocco precedente e’ importante perche’ crea la catena. Se qualcuno prova a modificare un blocco vecchio, cambia il suo hash. E se cambia quell’hash, non tornano piu’ tutti i blocchi successivi.
Ecco perche’ la blockchain non e’ magica. E’ una combinazione molto intelligente di crittografia, rete distribuita e regole condivise.
Come funziona Ethereum dentro questo ragionamento
Ethereum aggiunge una cosa fondamentale: non gestisce solo trasferimenti di valore, ma anche codice.
Questo codice si chiama smart contract.
In questo tutorial non partiamo dagli smart contract, perche’ voglio prima far vedere la parte piu’ essenziale: account, nodi, transazioni e messaggi.
Ogni account Ethereum ha:
- una chiave privata;
- una chiave pubblica;
- un indirizzo pubblico, quello che di solito inizia con
0x; - un saldo;
- un nonce, cioe’ un contatore delle transazioni inviate.
La chiave privata firma le transazioni. L’indirizzo pubblico riceve fondi o messaggi. La rete verifica che la firma sia valida.
Il punto importante e’: se io mando una transazione da un account a un altro, i nodi della rete ricevono l’informazione, la propagano, la includono in un blocco e poi aggiornano lo stato.
In pratica, tutti aggiornano lo stesso quaderno.
Perche’ usare Docker per una blockchain locale
Docker e’ perfetto per questo tipo di esperimento perche’ ci evita di installare manualmente client Ethereum, dipendenze, configurazioni di rete, chiavi dei validatori e strumenti di monitoraggio.
Nel nostro caso useremo Docker tramite Kurtosis, uno strumento pensato proprio per creare ambienti complessi e ripetibili. Kurtosis avvia container Docker isolati, li collega tra loro e ci permette di tirare su una rete Ethereum privata con un solo comando.
La parte interessante e’ questa: non stiamo lanciando un singolo nodo finto. Stiamo creando piu’ nodi che devono stare d’accordo sulla stessa catena.
Architettura del laboratorio
Il laboratorio sara’ composto da tre blocchi:
- Rete Ethereum locale
Avviata con Docker/Kurtosis, con 5 nodi. - Script Node.js
Invia transazioni tra account diversi e salva il traffico in un file JSON. - Ollama
Esegue una LLM locale che legge il traffico e produce un’analisi semplice: numero di transazioni, coppie mittente/destinatario, timestamp, messaggi e pattern.
Il flusso sara’ questo:
Account A -> Nodo 1 -> rete Ethereum locale -> Nodo 2/3/4/5
Account B -> Nodo 2 -> rete Ethereum locale -> blocchi condivisi
Account C -> Nodo 3 -> rete Ethereum locale -> log transazioni
Log JSON -> Ollama -> analisi leggibile
Prerequisiti
Servono pochi strumenti:
- Docker Desktop installato e avviato;
- Node.js 20 o superiore;
- Kurtosis CLI;
- almeno 8 GB di RAM liberi, meglio 16 GB;
- un terminale.
Per installare Kurtosis, usa la guida ufficiale in base al tuo sistema operativo. Su Windows conviene lavorare con PowerShell o WSL2.
Verifica che Docker funzioni:
docker version
Verifica Node.js:
node -v
npm -v
Verifica Kurtosis:
kurtosis version
Creiamo il progetto
Creiamo una cartella dedicata:
mkdir nodeai-ethereum-lab
cd nodeai-ethereum-lab
Dentro questa cartella metteremo:
nodeai-ethereum-lab/
network_params.yaml
package.json
send-messages.mjs
analyze-traffic.mjs
tx-log.json
Configuriamo la rete Ethereum locale con 5 nodi
Creiamo il file network_params.yaml.
Questo file dice a Kurtosis che vogliamo 5 partecipanti Ethereum, tutti con execution client Geth e consensus client Lighthouse.
In piu’ creiamo 5 account gia’ caricati con ETH finti nel genesis. Useremo chiavi note da ambiente di sviluppo. Sono chiavi pubbliche, da laboratorio, non da usare mai su mainnet.
participants:
- el_type: geth
cl_type: lighthouse
count: 5
network_params:
network: kurtosis
genesis_delay: 20
seconds_per_slot: 12
num_validator_keys_per_node: 32
prefunded_accounts:
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266":
balance: "100ETH"
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8":
balance: "100ETH"
"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC":
balance: "100ETH"
"0x90F79bf6EB2c4f870365E785982E1f101E93b906":
balance: "100ETH"
"0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65":
balance: "100ETH"
additional_services:
- dora
- spamoor
snooper_params:
enabled: true
spamoor_params:
spammers:
- scenario: eoatx
name: nodeai-background-transactions
config:
throughput: 2
Cosa abbiamo scritto?
participantscrea i nodi della rete.count: 5significa che vogliamo cinque nodi con la stessa combinazione client.prefunded_accountscarica saldo finto dentro alcuni account.doraaggiunge un explorer locale.spamoorgenera traffico di background.snooper_params.enabledabilita il tracciamento delle chiamate JSON-RPC.
Questa e’ gia’ una piccola rete Ethereum privata.
Avviamo la rete
Lanciamo:
kurtosis run --enclave nodeai-chain github.com/ethpandaops/ethereum-package --args-file ./network_params.yaml
La prima volta Docker scarichera’ diverse immagini. Qui serve un po’ di pazienza.
Quando il comando finisce, controlliamo i servizi:
kurtosis enclave inspect nodeai-chain
Vedrai una lista di container: execution client, consensus client, validator client e servizi aggiuntivi.
Quello che ci serve e’ l’endpoint RPC di almeno un execution client. Kurtosis di solito pubblica porte locali tipo:
http://127.0.0.1:xxxxx
Annota 5 endpoint RPC, uno per ogni nodo execution. Se vuoi partire in modo semplice, puoi usare anche un solo endpoint RPC per inviare tutte le transazioni. Ma per il nostro laboratorio useremo piu’ endpoint, cosi’ vediamo i nodi come punti di ingresso diversi nella stessa rete.
Impostiamo una variabile ambiente con gli RPC separati da virgola:
export RPC_URLS="http://127.0.0.1:PORTA_NODO_1,http://127.0.0.1:PORTA_NODO_2,http://127.0.0.1:PORTA_NODO_3,http://127.0.0.1:PORTA_NODO_4,http://127.0.0.1:PORTA_NODO_5"
Su PowerShell:
$env:RPC_URLS="http://127.0.0.1:PORTA_NODO_1,http://127.0.0.1:PORTA_NODO_2,http://127.0.0.1:PORTA_NODO_3,http://127.0.0.1:PORTA_NODO_4,http://127.0.0.1:PORTA_NODO_5"
Prepariamo Node.js ed ethers
Inizializziamo il progetto:
npm init -y
npm install ethers
Ora creiamo lo script send-messages.mjs.
Questo script fa una cosa molto pratica: prende 5 wallet, ognuno con saldo finto, e li fa comunicare tra loro. Ogni transazione contiene anche un piccolo messaggio nel campo data.
import { ethers } from "ethers";
import fs from "node:fs/promises";
const rpcUrls = (process.env.RPC_URLS || "").split(",").map((item) => item.trim()).filter(Boolean);
if (rpcUrls.length < 1) {
throw new Error("Imposta RPC_URLS con almeno un endpoint RPC della rete Kurtosis.");
}
const privateKeys = [
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
"0x59c6995e998f97a5a0044966f094538e4e22c994f3371dcb4d30d8c6d557378c",
"0x5de4111a0605e5ad348e69c646323a03ca8b7a165bd2b05b5cf558e04e4a2",
"0x7c8521182943a5eab3e3f2c0987ee1cf9c0e5d86eec6811048f0b5b7bb9a2",
"0x47e179ec197488433ba80eacc38a093997d63ee5a459f9b825ad8e9d74c7b8"
];
const providers = rpcUrls.map((url) => new ethers.JsonRpcProvider(url));
const wallets = privateKeys.map((key, index) => new ethers.Wallet(key, providers[index % providers.length]));
const log = [];
for (let round = 0; round < 3; round++) {
for (let index = 0; index < wallets.length; index++) {
const sender = wallets[index];
const receiver = wallets[(index + 1) % wallets.length];
const providerIndex = index % providers.length;
const message = `nodeai-msg round=${round + 1} from=${index + 1} to=${((index + 1) % wallets.length) + 1}`;
const tx = await sender.sendTransaction({
to: receiver.address,
value: ethers.parseEther("0.001"),
data: ethers.hexlify(ethers.toUtf8Bytes(message))
});
const sentAt = new Date().toISOString();
const receipt = await tx.wait();
log.push({
hash: tx.hash,
from: sender.address,
to: receiver.address,
valueEth: "0.001",
message,
rpcUrl: rpcUrls[providerIndex],
blockNumber: receipt.blockNumber,
timestamp: sentAt,
status: receipt.status
});
console.log(`[ok] ${tx.hash} ${sender.address} -> ${receiver.address}`);
}
}
await fs.writeFile("tx-log.json", JSON.stringify(log, null, 2));
console.log(`Salvate ${log.length} transazioni in tx-log.json`);
Lanciamolo:
node send-messages.mjs
Se tutto e’ corretto, vedrai una lista di hash transazione.
Ogni hash e’ una prova: la transazione e’ stata firmata, inviata a un nodo, propagata alla rete e inserita in un blocco.
Guardiamo cosa abbiamo ottenuto
Apriamo tx-log.json.
Dentro troveremo oggetti simili a questo:
{
"hash": "0x...",
"from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"to": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"valueEth": "0.001",
"message": "nodeai-msg round=1 from=1 to=2",
"rpcUrl": "http://127.0.0.1:xxxxx",
"blockNumber": 12,
"timestamp": "2026-06-18T10:15:00.000Z",
"status": 1
}
Questo file e’ molto piu’ interessante di quanto sembri.
Perche’ contiene la storia del nostro piccolo sistema:
- chi ha inviato;
- chi ha ricevuto;
- quando e’ stata inviata la transazione;
- in quale blocco e’ entrata;
- quale messaggio abbiamo scritto nella transazione;
- quale nodo RPC abbiamo usato come punto di ingresso.
In una blockchain pubblica questi dati possono diventare enormi. Qui invece li abbiamo in piccolo, leggibili, sotto controllo.
Avviamo Ollama in locale
Ora aggiungiamo la parte AI.
Ollama ci permette di usare modelli locali senza mandare i dati fuori dalla macchina. Questo e’ perfetto per un esperimento su traffico blockchain, chiavi pubbliche, indirizzi e timestamp.
Avviamo Ollama con Docker:
docker run -d --name nodeai-ollama -p 11434:11434 -v ollama:/root/.ollama ollama/ollama
Scarichiamo un modello leggero:
docker exec -it nodeai-ollama ollama pull llama3.2
Se hai una macchina meno potente, puoi usare un modello piu’ piccolo. Se hai piu’ risorse, puoi usare un modello piu’ capace.
Verifichiamo che l’API risponda:
curl http://localhost:11434/api/tags
Facciamo analizzare il traffico alla LLM locale
Creiamo analyze-traffic.mjs.
Lo script legge tx-log.json, calcola qualche statistica di base e poi passa tutto a Ollama chiedendo una sintesi tecnica ma leggibile.
import fs from "node:fs/promises";
const model = process.env.OLLAMA_MODEL || "llama3.2";
const ollamaUrl = process.env.OLLAMA_URL || "http://localhost:11434/api/chat";
const raw = await fs.readFile("tx-log.json", "utf8");
const transactions = JSON.parse(raw);
const stats = {
totalTransactions: transactions.length,
bySender: {},
byReceiver: {},
pairs: {},
firstTimestamp: null,
lastTimestamp: null,
blocks: [...new Set(transactions.map((tx) => tx.blockNumber))].sort((a, b) => a - b)
};
for (const tx of transactions) {
stats.bySender[tx.from] = (stats.bySender[tx.from] || 0) + 1;
stats.byReceiver[tx.to] = (stats.byReceiver[tx.to] || 0) + 1;
const pair = `${tx.from} -> ${tx.to}`;
stats.pairs[pair] = (stats.pairs[pair] || 0) + 1;
if (!stats.firstTimestamp || tx.timestamp < stats.firstTimestamp) {
stats.firstTimestamp = tx.timestamp;
}
if (!stats.lastTimestamp || tx.timestamp > stats.lastTimestamp) {
stats.lastTimestamp = tx.timestamp;
}
}
const prompt = `
Sei un analista blockchain.
Analizza questo traffico di una rete Ethereum locale.
Devi rispondere in italiano, con tono chiaro e pratico.
Spiega:
- quante transazioni sono state osservate;
- quali chiavi pubbliche inviano piu' transazioni;
- quali chiavi pubbliche ricevono piu' transazioni;
- quali coppie mittente/destinatario comunicano;
- quali timestamp delimitano l'esperimento;
- quali messaggi sono stati scambiati;
- se il traffico sembra bilanciato o concentrato.
Statistiche:
${JSON.stringify(stats, null, 2)}
Transazioni:
${JSON.stringify(transactions, null, 2)}
`;
const response = await fetch(ollamaUrl, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
model,
stream: false,
messages: [
{
role: "user",
content: prompt
}
]
})
});
if (!response.ok) {
throw new Error(`Errore Ollama: ${response.status} ${await response.text()}`);
}
const result = await response.json();
console.log(result.message.content);
Lanciamo:
node analyze-traffic.mjs
Il risultato sara’ un report naturale, ad esempio:
Sono state osservate 15 transazioni distribuite tra 5 indirizzi.
Ogni indirizzo ha inviato 3 transazioni e ne ha ricevute 3.
Il traffico appare bilanciato, perche' ogni nodo/account partecipa allo scambio.
I messaggi indicano tre round di comunicazione...
Ed ecco il punto: abbiamo usato una LLM locale per leggere dati prodotti da una blockchain locale.
Nessun dato mandato a un servizio esterno. Nessuna API key. Nessun costo di gas. Solo il nostro computer.
Cosa sta succedendo davvero sotto il cofano
Quando eseguiamo send-messages.mjs, lo script firma una transazione con una chiave privata.
La transazione contiene:
- destinatario;
- valore;
- messaggio nel campo
data; - gas;
- nonce;
- firma crittografica.
La transazione viene inviata a uno degli endpoint RPC.
Il nodo che la riceve la mette nella sua mempool, cioe’ una zona temporanea dove restano le transazioni in attesa di essere inserite in un blocco.
Gli altri nodi vengono informati.
Quando arriva il momento di produrre un blocco, la transazione viene inclusa nella catena. Da quel momento possiamo recuperarla tramite hash e verificare che sia nello stesso stato condiviso da tutti i nodi.
Questo e’ il cuore della blockchain: tanti nodi indipendenti che arrivano alla stessa versione della verita’.
Perche’ i messaggi dentro le transazioni sono utili
In Ethereum il campo data puo’ essere usato per chiamare smart contract, ma puo’ anche contenere dati arbitrari.
Nel nostro laboratorio lo usiamo per scrivere messaggi semplici:
nodeai-msg round=1 from=1 to=2
Non e’ un sistema di chat. Non e’ pensato per messaggi privati. Tutto cio’ che mettiamo in una transazione blockchain deve essere considerato pubblico.
Pero’ per un laboratorio e’ utilissimo, perche’ ci permette di vedere la blockchain come sistema di comunicazione tra identita’ crittografiche.
Ogni messaggio ha:
- un mittente verificabile;
- un destinatario;
- un timestamp lato applicazione;
- un blocco;
- un hash transazione.
Questi elementi sono la base per costruire sistemi piu’ avanzati: audit trail, notarizzazione, log distribuiti, tracciamento eventi, supply chain, identita’ digitale.
Fermare e pulire tutto
Quando hai finito, puoi fermare la rete:
kurtosis enclave stop nodeai-chain
Per eliminarla:
kurtosis enclave rm -f nodeai-chain
Per fermare Ollama:
docker stop nodeai-ollama
docker rm nodeai-ollama
Se vuoi cancellare anche i modelli scaricati nel volume Docker:
docker volume rm ollama
Errori comuni
Docker non e’ avviato
Se Kurtosis non parte, spesso il problema e’ Docker Desktop spento.
Controlla:
docker ps
Non trovo gli endpoint RPC
Esegui:
kurtosis enclave inspect nodeai-chain
Cerca i servizi execution client e le porte pubblicate verso 127.0.0.1.
Le transazioni falliscono per saldo insufficiente
Controlla che gli indirizzi in prefunded_accounts siano gli stessi derivati dalle chiavi private nello script.
Gli indirizzi usati in questa guida sono account di sviluppo noti, e devono avere ETH finti nel genesis.
Ollama non risponde
Controlla il container:
docker ps
docker logs nodeai-ollama
E verifica:
curl http://localhost:11434/api/tags
Cosa abbiamo imparato
Abbiamo visto una blockchain non come parola da conferenza, ma come sistema vivo.
Abbiamo creato una rete Ethereum locale con Docker, composta da 5 nodi. Abbiamo fatto viaggiare transazioni tra account diversi. Abbiamo inserito messaggi dentro le transazioni. Abbiamo salvato hash, chiavi pubbliche, blocchi e timestamp. Infine abbiamo usato Ollama per analizzare il traffico con una LLM locale.
Questo tipo di laboratorio e’ molto potente perche’ toglie la blockchain dalla nebbia.
La blockchain diventa una cosa che puoi avviare, rompere, osservare, spegnere e ricostruire.
Ed e’ qui che si impara davvero.
Possibili evoluzioni
Da qui puoi andare avanti in diversi modi:
- aggiungere uno smart contract Solidity;
- salvare eventi on-chain invece dei messaggi nel campo
data; - creare una dashboard web per leggere i blocchi;
- usare Ollama per classificare transazioni sospette;
- confrontare traffico normale e traffico anomalo;
- esportare le metriche su Grafana;
- creare un agente AI che interroga la blockchain locale.
Il passo successivo naturale e’ questo: invece di far analizzare a Ollama solo un file JSON, possiamo collegarlo a un piccolo servizio che osserva i nuovi blocchi in tempo reale.
A quel punto la rete Ethereum locale diventa un laboratorio per agenti AI, auditing, sicurezza e automazione.
Fonti tecniche
- Ethereum Package di ethPandaOps: https://github.com/ethpandaops/ethereum-package
- Kurtosis: https://docs.kurtosis.com/
- Ollama: https://github.com/ollama/ollama
- ethers.js: https://docs.ethers.org/
Disclaimer personale
Questo articolo nasce anche come raccolta di appunti personali.
Mi serve per riorganizzare le idee, mettere in fila i passaggi tecnici e preparare in modo piu’ chiaro il mio prossimo intervento, dove l’obiettivo sara’ spiegare e dimostrare dal vivo il funzionamento di una blockchain.
Il percorso descritto qui non vuole essere una guida definitiva alla produzione di reti Ethereum, ma un laboratorio ragionato: un modo pratico per osservare nodi, transazioni, messaggi, timestamp e analisi del traffico in un ambiente locale, controllato e a costo zero.
Ringrazio quindi anche questo articolo, in un certo senso, per avermi aiutato a confermare e ordinare meglio i concetti che voglio portare in aula: la blockchain si capisce davvero quando la si vede funzionare.