Armando una Blockchain (11)

Published on Author lopezLeave a comment

Anterior Post
Siguiente Post

En estos dias estuve refactorizando la implementación de la blockchain de mi proyecto personal en JavaScript/NodeJS:

https://github.com/ajlopez/SimpleBlockchain

En el anterior post había mostrado algunos de los tests que escribí siguiendo el flujo de TDD  (Test-Driven Development). Hoy, quiero mostrar la implementación actual, que es la evolución de la anterior.

La implementación está en:

https://github.com/ajlopez/SimpleBlockchain/blob/master/lib/blockchains.js

Hay ahí una “clase” JavaScript, implementada como función:

function Blockchain(block) {
    var self = this;
    var blocks = [block];
    var blockstore = stores.blockstore();
    blockstore.save(block);
    
    this.bestBlock = function () { return blocks[blocks.length - 1]; }

En mi nueva implementación, estoy usando un almacén de bloques (por ahora en memoria). Este objeto puede registrar/salvar un bloque, recuperar un bloque por hash, recuperar bloques del mismo número de altura. Pero ahora le agregué recuperar bloques que tengan en mismo bloque padre, por hash del bloque padre. De esta forma, puedo recuperar los hijos inmediatos de un bloque. y también sus descendientes. Ahora puedo reconstruir desde ese almacén/store todo el árbol de bloques partiendo de uno inicial.

Vean que el estado de la blockchain tiene una implementación ingenua, sencilla: solo es un arreglo JavaScript, con índice el número de bloque, comenzando con el bloque génesis, que tiene número 0. Mi plan es refactorizar esta implementación para soportar muchos bloques, usando una lista grabada en disco. Pero por ahora, usando esta implementación ingenua, puedo armar la conducta esperada de la aplicación que estoy escribiendo.

Un método que exponso hacia afuera es agregar un bloque:

this.add = function (block) {
    if (blockstore.hasBlockHash(block.hash))
        return;
        
    blockstore.save(block);
    
    if (getUnknownAncestor(block) != null)
        return;
    
    tryAdd(block);
}

Si este bloque es conocido, entonces implica que ya fue procesado, y se sale de la función.

Si no, el bloque es agregado al almacén en memoria, y se calcula el primer ancestro no conocido, pues bien puede que todavía no haya llegado al nodo. Si hay algún ancestro desconocido, no se puede formar todavía una cadena con este bloque, y salimos de la función.

Si el bloque tiene una cadena de ancestros que se termina conectando con el bloque génesis, tratamos de agregarlo a la cadena principal:

function tryAdd(block) {
    if (isBestBlockChild(block))
        blocks.push(block);
    else if (isBetterBestBlock(block))
        tryFork(block);
        
    tryChildren(block);
}

Si es un hijo directo del mejor bloque conocido, lo agregamos a la cadena. Si no, pero es un mejor bloque (por ejemplo, porque tiene una mejor altura que la cadena principal conocida), se intenta armar un fork de la cadena, posiblemente cambiando a otra cadena mejor. En todo caso, se procesan sus hijos inmediatos: tal vez la aparición del bloque permite completar cadenas que estaban pendientes de proceso.

Estos son los predicados que uso:

function isBestBlockChild(block) {
    var bblock = self.bestBlock();
    
    return bblock.hash === block.parentHash && bblock.number === block.number - 1;
}

function isBetterBestBlock(block) {
    var bblock = self.bestBlock();
    
    return bblock.number < block.number;
}

Este es el proceso de los hijos:

function tryChildren(block) {
    var children = blockstore.getChildren(block.hash);

    for (var n in children)
        tryAdd(children[n]);
}

Noten que es una implementación ingenua que implica recursión. Puedo refactorizar el código para no usar recursión.

Dado un bloque, esta función calcula la cadena de ancestros que se une en alguna parte a la cadena principal, y cambia a esa lista como blockchain actual:

function tryFork(block) {
    var newbranch = [block];
    var parentHash = block.parentHash;
    
    while (parentHash && blockstore.hasBlockHash(parentHash)) {
        var parent = blockstore.getByHash(parentHash);
        
        if (parent.hash === blocks[parent.number].hash)
            return switchToFork(newbranch);
            
        newbranch.push(parent);
        
        parentHash = parent.parentHash;
    }
}

Ubicando el ancestro no conocido de un bloque:

function getUnknownAncestor(block) {
    var parentHash = block.parentHash;

    while (parentHash && blockstore.hasBlockHash(parentHash)) {
        var parent = blockstore.getByHash(parentHash);
        
        if (parent.hash === blocks[parent.number].hash)
            return null;
        
        parentHash = parent.parentHash;
    }
    
    return parentHash;
}

Cambiando la cadena actual a una nueva cadena:

function switchToFork(newbranch) {
    for (var n = newbranch.length; n-- > 0;) {
        var block = newbranch[n];
        blocks[block.number] = block;
    }
}

Todos los tests pasan. Como mencioné, mi plan es ir mejorando la implementación. Pero en mi flujo de trabajo con TDD prefiero ir dando pasos pequeños (“baby steps”), y luego “make it works” para recién ahí pasar a “make it right”, “make it fast”. He tenido buenas experiencias usando este manera de ir escribiendo código, obteniendo implementaciones simples, sólidas, probadas, y fácilmente cambiables y mejorables, donde cada artefacto se agrega ante un caso de uso/test específico, en lugar de armar “arquitecturas de astronauta”.

Animado por este resultado, también refactoricé mi implementación de C#:

https://github.com/ajlopez/BlockchainSharp

Comencé a usar un almacén de bloques que usa GetByParentHash en mi código de BlockChain:

https://github.com/ajlopez/BlockchainSharp/blob/master/Src/BlockchainSharp/Core/BlockChain.cs

Escribí sobre mi anterior implementación en este post.

Nos leemos!

Angel “Java” Lopez

@ajlopez

Leave a Reply

Your email address will not be published. Required fields are marked *