Escribiendo un blockchain en RxJS

Diego Barahona
Web3 Costa Rica
Published in
7 min readNov 30, 2017

--

¿Has escuchado sobre el término blockchain? De seguro que sí. Todos están hablando sobre blockchain hoy en día, pero no todos entienden en realidad como funciona. No, no se trata sobre bitcoin, ni siquiera es sobre criptomonedas; estas solamente están basadas en un blockchain. Pero incluso hay criptomonedas que no utilizan un blockchain.

Entonces ¿qué es? Desde una perspectiva técnica, es solo una lista de registros almacenada en un lugar, como una base de datos, pero en una forma muy peculiar. Cada modificación (o transacción) es asegurada criptográficamente, usando información de la transacción, o bloque, anterior (por eso el nombre chain o cadena). Esto significa que si actualizas un bloque viejo, tendrás que actualizar todos las transacciones subsecuentes en la cadena. Esto lo hace muy difícil de romper.

¿Por qué decidí construir un blockchain en RxJS? Simple, por diversión. Me encanta Rx y todo el paradigma de programación reactiva. Y si te tomas un momento para pensarlo, tiene sentido juntar el stream de un observable y los blockchains. Ambos tienen valores emitidos en el tiempo, y ambos deberían de ser inmutables. Además, uno de las grandes características de un blockchain es que puede ser distribuido y descentralizado. Para poder lograr lo anterior se debe de escuchar y reaccionar a los cambios, y Rx es extraordinariamente bueno en esto.

Necesitamos explorar unos cuantos conceptos antes de poder escribir algunas lineas de código

  • Hash es una función criptográfica que traduce cualquier parámetro en una representación fija de esa parámetro. Cuando se pasa un parámetro a la función de hash, esta retornará el mismo resultado hash, si alguna cosa cambia en el parámetro, entonces el hash es completamente distinto
  • Minería es la acción de generar un hash para un parámetro respetando el PoW con el propósito de obtener una recompensa por hacerlo
  • PoW o prueba de esfuerzo por sus siglas en inglés, es un test usado en el minado de un blockchain. La idea es hacer el minado mas difícil ajustando los requerimientos de aceptación de un hash. Por ejemplo, un hash debe contener 4 ceros al inicio, entonces el minero debe de generar un hash una y otra vez hasta conseguir uno que pase el PoW test
  • Nodo es una sola unidad en una red. Usualmente estos son los mineros, conectados a la red de Nodos. Estos actúan tanto como clientes y servidores, reenviando los mensajes

El reto

Necesitamos crear una cadena de bloques y que están compuestos por:

  • los datos que deseamos almacenar
  • el índice del bloque
  • la fecha y hora de creación como un timestamp en milisegundos
  • el hash ingresando la información del bloque
  • el hash del bloque previo (esto es lo que lo convierte una cadena)
  • el numero de ciclos para aprobar el PoW, llamado nonce

También necesitamos crear un grupo de datos, esperando por ser agregados a la cadena. A esto le llamaremos mina.

Crearemos un blockchain descentralizado, entonces necesitaremos que la cadena y la mina se puedan compartir.

Cada cadena y mina forma parte de un Nodo, el cual reenviará nuevos bloques así como nuevas peticiones de minado. Éste necesitará verificar cada nuevo bloque recibido, validar la cadena e incluirlo en su propia cadena. Si en algún momento recibe un bloque invalido, desconectarse de ese Nodo.

El Codigo

class Node {
static getEventHistory(replay$) {
const events = [];
replay$.takeUntil(time(1))
.subscribe(event => events.push(event));
return events;
}
static getLastEvent(replay$) {
const events = Node.getEventHistory(replay$);
return events.length ? events.pop() : Node.base;
}
get lastBlock() {
return Node.getLastEvent(this.chain$);
}
constructor(id) {
this.id = id;
this.chain$ = new ReplaySubject();
this.mine$ = new BehaviorSubject({});
}
}
Node.base = { hash: 0, index: -1 };

