Ethereum y LevelDB: Leer, Modificar y Compilar Geth

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 )

estructura de archivos de una blockchain Ethereum

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

datos Raw de la transacción desde el script en go para acceder a LevelDB y el cliente geth

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:

datos obtenidos desde la LevelDB
Datos obtenidos desde el cliente Geth

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

cliente geth mostrando el bloque modificado (sin transacciones)

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'
error al compilar go-ethereum

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…)

Cliente Geth modificado, lee el bloque 12, pero no descarga más, y sigue a la escuha
El bloque 12 se ha descargado

Repitiendo con un modo de log más detallado:

error en el merkel trie para el bloque 13

2 Comments

  1. Ethereum: explorando base de datos leveldb – Abraza la Web · sábado, 14 enero, 2023 Reply

    […] 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. […]

  2. Ethereum: leer LevelDB, Smart Contract y Storage Trie – Abraza la Web · viernes, 20 enero, 2023 Reply

    […] 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 […]

Leave a Reply

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.