Capítulo 4. Desarrollo de contratos inteligentes

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

En este capítulo, aprenderás sobre el desarrollo de contratos inteligentes de Fabric examinando un contrato inteligente sencillo y las API de Fabric utilizadas para implementar contratos inteligentes de Fabric. Una vez que entiendas los fundamentos de la codificación de un contrato inteligente y las API, podremos pasar al Capítulo 5, donde tomaremos el contenido de este capítulo y lo aplicaremos a la invocación de contratos inteligentes. Para empezar, primero tenemos que descargar las herramientas de desarrollo de Hyperledger Fabric. Proporcionan un comienzo rápido para desarrollar contratos inteligentes Fabric, encapsulando un tiempo de ejecución Fabric completo de dos organizaciones, con scripts para ponerlo en marcha y desmontarlo.

Vamos a utilizar los binarios proporcionados por Hyperledger y los proyectos de muestra del proyecto Fabric. Estos binarios y proyectos de muestra nos ayudarán a poner en marcha una red de prueba Fabric, y los proyectos de muestra proporcionan varios contratos inteligentes de ejemplo con los que aprender a desarrollar los tuyos propios. Este capítulo examina un contrato inteligente de ejemplo de un proyecto de muestra llamado Fabcar. Los binarios que utilizamos tienen el mismo nombre en todos los sistemas operativos compatibles.

Este capítulo te ayudará a alcanzar los siguientes objetivos prácticos:

  • Escribir un contrato inteligente Fabric utilizando el lenguaje de programación JavaScript

  • Instalar e instanciar un contrato inteligente Fabric

  • Validar y sanear entradas y argumentos en un contrato inteligente

  • Crear y ejecutar consultas simples o complejas

  • Trabajar con una colección privada de datos en Fabric

Instalación de requisitos previos y configuración de Hyperledger Fabric

Antes de que podamos desarrollar contratos inteligentes Fabric, necesitamos descargar e instalar el software necesario para descargar Hyperledger Fabric. Para descargar y configurar Hyperledger Fabric para desarrollar contratos inteligentes Fabric, ejecutaremos un script que requiere que exista cierto software en la plataforma en la que estés desarrollando: Windows, Linux o Mac. Necesitamos instalar Git, cURL, Node.js, npm, Docker y Docker Compose, y el script de instalación de Fabric.

Git

Git se utiliza para clonar el repositorio fabric-samples desde GitHub a tu máquina local. Si no tienes instalado Git, puedes descargarlo de https://git-scm.com/downloads. Una vez que lo hayas descargado e instalado, verifica la instalación de Git con el siguiente comando:

$ git --version
git version 2.26.2

cURL

Utilizamos cURL para descargar los binarios de Fabric desde la web. Puedes descargar cURL desde https://curl.haxx.se/download.html. Una vez descargado e instalado, verifica la instalación ejecutando el siguiente comando:

$ curl -V
curl 7.54.0 (x86_64-apple-darwin18.0) libcurl/7.54.0 LibreSSL/2.6.5 zlib/1.2.11 nghttp2/1.24.1
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp smb smbs smtp smtps
telnet tftp
Features: AsynchDNS IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz HTTP2 UnixSockets HTTPS-
proxy

Node.js y npm

Utilizaremos JavaScript para desarrollar nuestros contratos inteligentes Fabric. Fabric utiliza Node.js y npm para procesar los contratos inteligentes. Las versiones compatibles de Node.js son 10.15.3 y superiores, y 12.13.1 y superiores. Las versiones compatibles de npm son 6 y superiores. Node.js incluye npm en la instalación. Puedes descargar Node.js desde https://nodejs.org/en/download. Puedes verificar la instalación de Node.js y npm ejecutando los siguientes comandos:

$ node -v
v10.15.3

$ npm -v
6.11.2

Docker y Docker Compose

Hyperledger Fabric consta de varios componentes , cada uno de los cuales funciona como un servicio ejecutable independiente, por lo que Fabric mantiene imágenes Docker de cada componente. Las imágenes están alojadas en el sitio web oficial Docker Hub. Como mínimo, necesitas la versión 17.06.2-ce de Docker. Puedes obtener la última versión de Docker en https://www.docker.com/get-started. Cuando se instala Docker, también se instala Docker Compose. Puedes verificar la versión de Docker ejecutando el siguiente comando en :

$ docker -v
Docker version 19.03.13, build 4484c46d9d

A continuación, verifica tu versión de Docker Compose ejecutando lo siguiente:

$ docker-compose --version
docker-compose version 1.27.4, build 40524192

Antes de continuar, inicia Docker, porque Docker necesita estar en ejecución para completar la instalación del script de instalación de Fabric.

Guión de instalación del tejido

Crea y cambia al directorio que utilizarás para instalar los binarios de Fabric y los proyectos de muestra. Docker debe estar ejecutándose porque el script requiere Docker para descargar las imágenes de Fabric.

El script hará lo siguiente

  1. Descarga los binarios de Fabric

  2. Clona fabric-samples del repositorio de GitHub

  3. Descarga las imágenes Docker de Hyperledger Fabric

Este es el comando para ejecutar el script:

curl -sSL https://bit.ly/2ysbOFE | bash -s

Pero aún no vamos a ejecutar esta orden. Primero vamos a guardar la salida del comando, para poder examinarla. Asegúrate de que estás en el directorio que has creado para instalar los binarios de Fabric y los proyectos de ejemplo, y ejecuta el siguiente comando:

curl -sSL https://bit.ly/2ysbOFE > FabricDevInstall.sh

Ahora puedes abrir FabricDevInstall.sh en tu editor favorito y examinar el script para ver cómo clona fabric-samples de GitHub, descarga los binarios de Fabric y descarga las imágenes Docker. Comprender el funcionamiento de este script puede ayudarte más adelante si quieres personalizar tu entorno de desarrollo de Fabric u optimizarlo en función de tu flujo de trabajo.

Cuando termines de examinar el script, abre un intérprete de comandos y cambia FabricDevInstall.sh a ejecutable ejecutando el siguiente comando:

$ chmod +x FabricDevInstall.sh

Ahora vamos a ejecutar FabricDevInstall.sh con el siguiente comando:

FabricDevInstall.sh

Una vez que el script finalice su ejecución, ya deberíamos estar listos. El directorio fabric-samples, las imágenes Docker y los binarios Fabric están instalados en el directorio fabric-samples/bin. En el directorio donde hayas ejecutado el comando, ahora debería haber un directorio llamado fabric-samples. Todo lo que necesitamos está en fabric-samples. En primer lugar, profundizaremos en el contrato inteligente de muestra Fabcar, que puedes encontrar en el directorio fabric-samples.

Requisitos fundamentales de un contrato inteligente

El contrato inteligente de ejemplo de Fabcar es un ejemplo válido y funcional de un contrato inteligente básico. Tenemos mucho más que añadir antes de que esté listo para la producción, incluyendo seguridad, gestión de errores, informes, monitoreo y pruebas. Queremos recordar varios puntos importantes de este contrato inteligente de ejemplo. Vamos a repasarlos:

