El componente más importante de cualquier tecnología Blockchain es la transacción. Una transacción implementa la operación básica y atómica que se puede realizar en una Blockchain. Aunque cada tecnología Blockchain tiene su propia arquitectura de transacción, todas se basan en un esquema muy parecido al siguiente:
- Dirección origen. Dirección de la identidad que ha emitido la transacción.
- Dirección destino. Dirección de la identidad a la que se envía la transacción.
- Firma digital. Consiste en la firma digital que realiza la identidad de origen y que permite a la identidad destino comprobar la autoría de la transacción.
- Datos. Corresponde al conjunto de datos que transporta la transacción.
Aunque este es un esquema muy sencillo, es la base sobre la que se construyen todos los tipos de transacciones de las distintas tecnologías Blockchain.
Transacciones de tipo UTXO
Algunos tipos de transacciones tiene esquemas sencillos, porque los casos de usos para los que fueron diseñadas solo sencillo, por ejemplo, una transacción Bitcoin implementa un esquema UTXO (unspent transaction output ), que consiste incluir en la transacción, una o varias transacciones de entrada y una o varias transacciones de salida. El siguiente cuadro representa un ejemplo de una transacción Bitcoin, en la que podemos ver:
- txid. El identificador de la transacción.
- inputs. Con la transacción de origen.
- outputs. Con las direcciones de destino.
{ "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", "size": 275, "version": 1, "locktime": 0, "fee": 0, "inputs": [ { "coinbase": false, "txid": "0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9", "output": 0, "sigscript": "47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901", "sequence": 4294967295, "pkscript": "410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac", "value": 5000000000, "address": "12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S", "witness": [] } ], "outputs": [ { "address": "1Q2TWHE3GMdB6BZKafqwxXtWAWgFt5Jvm3", "pkscript": "4104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84cac", "value": 1000000000, "spent": true, "spender": { "txid": "ea44e97271691990157559d0bdd9959e02790c34db6c006d779e82fa5aee708e", "input": 0 } }, { "address": "12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S", "pkscript": "410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac", "value": 4000000000, "spent": true, "spender": { "txid": "a16f3ce4dd5deb92d98ef5cf8afeaf0775ebca408f708b2146c4fb42b41e14be", "input": 0 } } ], "block": { "height": 170, "position": 1 }, "deleted": false, "time": 1231731025, "rbf": false, "weight": 1100 }
Como se puede ver, el propósito de este tipo de transacciones es poder implementar una forma segura de transferir valor, en este caso bitcoins, sin la necesidad de implementar una gestión de saldos. El saldo de un usuario está formado, por la suma de todas las transacciones que tengan un output dirigido a su dirección y que no exista una transacción que tenga un input asociado a dichos outputs.
Transacciones Hyperledger Fabric
Las transacciones de Fabric tienen algunas peculiaridades que las diferencian de otras tecnologías Blockchain. Por ejemplo, no existe la dirección de destino, de hecho en Fabric las transacciones tienen como destino un chaincode, nunca la dirección de una identidad digital. Esto puede parecer chocante al principio, pero hay que tener en cuenta que Fabric se pensó para trabajar con procesos, los cuales, deben estar implementados con los chaincodes. Pero esta no es la única particularidad de las transacciones de Fabric, otra bastante curiosa es que la transacción incorpora varias firmas digitales, no solo la del emisor de la transacción. El ciclo de vida de una transacción de Fabric pasa por varias fases, una de ellas es la fase de Endorsement, en la que uno o varios peer deben ejecutar la transacción y devolver una respuesta. Esta respuesta está firmada digitalmente por los nodos que participan en el Endorsement.
Pero antes de meternos a fondo a ver la anatomía de una transacción de Fabric, hagamos una pequeña introducción sobre la estructura de un bloque. Porque también tiene algunas peculiaridades interesantes de conocer.
Para representar y explicar las estructuras de datos y los tipos, voy a utilizar las estructura de Go que tienes disponible en el código fuente de Fabric y la salida de nuestra herramienta InspectorBlock, que desarrollamos para inspeccionar transacciones y bloques utilizando los ficheros de la Blockchain.
Un bloque de Fabric
La estructura de un bloque está formada por tres atributos, los cuales se utilizan para gestionar la cabecera, los datos y los metadatos. El siguiente bloque muestra la estructura con los atributos y los tipos de cada uno de los atributos.
type Block struct {
Header *BlockHeader
Data *BlockData
Metadata *BlockMetadata
}
Cabecera del bloque
La cabecera del bloque se gestiona con la estructura de datos BlockHeader, la cual está formada por tres atributos como se puede ver en el siguiente cuadro.
- Number, que identifica al bloque de manera numérica.
- PreviousHash, es un array de bytes que almacena al hash del bloque anterior.
- DataHash, es un array de bytes que almacena el hash de la sección Data del bloque, consiste en el hash del bloque.
type BlockHeader struct {
Number uint64
PreviousHash []byte
DataHash []byte
}
Datos del bloque
Los datos del bloque se gestionan con una estructura de tipo BlockData, la cual tiene un único atributo, Data que es un array de arrays de bytes. Este array los forman las transacciones que se han incluido en el bloque. Debemos utilizar el tipo Envelope, para convertir los arrays de bytes en estructura de tipo Envelope.
type BlockData struct {
Data [][]byte
}
type Envelope struct {
Payload []byte
Signature []byte
}
La estructura Envelope es la base de la transacción de Fabric. Envelope está formada por dos atributos, Payload y Signature, que forman la transacción y que veremos con mayor detalle posteriormente.
Metadatos del bloque
Cada bloque tiene una estructura de tipo BlockMetadata para gestionar los metadatos. Esta estructura tiene un único atributo que es un array de arrays de bytes. Este array tiene 5 posiciones, cada una de ellas con un conjunto de datos distintos. En el cuadro se pueden ver las 5 constantes definidas en el código de Fabric para identificar a las distintas posiciones del array.
type BlockMetadata struct {
Metadata [][]byte
}
const (
BlockMetadataIndex_SIGNATURES BlockMetadataIndex = 0
BlockMetadataIndex_LAST_CONFIG BlockMetadataIndex = 1
BlockMetadataIndex_TRANSACTIONS_FILTER BlockMetadataIndex = 2
BlockMetadataIndex_ORDERER BlockMetadataIndex = 3
BlockMetadataIndex_COMMIT_HASH BlockMetadataIndex = 4
)
- BlockMetadataIndex_SIGNATURES. Esta sección registra las firmas de los nodos del servicio de Orderer que han generado el bloque.
- BlockMetadataIndex_LAST_CONFIG. Esta sección almacena la posición del último bloque de configuración.
- BlockMetadataIndex_TRANSACTIONS_FILTER. Aquí se almacenan el estado de validación de cada una de las transacciones que forman el bloque. Consiste en un array con los códigos de estado de cada una de las transacciones del bloque.
- BlockMetadataIndex_COMMIT_HASH. Es el hash del TRANSACTIONS_FILTER, de los updates y el COMMIT_HASH del bloque anterior.
Estructura de la transacción
Ya hemos dado un pequeño repaso a la estructura de un bloque en Fabric, ahora vamos a ver cuál es la estructura de una transacción, porque como hemos comentado anteriormente, existen alguna peculiaridades que son interesante que conozcamos.
Como vimos en la sección anterior, el bloque dispone de una estructura de datos de tipo BlockData, la cual contiene un array de elementos de tipo Envelope. Cada uno de estos elementos de tipo Envelope corresponde con una transacción. También hemos visto que el tipo Envelope está formado por dos atributos, Payload que contiene los datos relacionados con la transacción y Signature que contiene la firma digital de la transacción.
type Envelope struct {
Payload []byte
Signature []byte
}
Vamos a ver el atributo Payload, que es de tipo Payload y está formado por dos atributo, Header con la información de la cabecera y Data con el cuerpo de la transacción.
type Payload struct {
Header *Header
Data []byte
}
El atributo Header es de tipo Header, el cuál tiene la siguiente estructura:
Payload
Header *Header
ChannelHeader
TxId: 823af8439bbfe63e5d0b4df3934eb779300c5d57619ac657aeb1598640d26f30
ChannelId: test
Version: 0
Type: ENDORSER_TRANSACTION
Timestamp: 2023-06-07 15:16:59 +0000 UTC
Epoch: 0
Extensions
Path:
Name: prueba01
Version:
SignatureHeader
Creator []byte
SerializedIdentity
MspId : org01
IdBytes: OU=admin,C=US,CN=admin@org01.test01.zz
- ChannelHeader, con la información del tipo de transacción, el identificador de la transacción,Timestamp o el identificador del canal.
- SignatureHeader, que tiene la información de la identidad del creador de la transacción. Para el ejemplo, el usuario “admin@org01.test01.zz”. El atributo IdBytes en realidad contiene el certificado como un array de bytes, aquí los hemos representado con el “Subject” del x.509.
Ahora veamos el otro atributo de la estructura Payload, hablamos del atributo Data, el cual puede convertirse en varios tipos de datos distintos, en función del tipo de transacción.
type Payload struct {
Header *Header
Data []byte
}
Para saber el tipo de transacción debemos utilizar el atributo Payload.Header.ChannelHeader.Type, el cual nos facilitará alguna de las siguientes:
const (
HeaderType_MESSAGE HeaderType = 0
HeaderType_CONFIG HeaderType = 1
HeaderType_CONFIG_UPDATE HeaderType = 2
HeaderType_ENDORSER_TRANSACTION HeaderType = 3
HeaderType_ORDERER_TRANSACTION HeaderType = 4
HeaderType_DELIVER_SEEK_INFO HeaderType = 5
HeaderType_CHAINCODE_PACKAGE HeaderType = 6
HeaderType_PEER_ADMIN_OPERATION HeaderType = 8
)
Cada uno de estos tipos de transacciones nos ayudan a conocer cómo debemos convertir el array de bytes que es el atributo Data en tipo de datos relacionado con el tipo de transacción. Por ejemplo, si tenemos un transacción de tipo:
- HeaderType_CONFIG debemos convertir Data a tipo ConfigEnvelope
- HeaderType_CONFIG_UPDATE debemos convertir Data a tipo ConfigUpdateEnvelope
- HeaderType_ENDORSER_TRANSACTION debemos convertir Data a tipo Transaction
- HeaderType_ORDERER_TRANSACTION debemos convertir Data a tipo Envelope
Para nuestro ejemplo, el tipo de transacción es HeaderType_ENDORSER_TRANSACTION, por lo tanto, debemos convertir el contenido del atributo Data al tipo Transaction. El tipo Transaction tiene un único atributo, que es un array de elementos de tipo TransactionAction.
type Transaction struct {
Actions []*TransactionAction
}
Y aquí vamos a detenernos un momento, porque para continuar debemos aclarar este concepto. Y es que en Fabric, una transacción puede estar formada por varias acciones. Normalmente, una transacción tiene una única acción, pero podríamos construir transacciones formadas por varias acciones. ¿Pero cuándo podríamos necesitar incluir en una transacción varias acciones? El objetivo de disponer de varias acciones dentro de la misma transacción, es ejecutar todas las acciones de manera atómica y en caso de que alguna de ellas no pueda ser validada, el resto de acciones quedarán invalidadas también. Es decir, utilizar grupos de acciones, nos permite agruparlas de manera que se validan todas o no se validan ninguna.
Las acciones que forman parte de la transacción son manejadas con una estructura de tipo TransactionAction, que tiene dos atributos, Header y Payload, ambos son un array de bytes.
type TransactionAction struct {
Header []byte
Payload []byte
}
El contenido del atributo Header se convierte a un tipo SignatureHeader. Igual que ocurre con Envelope.Payload.Header.SignatureHeader, este tipo gestiona la identidad del creador de la transacción, en este caso, sería la identidad del creador de la acción. Para transacciones que tengan una sola acción, ambos creadores suelen ser el mismo.
TransactionAction
Header []byte
SignatureHeader
Creator []byte
SerializedIdentity
MspId : org01
IdBytes: OU=admin,C=US,CN=admin@org01.test01.zz
El otro atributo de la estructura TransactionAction es Payload, cuyo contenido se convierte al tipo ChaincodeActionPayload, que cuenta con dos atributos, tal como se ve en el siguiente cuadro, un atributo ChaincodeProposalPayload y otro Action. El primero contiene información sobre la llamada al chaincode, nombre, argumentos, y tipo.
TransactionAction
Payload []byte
ChaincodeActionPayload
ChaincodeProposalPayload []byte
Input []byte
ChaincodeSpec
type:Golang
name: prueba01
args[0]: ChangeContador
args[1]: contador108
TransientMap
Action *ChaincodeEndorsedAction
El segundo atributo de ChaincodeActionPayload es Action y es de tipo *ChaincodeEndorsedAction. Este tipo tiene dos atributos, ProposalResponsePayload que es un array de bytes y Endorsements que es un array de tipo Endorsement.
TransactionAction
Payload []byte
ChaincodeActionPayload
Action *ChaincodeEndorsedAction
ProposalResponsePayload []byte
Endorsements []*Endorsement
Vamos a ver primero el atributo Endorsements. Es un array con la información de todos los Peers que han participado en la fase de endorsement de la transacción. Para el ejemplo, han participado un peer de cada una de las dos organizaciones que pertenecen al channel, que han sido los Peers peer01.org01.test01.zz y peer01.org02.test01.zz. Además se incluye la firma digital de la ProposalResponsePayload que ha ejecutado cada Peer.
Endorsements []*Endorsement
Endorsements[0]
Endorser
Creator
MspID: org01
IdBytes: OU=peer,CN=peer01.org01.test01.zz
Signature: 304509a257...f08cd6a1785c8f06e5ad2a1aabcad258e6fbd860c559
Endorsements[1]
Endorser
Creator
MspID: org02
IdBytes: OU=peer,CN=peer01.org02.test01.zz
Signature: 304402...3014fc21c462c83fb508fe85ccf677c70023f
Una vez visto el contenido del atributo Endorsements, volvamos a ProposalResponsePayload. Este atributo es un array de bytes que se debe transformar al tipo ProposalResponsePayload, que tiene dos atributos, ProposalHash que es un hash de la propuesta y el atributo Extension con todas las acciones que el chaincode ha realizado sobre el worldstate.
ProposalResponsePayload []bytes
ProposalResponsePayload
ProposalHash: ba1c2da0606d915c136d6fe32c692e6c01de384de25abf120d345d3864d66b58
Extension []byte
ChaincodeAction
Results []byte
TxRwSet
NsRwSets[0]
NsRwSet
NameSpace: prueba01
KvRwSet
Reads
KVRead
Key: contador108
Version: 58.3
Writes
KVWrite
Key: contador108
IsDelete: false
Value: 1
En el ejemplo del recuadro anterior podemos ver, como la acción que realiza el chaincode prueba01 consiste en leer del worldstate la key con valor contador108 y generar una escritura sobre la misma key, actualizando su valor con el dato “1”. La lectura se ha realizado sobre la versión “58.3”, esta forma de versionar los datos consiste en utilizar el siguiente esquema:
<#bloque>.<#tx>
Para nuestro ejemplo, la última modificación de la key contador108 se produjo en la transacción número 3 del bloque 58. Si vamos a la blockchain y localizamos ese bloque y transacción, podremos comprobar que hay una escritura sobre la key “contador108”.
La siguiente imagen muestra un ejemplo de la estructura básica de un bloque, en la que se puede ver de forma fácil, todo lo que hemos comentado a lo largo del post.
Conclusión
En este post hemos analizado la estructura de uno de los tipos de transacciones que tiene Fabric, el tipo ENDORSER_TRANSACTION, es la transacción más habitual en cualquier infraestructura Fabric y es importante conocer su estructura, para entender cómo Fabric gestiona el ciclo de vida las transacciones, los datos que maneja sobre el chaincode, los peers que participan en el proceso de endorsement o el estado de validación. He dejado en el tintero para futuros post varios temas relacionados con la forma en la que Fabric gestiona las transacciones, por ejemplo, los otros tipos de transacciones que existen como CONFIG_UPDATE o la forma en la que la transacción maneja los datos cuando utilizamos las colecciones privadas de Fabric.
José Juan Mora Pérez
CTO & Founder