¿Por qué ReplaySubject y BehaviorSubject?

Necesitamos que la cadena sea capaz de recordar todos los bloques anteriores para poder validar la información, y por supuesto, por que ahí es donde toda la información va a ser almacenada. Por eso, ReplaySubject es una solución perfecta dado que están hechas para recordar todos los eventos emitidos.

ReplaySubject es una solución perfecta dado que están hechas para recordar todos los eventos emitidos

Para la mina realmente no necesitamos el historial de las peticiones de minado. Sin embargo, si necesitaremos la última petición de minado, solo para asegurar que no tendremos ningún "eco" cuando reenviemos los eventos de la red.

También ocupamos una identificación y una referencia al ultimo bloque minado. Por desgracia, ReplaySubject no tiene un método getValues (como BehaviorSubject) entonces necesitamos un método propio para extraer el ultimo evento. Esos son los métodos estáticos getEventHistory y getLastEvent.

Petición de minado

class Node {
...
process(data) {
this.mine$.next({
data,
_ref: this.id,
_time: Date.now();
});
}
}

Vamos a enviar información adicional ademas de los datos, una referencia al identificador del nodo y el tiempo actual. Hablaremos al respecto más adelante.

Minado

class Node {
...
listen() {
this.mine$
.filter(event => event && event.data)
.concatMap(({ data }) => this.mine(data))
.filter(block =>
Node.ValidateBlock(this.lastBlock, block))
.subscribe(this.chain$);
}
mine(data){
const block = {
data,
nonce: 0,
minedBy: this.id,
prev: this.lastBlock.hash,
index: this.lastBlock.index + 1,
timestamp: Date.now
};
return interval(0)
.do(nonce => block.nonce = nonce)
.map(() => SHA256(JSON.stringify(block)))
.first(hash => Node.validateDificulty(hash))
.map(hash => ({ ...block, hash: hash.toString() }));
}
}

En el momento en que un nuevo evento entra en la mina inmediatamente lo procesamos. Lo primero es asegurarse de que tiene algún dato para ser procesado.

Luego, el calculo del hash. Empezamos creando el nuevo bloque pero sin incluir el hash. Usamos el último bloque de la cadena para obtener su hash e índice.
Necesitamos generar un hash que apruebe el PoW test, pero si ingresamos los mismos parámetros a la función de hash, esta retornará el mismo valor, por eso necesitamos que el nonce incremente 1 cada vez. Entonces, generamos un nuevo hash y le solicitamos a RxJS el primer evento que apruebe la validación.

Continuando con el ciclo, una vez que obtenemos el valor del minado, lo validamos. ¿Por qué? Cuando estuvimos ocupados generando el hash, algún otro Nodo pudo haberlo encontrado. Si todo hasta acá resultó positivo, añadimos el bloque a la cadena.

Validaciones

class Node {
...
static validateChain(lastBlock, block) {
if (!lastBlock || !Node.validateBlock(lastBlock, block)) {
return;
}
return block;
}
static validateBlock(lastBlock, block) {
return (
Node.validateParent(lastBlock, block) &&
Node.validateDifficulty(block.hash) &&
Node.validateHash(block)
);
}
static validateHash(block) {
const { hash } = block;
const tempBlock = { ...block };
delete tempBlock.hash;
return hash === SHA256(JSON.stringify(tempBlock)).toString();
}
static validateParent(lastBlock, block) {
return lastBlock.hash === block.prev &&
lastBlock.index + 1 === block.index;
}
static validateDifficulty(hash) {
return /^00/.test(hash);
}
}

validateChain es usado en un método tipo map-reduce, donde tomas el resultado previo y el resultado actual y retornas un nuevo valor. Acá retornaremos el bloque actual solo si es válido.

validateParent se asegura de que el lastBlock de verdad esté conectado al nuevo bloque con el correspondiente hash e índice.

validateDifficulty verifica el PoW, en este caso solo estamos solicitando 2 ceros iniciales. Si incrementamos esto a 4 en vez de 2, tomaría un buen tiempo minar cada bloque.