Contract clase
Los contratos inteligentes amplían la clase Contract. Se trata de una clase sencilla con pocas funciones. En veremos esta clase más adelante en el capítulo.
Contexto de la transacción
Todas las funciones de transacción de los contratos inteligentes pasan un objeto de contexto de transacción como primer argumento. Este objeto de contexto de transacción es una clase Context. Cuando examinemos la clase Contract, también examinaremos esta clase.
Constructor
Todos los contratos inteligentes deben tener un constructor . El argumento constructor es opcional y representa el nombre del contrato. Si no se pasa, se utilizará el nombre de la clase. Te recomendamos que pases un nombre único y pienses en ello en términos de un espacio de nombres, como una estructura inversa de nombres de dominio.
Función de transacción
Se puede crear una función de transacción para inicializar un contrato inteligente y llamarla antes de las peticiones de los clientes. Puedes utilizarla para configurar y ejecutar tu contrato inteligente con los recursos necesarios. Estos recursos pueden ser tablas o mapas de datos utilizados para búsquedas, traducciones, descodificación, validación, enriquecimiento, seguridad, etc.
Estado mundial
Podemos consultar el estado del mundo en de múltiples formas. La consulta simple es una búsqueda de claves, y una consulta de rango obtiene un conjunto. Existe otra llamada consulta rica. Veremos el estado del mundo en el Capítulo 5.
putState
Para escribir datos en el libro mayor, utilizamos la función putState. Toma como argumentos un key y un value. El value es una matriz de bytes, por lo que el libro mayor puede almacenar cualquier dato. Normalmente, almacenaremos el equivalente a objetos de negocio que se convierten en matrices de bytes antes de pasarlos como argumento a value.
ChaincodeStub
La clase ChaincodeStub contiene varias funciones utilizadas para interactuar con el ledger y el estado del mundo. Todos los contratos inteligentes obtienen una implementación de esta clase como el objeto stub contenido y expuesto por la implementación de la clase Context llamada ctx, que todas las funciones de transacción reciben como primer argumento.
Transacciones de lectura/escritura
Una actualización en un contrato inteligente se ejecuta en tres pasos: una transacción de lectura, una actualización de los datos en memoria devueltos por la transacción de lectura, seguida de una transacción de escritura. Esto crea un nuevo estado del mundo para la clave, al tiempo que mantiene el historial de la clave en el libro mayor inmutable basado en archivos.

Es importante recordar este punto: no puedes actualizar (o escribir en) el libro mayor y volver a leer lo que escribiste en la misma transacción. No importa a cuántas otras funciones de transacción llames desde una función de transacción. Tienes que pensar en el flujo de datos de una solicitud de transacción. Los clientes envían solicitudes de transacción a los pares, que endosan la transacción solicitada (aquí es donde se ejecuta el contrato inteligente); los endosos con conjuntos de lectura y escritura se devuelven a los clientes; y las solicitudes endosadas se envían a un ordenante, que ordena las transacciones y crea bloques. El ordenante envía las solicitudes ordenadas a los pares de confirmación, que validan los conjuntos de lectura y escritura antes de confirmar las escrituras en el libro mayor y actualizar el estado del mundo.

En su forma más simple, un contrato inteligente es una envoltura alrededor de ChaincodeStub, porque los contratos inteligentes deben utilizar la interfaz expuesta a través de esta clase para interactuar con el ledger y el estado del mundo. Este es un punto importante que debes recordar. Debes considerar la implementación de la lógica empresarial en un diseño modular, tratando tu subclase Contract como una fuente de datos. Esto facilitará la evolución de tu código a lo largo del tiempo y la partición de la lógica en componentes funcionales que puedan compartirse y reutilizarse. En el Capítulo 5, examinamos el diseño en el contexto del empaquetado y la implementación, y en el Capítulo 6, profundizamos en el diseño y la implementación modulares para facilitar el mantenimiento y las pruebas.

Varios pares, los pares endosantes, ejecutarán tus contratos inteligentes. En la actualidad, la arquitectura coloca los contratos inteligentes detrás de una pasarela, que es un middleware del SDK de contratos inteligentes. La pasarela recibe solicitudes de contratos inteligentes, las procesa y las envía a uno o más pares. Los pares instancian el contrato inteligente para su ejecución.

SDK

Fabric proporciona un SDK implementado en Go, Java y Node.js (JavaScript) para desarrollar contratos inteligentes. Nos interesa el SDK de desarrollo de contratos inteligentes de Hyperledger Fabric para Node.js, que se llama fabric-chaincode-node. Aunque no necesitas descargarlo para desarrollar contratos inteligentes, puedes descargar o clonar fabric-chaincode-node desde GitHub.

El SDK fabric-chaincode-node tiene muchas cosas. Nos interesan unos pocos componentes que son fundamentales para desarrollar contratos inteligentes. Los archivos restantes son interfaces de bajo nivel, artefactos de apoyo, herramientas y más, necesarios para implementar la interfaz Contract con Node.js. Este SDK ayuda a los desarrolladores como nosotros proporcionando una API de alto nivel para que podamos aprender rápido y centrarnos en la lógica y el diseño de nuestro negocio de contratos inteligentes.

La primera API que nos interesa es fabric-contract-api. Se encuentra en el subdirectorio apis de fabric-chaincode-node. La otra API que ves, fabric-shim-api, es la definición de tipos y la interfaz pura de la biblioteca fabric-shim, que veremos más adelante en este capítulo.

Cuando iniciemos nuestro proyecto de contrato inteligente y ejecutemos npm install, lo que haremos en el Capítulo 5, npm descargará fabric-contract-api como módulo del repositorio público npm, así como fabric-shim. Esta descarga se produce porque tenemos dos dependencias explícitas del contrato inteligente para desarrollar contratos inteligentes Hyperledger Fabric. Éstas se muestran en este extracto del archivo package.json del contrato inteligente Fabcar:

"dependencies": {
    "fabric-contract-api": "^2.0.0",
    "fabric-shim": "^2.0.0"
},

fabric-contract-api y fabric-shim son los únicos módulos que necesitamos para desarrollar nuestros contratos inteligentes. fabric-contract-api contiene los archivos contract.js y context.js, que implementan las clases Contract y Context.

Clase de contrato

Contract es una clase simple de . Más allá del constructor hay funciones de utilidad que puedes sobrescribir para implementar la lógica antes y después de las transacciones:

constructor(name) {
    this.__isContract = true;
    if (typeof name === 'undefined' || name === null) {
        this.name = this.constructor.name;
    } else {
        this.name = name.trim();
    }
    logger.info('Creating new Contract', name);
}

Se llama a la función beforeTransaction antes de invocar cualquier función de transacción del contrato. Puedes anular este método para implementar tu propia lógica personalizada:

async beforeTransaction(ctx) {
// default implementation is do nothing
}

La función afterTransaction se llama después de invocar cualquier función de transacción del contrato. Puedes anular este método para implementar tu propia lógica personalizada:

async afterTransaction(ctx, result) {
    // default implementation is do nothing
}

La función getName es un getter que devuelve el nombre del contrato:

getName() {
    return this.name;
}

Y la función createContext crea un contexto de transacción personalizado :

createContext() {
    return new Context();
}

Contexto de la transacción

Puedes crear un contexto personalizado de transacción para almacenar tus propios objetos a los que tus funciones puedan acceder a través del objeto ctx, que todas las funciones de transacción reciben como primer argumento. He aquí un ejemplo de creación de un contexto personalizado:

const AssetList = require('./assetlist.js');

class MyContext extends Context {
    constructor() {
        super();
        this.assetList = new AssetList(this);
    }

}

class AssetContract extends Contract {
    constructor() {
        super('org.my.asset');
    }

    createContext() {
        return new MyContext();
    }
}

Con el contexto personalizado MyContext, las transacciones pueden acceder a assetList como ctx.assetList.

Como puedes ver, crear un contrato inteligente sencillo es fácil. Importas Contract de fabric-contract-api y lo extiendes. Luego creas un constructor sin argumentos y exportas nuestro contrato. Y ya está.

