Node.js

The Auditor is a cautious observer of the ledger that routinely checks the ledger's integrity.

For example, instead of requesting transaction-by-transaction (a transaction in PAD means either a data request or a Trustee/Validator response), an Auditor would request the ledger as raw blocks, which can be used to verify the integrity of the ledger. Trustee attestations allow the Auditor to confirm they have been provided with the same ledger used by the Trustees.

Computing digest of a ledger

The Auditor computes a succinct representation of the ledger, which can then be tested for consistency against the Trustee attestations. Since the ledger forms a blockchain, we define this succinct representation as the hash of the most recent block in the blockchain.

Occasionally, an Auditor may want to check on the transaction details. In the following sections, we will show how an Auditor can:

  • Compute hash of a block

  • Compute hash of a block's data section

  • Extract transactions from a block's data

Computing hash of a block

A block is composed of a header and a data section. The block header section contains the block number, previous block's hash, and the hash of the current block's data section.

Example

{
    "header": {
        "number": "1",
        "previousHash": "0ede53fd38d632f3a7d849f8ba8f70c851d772b53cecbc9d858fe7c8af03a858",
        "dataHash": "ee967360a911deb8f6bd77cbb49b334e1837c006c9b6cb2d59d8acd41964a6ac",
    },
}

When we say "hash" of a block, we mean the hash of its header section. Below is a Node.js example of how to compute the hash given a JSON representation of the header. The dependency cryptoUtil is defined here.

const asn1 = require('asn1.js');

const cryptoUtil = require('./cryptoUtil');

/**
 * @typedef {Object} Header
 * @property {string|number} [number]
 * @property {string|Buffer} [previousHash]
 * @property {string|Buffer} dataHash
 */
/**
 * @param {Header} header
 * @return {Buffer}
 */
function computeBlockHash(header) {
  const body = function() {
    this.seq().obj(
      this.key('number').int(),
      this.key('previousHash').octstr(),
      this.key('dataHash').octstr(),
    );
  };
  const headerAsn1 = asn1.define('headerAsn1', body);
  let {number, previousHash, dataHash} = header;
  if (previousHash == null) {
    previousHash = '';
  }
  if (typeof(number) !== 'number') {
    number = parseInt(number);
  }
  if (!Buffer.isBuffer(previousHash)) {
    previousHash = Buffer.from(previousHash, 'hex');
  }
  if (!Buffer.isBuffer(dataHash)) {
    dataHash = Buffer.from(dataHash, 'hex');
  }
  const encoded = headerAsn1.encode({
    number,
    previousHash,
    dataHash,
  });
  const hash = cryptoUtil.hash(encoded);
  return hash;
}

const header1 = {
  dataHash: "af34032c92ef85b976db007fa339293253bc4e58f144cf648c6ffcd5a1150791",
};
console.log(computeBlockHash(header1).toString('hex'));
// 1c2cf6ed047ab1d35b2ed3bfbba376d99626db2213632dd4b955ceb4c05f3ba8

const header2 = {
    number: 1,
    previousHash: "1c2cf6ed047ab1d35b2ed3bfbba376d99626db2213632dd4b955ceb4c05f3ba8",
    dataHash: "cf8289074798c7e8e1d267f0c0fb83acde339fb3007ff5246fb6745a94d55883",
};
console.log(computeBlockHash(header2).toString('hex'));
// 1af3275c9db7305fc85a3ded00a7829b5d6a99deacd35ed48cd31b79c8689275

Computing data hash

When a user has utilised the data section of a block (for example, he/she extracted a data request and used it for some business logic), it is crucial for him/her to check its consistency against the dataHash entry in the block header.

The data section is an array of base64-encoded strings. Its hash is defined as the hash on the concatenation of these binary data. Below is a sample script for computing the data hash of a block:

const cryptoUtil = require('./cryptoUtil');

/**
 * @param {string[]} data
 * @return {Buffer}
 */
function computeDataHash(data) {
  const dataArray = data.map((d) => {
    return Buffer.from(d, 'base64');
  });
  return cryptoUtil.hash(...dataArray);
}

const block = {
  header: {
    dataHash: "4a148a98c3f5216c480bed070e267186751da47d3782a4282b2c383879097c80",
  },
  data: {
    data: ["datum1==", "datum2=="], // fake data
  },
};

const dataHashHex = block.header.dataHash;
const dataHash = Buffer.from(dataHashHex, 'hex');

const data = block.data.data;
console.log(computeDataHash(data).equals(dataHash)); // true

Extracting transactions from block data

Functions and parameters

There are 4 smart contract functions that write on the ledger. Their parameters are as follows:

  • Init: writes metadata of the instance

    1. stringified Trustee public keys

    2. trustee threshold

    3. stringified Validator public keys

    4. validator threshold

  • PostDataRequest: posts a data request

    1. a token

  • PostTrusteeResponse: posts a Trustee response

    1. Trustee response payload

    2. Trustee's digital signature on 1

  • PostValidatorResponse: posts a Validator response

    1. Validator response payload

    2. Validator's digital signature on 1

A piece of block data may contain one of these functions' invocations and their parameters.

