Ethereum y (otras blockchain) basa su almacenamiento en la base de datos LevelDB (de Google), que es una base de datos que almacena pares clave/valor, teniendo en cuenta que además Ethereum usa hashes y codifica algunos valores con RLP.
Las funcionalidades básicas que se pueden realizar con LevelDB están indicadas en su web https://github.com/google/leveldb/blob/main/doc/index.md, pero dada la complejidad añadida por Ethereum es mejor usar las utilidades que proporciona esta blockchain.
Vamos a tratar de explorar los datos de una blockchain Ethereum privada usando la base de datos que se almacena en los nodos (usaré en este post la blockchain privada del post Ethereum Blockchain privada), modificar los datos de un bloque (eliminar las transacciones) y ver cómo podemos modificar el cliente geth, aunque los árboles de merkle fallarán por la inconsistencia y tendremos un error.
Estos dos enlaces siguientes muestran código para interaccionar con LevelDB-Ethereum
https://ethereum.stackexchange.com/questions/28976/leveldb-in-geth-key-and-values
https://github.com/ethereum/go-ethereum/issues/26353
Cuando se ejecuta un nodo geth, la estructura de archivos que se crea es la siguiente (en este caso para una red privada). La base de datos leveldb está en la carpeta chaindata, mientras que la carpeta ancient guarda en otra estructura los datos antiguos (ver https://www.superlunar.com/post/geth-freezer-files-block-data-done-fast )
La blockchain propiamente dicha, es decir, la base de datos LevelDB se encuentra en el directorio chaindata
LevelDB y lenguage Go
Puesto que vamos a usar el cliente geth de Ethereum, necesitamos instalar go. Usaré una máquina virtual Ubuntu.
Instalando desde los paquetes oficiales (sudo apt install golang) provoca un error a la hora de instalar los paquetes de Ethereum y ejecutar alguna aplicación en go, en particular con el módulo Sha3, por lo que instalo Go directamente desde su página web, siguiendo las instrucciones (descomprimir un archivo tar) https://go.dev/doc/install. Creamos un directorio /go en nuestro usuario (/home/usuario/go)
Y ahora instalamos los paquetes de ethereum, ejecutando estos 3 comandos:
$ go mod init github.com/ethereum $ go mod tidy $ go get github.com/ethereum/go-ethereum/
Ahora podemos usar el siguiente script en Go para interactuar con la blockchain de nuestro nodo, en este primer caso para leer un bloque (en particular el bloque 12 que es donde se validó la transacción que hice en el post de la blockchain privada, indicado antes). El código del script está basado en los enlaces web que indiqué al principio:
Nota: Aparece comentada la parte del código necesaria para sobrescribir datos del bloque, en particular el que corresponde con las transacciones que incluye ese bloque:
package main import ( "bytes" "encoding/binary" // "encoding/hex" // Necesaria a la hora de escribir datos, descomentarla si se escribe "fmt" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" "github.com/syndtr/goleveldb/leveldb" ) var ( headerPrefix = []byte("h") // headerPrefix + num (uint64 big endian) + hash -> header numSuffix = []byte("n") // headerPrefix + num (uint64 big endian) + numSuffix -> hash bodyPrefix = []byte("b") // bodyPrefix + num (uint64 big endian) + hash -> block body ) func main() { // Connection to leveldb db, _ := leveldb.OpenFile("/home/user/Documents/ethereum/network_ubuntu/geth/chaindata", nil) num := 12 // transaccion en bloque 12 blockNumber := make([]byte, 8) binary.BigEndian.PutUint64(blockNumber, uint64(num)) fmt.Printf("Details of Blocknumber:- \nHex: %x \nBytes: %d\n\n\n", blockNumber, blockNumber) // create key to get hash (headerPrefix + num (uint64 big endian) + numSuffix) hashKey := append(headerPrefix, blockNumber...) // adding prefix hashKey = append(hashKey, numSuffix...) // adding suffix fmt.Printf("Details of leveldb key for Block Hash:- \nType: %T \nHex: %x \nbytes: %v \nLength: %d\n\n\n", hashKey, hashKey, hashKey, len(hashKey)) // Getting hash using hashKey blockHash, _ := db.Get(hashKey, nil) fmt.Printf("Details of Block hash:- \nType: %T \nHex: %x \nBytes: %v\n\n\n", blockHash, blockHash, blockHash) //Create key to get header (headerPrefix + num (uint64 big endian) + hash) headerKey := append(headerPrefix, blockNumber...) // adding prefix headerKey = append(headerKey, blockHash...) // adding suffix fmt.Printf("Details of leveldb key for Block Header:- \nType: %T \nHex: %x \nVytes: %v \nLength: %d\n\n\n", headerKey, headerKey, headerKey, len(headerKey)) //get Block Header data from db blockHeaderData, _ := db.Get(headerKey, nil) fmt.Printf("Details of Raw Block Header:- \nType: %T \nHex: %x \nBytes: %v \nLength: %d\n\n\n", blockHeaderData, blockHeaderData, blockHeaderData, len(blockHeaderData)) //new Blockheader type blockHeader := new(types.Header) // Read blockHeaderData in a tmp variable tmpByteData := bytes.NewReader(blockHeaderData) //Decode tmpByteData to new blockHeader rlp.Decode(tmpByteData, blockHeader) fmt.Printf("Details of Header RLP Decoded :- \nType: %T \nHex: %x \nValue: %v\n\n\n", blockHeader, blockHeader, blockHeader) fmt.Printf("Block Hash: %x \n\n\n", blockHeader.Hash()) bodyKey := append(bodyPrefix, blockNumber...) bodyKey = append(bodyKey, blockHeader.Hash().Bytes()...) blockBodyData, _ := db.Get(bodyKey, nil) blockBody := new(types.Body) tmpBodyByteData := bytes.NewReader(blockBodyData) fmt.Printf("blockBodyData: \nHex: %x \nValue %v\n\n", blockBodyData, blockBodyData) // c2c0c0 = Vacío, sin datos fmt.Printf("blockBody: %v \n\n\n", blockBody) // bloque 12 original f86cf869f86780843b9aca0082520894d00411828e14f75b0bada0d5173974e94515861582271080820636a0742fa012f5e8fc39e22cc43c79e775ae8d1fb253a12e9e4e92980c969a4e9deaa07ab20a94a4e742d82f926701b8a62543beec0a0d39144fa830f74231ea9a61f4c0 // descomentar lo siguiente para escribir en base de datos leveldb // fmt.Printf("\n=======ESCRIBIR blockData\n") // newBlockBody :="c2c0c0" // eliminar transacción // newHexBlockBody, _ := hex.DecodeString(newBlockBody) // err := db.Put(bodyKey,newHexBlockBody,nil) // fmt.Printf("Resultado %v\n",err) }
Si ejecutamos ese script (con el comando go run script.go) y comparamos los resultados con aquellos obtenidos de la consola al conectarnos al cliente geth (el script en go no puede ejecutarse mientras está el nodo ejecutándose, pues está usando la base de datos leveldb, hay que pararlo), entonces comprobamos que los valores son correctos, salvo unos pequeños bytes al inicio y al final en los datos devueltos por leveldb, correspondientes a la estructura JSON codificada. Estos bytes son f86cf869……c0
En esta red privada funciona perfectamente la herramienta https://toolkit.abdk.consulting/ethereum para poder obtener y decodificar RLP todos los valores de una manera sencilla, y permiten comprobar esa pequeña diferencia que reporta el scritp en go, que no afecta a los resultados, pues es una cuestión de formato:
Modificación de la base de datos LevelDB
Descomentando en el script anterior las líneas indicadas para escribir en la base de datos, podemos modificar realmente ese bloque de la blockchain, en este caso borrando el contenido de las transacciones, cuyo resultado es:
Lanzando el cliente geth de nuevo en Ubuntu, comprobamos que el bloque está modificado sin ningún error, es importante hacer notar que este nodo Ubuntu no reporta este problema, pues al parar/rearrancar no está comprobando la validez de toda la cadena, pues parte ya de la base de datos creada, quizá si se reinicia desde el bloque génesis, sin borrar la base de datos salte el error
Leer toda la base de datos LevelDB
Con el siguiente script mostramos toda la base de datos LevelDB correspondiente a la blockchain almacenada
import ( "fmt" "github.com/syndtr/goleveldb/leveldb" ) var ( headerPrefix = []byte("h") // headerPrefix + num (uint64 big endian) + hash -> header numSuffix = []byte("n") // headerPrefix + num (uint64 big endian) + numSuffix -> hash bodyPrefix = []byte("b") // bodyPrefix + num (uint64 big endian) + hash -> block body ) func main() { // Connection to leveldb db, _ := leveldb.OpenFile("/home/user/Documents/ethereum/network_ubuntu/geth/chaindata", nil) iter := db.NewIterator(nil, nil) for iter.Next() { // Remember that the contents of the returned slice should not be modified, and // only valid until the next call to Next. key := iter.Key() value := iter.Value() fmt.Printf("Key: %x \nValue: %x\n\n\n", key, value) } iter.Release() err := iter.Error() fmt.Printf("Error: %v\n\n\n", err) }
La API para interactuar con LevelDB (para obtener el valor key) está en el archivo schema.go (directorio /core/rawdb en el proyecto go-ethereum: https://github.com/ethereum/go-ethereum/blob/9b9a1b677d894db951dc4714ea1a46a2e7b74ffc/core/rawdb/schema.go)
Modificar cliente Geth Ethereum y compilarlo
Para modificar el cliente Geth hay que descargarse e código fuente y compilarlo. Las instrucciones están en https://geth.ethereum.org/docs/getting-started/installing-geth. Lo haré en una máquina virtual Windows 10, siguiendo esas instrucciones, pero al compilar aparece un error relativo a:
undefined reference to `__imp___iob_func'
Posiblemente sea por tema de versiones del compilador mingw que se descargó con el instalador de paquetes choco. Buscando en internet, un error similar se resuelve instalando otro paquete mingw https://forum.golangbridge.org/t/running-gcc-failed-exit-status-1/29465. Desistalo mingw con el comando choco uninstall mingw, y lo instalo según ese enlace, descargando desde https://jmeubank.github.io/tdm-gcc/articles/2021-05/10.3.0-release
Nota: de hecho, parece que el error está relacionado con la versión de mingw, parece que las versiones Universal C Runtime (UCRT) provocan el error, mientras que la versión MS Runtime (MSVCRT) no.
Al compilar go-ethereum esta vez sí resulta exitoso, generando los correspondientes ejecutables en C:/Users/usuario/go/bin
Sincronizar el cliente geth modificado a la blockchain modificada (base de datos leveldb modificada)
Volviendo con nuestro ejemplo de la red privada, con el nodo en Ubuntu donde hemos tocado la base de datos leveldb para borrar las transacciones del bloque 12, modificamos el código fuente de geth para tratar de saltar la validación de ese bloque 12, y lo lanzamos desde el otro nodo en Windows, pero esta vez no será un nodo validador o Sealer.
En este nodo Windows, empezaremos inicializado la cadena con el bloque génesis y previamente borrando el contenido de las carpetas que contienen la blockchain, para forzar así a que se descarguen todos los bloques.
Los comandos serán:
.\geth.exe init --datadir network_win2 '.\network_win2\bloque_genesis.json' .\geth.exe --datadir network_win2 --networkid 888 --port 30304 --nat extip:192.168.1.151 --bootnodes "enode://f1125b2e5d5d2816789c1fc06f8127f8cf6fd6247517d865710267527f74475aa58fa0986babe52ea0ea47b8b38b90cd82569d3661045e66b88fee11d0f674c0@192.168.1.53:30304?discport=30304" --syncmode 'full'
Se irán generando errores, y basta seguir el código fuente para tratar de ir viendo dónde se van produciendo, para modificarlo y así salvar los errores. Varias funciones son las afectadas, la principal block_validator.go
Llega un momento en el que puede saltarse la validación del bloque 12, es decir, el cliente Windows acepta ese bloque modificado, aunque no prosigue descargando la cadena debido a la inconsistencia que se produce en los Merkel Trie para el bloque 13 (quizá podría solventarse también…)
Repitiendo con un modo de log más detallado:
[…] Partiremos de un caso de una blockchain privada con el cliente geth, y de las herramientas en go, tomando como base los anteriores posts Ethereum Blockchain privada y Ethereum y LevelDB: Leer, Modificar y Compilar Geth. […]
[…] explorando base de datos leveldb, donde se explora el State Trie y Ethereum Blockchain privada, Ethereum y LevelDB: Leer, Modificar y Compilar Geth donde se ve el entorno de una red privada y más) veremos con detalle qué ocurre con la creación […]
[…] la entrada anterior Ethereum y LevelDB: Leer, Modificar y Compilar Geth se trataba de modificar la base de datos LevelDB local de un nodo Ethereum, eliminando los datos de […]