Clase de contexto

Vale, esa es la clase Contract, pero ¿qué de la clase Context? Acabas de aprender a crear un contexto de transacción personalizado, pero ¿qué contiene la clase Context? Como has aprendido, cada función de transacción obtiene como primer argumento un objeto Context llamado ctx. Éste es el contexto de transacción. Contiene dos objetos importantes: stub y clientIdentity. El stub es una implementación de la clase ChaincodeStub, y el clientIdentity es una implementación de la clase ClientIdentity. A continuación hablaremos de estas clases.

ChaincodeStub tiene todas las funciones que necesitamos para interactuar con el libro mayor y el estado mundial. Es nuestra API para el libro mayor y el estado mundial. Está contenida en el módulo fabric-shim del directorio lib e implementada en el archivo stub.js. Las dos funciones principales son getState y putState. Hay disponibles varias funciones adicionales . La mayoría pueden agruparse en las siguientes categorías:

  • Relacionados con el Estado

  • Consulta relacionada

  • Transacciones relacionadas

  • Datos privados relacionados

Estos cuatro grupos representan la mayoría de las funciones. Las funciones relacionadas con el estado se utilizan para leer y escribir en el libro mayor. Utilizan o implican el uso de una clave.

Las funciones relacionadas con las consultas son dos funciones de consulta enriquecida, una de ellas con paginación. Las consultas enriquecidas son consultas de cadena nativas de la base de datos. Para utilizar consultas enriquecidas, necesitas utilizar CouchDB para la base de datos. Lo haremos en el Capítulo 5, donde aprenderás a invocar contratos inteligentes . Otra función de consulta única es getHistoryForKey, que toma una clave y devuelve el historial de la misma. Esto puede utilizarse para auditar cambios y encontrar transacciones que fallaron.

Hyperledger tiene cinco funciones relacionadas con las transacciones:

  • getTxID(): string;

  • getChannelID(): string;

  • getCreator(): SerializedIdentity;

  • getMspID(): string;

  • getTransient(): Map<string, Uint8Array>;

Utiliza getTxID para recuperar el ID de la transacción, getChannelID para el ID del canal, getCreator para el cliente, getMspID para la organización a la que pertenece el cliente y getTransient para los datos privados. Ejecutaremos cada uno de ellos en los dos capítulos siguientes.

Hyperledger tiene nueve funciones privadas relacionadas con los datos:

  • getPrivateData(collection: string, key: string): Promise​<Uint8Array>;

  • getPrivateDataHash(collection: string, key: string): Promise​<Uint8Array>;

  • putPrivateData(collection: string, key: string, value: Uint8Array): Promise<void>;

  • deletePrivateData(collection: string, key: string): Promise<void>;

  • setPrivateDataValidationParameter(collection: string, key: string, ep: Uint8Array): Promise<void>;

  • getPrivateDataValidationParameter(collection: string, key: string): Promise<Uint8Array>;

  • getPrivateDataByRange(collection: string, startKey: string, endKey: string): Promise<Iterators.StateQueryIterator> & AsyncIterable<Iterators.KV>;

  • getPrivateDataByPartialCompositeKey(collection: string, objectType: string, attributes: string[]): Promise<Iterators.StateQueryIterator> & AsyncIterable<Iterators.KV>;

  • getPrivateDataQueryResult(collection: string, query: string): Promise<Iterators.StateQueryIterator> & AsyncIterable<Iterators.KV>;

Estas nueve funciones de datos privados proporcionan la capacidad de leer y escribir en una colección de datos privados, obtener un hash de datos privados, borrar de datos privados, establecer y obtener una política de aprobación para la validación de datos privados, y obtener datos privados por rango, clave compuesta parcial o consulta enriquecida. Ejecutaremos algunas de ellas en los Capítulos 5 y 6, cuando empleemos el uso de datos privados con nuestras transacciones de contratos inteligentes.

Ahora que ya tienes una buena idea de qué tipo de funcionalidad está disponible a través del objeto stub que obtenemos del objeto ctx, veamos ClientIdentity, el otro objeto del contexto de la transacción.

El objeto clientIdentity contenía en el contexto de la transacción, ya que ctx.clientIdentity es la implementación de la clase ClientIdentity, que es una clase pequeña con sólo cinco funciones:

  • assertAttributeValue(attrName: string, attrValue: string): boolean;

  • getAttributeValue(attrName: string): string | null;

  • getID(): string;

  • getIDBytes(): Uint8Array;

  • getMSPID(): string;

Las funciones assertAttributeValue y getAttributeValue operan sobre el certificado del cliente. Con estas funciones se puede implementar la seguridad granular empleando los valores de los atributos del certificado. Las funciones getID y getIDBytes recuperan la identidad del cliente, y getMSPID se utiliza para obtener la organización a la que pertenece el cliente. Utilizando estas funciones, puedes implementar una amplia gama de patrones de diseño de autenticación y autorización .

Funciones de transacción

Las funciones detransacción son las funciones del contrato inteligente a las que llaman los clientes. Son las funciones comerciales que diseñas e implementas en tus contratos inteligentes. He aquí un ejemplo de tres funciones de transacción del contrato inteligente Fabcar. Las definiremos en "Definir un contrato inteligente":

async queryCar(ctx, carNumber) 
async createCar(ctx, carNumber, make, model, color, owner)
async queryAllCars(ctx)

Todas las funciones de transacción reciben como primer argumento el contexto de transacción, el objeto ctx. Las funciones de transacción utilizan este objeto para hacer referencia a los objetos stub y clientIdentity -por ejemplo, ctx.stub y ctx.clientIdentity. El stub es una instancia de ChaincodeStub. El clientIdentity es una implementación de C⁠l⁠i⁠e⁠n⁠t​I⁠d⁠e⁠n⁠t⁠i⁠t⁠y y expone funciones para obtener el ID de la transacción, el ID del cliente, cualquier atributo del cliente y el ID de la organización. Estas funciones pueden utilizarse para la autenticación y autorización específicas de la aplicación y la transacción.

Es habitual que la mayoría de las funciones de transacción contengan una llamada al libro mayor o al estado del mundo. La página stub proporciona las funciones para leer del estado mundial y escribir en el libro mayor, que actualiza el estado mundial.

La forma en que diseñes tus funciones de transacción está completamente bajo tu control. Puedes agruparlas, mezclarlas, crear bibliotecas y mucho más. Recuerda que, para las transacciones que se escriben, todos los pares endosantes designados deben ejecutar tus contratos inteligentes.

Las políticas de respaldo determinan si una transacción se compromete. Por ejemplo, una política de aprobación puede establecer que tres de cada cuatro pares deben aprobar la transacción. Si, por alguna razón, menos de tres pares pueden aprobar la transacción, ésta no se confirmará, lo que significa que los datos no estarán disponibles en el libro mayor.

Un punto a recordar es que aunque una transacción de escritura pueda fallar, se marcará como inválida y se escribirá en el libro mayor. Una transacción inválida no formará parte del estado mundial.

Como buenas prácticas, los contratos inteligentes Fabric exigen un código determinista. Muchos pares tienen que ejecutar el código, y todos tienen que llegar al mismo resultado. Por lo tanto, las entradas deben devolver siempre el mismo resultado. No debe importar qué peer ejecute el código. Dadas las mismas entradas, el par debe devolver los mismos resultados. Esto debe ocurrir independientemente del número de veces que se ejecute el código.

El código debe tener un principio y un final. Nunca debe depender de datos dinámicos ni de largas ejecuciones aleatorias. El código debe ser rápido y eficaz, con flujos lógicos claros y no circulares. Por ejemplo:

