UNPKG

@logsn/arweave

Version:
289 lines (288 loc) 10.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.debug = exports.validatePath = exports.arrayCompare = exports.bufferToInt = exports.intToBuffer = exports.arrayFlatten = exports.generateProofs = exports.buildLayers = exports.generateTransactionChunks = exports.generateTree = exports.computeRootHash = exports.generateLeaves = exports.chunkData = exports.MIN_CHUNK_SIZE = exports.MAX_CHUNK_SIZE = void 0; /** * @see {@link https://github.com/ArweaveTeam/arweave/blob/fbc381e0e36efffa45d13f2faa6199d3766edaa2/apps/arweave/src/ar_merkle.erl} */ const common_1 = require("../common"); const utils_1 = require("./utils"); exports.MAX_CHUNK_SIZE = 256 * 1024; exports.MIN_CHUNK_SIZE = 32 * 1024; const NOTE_SIZE = 32; const HASH_SIZE = 32; /** * Takes the input data and chunks it into (mostly) equal sized chunks. * The last chunk will be a bit smaller as it contains the remainder * from the chunking process. */ async function chunkData(data) { let chunks = []; let rest = data; let cursor = 0; while (rest.byteLength >= exports.MAX_CHUNK_SIZE) { let chunkSize = exports.MAX_CHUNK_SIZE; // If the total bytes left will produce a chunk < MIN_CHUNK_SIZE, // then adjust the amount we put in this 2nd last chunk. let nextChunkSize = rest.byteLength - exports.MAX_CHUNK_SIZE; if (nextChunkSize > 0 && nextChunkSize < exports.MIN_CHUNK_SIZE) { chunkSize = Math.ceil(rest.byteLength / 2); // console.log(`Last chunk will be: ${nextChunkSize} which is below ${MIN_CHUNK_SIZE}, adjusting current to ${chunkSize} with ${rest.byteLength} left.`) } const chunk = rest.slice(0, chunkSize); const dataHash = await common_1.default.crypto.hash(chunk); cursor += chunk.byteLength; chunks.push({ dataHash, minByteRange: cursor - chunk.byteLength, maxByteRange: cursor, }); rest = rest.slice(chunkSize); } chunks.push({ dataHash: await common_1.default.crypto.hash(rest), minByteRange: cursor, maxByteRange: cursor + rest.byteLength, }); return chunks; } exports.chunkData = chunkData; async function generateLeaves(chunks) { return Promise.all(chunks.map(async ({ dataHash, minByteRange, maxByteRange }) => { return { type: "leaf", id: await hash(await Promise.all([hash(dataHash), hash(intToBuffer(maxByteRange))])), dataHash: dataHash, minByteRange, maxByteRange, }; })); } exports.generateLeaves = generateLeaves; /** * Builds an arweave merkle tree and gets the root hash for the given input. */ async function computeRootHash(data) { const rootNode = await generateTree(data); return rootNode.id; } exports.computeRootHash = computeRootHash; async function generateTree(data) { const rootNode = await buildLayers(await generateLeaves(await chunkData(data))); return rootNode; } exports.generateTree = generateTree; /** * Generates the data_root, chunks & proofs * needed for a transaction. * * This also checks if the last chunk is a zero-length * chunk and discards that chunk and proof if so. * (we do not need to upload this zero length chunk) * * @param data */ async function generateTransactionChunks(data) { const chunks = await chunkData(data); const leaves = await generateLeaves(chunks); const root = await buildLayers(leaves); const proofs = await generateProofs(root); // Discard the last chunk & proof if it's zero length. const lastChunk = chunks.slice(-1)[0]; if (lastChunk.maxByteRange - lastChunk.minByteRange === 0) { chunks.splice(chunks.length - 1, 1); proofs.splice(proofs.length - 1, 1); } return { data_root: root.id, chunks, proofs, }; } exports.generateTransactionChunks = generateTransactionChunks; /** * Starting with the bottom layer of leaf nodes, hash every second pair * into a new branch node, push those branch nodes onto a new layer, * and then recurse, building up the tree to it's root, where the * layer only consists of two items. */ async function buildLayers(nodes, level = 0) { // If there is only 1 node left, this is going to be the root node if (nodes.length < 2) { const root = nodes[0]; // console.log("Root layer", root); return root; } const nextLayer = []; for (let i = 0; i < nodes.length; i += 2) { nextLayer.push(await hashBranch(nodes[i], nodes[i + 1])); } // console.log("Layer", nextLayer); return buildLayers(nextLayer, level + 1); } exports.buildLayers = buildLayers; /** * Recursively search through all branches of the tree, * and generate a proof for each leaf node. */ function generateProofs(root) { const proofs = resolveBranchProofs(root); if (!Array.isArray(proofs)) { return [proofs]; } return arrayFlatten(proofs); } exports.generateProofs = generateProofs; function resolveBranchProofs(node, proof = new Uint8Array(), depth = 0) { if (node.type == "leaf") { return { offset: node.maxByteRange - 1, proof: (0, utils_1.concatBuffers)([ proof, node.dataHash, intToBuffer(node.maxByteRange), ]), }; } if (node.type == "branch") { const partialProof = (0, utils_1.concatBuffers)([ proof, node.leftChild.id, node.rightChild.id, intToBuffer(node.byteRange), ]); return [ resolveBranchProofs(node.leftChild, partialProof, depth + 1), resolveBranchProofs(node.rightChild, partialProof, depth + 1), ]; } throw new Error(`Unexpected node type`); } function arrayFlatten(input) { const flat = []; input.forEach((item) => { if (Array.isArray(item)) { flat.push(...arrayFlatten(item)); } else { flat.push(item); } }); return flat; } exports.arrayFlatten = arrayFlatten; async function hashBranch(left, right) { if (!right) { return left; } let branch = { type: "branch", id: await hash([ await hash(left.id), await hash(right.id), await hash(intToBuffer(left.maxByteRange)), ]), byteRange: left.maxByteRange, maxByteRange: right.maxByteRange, leftChild: left, rightChild: right, }; return branch; } async function hash(data) { if (Array.isArray(data)) { data = common_1.default.utils.concatBuffers(data); } return new Uint8Array(await common_1.default.crypto.hash(data)); } function intToBuffer(note) { const buffer = new Uint8Array(NOTE_SIZE); for (var i = buffer.length - 1; i >= 0; i--) { var byte = note % 256; buffer[i] = byte; note = (note - byte) / 256; } return buffer; } exports.intToBuffer = intToBuffer; function bufferToInt(buffer) { let value = 0; for (var i = 0; i < buffer.length; i++) { value *= 256; value += buffer[i]; } return value; } exports.bufferToInt = bufferToInt; const arrayCompare = (a, b) => a.every((value, index) => b[index] === value); exports.arrayCompare = arrayCompare; async function validatePath(id, dest, leftBound, rightBound, path) { if (rightBound <= 0) { return false; } if (dest >= rightBound) { return validatePath(id, 0, rightBound - 1, rightBound, path); } if (dest < 0) { return validatePath(id, 0, 0, rightBound, path); } if (path.length == HASH_SIZE + NOTE_SIZE) { const pathData = path.slice(0, HASH_SIZE); const endOffsetBuffer = path.slice(pathData.length, pathData.length + NOTE_SIZE); const pathDataHash = await hash([ await hash(pathData), await hash(endOffsetBuffer), ]); let result = (0, exports.arrayCompare)(id, pathDataHash); if (result) { return { offset: rightBound - 1, leftBound: leftBound, rightBound: rightBound, chunkSize: rightBound - leftBound, }; } return false; } const left = path.slice(0, HASH_SIZE); const right = path.slice(left.length, left.length + HASH_SIZE); const offsetBuffer = path.slice(left.length + right.length, left.length + right.length + NOTE_SIZE); const offset = bufferToInt(offsetBuffer); const remainder = path.slice(left.length + right.length + offsetBuffer.length); const pathHash = await hash([ await hash(left), await hash(right), await hash(offsetBuffer), ]); if ((0, exports.arrayCompare)(id, pathHash)) { if (dest < offset) { return await validatePath(left, dest, leftBound, Math.min(rightBound, offset), remainder); } return await validatePath(right, dest, Math.max(leftBound, offset), rightBound, remainder); } return false; } exports.validatePath = validatePath; /** * Inspect an arweave chunk proof. * Takes proof, parses, reads and displays the values for console logging. * One proof section per line * Format: left,right,offset => hash */ async function debug(proof, output = "") { if (proof.byteLength < 1) { return output; } const left = proof.slice(0, HASH_SIZE); const right = proof.slice(left.length, left.length + HASH_SIZE); const offsetBuffer = proof.slice(left.length + right.length, left.length + right.length + NOTE_SIZE); const offset = bufferToInt(offsetBuffer); const remainder = proof.slice(left.length + right.length + offsetBuffer.length); const pathHash = await hash([ await hash(left), await hash(right), await hash(offsetBuffer), ]); const updatedOutput = `${output}\n${JSON.stringify(Buffer.from(left))},${JSON.stringify(Buffer.from(right))},${offset} => ${JSON.stringify(pathHash)}`; return debug(remainder, updatedOutput); } exports.debug = debug;