Transacciones privadas con Hyperledger Besu

Hyperledger Besu (conocido como Pantheon) es un cliente Ethereum de código abierto desarrollado y escrito en Java. Se puede ejecutar en la red pública Ethereum o en redes privadas permisionadas, así como en redes de prueba como Rinkeby, Ropsten y Görli. Hyperledger Besu incluye varios algoritmos de consenso, incluidos PoW, PoA e IBFT, y tiene esquemas de permisos integrales diseñados específicamente para su uso en un entorno de consorcio.

Besu tiene varias características que lo hacen atractivo para su uso, pero en este documento voy a referirme a la privacidad, siendo ésta la capacidad de mantener las transacciones privadas entre las partes involucradas, permitiendo que otras partes no puedan acceder al contenido de la transacción, quién envía o lista de los participantes. Besu utiliza un gestor de transacciones privada para implementar la privacidad (ORION).

Besu al ser un cliente Ethereum es compatible con las herramientas que usamos en el día a día para hacer nuestros proyectos, como Web3J, Truffle y Remix. Para comenzar con el proyecto se requiere que tengamos por lo menos dos nodos Besu y Orion respectivamente para el manejo de las transacciones privadas.

Para agilizar el proceso existe un Docker con toda la configuración necesaria para correr 3 nodos con Privacidad habilitada (seguir tutorial), teniendo en cuenta de ejecutar el script con la siguiente opción:

$ ./run-privacy.sh -c ibft2.

Para esta prueba vamos a realizar transacciones privadas entre sólo dos de los nodos configurados. Ahora vamos a crear un identificador de grupo (privacyGroupId) con el cual se identifican los nodos participantes de las transacciones privadas. Cada nodo Besu tiene su respectivo nodo Orion del cual vamos a usar sus llaves públicas para generar un identificador de grupo.

La siguiente lista muestra la llave pública de cada nodo Orion que se está ejecutando en el Docker:

Orion 1
A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=
Orion 2
Ko2bVqD+nNlNYL5EE7y3IdOnviftjiizpjRt+HTuFBs=
Orion 3
k2zXEin4Ip/qBGlRkJejnGWdP9cjkK+DAvKNW31L2C8=

Aquí se indican las direcciones de cada uno de los nodos Besu:

Besu 1
http://localhost:20000
Besu 2
http://localhost:20002
Besu 3
http://localhost:20004

Para crear el grupo vamos usar la llave pública del Orion 1 y el Orion 2 y generamos la petición de la siguiente manera:

{
"jsonrpc":"2.0",
"method": "priv_createPrivacyGroup",
"params": [{
"addresses":[
"A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=",
"Ko2bVqD+nNlNYL5EE7y3IdOnviftjiizpjRt+HTuFBs="],
"name":"Test",
"description":"Test"
}],
"id":1
}

Esta petición la enviamos como un POST al nodo 1 (o al nodo 2) para crear el identificador de grupo:

Como respuesta nos va a dar el identificador del grupo el cual vamos a usar cada vez que queramos hacer transacciones privadas entre el nodo 1 y 2. También es posible tener varios identificadores de grupo para los mismos nodos involucrados.

Ahora vamos a la parte del contrato inteligente que vamos a usar para la prueba:

pragma solidity ^0.4.26;contract ProofOfExistence {// Aquí se almacenan las marcas de tiempo para cada hash
mapping (bytes32 => uint) public stamps;
event DocumentStamped(bytes32 indexed documentHash);function stampDocument(bytes32 documentHash) public {
if (stamps[documentHash] == 0) {
stamps[documentHash] = block.timestamp;
}
emit DocumentStamped(documentHash);
}
}

Este contrato inteligente sirve para asignar una marca de tiempo a un hash dado la cual se puede efectuar una sola vez. Básicamente el contrato actuá como un sellador de documentos y permite saber si un hash de un documento dado fue verificado en algún momento del tiempo.

Vamos a crear un projecto en Java usando maven con el comando:

$ mvn -B archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=com.proofexistence.app -DartifactId=proof-existence-app

Deberían quedar con la siguiente estructura:

Para el ejemplo voy a crear una carpeta que contendrá el contrato y sus respectivos archivos compilados.

Ahora vamos a proceder a compilar el contrato inteligente de solidity, para eso instalamos el compilador. En mi caso voy a usar la versión de node:

$ npm install -g solc

Ya instalado el compilador de solidity procedemos a generar los archivos “.bin” y “.abi” del contrato. Para el que no instaló la versión de nodejs usa el siguiente comando:

solc <contract>.sol --bin --abi --optimize -o <output-dir>/

para la version nodejs:

solcjs <contract>.sol --bin --abi --optimize -o <output-dir>/

También se puede usar una extension para Visual Studio que permite compilar directamente los contratos solidity (extensión). Para la extensión se recomienda especificar la versión del compilador solidity que se va a usar. Esta es la configuración que tengo actualemente:

En la parte de “compileUsingLocalVersion” pueden especificar la versión exacta del compilador de solidity a usar. Solo es decargar el archivo correspondiente a la versión que se quiere usar y especificar su ruta.

Cuando se compile deberían tener los siguientes archivos:

Vamos a requerir el archivo “.abi” y “.bin” para generar nuestra clase Java con toda la lógica necesaria para ejecutar las funciones del contrato inteligente.

Ahora procedemos a instalar para la línea de comandos el web3j (tutorial). Al escribir web3j en consola debería mostrarnos lo siguiente:

Dentro de la carpeta donde está el contrato inteligente vamos a ejecutar el siguiente comando para generar la clase Java:

$ web3j solidity generate -b ProofOfExistence.bin -a ProofOfExistence.abi -o ./ -p com.proofexistence.contracts

Al final debería quedar la estructura de archivos de la siguiente manera:

Ahora ya tenemos nuestra clase Java con la cual vamos a interactuar con nuestro contrato inteligente.

Lo siguiente es agregar las dependencias de web3j y besu al archivo “pom.xml” las cuales usaremos para programar nuestro proyecto:

Usaré la clase java “App.java” para escribir mi código. Para empezar voy a definir las siguientes variables:

En la línea 18 se declara una variable tipo Besu que extiende de web3j y agrega funcionalidades propias para el uso de transacciones privadas.

En la línea 19 y 20 declaro el precio del gas y el límite que se va usar en la red.

En la línea 21 declaro un entero que uso internamente para especificar una cantidad de intentos que debo realizar en caso que una transacción tome un tiempo en pasar.

En la línea 22 declaro una variable tipo Credentials que sirve para crear una dirección ethereum a partir de una llave privada dada.

En la línea 23 declaro una variable para almacenar el identificador de grupo para las transacciones privadas entre un grupo de nodos Orion (privacyGroupId).

En la línea 24 declaro una variable para almacenar la llave pública del nodo Orion a usar, debe ser el correspondiente al nodo besu al que nos vayamos a conectar.

En la línea 25 declaro una variable para almacenar el identificador de la red.

En la línea 26 declaro una variable que va a servir para gestionar las transacciones privadas con el nodo.

Ahora vamos al contructor:

public App(String host, String privateKey, String privGroupId, String privFrom) {try {besu = Besu.build(new HttpService(host));credentials = Credentials.create(privateKey);privacyGroupId = Base64String.wrap(privGroupId);privateFrom = Base64String.wrap(privFrom);networkId = besu.netVersion().send().getResult();transactionManager = new BesuPrivateTransactionManager(besu, getDefaultBesuPrivacyGasProvider(), credentials, Long.parseLong(networkId), privateFrom, privacyGroupId);} catch (Exception e) {log.info("Error: " + e.getMessage());}}

Como parámetros voy a recibir la ip del nodo besu al cual me voy a conectar, la llave privada a usar para firmar las transacciones, el identificador de grupo y la llave pública del nodo orion correspondiente al nodo besu. Aquí inicializamos las variables previamente declaradas.

La función “getDefaultBesuPrivacyGasProvider” únicamente es para definir los valores a usar para el precio del gas y el límite:

BesuPrivacyGasProvider getDefaultBesuPrivacyGasProvider() {BesuPrivacyGasProvider besuPrivacyGasProvider = new BesuPrivacyGasProvider(gasPrice, gasLimit);return besuPrivacyGasProvider;}

Para desplegar un contrato vamos a importar la clase Java generada para el contrato ProofOfExistence:

import com.proofexistence.contracts.ProofOfExistence;public String deployContract() throws Exception {ProofOfExistence contract = ProofOfExistence.deploy(besu, transactionManager, getDefaultBesuPrivacyGasProvider()).send();return contract.getContractAddress();}

La clase java del contrato ya cuenta con una función para desplegar un nuevo contrato, solo es pasar los parámetros del objeto web3 a usar (en este caso usando el protocolo de besu), el gestor de transacciones y el gas provider. Usamos la función .send() para enviar la petición y esperar la respuesta. Para el ejemplo estoy retornando el address del contrato desplegado.

Ahora vamos a usar la función “stampDocument” del contrato:

public String stampDocument(String contractAddress, String hash) throws Exception {ProofOfExistence contract = ProofOfExistence.load(contractAddress, besu, transactionManager, getDefaultBesuPrivacyGasProvider());String encodeFunction = contract.stampDocument(stringToBytes(hash, 32)).encodeFunctionCall();String txHash = sendPrivTransaction(contractAddress, encodeFunction);return hash;}

Primero creamos una instancia del contrato usando la función .load(), para esto pasamos como parámetros la dirección del contrato previamente desplegado, el objeto web3, el gestor de transacciones y el gas provider. Para hacer la transacción privada vamos a obtener el bytecode que representa ejecutar la función en el contrato con ciertos parámetros, para eso se usa “encodeFunctionCall()”. Hacemos esto para enviar la transacción usando el gestor de transacciones de besu.

Uso la siguiente función para pasar de string a bytes32:

public static byte[] stringToBytes(String string, int lenght) {byte[] byteValue = string.getBytes();byte[] byteValueLen = new byte[lenght];System.arraycopy(byteValue, 0, byteValueLen, 0, lenght);return byteValueLen;}

Para envíar la transacción he creado la siguiente función:

private String sendPrivTransaction(String contractAddress, String encodeFunction) throws Exception {String hash = transactionManager.sendTransaction(gasPrice, gasLimit, contractAddress, encodeFunction, BigInteger.valueOf(0), false).getTransactionHash();PrivGetTransactionReceipt receipt = getPrivTransactionReceipt(hash, retries);if (receipt == null) throw new Exception("Transaction not mined");return hash;}

Se espera la dirección del contrato y el string con el bytecode de la función a ejecutar. Usamos el gestor de transacciones el cual recibe el precio del gas y el límite, la dirección del contrato, el bytecode a ejecutar y un valor a enviar (en esta caso como la función no recibe ethers usamos 0 por defecto). Luego esperamos a que la transacción sea procesada:

PrivGetTransactionReceipt getPrivTransactionReceipt(String txHash, int retries) throws Exception {PrivGetTransactionReceipt transactionReceipt = null;int cont = 0;while (cont <= retries) {transactionReceipt = besu.privGetTransactionReceipt(txHash).send();if (transactionReceipt.getResult() != null) {break;}cont++;Thread.sleep(1 * 1000);}return transactionReceipt;}

La función se ejecuta cada segundo con un máximo de reintentos especificados en el parámetro hasta que la transacción sea procesada.

Para obtener el valor almacenado en el contrato vamos a definir la siguiente función:

public BigInteger getTimeDocument(String contractAddress, String hash) throws Exception {ProofOfExistence contract = ProofOfExistence.load(contractAddress, besu, transactionManager, getDefaultBesuPrivacyGasProvider());BigInteger timeDocument = contract.stamps(stringToBytes(hash, 32)).send();return timeDocument;}

Recibe la dirección del contrato y el hash que vamos a validar. Para consultar solo basta con tener la instancia del contrato y ejecutar la función o llamar la variable pública del contrato, usamos .send() para obtener la respuesta.

Ya con esto terminamos las funciones respecitvas para comunicarnos con el contrato inteligente de forma privada entre un grupo de nodos.

Para validar que las transacciones son privadas basta con especificar en la librería la dirección ip de un nodo besu que no esté dentro del grupo privado e interrogar por el valor previamente almacenado, el cual debería dar como resultado 0 puesto que el contrato no es accesible para el nodo.

La librería completa la pueden bajar del siguiente repositorio:

Author: Camilo Henao — Blockchain Developer at Digital Lab Blockchain/DLT Perú in everis

Editor: Juan José Miranda — Director at Digital Technology Innovation & labs Perú in everis

--

--