From 2b4d5771d1463cf8ec95e6a4fb00f9d1630996bc Mon Sep 17 00:00:00 2001 From: christopherbrumm Date: Mon, 3 Jun 2024 11:28:36 +0200 Subject: [PATCH] update: add merkle root to tendermint bundle summary --- integrations/tendermint/src/runtime.ts | 10 ++++- integrations/tendermint/utils/merkle.ts | 56 +++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 integrations/tendermint/utils/merkle.ts diff --git a/integrations/tendermint/src/runtime.ts b/integrations/tendermint/src/runtime.ts index da60d30e..b7b33726 100644 --- a/integrations/tendermint/src/runtime.ts +++ b/integrations/tendermint/src/runtime.ts @@ -4,6 +4,7 @@ import axios from 'axios'; import Ajv from 'ajv'; import block_schema from './schemas/block.json'; import block_results_schema from './schemas/block_result.json'; +import { createHashesFromTendermintBundle, generateMerkleRoot } from "../utils/merkle"; const ajv = new Ajv(); @@ -236,8 +237,13 @@ export default class Tendermint implements IRuntime { } async summarizeDataBundle(_: Validator, bundle: DataItem[]): Promise { - // use latest block height as bundle summary - return bundle.at(-1)?.value?.block?.block?.header?.height ?? ''; + const hashes: Uint8Array[] = createHashesFromTendermintBundle(bundle); + + const merkleRoot: Uint8Array = generateMerkleRoot(hashes); + + return JSON.stringify({ + "merkle_root": Buffer.from(merkleRoot).toString("hex") + }) } async nextKey(_: Validator, key: string): Promise { diff --git a/integrations/tendermint/utils/merkle.ts b/integrations/tendermint/utils/merkle.ts new file mode 100644 index 00000000..ffa6e822 --- /dev/null +++ b/integrations/tendermint/utils/merkle.ts @@ -0,0 +1,56 @@ +import * as crypto from '@cosmjs/crypto'; + +// Creates an Array of hashes from an array of data items (bundle). +// The hash of a data item consists of the Merkle root from the block and the block +// results (only two leafs) and the key of the data item. This allows the +// Trustless API to serve block and block results independently. +export function createHashesFromTendermintBundle(bundle: any[]): Uint8Array[] { + return bundle.map(dataItem => { + const blockHashes: Uint8Array[] = [dataItemToSha256(dataItem.value?.block), dataItemToSha256(dataItem.value?.block_results)]; + + const merkleRoot: Uint8Array = generateMerkleRoot(blockHashes); + + return tendermintDataItemToSha256(dataItem.key, merkleRoot) + }) +} + +function tendermintDataItemToSha256(key: string, merkleRoot: Uint8Array): Uint8Array { + const keyBytes = crypto.sha256(Buffer.from(key, 'utf-8')); + + const combined = Buffer.concat([keyBytes, merkleRoot]); + + return crypto.sha256(combined); +} + +function dataItemToSha256(data: any): Uint8Array { + // Encode the serialized object to UTF-8 + const encoded_obj: Uint8Array = Buffer.from(JSON.stringify(data), 'utf-8'); + // Calculate the SHA-256 hash + return crypto.sha256(encoded_obj) +} + +export function generateMerkleRoot(hashes: Uint8Array[]): Uint8Array { + if (!hashes || hashes.length == 0) { + return Buffer.from(""); + } + + // Ensure number of hashes (leafs) are even by copying the + // last hash (the very right leaf) if the amount is odd + if (hashes.length % 2 !== 0) { + hashes.push(hashes[hashes.length - 1]); + } + + const combinedHashes: Uint8Array[] = []; + for(let i = 0; i < hashes.length; i += 2) { + const hashesConcatenated = new Uint8Array([ ...hashes[i], ...hashes[i+1]]) + const hash = crypto.sha256(hashesConcatenated); + combinedHashes.push(hash); + } + + // If the combinedHashes length is 1, it means that we have the merkle root already, + // and we can return the hex representation + if (combinedHashes.length === 1) { + return combinedHashes[0]; + } + return generateMerkleRoot(combinedHashes); +} \ No newline at end of file