@logsn/arweave
Version:
Arweave JS client library
289 lines (288 loc) • 10.2 kB
JavaScript
;
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;