async CreateAsset(ctx, id, amount, owner) {
        const asset = {
            ID: id,
            Amount: amount,
            Owner: owner
        };
        return ctx.stub.putState(id,Buffer.from(
           JSON.stringify(asset)));
}

Al crear un activo, esperamos el ID del activo, la cantidad y el propietario como entradas.

Validar y sanear argumentos

Las transacciones deben validar y sanear sus argumentos. Esto no es exclusivo de los contratos inteligentes. Es una práctica sabia por defecto si quieres garantizar la integridad y disponibilidad de tu contrato inteligente.

Emplea técnicas conocidas para validar tus argumentos y evitar cualquier dato que pueda perjudicar a tu contrato inteligente. También quieres sanear tus argumentos y garantizar la calidad de los datos que esperas. Quieres limitar el procesamiento innecesario de datos que más adelante en tu lógica causarán un fallo o un caso de perímetro no cubierto. He aquí un ejemplo que comprueba si la función es Process; si no lo es, lanzaremos una excepción:

func (c *Asset) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
    function, args := stub.GetFunctionAndParameters()
    if function == "Process" {
        return c.Process(stub, args)
    } 
    return shim.Error("Invalid function name")
}

Interacción de estado simple (obtener, poner, eliminar)

Los contratos inteligentes Fabric en el núcleo son máquinas de estados que evolucionan con el tiempo, manteniendo un historial inmutable de todos los estados anteriores. Las tres funciones principales de stub que utilizarás son las siguientes:

getState(key: string): Promise<Uint8Array>;
putState(key: string, value: Uint8Array): Promise<void>;
deleteState(key: string): Promise<void>;

Las funciones de stub proporcionan a tus contratos inteligentes la funcionalidad para leer del estado del mundo, escribir en el libro mayor y borrar del estado del mundo.

El libro mayor y el estado del mundo son almacenes de datos clave-valor. Esto lo mantiene simple y sencillo, pero permite almacenar datos ricos en el libro mayor, consultarlos y visualizarlos mediante el estado del mundo, un documento o una base de datos NoSQL. Actualmente, LevelDB y CouchDB son compatibles. LevelDB es un sencillo almacén de datos clave-valor , mientras que CouchDB es una rica y robusta base de datos de documentos NoSQL. Esto significa que puedes leer y escribir estructuras de datos JSON de simples a complejas en el libro mayor y consultar la base de datos de estado mundial para obtener datos enriquecidos. Hay varias funciones disponibles para realizar consultas.

Crear y ejecutar consultas

Los contratos inteligentes a menudo necesitan buscar en o consultar datos del estado del mundo mientras procesan una transacción. ¿Recuerdas la actualización de nuestro contrato inteligente de ejemplo Fabcar? Para realizar una actualización, un contrato inteligente normalmente debe encontrar y cargar el objeto existente que desea actualizar. A continuación, actualiza los datos en memoria y escribe los datos actualizados en el libro mayor: recuerda putState para escribir y getState para leer.

A diferencia de una base de datos relacional en la que podemos actualizar un campo sin seleccionar primero la fila, con los contratos inteligentes Fabric, debemos cargar el valor de una clave y actualizar el valor. Eso puede ser un único campo en una estructura de datos JSON muy grande y compleja, o puede ser un simple objeto con un solo campo, el campo que estamos actualizando. ¿Por qué ocurre esto? Porque todos los datos están vinculados a una clave única. Si el valor asociado a una clave es un objeto con cuatro campos, pensamos en cada campo como un valor, pero para el libro mayor, todo es un único objeto de valor identificado por una única clave única.

Estas son las funciones disponibles en stub que puedes utilizar para buscar datos:

  • getState(key: string): Promise<Uint8Array>;

  • getStateByRange(startKey: string, endKey: string): Promise<Iterators.StateQueryIterator> & AsyncIterable<Iterators.KV>;

  • getStateByRangeWithPagination(startKey: string, endKey: string, pageSize: number, bookmark?: string): Promise<StateQueryResponse<Iterators.StateQueryIterator>> & AsyncIterable<Iterators.KV>;

  • getQueryResult(query: string): Promise<Iterators.StateQueryIterator> & AsyncIterable<Iterators.KV>;

  • getQueryResultWithPagination(query: string, pageSize: number, bookmark?: string): Promise<StateQueryResponse<Iterators.StateQueryIterator>> & AsyncIterable<Iterators.KV>;

  • getHistoryForKey(key: string): Promise<Iterators.HistoryQueryIterator> & AsyncIterable<Iterators.KeyModification>;

Hablamos de ellos en el Capítulo 5, cuando los utilizamos en un contrato inteligente y los invocamos desde un cliente.

Definición de un Contrato Inteligente

Empecemos con el sencillo contrato inteligente Fabcar . En el directorio fabric-samples, localiza el directorio chaincode. En el directorio chaincode, localiza el directorio fabcar. En el directorio fabcar, localiza el directorio javascript. Cambia a este directorio en tu shell y ejecuta el siguiente comando:

$ npm install

Esto creará el directorio node_modules e instalará los módulos dependientes definidos en package.json. Hicimos esto porque dependemos de los módulos fabric-contract-api y fabric-shim. Estos son los dos módulos que utilizamos cuando desarrollamos contratos inteligentes Fabric en JavaScript. Los veremos después de examinar el contrato inteligente Fabcar.

Examinemos ahora el contrato inteligente Fabcar. Este sencillo contrato inteligente es un gran ejemplo para aprender el desarrollo de contratos inteligentes en Fabric, porque contiene los detalles necesarios para formar una base desde la que podemos pasar a contratos inteligentes más avanzados. Se encuentra en el directorio lib del directorio actual, que debería ser el directorio fabric-samples/chaincode/fabcar/javascript. Abre fabcar.js en tu editor favorito; el Ejemplo 4-1 muestra el código fuente.

Ejemplo 4-1. fabcar.js
/*
 * Copyright IBM Corp. All Rights Reserved.
 *
 * SPDX-License-Identifier: Apache-2.0
 */