Extracting the transactions

It is possible to extract the invoked smart contract functions and parameters from block data. The resulting value is an array of arrays of strings where the first strings in the inner arrays are the functions and the succeeding strings are the parameters. Since the blocks are protocol-buffer-encoded, we need to use a library protobufjs for decoding.

const {Type, Field} = require('protobufjs');

const protoChaincodeInput = new Type('ChaincodeInput')
    .add(new Field('args', 1, 'string', 'repeated'));

const protoChaincodeSpec = new Type('ChaincodeSpec')
    .add(new Field('input', 3, 'ChaincodeInput')).add(protoChaincodeInput);

const protoChaincodeProposalPayloadInput = new Type('ChaincodeProposalPayloadInput')
    .add(new Field('chaincodeSpec', 1, 'ChaincodeSpec')).add(protoChaincodeSpec);

const protoChaincodeProposalPayload = new Type('ChaincodeProposalPayload')
    .add(new Field('input', 1, 'ChaincodeProposalPayloadInput')).add(protoChaincodeProposalPayloadInput);

const protoChaincodeActionPayload = new Type('ChaincodeActionPayload')
    .add(new Field('chaincodeProposalPayload', 1, 'ChaincodeProposalPayload')).add(protoChaincodeProposalPayload);

const protoTransactionAction = new Type('TransactionAction')
    .add(new Field('payload', 2, 'ChaincodeActionPayload')).add(protoChaincodeActionPayload);

const protoTransaction = new Type('Transaction')
    .add(new Field('actions', 1, 'TransactionAction', 'repeated')).add(protoTransactionAction);

const protoPayload = new Type('Payload')
    .add(new Field('data', 2, 'Transaction')).add(protoTransaction);

const protoEnvelope = new Type('Envelope')
    .add(new Field('payload', 1, 'Payload')).add(protoPayload);

function getTransactions(bytes) {
  const txs = [];
  const envelope = protoEnvelope.decode(bytes);
  const actions = envelope.toJSON().payload.data.actions;
  for (const action of actions) {
    txs.push(action.payload.chaincodeProposalPayload
        .input.chaincodeSpec.input.args);
  }
  return txs;
}

const block = {
  header: {
    // ...
  },
  data: {
    data: [
      'CpsDEpgDCpUDEpIDCo8DCowDCokDGoYDChNQb3N0VHJ1c3RlZVJlc3BvbnNlCrIBeyJ0eXBlIjoidHJ1c3RlZV9yZXNwb25zZSIsInBhZE5hbWUiOiJteS1wYWQtMS4wIiwidHJ1c3RlZUlkIjoidHJ1c3RlZTEiLCJ0b2tlbiI6ImYxMzQxZmM5MDU5Zjg1N2MyMThmNTEwNzcxZTJjODlmIiwidHJ1c3RlZVNoYXJlIjoiQWtGdVdiQUcxU2NjbDY5UlduMnhIU2lRUGRsaFlvbVpuRnF5SE81bFFEZGwifQq5AXsic2lnbmVyTWV0YWRhdGEiOnsiaWQiOiJ0cnVzdGVlMSIsImZ1bGxOYW1lIjoiVHJ1c3RlZS0xIiwicm9sZSI6IlRydXN0ZWUifSwicGF5bG9hZCI6Ik1FWUNJUUNCMHJFa2FvZGR6VWQ0eXVJUmFuTlZ5ZVlDK21JZmxPSm5Mdm82RnF0QXRBSWhBSys3cWxtenJnZ1diRTEzVDJvcUErNS9wSUdiZmluZFl3OEFTbU1xV21vNSJ9',
      'CkESPwo9EjsKOQo3CjUaMwoPUG9zdERhdGFSZXF1ZXN0CiAyMWJmNTdkZDUyYTNiODEzMjhmZTEzOGFjNGU4ZWQxYg==',
    ],
  },
}
const blockData = block.data.data;

const data0B64 = blockData[0];
const data0 = Buffer.from(data0B64, 'base64');

console.log(getTransactions(data0));
// [
//   [
//     'PostTrusteeResponse',
//     '{"type":"trustee_response","padName":"my-pad-1.0","trusteeId":"trustee1","token":"f1341fc9059f857c218f510771e2c89f","trusteeShare":"AkFuWbAG1Sccl69RWn2xHSiQPdlhYomZnFqyHO5lQDdl"}',
//     '{"signerMetadata":{"id":"trustee1","fullName":"Trustee-1","role":"Trustee"},"payload":"MEYCIQCB0rEkaoddzUd4yuIRanNVyeYC+mIflOJnLvo6FqtAtAIhAK+7qlmzrggWbE13T2oqA+5/pIGbfindYw8ASmMqWmo5"}'
//   ]
// ]

const data1B64 = blockData[1];
const data1 = Buffer.from(base1B64, 'base64');

console.log(getTransactions(data1));
// [
//   [
//     'PostTrusteeResponse',
//     '21bf57dd52a3b81328fe138ac4e8ed1b'
//   ]
// ]

Examples of transactions and parameters

Coming soon!

Last updated