UNPKG

@bsv/wallet-toolbox-client

Version:
814 lines 38.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BulkFileDataReader = exports.BulkFileDataManager = void 0; exports.selectBulkHeaderFiles = selectBulkHeaderFiles; const sdk_1 = require("../../../../sdk"); const sdk_2 = require("@bsv/sdk"); const utilityHelpers_noBuffer_1 = require("../../../../utility/utilityHelpers.noBuffer"); const validBulkHeaderFilesByFileHash_1 = require("./validBulkHeaderFilesByFileHash"); const HeightRange_1 = require("./HeightRange"); const blockHeaderUtilities_1 = require("./blockHeaderUtilities"); const ChaintracksFetch_1 = require("./ChaintracksFetch"); const TestUtilsWalletStorage_1 = require("../../../../../test/utils/TestUtilsWalletStorage"); const SingleWriterMultiReaderLock_1 = require("./SingleWriterMultiReaderLock"); /** * Manages bulk file data (typically 8MB chunks of 100,000 headers each). * * If not cached in memory, * optionally fetches data by `sourceUrl` from CDN on demand, * optionally finds data by `fileId` in a database on demand, * and retains a limited number of files in memory, * subject to the optional `maxRetained` limit. */ class BulkFileDataManager { static createDefaultOptions(chain) { return { chain, maxPerFile: 100000, maxRetained: 2, fetch: new ChaintracksFetch_1.ChaintracksFetch(), fromKnownSourceUrl: 'https://cdn.projectbabbage.com/blockheaders' }; } constructor(options) { this.bfds = []; this.fileHashToIndex = {}; this.lock = new SingleWriterMultiReaderLock_1.SingleWriterMultiReaderLock(); if (typeof options === 'object') options = options; else options = BulkFileDataManager.createDefaultOptions(options); this.chain = options.chain; this.maxPerFile = options.maxPerFile; this.maxRetained = options.maxRetained; this.fromKnownSourceUrl = options.fromKnownSourceUrl; this.fetch = options.fetch; this.deleteBulkFilesNoLock(); } async createReader(range, maxBufferSize) { range = range || (await this.getHeightRange()); maxBufferSize = maxBufferSize || 1000000 * 80; // 100,000 headers, 8MB return new BulkFileDataReader(this, range, maxBufferSize); } async updateFromUrl(cdnUrl) { if (!this.fetch) throw new sdk_1.WERR_INVALID_OPERATION('fetch is not defined in the BulkFileDataManager.'); const toUrl = (file) => this.fetch.pathJoin(cdnUrl, file); const url = toUrl(`${this.chain}NetBlockHeaders.json`); const availableBulkFiles = (await this.fetch.fetchJson(url)); if (!availableBulkFiles) throw new sdk_1.WERR_INVALID_PARAMETER(`cdnUrl`, `a valid BulkHeaderFilesInfo JSON resource available from ${url}`); const selectedFiles = selectBulkHeaderFiles(availableBulkFiles.files, this.chain, this.maxPerFile || availableBulkFiles.headersPerFile); for (const bf of selectedFiles) { if (!bf.fileHash) { throw new sdk_1.WERR_INVALID_PARAMETER(`fileHash`, `valid for all files in json downloaded from ${url}`); } if (!bf.chain || bf.chain !== this.chain) { throw new sdk_1.WERR_INVALID_PARAMETER(`chain`, `"${this.chain}" for all files in json downloaded from ${url}`); } if (!bf.sourceUrl || bf.sourceUrl !== cdnUrl) bf.sourceUrl = cdnUrl; } const rangeBefore = await this.getHeightRange(); const r = await this.merge(selectedFiles); const rangeAfter = await this.getHeightRange(); let log = 'BulkDataFileManager.updateFromUrl\n'; log += ` url: ${url}\n`; log += ` bulk range before: ${rangeBefore}\n`; log += ` bulk range after: ${rangeAfter}\n`; (0, TestUtilsWalletStorage_1.logger)(log); } async setStorage(storage) { return this.lock.withWriteLock(async () => this.setStorageNoLock(storage)); } async setStorageNoLock(storage) { this.storage = storage; // Sync bfds with storage. Two scenarios supported: const sfs = await this.storage.getBulkFiles(); if (sfs.length === 0) { // 1. Storage has no files: Update storage to reflect bfds. for (const bfd of this.bfds) { await this.ensureData(bfd); bfd.fileId = await this.storage.insertBulkFile(bfdToInfo(bfd, true)); } } else { // 2. bfds are a prefix of storage, including last bfd having same firstHeight but possibly fewer headers: Merge storage to bfds. const r = await this.mergeNoLock(sfs); } } async deleteBulkFiles() { return this.lock.withWriteLock(async () => this.deleteBulkFilesNoLock()); } deleteBulkFilesNoLock() { this.bfds = []; this.fileHashToIndex = {}; if (this.fromKnownSourceUrl) { const files = selectBulkHeaderFiles(validBulkHeaderFilesByFileHash_1.validBulkHeaderFiles.filter(f => f.sourceUrl === this.fromKnownSourceUrl), this.chain, this.maxPerFile); for (const file of files) { this.add({ ...file, fileHash: file.fileHash, mru: Date.now() }); } } } async merge(files) { return this.lock.withWriteLock(async () => this.mergeNoLock(files)); } async mergeNoLock(files) { const r = { inserted: [], updated: [], unchanged: [], dropped: [] }; for (const file of files) { const hbf = this.getBfdForHeight(file.firstHeight); if (hbf && file.fileId) hbf.fileId = file.fileId; // Always update fileId if provided const lbf = this.getLastBfd(); if (hbf && hbf.fileHash === file.fileHash && hbf.count === file.count && hbf.lastHash === file.lastHash && hbf.lastChainWork === file.lastChainWork) { // We already have an identical matching file... r.unchanged.push(bfdToInfo(hbf)); continue; } const vbf = await this.validateFileInfo(file); if (hbf) { // We have a matching file by firstHeight but count and fileHash differ await this.update(vbf, hbf, r); } else if (isBdfIncremental(vbf) && lbf && isBdfIncremental(lbf)) { await this.mergeIncremental(lbf, vbf, r); } else { const added = this.add(vbf); r.inserted.push(added); if (this.storage) { vbf.fileId = await this.storage.insertBulkFile(added); } } } (0, TestUtilsWalletStorage_1.logger)(`BulkFileDataManager.merge:\n${this.toLogString(r)}\n`); return r; } async mergeIncremental(lbf, vbf, r) { lbf.count += vbf.count; lbf.lastHash = vbf.lastHash; lbf.lastChainWork = vbf.lastChainWork; await this.ensureData(lbf); const newData = new Uint8Array(lbf.data.length + vbf.data.length); newData.set(lbf.data); newData.set(vbf.data, lbf.data.length); lbf.data = newData; delete this.fileHashToIndex[lbf.fileHash]; lbf.fileHash = (0, utilityHelpers_noBuffer_1.asString)(sdk_2.Hash.sha256((0, utilityHelpers_noBuffer_1.asArray)(newData)), 'base64'); this.fileHashToIndex[lbf.fileHash] = this.bfds.length - 1; lbf.mru = Date.now(); const lbfInfo = bfdToInfo(lbf, true); r.updated.push(lbfInfo); if (this.storage && lbf.fileId) { await this.storage.updateBulkFile(lbf.fileId, lbfInfo); } } toLogString(what) { let log = ''; if (!what) { log += this.toLogString(this.bfds); } else if (what['updated']) { what = what; for (const { category, bfds } of [ { category: 'unchanged', bfds: what.unchanged }, { category: 'dropped', bfds: what.dropped }, { category: 'updated', bfds: what.updated }, { category: 'inserted', bfds: what.inserted } ]) { if (bfds.length > 0) { log += ` ${category}:\n`; log += this.toLogString(bfds); } } } else if (Array.isArray(what)) { what = what; let i = -1; for (const bfd of what) { i++; log += ` ${i}: ${bfd.fileName} fileId=${bfd.fileId} ${bfd.firstHeight}-${bfd.firstHeight + bfd.count - 1}\n`; } } return log; } async mergeIncrementalBlockHeaders(newBulkHeaders, incrementalChainWork) { if (newBulkHeaders.length === 0) return; return this.lock.withWriteLock(async () => { const lbf = this.getLastFileNoLock(); const nextHeight = lbf ? lbf.firstHeight + lbf.count : 0; if (nextHeight > 0 && newBulkHeaders.length > 0 && newBulkHeaders[0].height < nextHeight) { // Don't modify the incoming array... newBulkHeaders = [...newBulkHeaders]; // If we have more headers than we need, drop the incoming headers. while (newBulkHeaders.length > 0 && newBulkHeaders[0].height < nextHeight) { const h = newBulkHeaders.shift(); if (h && incrementalChainWork) { incrementalChainWork = (0, blockHeaderUtilities_1.subWork)(incrementalChainWork, (0, blockHeaderUtilities_1.convertBitsToWork)(h.bits)); } } } if (newBulkHeaders.length === 0) return; if (!lbf || nextHeight !== newBulkHeaders[0].height) throw new sdk_1.WERR_INVALID_PARAMETER('newBulkHeaders', 'an extension of existing bulk headers'); if (!lbf.lastHash) throw new sdk_1.WERR_INTERNAL(`lastHash is not defined for the last bulk file ${lbf.fileName}`); const fbh = newBulkHeaders[0]; const lbh = newBulkHeaders.slice(-1)[0]; let lastChainWork = lbf.lastChainWork; if (incrementalChainWork) { lastChainWork = (0, blockHeaderUtilities_1.addWork)(incrementalChainWork, lastChainWork); } else { // If lastChainWork is not provided, calculate it from the last file with basic validation. let lastHeight = lbf.firstHeight + lbf.count - 1; let lastHash = lbf.lastHash; for (const h of newBulkHeaders) { if (h.height !== lastHeight + 1 || h.previousHash !== lastHash) { throw new sdk_1.WERR_INVALID_PARAMETER('headers', `an extension of existing bulk headers, header with height ${h.height} is non-sequential`); } lastChainWork = (0, blockHeaderUtilities_1.addWork)(lastChainWork, (0, blockHeaderUtilities_1.convertBitsToWork)(h.bits)); lastHeight = h.height; lastHash = h.hash; } } const data = (0, blockHeaderUtilities_1.serializeBaseBlockHeaders)(newBulkHeaders); const fileHash = (0, utilityHelpers_noBuffer_1.asString)(sdk_2.Hash.sha256((0, utilityHelpers_noBuffer_1.asArray)(data)), 'base64'); const bf = { fileId: undefined, chain: this.chain, sourceUrl: undefined, fileName: 'incremental', firstHeight: fbh.height, count: newBulkHeaders.length, prevChainWork: lbf.lastChainWork, lastChainWork, prevHash: lbf.lastHash, lastHash: lbh.hash, fileHash, data }; await this.mergeNoLock([bf]); }); } async getBulkFiles(keepData) { return this.lock.withReadLock(async () => { return this.bfds.map(bfd => bfdToInfo(bfd, keepData)); }); } async getHeightRange() { return this.lock.withReadLock(async () => { if (this.bfds.length === 0) return HeightRange_1.HeightRange.empty; const first = this.bfds[0]; const last = this.bfds[this.bfds.length - 1]; return new HeightRange_1.HeightRange(first.firstHeight, last.firstHeight + last.count - 1); }); } async getDataFromFile(file, offset, length) { const bfd = await this.getBfdForHeight(file.firstHeight); if (!bfd || bfd.count < file.count) throw new sdk_1.WERR_INVALID_PARAMETER('file', `a match for ${file.firstHeight}, ${file.count} in the BulkFileDataManager.`); return this.lock.withReadLock(async () => this.getDataFromFileNoLock(bfd, offset, length)); } async getDataFromFileNoLock(bfd, offset, length) { const fileLength = bfd.count * 80; offset = offset || 0; if (offset > fileLength - 1) return undefined; length = length || bfd.count * 80 - offset; length = Math.min(length, fileLength - offset); let data; if (bfd.data) { data = bfd.data.slice(offset, offset + length); } else if (bfd.fileId && this.storage) { data = await this.storage.getBulkFileData(bfd.fileId, offset, length); } if (!data) { await this.ensureData(bfd); if (bfd.data) data = bfd.data.slice(offset, offset + length); } if (!data) return undefined; return data; } async findHeaderForHeightOrUndefined(height) { return this.lock.withReadLock(async () => { if (!Number.isInteger(height) || height < 0) throw new sdk_1.WERR_INVALID_PARAMETER('height', `a non-negative integer (${height}).`); const file = this.bfds.find(f => f.firstHeight <= height && f.firstHeight + f.count > height); if (!file) return undefined; const offset = (height - file.firstHeight) * 80; const data = await this.getDataFromFileNoLock(file, offset, 80); if (!data) return undefined; const header = (0, blockHeaderUtilities_1.deserializeBlockHeader)(data, 0, height); return header; }); } async getFileForHeight(height) { return this.lock.withReadLock(async () => { const bfd = this.getBfdForHeight(height); if (!bfd) return undefined; return bfdToInfo(bfd); }); } getBfdForHeight(height) { if (!Number.isInteger(height) || height < 0) throw new sdk_1.WERR_INVALID_PARAMETER('height', `a non-negative integer (${height}).`); const file = this.bfds.find(f => f.firstHeight <= height && f.firstHeight + f.count > height); return file; } getLastBfd(fromEnd = 1) { if (this.bfds.length < fromEnd) return undefined; const bfd = this.bfds[this.bfds.length - fromEnd]; return bfd; } async getLastFile(fromEnd = 1) { return this.lock.withReadLock(async () => this.getLastFileNoLock(fromEnd)); } getLastFileNoLock(fromEnd = 1) { const bfd = this.getLastBfd(fromEnd); if (!bfd) return undefined; return bfdToInfo(bfd); } async getDataByFileHash(fileHash) { const index = this.fileHashToIndex[fileHash]; if (index === undefined) throw new sdk_1.WERR_INVALID_PARAMETER('fileHash', `known to the BulkFileDataManager. ${fileHash} is unknown.`); const bfd = this.bfds[index]; const data = await this.ensureData(bfd); return data; } async getDataByFileId(fileId) { const bfd = this.bfds.find(f => f.fileId === fileId); if (bfd === undefined) throw new sdk_1.WERR_INVALID_PARAMETER('fileId', `known to the BulkFileDataManager. ${fileId} is unknown.`); const data = await this.ensureData(bfd); return data; } async validateFileInfo(file) { var _a; if (file.chain !== this.chain) throw new sdk_1.WERR_INVALID_PARAMETER('chain', `${this.chain}`); if (file.count <= 0) throw new sdk_1.WERR_INVALID_PARAMETER('bf.count', `expected count to be greater than 0, but got ${file.count}`); if (file.count > this.maxPerFile && file.fileName !== 'incremental') throw new sdk_1.WERR_INVALID_PARAMETER('count', `less than or equal to maxPerFile ${this.maxPerFile}`); if (!file.fileHash) throw new sdk_1.WERR_INVALID_PARAMETER('fileHash', `defined`); if (!file.sourceUrl && !file.fileId && !file.data) throw new sdk_1.WERR_INVALID_PARAMETER('data', `defined when sourceUrl and fileId are undefined`); let bfd = { ...file, fileHash: file.fileHash, mru: Date.now() }; if (!bfd.validated) { await this.ensureData(bfd); if (!bfd.data || bfd.data.length !== bfd.count * 80) throw new sdk_1.WERR_INVALID_PARAMETER('file.data', `bulk file ${bfd.fileName} data length ${(_a = bfd.data) === null || _a === void 0 ? void 0 : _a.length} does not match expected count ${bfd.count}`); bfd.fileHash = (0, utilityHelpers_noBuffer_1.asString)(sdk_2.Hash.sha256((0, utilityHelpers_noBuffer_1.asArray)(bfd.data)), 'base64'); if (file.fileHash && file.fileHash !== bfd.fileHash) throw new sdk_1.WERR_INVALID_PARAMETER('file.fileHash', `expected ${file.fileHash} but got ${bfd.fileHash}`); if (!(0, validBulkHeaderFilesByFileHash_1.isKnownValidBulkHeaderFile)(bfd)) { const pbf = bfd.firstHeight > 0 ? this.getBfdForHeight(bfd.firstHeight - 1) : undefined; const prevHash = pbf ? pbf.lastHash : '00'.repeat(32); const prevChainWork = pbf ? pbf.lastChainWork : '00'.repeat(32); const { lastHeaderHash, lastChainWork } = (0, blockHeaderUtilities_1.validateBufferOfHeaders)(bfd.data, prevHash, 0, undefined, prevChainWork); if (bfd.lastHash && bfd.lastHash !== lastHeaderHash) throw new sdk_1.WERR_INVALID_PARAMETER('file.lastHash', `expected ${bfd.lastHash} but got ${lastHeaderHash}`); if (bfd.lastChainWork && bfd.lastChainWork !== lastChainWork) throw new sdk_1.WERR_INVALID_PARAMETER('file.lastChainWork', `expected ${bfd.lastChainWork} but got ${lastChainWork}`); bfd.lastHash = lastHeaderHash; bfd.lastChainWork = lastChainWork; if (bfd.firstHeight === 0) { (0, blockHeaderUtilities_1.validateGenesisHeader)(bfd.data, bfd.chain); } } bfd.validated = true; } return bfd; } async ReValidate() { return this.lock.withReadLock(async () => this.ReValidateNoLock()); } async ReValidateNoLock() { for (const file of this.bfds) { await this.ensureData(file); file.validated = false; // Reset validation to re-validate on next access const bfd = await this.validateFileInfo(file); if (!bfd.validated) throw new sdk_1.WERR_INTERNAL(`BulkFileDataManager.ReValidate failed for file ${bfd.fileName}`); file.validated = true; } } validateBfdForAdd(bfd) { if (this.bfds.length === 0 && bfd.firstHeight !== 0) throw new sdk_1.WERR_INVALID_PARAMETER('firstHeight', `0 for the first file`); if (this.bfds.length > 0) { const last = this.bfds[this.bfds.length - 1]; if (bfd.firstHeight !== last.firstHeight + last.count) throw new sdk_1.WERR_INVALID_PARAMETER('firstHeight', `the last file's firstHeight + count`); if (bfd.prevHash !== last.lastHash || bfd.prevChainWork !== last.lastChainWork) throw new sdk_1.WERR_INVALID_PARAMETER('prevHash/prevChainWork', `the last file's lastHash/lastChainWork`); } } add(bfd) { this.validateBfdForAdd(bfd); const index = this.bfds.length; this.bfds.push(bfd); this.fileHashToIndex[bfd.fileHash] = index; this.ensureMaxRetained(); return bfdToInfo(bfd, true); } replaceBfdAtIndex(index, update) { const oldBfd = this.bfds[index]; delete this.fileHashToIndex[oldBfd.fileHash]; this.bfds[index] = update; this.fileHashToIndex[update.fileHash] = index; } /** * Updating an existing file occurs in two specific contexts: * * 1. CDN Update: CDN files of a specific `maxPerFile` series typically ends in a partial file * which may periodically add more headers until the next file is started. * If the CDN update is the second to last file (followed by an incremental file), * then the incremental file is updated or deleted and also returned as the result (with a count of zero if deleted). * * 2. Incremental Update: The last bulk file is almost always an "incremental" file * which is not limited by "maxPerFile" and holds all non-CDN bulk headers. * If is updated with new bulk headers which come either from non CDN ingestors or from live header migration to bulk. * * Updating preserves the following properties: * * - Any existing headers following this update are preserved and must form an unbroken chain. * - There can be at most one incremental file and it must be the last file. * - The update start conditions (height, prevHash, prevChainWork) must match an existing file which may be either CDN or internal. * - The update fileId must match, it may be undefind. * - The fileName does not need to match. * - The incremental file must always have fileName "incremental" and sourceUrl must be undefined. * - The update count must be greater than 0. * - The update count must be greater than current count for CDN to CDN update. * * @param update new validated BulkFileData to update. * @param hbf corresponding existing BulkFileData to update. */ async update(update, hbf, r) { if (!hbf || hbf.firstHeight !== update.firstHeight || hbf.prevChainWork !== update.prevChainWork || hbf.prevHash !== update.prevHash) throw new sdk_1.WERR_INVALID_PARAMETER('file', `an existing file by height, prevChainWork and prevHash`); if (isBdfCdn(update) === isBdfCdn(hbf) && update.count <= hbf.count) throw new sdk_1.WERR_INVALID_PARAMETER('file.count', `greater than the current count ${hbf.count}`); const lbf = this.getLastBfd(); let index = this.bfds.length - 1; let truncate = undefined; let replaced = undefined; let drop = undefined; if (hbf.firstHeight === lbf.firstHeight) { // If the update is for the last file, there are three cases: if (isBdfIncremental(update)) { // 1. Incremental file may only be extended with more incremental headers. if (!isBdfIncremental(lbf)) throw new sdk_1.WERR_INVALID_PARAMETER('file', `an incremental file to update an existing incremental file`); } else { // The update is a CDN bulk file. if (isBdfCdn(lbf)) { // 2. An updated CDN file replaces a partial CDN file. if (update.count <= lbf.count) throw new sdk_1.WERR_INVALID_PARAMETER('update.count', `CDN update must have more headers. ${update.count} <= ${lbf.count}`); } else { // 3. A new CDN file replaces some or all of current incremental file. // Retain extra incremental headers if any. if (update.count < lbf.count) { // The new CDN partially replaces the last incremental file, prepare to shift work and re-add it. await this.ensureData(lbf); truncate = lbf; } } } } else { // If the update is NOT for the last file, then it MUST be for the second to last file which MUST be a CDN file: // - it must be a CDN file update with more headers than the current CDN file. // - the last file must be an incremental file which is updated or deleted. The updated (or deleted) last file is returned. const lbf2 = this.getLastBfd(2); if (!lbf2 || hbf.firstHeight !== lbf2.firstHeight) throw new sdk_1.WERR_INVALID_PARAMETER('file', `an update to last or second to last file`); if (!isBdfCdn(update) || !isBdfCdn(lbf2) || update.count <= lbf2.count) throw new sdk_1.WERR_INVALID_PARAMETER('file', `a CDN file update with more headers than the current CDN file`); if (!isBdfIncremental(lbf)) throw new sdk_1.WERR_INVALID_PARAMETER('file', `a CDN file update followed by an incremental file`); if (!update.fileId) update.fileId = lbf2.fileId; // Update fileId if not provided if (update.count >= lbf2.count + lbf.count) { // The current last file is fully replaced by the CDN update. drop = lbf; } else { // If the update doesn't fully replace the last incremental file, make sure data is available to be truncated. await this.ensureData(lbf); truncate = lbf; // The existing second to last file is fully replaced by the update. replaced = lbf2; } index = index - 1; // The update replaces the second to last file. } // In all cases the bulk file at the current fileId if any is updated. this.replaceBfdAtIndex(index, update); if (truncate) { // If there is a bulk file to be truncated, it becomes the new (reduced) last file. await this.shiftWork(update, truncate, replaced); } if (drop) { this.dropLastBulkFile(drop); } const updateInfo = bfdToInfo(update, true); const truncateInfo = truncate ? bfdToInfo(truncate, true) : undefined; if (this.storage) { // Keep storage in sync. if (update.fileId) { await this.storage.updateBulkFile(update.fileId, updateInfo); } if (truncate && truncateInfo) { if (replaced) { await this.storage.updateBulkFile(truncate.fileId, truncateInfo); } else { truncateInfo.fileId = undefined; // Make sure truncate is a new file. truncate.fileId = await this.storage.insertBulkFile(truncateInfo); } } if (drop && drop.fileId) { await this.storage.deleteBulkFile(drop.fileId); } } if (r) { // Update results for logging... r.updated.push(updateInfo); if (truncateInfo) { if (replaced) { r.updated.push(truncateInfo); } else { r.inserted.push(truncateInfo); } } if (drop) { r.dropped.push(bfdToInfo(drop)); } } this.ensureMaxRetained(); } dropLastBulkFile(lbf) { delete this.fileHashToIndex[lbf.fileHash]; const index = this.bfds.indexOf(lbf); if (index !== this.bfds.length - 1) throw new sdk_1.WERR_INTERNAL(`dropLastBulkFile requires lbf is the current last file.`); this.bfds.pop(); } /** * Remove work (and headers) from `truncate` that now exists in `update`. * There are two scenarios: * 1. `replaced` is undefined: update is a CDN file that splits an incremental file that must be truncated. * 2. `replaced` is valid: update is a CDN update that replaced an existing CDN file and splits an incremental file that must be truncated. * @param update the new CDN update file. * @param truncate the incremental file to be truncated (losing work which now exists in `update`). * @param replaced the existing CDN file that was replaced by `update` (if any). */ async shiftWork(update, truncate, replaced) { var _a; const updateIndex = this.fileHashToIndex[update.fileHash]; // replaced will be valid if the update replaced it and it must become the new last file. // truncateIndex will be updateIndex + 1 if the existing last file is being truncated and update is second to last. const truncateIndex = this.fileHashToIndex[truncate.fileHash]; if (truncateIndex !== undefined && truncateIndex !== updateIndex + 1) throw new sdk_1.WERR_INTERNAL(`shiftWork requires update to have replaced truncate or truncate to follow update`); if (truncateIndex !== undefined && !replaced) throw new sdk_1.WERR_INTERNAL(`shiftWork requires valid replaced when update hasn't replaced truncate`); truncate.prevHash = update.lastHash; truncate.prevChainWork = update.lastChainWork; // truncate.lastChainWork, truncate.lastHash remain unchanged let count = update.count; if (replaced) { count -= replaced.count; } else { // The truncated file is itself being replaced by the update and must be inserted as a new file. truncate.fileId = undefined; this.bfds.push(truncate); // Add the truncated file as a new entry. } truncate.count -= count; truncate.firstHeight += count; truncate.data = (_a = truncate.data) === null || _a === void 0 ? void 0 : _a.slice(count * 80); delete this.fileHashToIndex[truncate.fileHash]; truncate.fileHash = (0, utilityHelpers_noBuffer_1.asString)(sdk_2.Hash.sha256((0, utilityHelpers_noBuffer_1.asArray)(truncate.data)), 'base64'); this.fileHashToIndex[truncate.fileHash] = updateIndex + 1; } /** * * @param bfd * @returns */ async ensureData(bfd) { if (bfd.data) return bfd.data; if (this.storage && bfd.fileId) { bfd.data = await this.storage.getBulkFileData(bfd.fileId); if (!bfd.data) throw new sdk_1.WERR_INVALID_PARAMETER('fileId', `valid, data not found for fileId ${bfd.fileId}`); } if (!bfd.data && this.fetch && bfd.sourceUrl) { // TODO - restore this change const url = this.fetch.pathJoin(bfd.sourceUrl, bfd.fileName); //const url = this.fetch.pathJoin('http://localhost:8842/blockheaders', bfd.fileName) try { bfd.data = await this.fetch.download(url); } catch (err) { bfd.data = await this.fetch.download(url); } if (!bfd.data) throw new sdk_1.WERR_INVALID_PARAMETER('sourceUrl', `data not found for sourceUrl ${url}`); } if (!bfd.data) throw new sdk_1.WERR_INVALID_PARAMETER('data', `defined. Unable to retrieve data for ${bfd.fileName}`); bfd.mru = Date.now(); // Validate retrieved data. const fileHash = (0, utilityHelpers_noBuffer_1.asString)(sdk_2.Hash.sha256((0, utilityHelpers_noBuffer_1.asArray)(bfd.data)), 'base64'); if (fileHash !== bfd.fileHash) throw new sdk_1.WERR_INVALID_PARAMETER('fileHash', `a match for retrieved data for ${bfd.fileName}`); this.ensureMaxRetained(); return bfd.data; } ensureMaxRetained() { if (this.maxRetained === undefined) return; let withData = this.bfds.filter(bfd => bfd.data && (bfd.fileId || bfd.sourceUrl)); let countToRelease = withData.length - this.maxRetained; if (countToRelease <= 0) return; const sorted = withData.sort((a, b) => a.mru - b.mru); while (countToRelease-- > 0 && sorted.length > 0) { const oldest = sorted.shift(); // Release the least recently used data oldest.data = undefined; // Release the data } } async exportHeadersToFs(toFs, toHeadersPerFile, toFolder, sourceUrl, maxHeight) { const chain = this.chain; const toFileName = (i) => `${chain}Net_${i}.headers`; const toPath = (i) => toFs.pathJoin(toFolder, toFileName(i)); const toJsonPath = () => toFs.pathJoin(toFolder, `${chain}NetBlockHeaders.json`); const toBulkFiles = { rootFolder: sourceUrl || toFolder, jsonFilename: `${chain}NetBlockHeaders.json`, headersPerFile: toHeadersPerFile, files: [] }; let range = await this.getHeightRange(); if (maxHeight) range = range.intersect(new HeightRange_1.HeightRange(0, maxHeight)); const reader = await this.createReader(range, toHeadersPerFile * 80); let firstHeight = 0; let lastHeaderHash = '00'.repeat(32); let lastChainWork = '00'.repeat(32); let i = -1; for (;;) { i++; const data = await reader.read(); if (!data || data.length === 0) { break; } const last = (0, blockHeaderUtilities_1.validateBufferOfHeaders)(data, lastHeaderHash, 0, undefined, lastChainWork); await toFs.writeFile(toPath(i), data); const fileHash = (0, utilityHelpers_noBuffer_1.asString)(sdk_2.Hash.sha256((0, utilityHelpers_noBuffer_1.asArray)(data)), 'base64'); const file = { chain, count: data.length / 80, fileHash, fileName: toFileName(i), firstHeight, lastChainWork: last.lastChainWork, lastHash: last.lastHeaderHash, prevChainWork: lastChainWork, prevHash: lastHeaderHash, sourceUrl }; toBulkFiles.files.push(file); firstHeight += file.count; lastHeaderHash = file.lastHash; lastChainWork = file.lastChainWork; } await toFs.writeFile(toJsonPath(), (0, utilityHelpers_noBuffer_1.asUint8Array)(JSON.stringify(toBulkFiles), 'utf8')); } } exports.BulkFileDataManager = BulkFileDataManager; function selectBulkHeaderFiles(files, chain, maxPerFile) { const r = []; let height = 0; for (;;) { const choices = files.filter(f => f.firstHeight === height && f.count <= maxPerFile && f.chain === chain); // Pick the file with the maximum count const choice = choices.reduce((a, b) => (a.count > b.count ? a : b), choices[0]); if (!choice) break; // no more files to select r.push(choice); height += choice.count; } return r; } function isBdfIncremental(bfd) { return bfd.fileName === 'incremental' && !bfd.sourceUrl; } function isBdfCdn(bfd) { return !isBdfIncremental(bfd); } function bfdToInfo(bfd, keepData) { return { chain: bfd.chain, fileHash: bfd.fileHash, fileName: bfd.fileName, sourceUrl: bfd.sourceUrl, fileId: bfd.fileId, count: bfd.count, prevChainWork: bfd.prevChainWork, lastChainWork: bfd.lastChainWork, firstHeight: bfd.firstHeight, prevHash: bfd.prevHash, lastHash: bfd.lastHash, validated: bfd.validated || false, data: keepData ? bfd.data : undefined }; } class BulkFileDataReader { constructor(manager, range, maxBufferSize) { this.manager = manager; this.range = range; this.maxBufferSize = maxBufferSize; this.nextHeight = range.minHeight; } /** * Returns the Buffer of block headers from the given `file` for the given `range`. * If `range` is undefined, the file's full height range is read. * The returned Buffer will only contain headers in `file` and in `range` * @param file * @param range */ async readBufferFromFile(file, range) { // Constrain the range to the file's contents... let fileRange = new HeightRange_1.HeightRange(file.firstHeight, file.firstHeight + file.count - 1); if (range) fileRange = fileRange.intersect(range); if (fileRange.isEmpty) return undefined; const offset = (fileRange.minHeight - file.firstHeight) * 80; const length = fileRange.length * 80; return await this.manager.getDataFromFile(file, offset, length); } /** * @returns an array containing the next `maxBufferSize` bytes of headers from the files. */ async read() { if (this.nextHeight === undefined || !this.range || this.range.isEmpty || this.nextHeight > this.range.maxHeight) return undefined; let lastHeight = this.nextHeight + this.maxBufferSize / 80 - 1; lastHeight = Math.min(lastHeight, this.range.maxHeight); let file = await this.manager.getFileForHeight(this.nextHeight); if (!file) throw new sdk_1.WERR_INTERNAL(`logic error`); const readRange = new HeightRange_1.HeightRange(this.nextHeight, lastHeight); let buffers = new Uint8Array(readRange.length * 80); let offset = 0; while (file) { const buffer = await this.readBufferFromFile(file, readRange); if (!buffer) break; buffers.set(buffer, offset); offset += buffer.length; file = await this.manager.getFileForHeight(file.firstHeight + file.count); } if (!buffers.length || offset !== readRange.length * 80) return undefined; this.nextHeight = lastHeight + 1; return buffers; } } exports.BulkFileDataReader = BulkFileDataReader; //# sourceMappingURL=BulkFileDataManager.js.map