'use strict';
const { Contract } = require('fabric-contract-api'); 1
class FabCar extends Contract { 2
    async initLedger(ctx) { 3
        console.info('============= START : Initialize Ledger ===========');

        const cars = [ 4
            {
                color: 'blue',
                make: 'Toyota',
                model: 'Prius',
                owner: 'Tomoko',
            },
...
        ];
        for (let i = 0; i < cars.length; i++) { 5
            cars[i].docType = 'car';
            await ctx.stub.putState('CAR' + i, 
            Buffer.from(JSON.stringify(cars[i]))); 6
        }
    }
    async queryCar(ctx, carNumber) { 7
        const carAsBytes = await ctx.stub.getState(
        carNumber); // get the car from chaincode state 8
        if (!carAsBytes || carAsBytes.length === 0) {
            throw new Error(`${carNumber} does not exist`);
        return carAsBytes.toString();
    }
    async createCar(ctx, carNumber, make, model, color, owner) { 9
        console.info('============= START : Create Car ===========');
        const car = {color, docType: 'car',make, model, owner}

        await ctx.stub.putState(carNumber, Buffer.from(
        JSON.stringify(car))); 10
    }
    async queryAllCars(ctx) { 11
        const startKey = '';
        const endKey = '';
        const allResults = [];
        for await (const {key, value} of ctx.stub.getStateByRange(
            startKey, endKey)) { 12
            const strValue = Buffer.from(value).toString('utf8');

            let record;
            try {
                record = JSON.parse(strValue);
            } catch (err) {
                record = strValue;
            }
            allResults.push({ Key: key, Record: record });
        }
        return JSON.stringify(allResults);
    }
    async changeCarOwner(ctx, carNumber, newOwner) { 13
        

const carAsBytes = await ctx.stub.getState(carNumber); 14
if (!carAsBytes || carAsBytes.length === 0) {
            throw new Error(`${carNumber} does not exist`);
        }
        const car = JSON.parse(carAsBytes.toString());
        car.owner = newOwner;
        await ctx.stub.putState(carNumber, Buffer.from(
JSON.stringify(car))); 15
    }
}
module.exports = FabCar; 16
1

Comenzamos importando el módulo fabric-contract-api.

2

Todos los contratos inteligentes Fabric extienden la clase Contract. Obtenemos la clase Contract del módulo fabric-contract-api que importamos en la línea 1.

3

Los contratos inteligentes pueden utilizar las transacciones para inicializarlos antes de procesar las peticiones de la aplicación cliente. Esta línea es el comienzo de la función que inicializa el contrato inteligente. Todas las funciones de los contratos inteligentes reciben como argumento un objeto de contexto de transacción, llamado por convención ctx.

4

En este ejemplo, la función initLedger está creando una matriz de objetos llamada cars. Cada matriz de objetos contiene pares clave-valor. Puedes pensar en la matriz de objetos como en registros de activos, y en los pares clave-valor de los objetos como en los campos. Esta función precarga efectivamente una matriz de objetos car para ejercer las funciones de transacción en el contrato inteligente.

5

A continuación, la función initLedger recorre la matriz de objetos de activos car y añade un campo llamado docType a cada objeto, y asigna el valor de cadena car a cada objeto.

6

Esta línea es el primer uso del objeto ctx (claseContext ) que se pasa como primer argumento de función a todas las funciones de transacción de la clase Contract. El objeto ctx contiene el objeto stub, que es una clase ChaincodeStub. La clase ChaincodeStub implementa una API para acceder al libro mayor. Esta línea llama a la función putState, que escribe la clave y el valor en el libro mayor y en el estado mundial.

Nota

Hyperledger Fabric implementa el libro mayor de la cadena de bloques en dos componentes: un componente basado en archivos y un componente de base de datos. El componente basado en archivos es la estructura de datos inmutable de bajo nivel que implementa el libro mayor, y el componente de base de datos expone el estado actual del libro mayor basado en archivos. El componente de base de datos se denomina estado mundial porque representa el estado actual del libro mayor. El componente basado en archivos mantiene el libro mayor perpetuo inmutable. La fabric-contact-api accede al estado mundial. Las API de nivel inferior acceden al libro mayor basado en archivos.

7

La primera función de transacción es la siguiente. Como ya se ha dicho, el primer argumento de todas las funciones de transacción de los contratos inteligentes es el objeto ctx, que representa el contexto de la transacción y es una clase Context. Cualquier otro argumento es opcional.

8

La función queryCars es una transacción de lectura. Utilizando el objeto ctx, llama a la función stub's getState, que leerá del estado-mundo de la base de datos. La función stub es una implementación de la clase ChaincodeStub, de la que hablaremos más adelante. Para esta función, el argumento llamado carNumber es el key pasado a la función getState, que buscará en la base de datos del estado del mundo el key y devolverá el valor asociado almacenado para él. El resto de la función comprueba si se han devuelto datos y, en caso afirmativo, convierte la matriz de bytes devuelta en una cadena y devuelve la cadena que representa el valor de la clave almacenada en el estado del mundo. Recuerda que el estado del mundo es una representación del estado actual del libro mayor perpetuo inmutable basado en archivos para cualquier par clave-valor almacenado en el libro mayor. Mientras que la base de datos puede ser mutable, el libro mayor basado en archivos no lo es. Por tanto, aunque se elimine un par clave-valor de la base de datos o del estado mundial, el par clave-valor sigue estando en el libro mayor basado en archivos, donde todo el historial se mantiene en el libro mayor inmutable perpetuo.

9

A continuación tenemos la segunda función de transacción. Pasa los valores necesarios para crear un nuevo objeto de registro car, que añadiremos al libro mayor.

10

Con el objeto de registro car construido a partir de los argumentos de la función, llamamos a la función de la API ChaincodeStub implementada por stub, llamada putState, que escribirá los key y value en el libro mayor y actualizará el estado actual del mundo. Los dos primeros argumentos que pasamos a la función putState son una clave y un valor, respectivamente. Tenemos que cambiar value, el objeto de registro car, por una matriz de bytes, que es lo que requieren las API de ChaincodeStub.

11

La siguiente función de transacción, llamada queryAllCars, es una transacción de lectura y demuestra una consulta de rango. Una consulta de rango, como todas las consultas, es ejecutada por el par que recibe la solicitud. Una consulta de rango toma dos argumentos: la clave de inicio y la clave de fin. Estas dos claves representan el principio y el final del rango. Todas las claves que caen dentro del rango se devuelven junto con sus valores asociados. Puedes pasar una cadena vacía para ambas claves para recuperar todas las claves y valores.

12

Se ejecuta un bucle for, que almacena todas las claves y valores asociados devueltos por la función de la API ChaincodeStub getStateByRange .

13

La última función de transacción, changeCarOwner, combina tareas de lectura y escritura para cambiar el estado del mundo. La lógica de negocio aquí es una transferencia de propiedad. Además del argumento ctx, se pasan dos argumentos: un objeto key llamado carNumber, y un objeto value llamado newOwner.

14

A continuación, tenemos que recuperar el objeto registro del estado del mundo, que representa la clave y el valor actuales de este registro. El key es carNumber. Lo utilizamos para ejecutar la API ChaincodeStub getState . Una vez que recuperamos el objeto de registro car actual para carNumber, cambiamos el campo owner por newOwner.

15

Tras recuperar los datos del libro mayor que representan el estado mundial y actualizar los datos recuperados, actualizamos el libro mayor para este objeto de registro car ejecutando la API ChaincodeStub putState . Esto escribe un nuevo key y value en el libro mayor que representa el estado mundial. Si ahora se recupera el objeto de registro car, el libro mayor no mostrará el nuevo propietario hasta que el objeto de registro sea consignado en el libro mayor. Es importante entender que, una vez consignado, se añade el libro mayor y se cambia el estado de la base de datos (se actualiza el estado del mundo). Puedes pensar en el libro mayor como una pila creciente de objetos, cada uno con un identificador único llamado clave. Puede haber muchas claves con el mismo valor, pero sólo una representa el estado actual o mundial. Así es como la base de datos implementa la vista del estado del mundo, mientras que el libro mayor basado en archivos implementa el historial inmutable de todas las claves en orden de fecha y hora.

Nota

El libro mayor basado en archivos almacena todas las transacciones de escritura de . Tanto las transacciones de escritura exitosas como las fallidas forman parte del libro mayor inmutable basado en archivos . Los indicadores controlan la validez de las transacciones almacenadas en el libro mayor inmutable basado en archivos. Esto facilita una auditoría de todas las transacciones de escritura enviadas.

16

Esta línea es específica de Node.js. Hablaremos de la exportación de módulos de contratos inteligentes en el Capítulo 5, cuando cubramos la ejecución de contratos inteligentes, incluyendo la estructura del proyecto, el empaquetado y la implementación.

Esto completa este sencillo contrato inteligente. Ahora lo discutiremos, en resumen, para señalar los requisitos fundamentales para desarrollar un contrato inteligente. A partir de este contrato inteligente básico, se pueden diseñar y desarrollar aplicaciones de contratos inteligentes complejos.

Definir activos mediante pares clave-valor

Al diseñar contratos inteligentes, puede que necesites pensar en términos de activos. Los activos son genéricos y pueden representar muchas cosas, incluidos objetos tangibles e intangibles. Pueden ser piezas de máquinas, comida para perros, divisas o derivados verdes. Utilizamos pares nombre-valor, o pares clave-valor, según cómo quieras verlo, para crear nuestras estructuras de datos. He aquí un ejemplo que podemos comentar:

const assets = [
    {
        color: 'blue',
        make: 'Honda',
        model: 'Accord',
        owner: 'Jones',
    },  
    {
        color: 'red',
        make: 'Ford',
        model: 'Mustang',
        owner: 'Smith',
    },
];
for (let i = 0; i < assets.length; i++) { 
    assets[i].docType = 'asset';
    await ctx.stub.putState('ASSET' + i,
      Buffer.from(JSON.stringify(assets[i])));
}

Ya lo hemos visto en el ejemplo de Fabcar. Es un buen ejemplo de procesamiento básico, a partir del cual puedes avanzar en función de tu caso de uso particular. Este ejemplo crea una matriz de objetos que representan activos. Los pares clave-valor definen en los atributos, o características, de cada activo. La matriz actúa como una simple base de datos de activos. Cada activo se escribe en el libro mayor llamando a ctx.stub.putState, que toma una clave y un valor. El valor debe ser una matriz de bytes, así que convertimos el objeto JSON en una cadena y luego convertimos la cadena en la matriz de bytes. Harás esto muchas veces y puede que quieras simplificarlo y empezar a construir una utilidad o biblioteca. Este código en concreto se utilizó para inicializar el contrato inteligente.

También podemos definir activos utilizando una transacción de contrato inteligente. La función de transacción createAsset que se muestra a continuación ilustra lo sencillo que es crear un activo y escribirlo en el libro mayor. Esta función sería llamada por un cliente. El cliente podría ser un usuario o un proceso. Lo que es importante recordar es que el activo no estará disponible hasta que la transacción se haya consignado en el libro mayor. Así que no puedes escribir un montón de activos en el libro mayor y más tarde en tu contrato inteligente esperar leer y utilizar sus datos para continuar el procesamiento. Este estado desconectado es algo en lo que debes pensar cuando empieces a diseñar y a hacer lluvia de ideas. Aquí tienes la función de transacción createAsset:

async createAsset(ctx, assetNumber, make, model, color, owner) {
        const asset = {
        color,
        docType: 'asset',
        make,
        model,
        owner,
        };
    await  ctx.stub.putState(assetNumber, Buffer.from(JSON.stringify(asset)));
}

Recoger datos privados

El conjunto de datos privados(CDP) es una partición de los datos del libro mayor perteneciente a una organización que almacena datos privados y mantiene los datos privados de otras organizaciones en ese canal. Esto incluye los datos privados y el valor hash de los datos privados. El Capítulo 9 proporciona más detalles. La necesidad de mantener privados datos específicos es importante para desarrollar contratos inteligentes. Muchos contratos inteligentes necesitan cumplir requisitos de privacidad y seguridad. Fabric admite datos privados para las funciones de transacción y los contratos inteligentes.

Los datos privados pueden compartirse o mantenerse aislados y seguros. Los datos privados pueden caducar tras la creación de una serie de bloques o bajo demanda. Los datos colocados en los CDP permanecen separados de los datos públicos, y los CDP son locales y están protegidos. El estado mundial puede utilizarse junto con los CDP mediante el uso de hashes, así como de claves públicas.

La Tabla 4-1 enumera varias funciones disponibles en stub.

Tabla 4-1. Comandos para trabajar con datos privados
API Nota
getPrivateData(collection: string, key: string): Promise<Uint8Array> Devuelve la política de endoso a partir del nombre de la colección y la clave especificada.
putPrivateData(collection: string, key: string, value: Uint8Array): Promise<void> Pone el nombre de la colección y la clave y el valor especificados en el writeSet privado de la transacción.
deletePrivateData(collection: string, key: string): Promise<void> Elimina la política de aprobación proporcionando el nombre de la colección y la clave de la variable de datos privados.
setPrivateDataValidationParameter(collection: string, key: string, ep: Uint8Array): Promise<void> Establece la política de aprobación proporcionando el nombre de la colección y la clave de la variable de datos privados.
getPrivateDataValidationParameter(collection: string, key: string): Promise<Uint8Array> Devuelve la política de aprobación proporcionando el nombre de la colección y la clave de la variable de datos privados.
getPrivateDataByRange(collection: string, startKey: string, endKey: string): Promise<Iterators.StateQueryIterator> & AsyncIterable<Iterators.KV> Devuelve la política de aprobación a partir del nombre de la colección y la clave de la variable de datos privados.
getPrivateDataByPartialCompositeKey(collection: string, objectType: string, attributes: string[]): Promise<Iterators.StateQueryIterator> & AsyncIterable<Iterators.KV> Consulta la política de endoso en un nombre de colección y una clave compuesta parcial dados.
getPrivateDataQueryResult(collection: string, query: string): Promise<Iterators.StateQueryIterator> & AsyncIterable<Iterators.KV> Realiza una consulta enriquecida contra una colección privada dada. Es compatible con bases de datos de estado que puedan ejecutar una consulta enriquecida (por ejemplo, CouchDB).

Emplear datos privados puede ser complicado, y existen patrones para utilizarlos en distintas circunstancias. Cubriremos la mayoría de estas funciones en el Capítulo 5, cuando implementemos funciones de datos privados para invocarlas, y de nuevo en el Capítulo 6, cuando las utilicemos en el mantenimiento y las pruebas.

Establecer control de acceso basado en atributos

Con el tiempo, necesitarás una forma de implementar la autenticación y autorización granular. Los clientes tienen una identidad que controla el acceso. Las identidades deben pertenecer a organizaciones autorizadas, y las organizaciones pertenecen a canales que alojan chaincode. Un certificado representa la identidad del cliente. Admite atributos que pueden utilizarse para implementar transacciones y políticas de control de autenticación y autorización a nivel de contrato inteligente.

Accedes a esta información desde el objeto clientIdentity contenido en el contexto de la transacción. Este objeto tiene dos funciones relacionadas con los valores de los atributos:

assertAttributeValue(attrName: string, attrValue: string): boolean;
getAttributeValue(attrName: string): string | null;

Utiliza assertAttributeValue para comprobar la presencia de un atributo y getAttributeValue para recuperar un atributo concreto. Es una buena práctica afirmar el atributo antes de recuperarlo. En los Capítulos 5 y 6 emplearemos atributos con fines de seguridad y otros.

Inicializar el Estado del Libro Mayor

La inicialización del libro mayor es a menudo una tarea necesaria. El siguiente ejemplo del código Fabcar que hemos visto antes ilustra cómo inicializar el estado de tu contrato inteligente. Lo inicializarás justo después de que se haya comprometido. Después de eso, puedes empezar a enviar transacciones y a consultar los datos del libro mayor invocando los métodos del contrato inteligente.

Creas una función a la que llamarás para ejecutar tu inicialización; aquí, se llama initLedger. En la función initLedger, puedes realizar lo que necesites para inicializar. En este ejemplo, creamos una matriz de objetos de negocio, recorremos la matriz cars y añadimos un atributo adicional docType a cada objeto car de la matriz cars. Ésta es la lógica initLedger:

async initLedger(ctx) {
    const cars = [
         {
              color: 'blue',
              make: 'Toyota',
              model: 'Prius',
              owner: 'Tomoko',
         },
         .
         .
         .
         {
              color: 'brown',
              make: 'Holden',
              model: 'Barina',
              owner: 'Shotaro',
              },
    ];
    for (let i = 0; i < cars.length; i++) {
              cars[i].docType = 'car';
              await ctx.stub.putState('CAR' + i,
              Buffer.from(JSON.stringify(cars[i])));
              console.info('Added <--> ', cars[i]);
    }
}

La función initLedger escribe los objetos de la matriz en el libro mayor utilizando la función putState. Para ejecutar la función initLedger, necesitamos invocar el contrato inteligente. Podemos utilizar el comando peer CLI invoke. Veamos cómo podemos llamar a initLedger a través del comando invoke.

Chaincode invocar init

Para ejecutar una función de transacción en nuestro contrato inteligente, podemos utilizar el comando invoke proporcionado por el binario par. Este binario ofrece muchos comandos, varios de los cuales aprenderás en los Capítulos 5 y 6. Aquí lo utilizamos para invocar nuestra función initLedger:

Nota

Hemos impreso el siguiente comando en varias líneas para facilitar su lectura. Cuando escribas la orden, debe estar en una sola línea, o no se ejecutará.

peer chaincode invoke 
-o localhost:7050 
--ordererTLSHostnameOverride orderer.example.com 
--tls true 
--cafile /OReilly/fabric-samples/test-network/organizations/ordererOrganizations
/example.com/orderers/orderer.example.com/msp/tlscacerts/tls/
ca.example.com-cert.pem 
-C mychannel 
-n fabcar 
--peerAddresses localhost:7051 
--tlsRootCertFiles /OReilly/fabric-samples/test-network/organizations
/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt 
--isInit 
-c '{"function":"initLedger","Args":[]}'

[chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200

El comando invoke ejecuta el objeto de comando que sigue a la bandera de argumentos del comando -c. El objeto de comando especifica la función a ejecutar y cualquier argumento de función. Aquí estamos ejecutando la función initLedger, y no hay argumentos de función.

Podemos comprobar los resultados de la función initLedger. Esperamos que devuelva el contenido de la matriz escrita en el libro mayor. Utilizaremos el comando query; veamos cómo podemos consultar los datos del libro mayor.

Consulta del código de la cadena

Utilizando el comando query del par, podemos ejecutar una de las funciones de consulta de nuestro contrato inteligente. En este caso, establecemos la bandera de comando -c para ejecutar queryAllCars:

peer chaincode query 
-C mychannel 
-n fabcar 
-c '{"Args":["queryAllCars"]}'

Aquí tienes la salida de retorno:

[{"Key":"CAR0","Record":{"color":"blue","docType":"car",
"make":"Toyota","model":"Prius","owner":"Tomoko"}},
.
.
.
{"Key":"CAR9","Record":{"color":"brown","docType":"car","make":"Holden",
"model":"Barina","owner":"Shotaro"}}]

El resultado muestra la función initLedger ejecutada, y nuestro contrato inteligente está inicializado.

Instalar e Instanciar un Contrato Inteligente

Como preparación para el Capítulo 5, vamos a repasar en lo que tenemos que hacer una vez que terminemos de codificar nuestro contrato inteligente. En esta sección se analizan varios pasos que debemos realizar para llegar a un punto en el que podamos invocar nuestro contrato inteligente desde la línea de comandos o desde un cliente de contratos inteligentes. Estos pasos son los siguientes

  1. Empaqueta el código de la cadena.

  2. Instala el código de la cadena.

  3. Consulta la instalación.

  4. Aprueba el paquete.

  5. Comprueba la disponibilidad de la confirmación.

  6. Confirma la definición del código de cadena.

  7. Consulta si el código de cadena está comprometido.

  8. Inicializa el contrato.

  9. Ejecuta una consulta.

El Capítulo 5 trata estos pasos con más detalle. Contienen ejemplos de código de línea de comandos, y algunos tienen salida. Los siguientes comandos de peer pueden consultarse en la documentación de Hyperledger Fabric.

Empaqueta el código de la cadena

Lo primero que tenemos que hacer es empaquetar nuestro código. Como puedes ver en el siguiente comando, utilizamos la CLI peer para realizar este paso y todos los pasos restantes.

Para preparar nuestro contrato inteligente, utilizamos el siguiente comando de paquete peer:

peer lifecycle chaincode package fabcar.tar.gz \ 
        --path ../chaincode/fabcar/javascript/ \
        --lang node \
        --label fabcar_1

Una vez completado este comando, tenemos un archivo tar.gz que contiene nuestro contrato inteligente. A continuación, tenemos que instalar este paquete.

Instala el código de la cadena

Después de empaquetar nuestro contrato inteligente , podemos instalarlo. Si colaboran varias organizaciones, no es necesario que todas ellas empaqueten los contratos inteligentes por separado. Todas las organizaciones pueden utilizar un único paquete de contratos inteligentes. Una vez que una organización recibe el paquete, se instala en sus pares que lo respaldan. El Capítulo 9 trata esto con más detalle.

Aquí tienes el comando de instalación, que muestra un mensaje de salida correcto:

peer lifecycle chaincode install fabcar.tar.gz

[cli.lifecycle.chaincode] submitInstallProposal -> INFO 001 Installed

Una vez instalado el contrato, es posible que quieras verificarlo.

Consulta la Instalación

Puedes ejecutar el siguiente comando para obtener los detalles del último chaincode instalado:

peer lifecycle chaincode queryinstalled

Installed chaincodes on peer:
Package ID: fabcar_1:5a00a40697…330bf5de39, 
Label: fabcar_1

Es posible que recibas muchos ID de paquetes, dependiendo del número de paquetes instalados. Puedes utilizar un script para filtrar la salida si necesitas automatizar una tarea que dependa de que un paquete concreto esté instalado o no. Una vez instalado un paquete chaincode, debe ser aprobado.

Aprueba el Paquete

Después de instalar un paquete, una organización de debe aprobarlo antes de que se pueda confirmar y acceder a él. Este comando tiene muchos parámetros. El de mayor interés, para nuestros fines, es –package-id. Podemos obtenerlo de la salida del comando queryinstalled precedente. package-id se utiliza como identificador del paquete de instalación chaincode:

peer lifecycle chaincode approveformyorg 
-o localhost:7050 
--ordererTLSHostnameOverride orderer.example.com 
--tls true 
--cafile /OReilly/fabric-samples/test-network/organizations/ordererOrganizations
/example.com/orderers/orderer.example.com/msp/tlscacerts/
tlsca.example.com-cert.pem 
--channelID mychannel 
--name fabcar 
--version 1 
--init-required 
--package-id fabcar_1:5a00a406972168ac5856857b5867f51d5244208b876206b7e0e418330bf5de39 
--sequence 1

Una vez completado el comando aprobar, estamos listos para determinar si podemos confirmarlo. Para ello utilizamos el comando checkcommitreadiness.

Comprueba la preparación para el compromiso

Un número de organizaciones debe aprobar el paquete chaincode antes de que pueda comprometerse. El número depende de la política, que puede exigir que lo aprueben todas las organizaciones o un subconjunto de organizaciones. Lo ideal es que todas las organizaciones lo aprueben. Podemos utilizar el comando checkcommitreadiness para determinar si podemos confirmar el paquete. En este caso, no podemos porque Org2 aún no lo ha aprobado. Una vez que lo haga, este comando mostrará true para Org1 y true para Org2:

peer lifecycle chaincode checkcommitreadiness 
--channelID mychannel 
--name fabcar 
--version 1 
--sequence 1 
--output json 
--init-required

{
    "approvals": {
        "Org1MSP": true,
        "Org2MSP": false
    }
}

En una configuración de Hyperledger, podemos definir distintos tipos de políticas de respaldo del ciclo de vida. La predeterminada es MAJORITY Endorsement. Esto requiere que la mayoría de los pares respalden una transacción chaincode para su validación y ejecución en el canal y comprometan la transacción en el libro mayor. El Capítulo 9 trata esto con más detalle. Una vez que todas las aprobaciones son verdaderas, podemos comprometer el paquete chaincode.

Comprometer la definición del código de la cadena

Una vez que todas las organizaciones o subconjuntos de la organización han sido aprobados para satisfacer las políticas mencionadas, se puede consignar el código de cadena en el libro mayor. Para confirmar el código de cadena, utilizamos el comando de confirmación que se muestra aquí:

peer lifecycle chaincode commit 
-o localhost:7050 
--ordererTLSHostnameOverride orderer.example.com 
--tls true 
--cafile /OReilly/fabric-samples/test-network/organizations/ordererOrganizations
/example.com/orderers/orderer.example.com/msp/tlscacerts/
tlsca.example.com-cert.pem 
--channelID mychannel 
--name fabcar 
--peerAddresses localhost:7051 
--tlsRootCertFiles /OReilly/fabric-samples/test-network/organizations
/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
--version 1 
--sequence 1
--init-required

Este es el resultado de la ejecución del comando:

[chaincodeCmd] ClientWait -> INFO 001 txid
[ef59101c320469be3242daa9ebe262771fc8cc8bd5cd0854c6424e1d2a0c61c2] committed with status (VALID) at
localhost:9051

A continuación, podemos comprobar si el código de la cadena está comprometido con querycommitted.

Consulta si el código de la cadena está comprometido

Chaincode debe estar comprometido antes de poder ser invocado. Para determinar si chaincode está comprometido, utiliza el comando querycommitted chaincode:

peer lifecycle chaincode querycommitted 
--channelID mychannel 
--name fabcar

Este es el resultado:

Committed chaincode definition for chaincode 'fabcar' on channel 'mychannel':
Version: 1, Sequence: 1, Endorsement Plugin: escc, Validation Plugin: vscc, Approvals: [Org1MSP: true,
Org2MSP: true]

La ejecución del comando querycommitted nos indica que nuestro chaincode está listo. Este chaincode contiene un contrato inteligente que necesita inicialización. Para inicializarlo, lo invocaremos.

Inicializar el Contrato

Por fin podemos inicializar nuestro contrato inteligente porque el chaincode está aprobado y comprometido. Podemos utilizar el comando invoke para ejecutar las transacciones del contrato inteligente:

peer chaincode invoke 
-o localhost:7050 
--ordererTLSHostnameOverride orderer.example.com 
--tls true 
--cafile /OReilly/fabric-samples/test-network/organizations/ordererOrganizations
/example.com/orderers/orderer.example.com/msp/tlscacerts/
tlsca.example.com-cert.pem 
-C mychannel 
-n fabcar 
--peerAddresses localhost:7051 
--tlsRootCertFiles /OReilly/fabric-samples/test-
network/organizations/peerOrganizations/org1.example.com/peers
/peer0.org1.example.com/tls/ca.crt 
--peerAddresses localhost:9051 
--tlsRootCertFiles /OReilly/fabric-samples/test-network/organizations
/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt 
--isInit
-c '{"function":"initLedger","Args":[]}'

Tras ejecutar el comando invoke, veremos la siguiente salida; devuelve un estado de respuesta 200 si la invocación de chaincode se realiza correctamente:

[chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200

En la parte inferior del comando invoke, vemos la bandera de comando -c y el objeto de comando, que contiene la clave y el valor de la función initLedger sin argumentos. Esto significa que se ejecutará la función de transacción del contrato inteligente initLedger. La salida muestra un resultado satisfactorio. Nuestro contrato inteligente está ahora inicializado y listo para los clientes. Ahora podemos probar nuestro contrato inteligente ejecutando una consulta.

Ejecutar una consulta

Hemos seguido los pasos para tomar tu proyecto de código fuente de contrato inteligente e instanciarlo. Podemos probarlo ejecutando una consulta como ésta:

peer chaincode query 
-C mychannel 
-n fabcar 
-c '{"Args":["queryAllCars"]}'

Aquí tienes la salida tras ejecutar el comando queryAllCars:

[{"Key":"CAR0","Record":{"color":"blue","docType":"car",
"make":"Toyota","model":"Prius","owner":"Tomoko"}},
.
.
.
{"Key":"CAR9","Record":{"color":"brown","docType":"car",
"make":"Holden","model":"Barina","owner":"Shotaro"}}]

Esta consulta ejecuta la función de transacción del contrato inteligente llamada queryAllCars. Las escrituras se comprometen y, por lo tanto, requieren aprobación, lo que implica a varios pares. Una consulta no debería tener que asignar tareas a más de un par para su ejecución. Esto es lo que el código del lado del cliente hace por nosotros. Este ejemplo ilustra cómo una función de transacción envuelve una función ChaincodeStub, en este caso una rangeQuery abstraída como queryAllCars.

Resumen

Empezamos configurando nuestro entorno de desarrollo Hyperledger Fabric como preparación para los Capítulos 5 y 6, y utilizándolo para explorar y examinar un contrato inteligente sencillo pero completo llamado Fabcar. El código que escribimos para los contratos inteligentes Fabric depende de las API proporcionadas por los SDK. Cubrimos el código de Fabcar porque es pequeño y sencillo de aprender. Esto nos permitió centrarnos en el código de los contratos inteligentes, las clases e interfaces empleadas y las API de contratos inteligentes Fabric de las que dependemos.

Los SDK de contratos inteligentes Fabric están disponibles para JavaScript, Java y Go, y próximamente habrá más. Hemos utilizado el SDK de contratos inteligentes Fabric de JavaScript para Node.js. Su uso nos permitió explorar fabric-contract-api, y las clases y objetos básicos que necesitamos para desarrollar contratos inteligentes Fabric.

Con el conocimiento de la API, cubrimos cómo crear un contrato inteligente y qué son las funciones de transacción de los contratos inteligentes. Las funciones ejecutan las transacciones de nuestros contratos inteligentes, por lo que era importante introducir varios temas importantes, como validar y sanear los argumentos de las funciones, inicializar los contratos inteligentes e interactuar con el libro mayor. La API del contrato Fabric proporciona la interfaz con el libro mayor, al que aprendiste a acceder en nuestros contratos inteligentes a través del contexto de transacción que recibe cada transacción. Había mucho que cubrir, pero hemos tratado de mantener la sencillez y a la vez proporcionarte una exposición a la fabric-contract-api, que contiene las interfaces que necesitas para diseñar e implementar contratos inteligentes robustos.

Una vez escrito el código del contrato inteligente, tenemos que empaquetarlo e implementarlo en la red Fabric. Esto requiere varios pasos para llevarlo a cabo. Paso a paso, repasamos cada uno de ellos. Es importante conocer estos pasos para llevar nuestros contratos inteligentes del código fuente al código instanciado de la cadena. Sólo podemos ejecutar código instanciado.

En los Capítulos 5 y 6, empaquetaremos e instanciaremos los contratos inteligentes que creemos utilizando los conocimientos aprendidos en este capítulo. Ahora podemos pasar al Capítulo 5 y centrarnos en la invocación de los contratos inteligentes.

Get Desarrollo práctico de contratos inteligentes con Hyperledger Fabric V2 now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.