Es una base de datos publica que está distribuida en múltiples nodos
Todos los datos que entren deben de ser confiable por todos los nodos
Podrías por ejemplo tener un 49% de los nodos que produjesen datos erróneos o malintencionados y la red podría recuperarse de ese desajuste
Un Blockchain implica multiples bloques que contienen la información que queremos en nuestra base de datos
Struct de un blockchain
type blockChain struct {
blocks []*block
}
En este struct básicamente tenemos un slice de punteros de bloques
Básicamente son los objetos que conforman un blockchain este tiene que tener 3 básicos como mínimo
Atributos
- Hash del propio bloque
- Hash del último bloque creado (Es el que nos permite enlazar bloques)
- El dato que guardamos pueden ser imagenes textos numeros etc
Struct de un bloque básico
type block struct {
Hash []byte
Data []byte
PrevHash []byte
}
Para el calculo del hash como standard se usa el algoritmo de encriptado SHA-256 debido a su equilibrio entre coste computacional y solidez si quieres aprender mas sobre este algoritmo de encriptación: Pincha Aqui
Para calcular el Hash usaremos este método:
func (b *block) CalculateHash() {
// Explicado mas abajo
info := bytes.Join([][]byte{b.Data, b.PrevHash}, []byte{})
// Lo encriptamos
hash := sha256.Sum256(info)
// Creamos una copia y se la asignamos al hash del struct
b.Hash = hash[:]
}
Lo que hace es juntar los slice de datos que se le pasan como primer parámetro [][]byte{b.Data, b.PrevHash}
(Pueden ser cualquier cantidad) teniendo como separador el 2º parámetro []byte{}
(En este caso vacío)
un ejemplo:
1º Parámetro
AAAA
BBBB
2º Parámetro
CC
Resultado:
AAAACCBBBB
Documentación función bytes.Join
Para crear un bloque deberías de llamar a esta función para asegurarte de que se calcula el hash por lo tanto sería conveniente hacer "privado" el struct del bloque para que nadie pueda instanciar un bloque de otra forma que no sea a traves de esta función
func CreateBlock(data string, prevHash []byte) *block {
// Creamos normal un struct
block := &block{
Hash: []byte{},
// Aqui adicionalmente pasamos de string a bytes
Data: []byte(data),
PrevHash: prevHash,
}
// Llamamos a la función que creamos previamente
block.CalculateHash()
// Lo retornamos
return block
}
para añadir un bloque a la blockchain debemos usar la anterior funcion
// Recibimos una blockchain
func (chain *blockChain) AddBlock(data string) {
// Cogemos el ultimos bloque
prevBlock := chain.Blocks[len(chain.Blocks)-1]
// A traves de la función de antes creamos el nuevo bloque
newBlock := CreateBlock(data, prevBlock.Hash)
// Se lo añadimos a la blockchain
chain.Blocks = append(chain.Blocks, newBlock)
}
Como hemos visto siempre referenciamos al hash del anterior bloque pero que pasa con el primer bloque este es imposible que pueda tener ningun hash previo ya que este es el primero, a este bloque se le llama "Genesis Block" que representa el primer bloque de la blockchain
Lo crearemos a traves de este método:
func Genesis() *block {
return CreateBlock("Genesis", []byte{})
}
Para crear la blockchain debemos usar la función anterior de tal manera:
func InitBlockChain() *blockChain {
return &blockChain{[]*block{Genesis()}}
}
Si quieres ver el estado del repositorio hasta ahora ve a este commit
Nuestro main sacará por consola esto:
Bloque:
Previous Hash:
Data in Block: Genesis
Hash: 81ddc8d248b2dccdd3fdd5e84f0cad62b08f2d10b57f9a831c13451e5c5c80a5
Bloque:
Previous Hash: 81ddc8d248b2dccdd3fdd5e84f0cad62b08f2d10b57f9a831c13451e5c5c80a5
Data in Block: 1º Block Despues del Genesis
Hash: 11f27e8a6ee5b1b8d1eada2d6ce758bd7028d86b47dcac4ac27b202eaeedead2
Bloque:
Previous Hash: 11f27e8a6ee5b1b8d1eada2d6ce758bd7028d86b47dcac4ac27b202eaeedead2
Data in Block: 2º Block Despues del Genesis
Hash: d81bbc87021060a1925f297a98fe3b6236481fe42e82c856bb42ea119b3f72bf
Bloque:
Previous Hash: d81bbc87021060a1925f297a98fe3b6236481fe42e82c856bb42ea119b3f72bf
Data in Block: 3º Block Despues del Genesis
Hash: 3f6628fe789a6518e1dfe77075e9f8f88028def45d281580b9f26d0590ee8317
No importa la veces que lo ejecutemos siempre será lo mismo, por lo tanto al ejecutar varias veces este main obtendras varias copias de la blockchain la manera de saber si está corrupto es comparando los hashes de las diferentes copias
Hay diferentes Algoritmos de consenso (Consensus algorithms / Proof Algorithms)
- Proof Of Work
- Proof Of Steak
- y mas...
Básicamente lo que quiere decir este es que forzamos a la red a realizar trabajo para añadir un bloque a la blockchain
Este "trabajo" es computacional, cuando hablamos de mineros minando Bitcoin nos referimos a esta "Prueba de trabajo" para añadir bloques a la blockchain la razón por la que estos consiguen bitcoins es esencialmente porque potencian a la red a escribir mas rápido ese bloque
Adicionalmente hacen que el dato de los bloques sea mas seguro, el Proof of work viene de la mano de la validación de esa prueba que es cuando un usuario hace el trabajo necesario para añadir ese bloque se requiere que demuestre ese trabajo realizado de ahi el nombre de "Prueba de trabajo" (Proof of Work)
Un concepto importante es que el trabajo realizado debe ser dificil pero la demostración del mismo debe ser relativamente fácil
Los pasos a seguir van a ser:
- Coger el dato del bloque
- Crear un contador que empiece en el 0 (Llamado nonce) que será incrementado en +1 teóricamente infinitas veces
- Crear el hash del dato + el nonce
- Verificar el hash para ver si cumple determinados requerimientos de aqui viene la llamada "dificultad"
- Requerimientos:
- Los primeros bytes deben contener 0
- Requerimientos:
Digamos que quieres escribir un bloque, si el hash de ese nuevo bloque no cumple los requerimientos tendrás que volver a generar el hash ese reintento es a lo que se le llama dificultad teniendo que recrear otro hash para ver si cumple con los requerimientos
En la proof of Work original de bitcoin la especificación se llama "Hash cash"
En la dificultad original se requería que 20 bytes consecutivos del hash fueran 0, con el paso del tiempo esa dificultad se ha incrementado por lo tanto se requiere de mas trabajo para poder escribir un bloque
Podemos aumentar la dificultad por ejemplo haciendo que el requerimiento de 20 ceros pase a 50
Para empezar definiremos una constante para definir la dificultad: const difficulty = 18
Tendremos una dificultad estática en nuestra prueba de concepto pero si quieres crear un algoritmo de blockchain de verdad tendrás que crear algún tipo de función que incremente la dificultad de poco en poco dentro de un periodo de tiempo largo, básicamente quieres que esto suceda para aumentar el número de mineros en la red y la potencia creciente de los ordenadores que pudieran venir en el futuro. Queremos que el tiempo de minado de un bloque sea uniforme
type ProofOfWork struct {
// Representa un puntero a un bloque
Block *block
// Representa un puntero que es el requerimiento que hemos definido arriba
// para entender esto necesitas saber como los bytes se comportan en el ordenador
Target *big.Int
}
Con está función desde un puntero a un bloque obtendríamos un puntero a una prueba de trabajo
func NewProof(b *block) *ProofOfWork {
// Crearemos nuestro target
target := big.NewInt(1)
// Despues tendremos que coger el 256 que es el número de bytes de los hashes
// y extraer la dificultad de ellos , para despues hacer un left shift (Lsh) de los bytes de ese número
target.Lsh(target, uint(256-difficulty))
// Ahora cogemos ese valor al que le hemos hecho el left shifted
// y lo metemos a nuestro struct
pow := &ProofOfWork{
Block: b,
Target: target,
}
// Despues lo retornamos
return pow
}
Este método será el que inicie los datos de la prueba de trabajo tendremos que combinar:
- Hash del bloque previo
- Hash del dato
- Hash del contador
- Hash de la dificultad
func (pow *ProofOfWork) InitData(nonce int) []byte {
// usaremos el bytes.Join
data := bytes.Join(
[][]byte{
// Meteremos el prevHash y el dato
pow.Block.PrevHash,
pow.Block.Data,
// Adicionalmente meteremos el nonce y la dificultad
// Acordarse de cuando hablamos del algoritmo de proof of work
// Crear el hash del dato + el nonce
// Para simplificar las cosas crearemos una nueva
// función que explicaremos a continuación y castearemos
// los int a int 64 para pasarseles a ToHex()
ToHex(int64(nonce)),
ToHex(int64(difficulty)),
},
[]byte{},
)
return data
}
// De un int64 obtendremos un slice de bytes simplemente
func ToHex(num int64) []byte {
buff := new(bytes.Buffer)
err := binary.Write(buff, binary.BigEndian, num)
if err != nil {
log.Panic(err)
}
return buff.Bytes()
}
func (pow *ProofOfWork) Run() (int, []byte) {
var intHash big.Int
var hash [32]byte
// Iniciamos el contador
nonce := 0
// Haremos una especie de do while (Si venís de otro lenguaje)
// En este loop prepararemos nuestro dato y
// luego lo hashearemos a sha-256
// Seguidamente convertiremos ese Hash a un biginteger
// Por ultimo compararemos ese biginteger generado con el del target
// Que estará dentro de nuestro struct de proof of work
for nonce < math.MaxInt64 {
// Llamaremos a nuestro InitData para preparar el dato
data := pow.InitData(nonce)
// Los hashearemos
hash = sha256.Sum256(data)
// Con fines de demostración hacemos un log en pantalla
fmt.Printf("\r%x", hash)
// haremos una copia del slice
intHash.SetBytes(hash[:])
// Ahora compararemos el hash generado con el del target
if intHash.Cmp(pow.Target) == -1 {
// Si el hash generado es menor nos salimos del bucle
// Ya que esto quiere decir que hemos podido firmar el bloque
break
}
// De otra forma seguimos incrementando el contador para repetir el proceso
nonce++
}
// hacemos un salto de línea para separar trazas
fmt.Println()
// retornamos el contador y una copia del slice
return nonce, hash[:]
}
Ahora necesitaremos cambiar el código del bloque para añadir el contador y poder implementar la validación de los requerimientos
type block struct {
Hash []byte
Data []byte
PrevHash []byte
Nonce int // Campo nuevo
}
func CreateBlock(data string, prevHash []byte) *block {
block := &block{
Hash: []byte{},
Data: []byte(data),
PrevHash: prevHash,
Nonce: 0, // inicializamos el nonce a 0
}
block.CalculateHash()
return block
}
Eliminaremos tambien la función CalculateHash()
func (b *block) CalculateHash() {
info := bytes.Join([][]byte{b.Data, b.PrevHash}, []byte{})
hash := sha256.Sum256(info)
b.Hash = hash[:]
}
y cambiamos completamente la función que creaba bloques
Para rellenar con el nonce nuestro struct:
- Crear la prueba de trabajo
- Ejecutar la prueba de trabajo
- Informar el nonce y el hash en el bloque
func CreateBlock(data string, prevHash []byte) *block {
block := &block{
Hash: []byte{},
Data: []byte(data),
PrevHash: prevHash,
Nonce: 0, // inicializamos el nonce a 0
}
// creamos la prueba de trabajo
pow := NewProof(block)
// Y la iniciamos
nonce, hash := pow.Run()
// Cuando hayamos completado la prueba de trabajo
// podremos rellenar el nonce y el hash obtenido
block.Hash = hash[:]
block.Nonce = nonce
return block
}
Básicamente lo que se quiere hacer aqui es ejecutar el ciclo que ha hecho la prueba de trabajo una vez mas para ver si ese hash que se ha obtenido de la 1º ejecución es válido esto podría evitar por ejemplo que de casualidad a fuerza bruta demos con un hash correcto
Es decir no valdría con solo proveer del hash correcto sino tambien proveer de los pasos que has hecho para llegar a ese hash gracias al nonce (contador) por fuerza bruta sería inviable ya que por cada hash tendrías que ejecutar N veces el nonce es decir:
Por fuerza bruta generamos (pongamos 3):
- 111222333
- Generamos el par con nonce 1
- Generamos el par con nonce 2
- etc
- 222333444
- Generamos el par con nonce 1
- Generamos el par con nonce 2
- etc
- 333444555
- Generamos el par con nonce 1
- Generamos el par con nonce 2
- etc
Por lo tanto si ya es dificil dar con el hash correcto imagínate tener que probar N veces con el nonce se hace prácticamente inviable ya que da millones y millones de combinaciones por no decir trillones...
func (pow *ProofOfWork) Validate() bool {
var intHash big.Int
// Aqui está el truco de la validación explicada mas abajo
data := pow.InitData(pow.Block.Nonce)
hash := sha256.Sum256(data)
intHash.SetBytes(hash[:])
return intHash.Cmp(pow.Target) == -1
}
Al crear el bloque el nonce que se le pasa es 0 pero aqui directamente es uno que se ha calculado por lo tanto de primeras crearemos el hash correcto de aqui viene lo que decíamos antes de que probar la validez de la prueba de trabajo es relativamente fácil
Como dijimos antes:
- Realizar la prueba de trabajo es dificil
- Pero probarla es relativamente fácil
Si quieres cambiar el hash de un bloque vas a tener que recalcular el hash propio (que eso ya es bastante costoso) y los hashes de los bloques siguientes y aparte hacer creer que ese bloque que has metido es un bloque confiable
Podemos validar un bloque relativamente rápido pero el trabajo para crear el bloque y firmarle es muy dificil por lo tanto podemos afirmar que es muy dificil manipular un bloque por una o una gran cantidad de entidades
Por últimos tenemos que añadir esta prueba de trabajo a nuestro main
Antes del final del loop añadiremos:
pow := model.NewProof(block)
fmt.Printf("Prueba de Trabajo: %s\n\n", strconv.FormatBool(pow.Validate()))
Usaremos BadgedDb que es una base de datos clave valor de bytes es decir no existe SQL columnas tablas etc se persiste en un fichero que definimos
Dependencia: go get github.com/dgraph-io/badger
En la especificación original de BitCoin cada bloque tendría su propio fichero esto se hace asi por tema de rendimiento ya que solo tendríamos que lidiar con 1 bloque a la vez en vez de varios en nuestr caso no lo haremos asi ya que nuestra blockchain va a ser pequeña
para poder persistir nuestro blockchain en badgeDb tendremos que añadir al struct un nuevo campo
type BlockChain struct {
LastHash []byte
Database *badger.DB
}
Ahora en vez de guardar todos los bloques se guardará el ultimo hash del bloque que estará persistido en la base de datos