validateHash simplemente hace lo que su nombre indica, genera un hash en base al contenido del bloque, y lo compara con el hash del bloque.

Conexiones

Hasta ahora, nuestro Nodo está listo para ser usado, es capas de minar nuevos datos, generar el hash y, una vez validado, añadirlo a la cadena. Pero si queremos poder distribuirlo necesitaremos compartir y conectar nuestra cadena y mina a otros nodos.

class Node {
...
connect(node) {
const history = Node.getEventHistory(node.chain$);
const isValid = history.reduce(Node.validateChain, Node.base);
if (!isValid) {
this.invalidate(node.id);
}
this.connectMine(node);
this.connectChain(node);
}
connectMine(node) {
node.mine$
.skip(1)
.filter(event => event._ref !== this.id)
.filter(event => event._time > this.mine$.value._time)
.subscribe(this.mine$);
}
connectChain(node) {
node.chain$
.distinctUntilKeyChanged('hash')
.scan((lastBlock, block) =>
Node.validateChain(lastBlock, block) ? block :
this.invalidate(node.id)
, Node.base)
.filter(block => block.index > this.lastBlock.index)
.subscribe(this.chain$);
}
invalidate(id) {
throw new Error(`Disconnected from node ${id}`);
}
}

Antes de intentar conectar nuestra cadena y mina con alguien mas, primero necesitamos asegurar que su cadena sea valida recorriendo su historial y validando cada bloque.

Para conectar nuestras minas, primero debemos saltarnos el primer valor, esto por que es un BehaviorSubject y el primer valor siempre es un evento del pasado. Necesitamos la propiedad _ref para saber si la petición de minado fue producida por nosotros. Si ese es el caso este evento seria un efecto“eco”, que significa que nosotros emitimos el evento y la red a la que estamos conectados lo envió de vuelta hacia nosotros. Algunos nodos pueden tener un retraso y nos enviarán eventos previos, nos protegeremos de estos usando la propiedad _time de la petición.
Si todo pasa sin problemas, enviamos el evento a nuestra mina y dejamos que cualquiera que nos escuche, pueda intentar minar el nuevo bloque también.

Para la cadena primero debemos de eliminar el efecto "eco" filtrando los bloques con el mismo hash, luego validamos todos los bloques usando el bloque anterior. De nuevo, esta cadena podría enviar un bloque que ya fue minado, por lo tanto omitiremos estos si es el caso, si no fuera el caso entonces son agregados a nuestra cadena.

El resultado

En este ejemplo, hay un solo nodo con el bloque Genesis, este será nuestro primer bloque in la cadena. Cualquier otro Nodo que quiera unirse a nuestra cadena contendrá todos los bloques minados hasta ese punto.

Todos los nodos conectados intentarán minarlo, pero solo el mas rápido será el ganador

Nótese que incluso si un request de minado es enviado a un Nodo en especifico, no significa que ese Nodo sea el que mine el bloque. Todos los nodos conectados intentarán minarlo, pero solo el mas rápido será el ganador, y será agregado a la cadena de todos los demás.

Conclusiones

Blockchain no funciona con magia, pero sí es una manera muy buena para resolver ciertos problemas, particularmente cuando se necesita descentralización o distribución de contenido.

Ahora que comprendemos mas sobre como un blockchain funciona, la diferencia entre esto y una criptomoneda, como el Bitcoin, son los datos que se agregan a los bloques. Bitcoin tiene un protocolo especial para esto, al igual que todas las otras monedas. Pero en realidad se puede almacenar imágenes, textos, virtualmente cualquier cosa en un blockchain, sin embargo esto no significa que sea lo ideal para todos los casos.

Artículo original en ingles

Sobre BlockchainCR

Blockchain Costa Rica es una organización fundada con la intención de convertir a Costa Rica en un país líder en el uso tecnologías de cadena de bloques, redes descentralizadas y libro mayor distribuído. Todos son bienvenidos!

blockchaincr.com

--

--