UNPKG

@ethereumjs/blockchain

Version:
199 lines 7.92 kB
import { createBlockFromBytesArray, createBlockHeaderFromBytesArray } from '@ethereumjs/block'; import { RLP } from '@ethereumjs/rlp'; import { EthereumJSErrorWithoutCode, KECCAK256_RLP, KECCAK256_RLP_ARRAY, bytesToBigInt, bytesToHex, equalsBytes, unprefixedHexToBytes, } from '@ethereumjs/util'; import { Cache } from "./cache.js"; import { DBOp, DBTarget } from "./operation.js"; /** * Abstraction over a DB to facilitate storing/fetching blockchain-related * data, such as blocks and headers, indices, and the head block. * @hidden */ export class DBManager { constructor(db, common) { this._db = db; this.common = common; this._cache = { td: new Cache({ max: 1024 }), header: new Cache({ max: 512 }), body: new Cache({ max: 256 }), numberToHash: new Cache({ max: 2048 }), hashToNumber: new Cache({ max: 2048 }), }; } /** * Fetches iterator heads from the db. */ async getHeads() { const heads = (await this.get(DBTarget.Heads)); if (heads === undefined) return heads; const decodedHeads = {}; for (const key of Object.keys(heads)) { // Heads are stored in DB as hex strings since Level converts Uint8Arrays // to nested JSON objects when they are included in a value being stored // in the DB decodedHeads[key] = unprefixedHexToBytes(heads[key]); } return decodedHeads; } /** * Fetches header of the head block. */ async getHeadHeader() { return this.get(DBTarget.HeadHeader); } /** * Fetches head block. */ async getHeadBlock() { return this.get(DBTarget.HeadBlock); } /** * Fetches a block (header and body) given a block id, * which can be either its hash or its number. */ async getBlock(blockId) { if (typeof blockId === 'number' && Number.isInteger(blockId)) { blockId = BigInt(blockId); } let number; let hash; if (blockId === undefined) return undefined; if (blockId instanceof Uint8Array) { hash = blockId; number = await this.hashToNumber(blockId); } else if (typeof blockId === 'bigint') { number = blockId; hash = await this.numberToHash(blockId); } else { throw EthereumJSErrorWithoutCode('Unknown blockId type'); } if (hash === undefined || number === undefined) return undefined; const header = await this.getHeader(hash, number); let body = await this.getBody(hash, number); // be backward compatible where we didn't use to store a body with no txs, uncles, withdrawals // otherwise the body is never partially stored and if we have some body, its in entirety if (body === undefined) { body = [[], []]; // Do extra validations on the header since we are assuming empty transactions and uncles if (!equalsBytes(header.transactionsTrie, KECCAK256_RLP)) { throw EthereumJSErrorWithoutCode('transactionsTrie root should be equal to hash of null'); } if (!equalsBytes(header.uncleHash, KECCAK256_RLP_ARRAY)) { throw EthereumJSErrorWithoutCode('uncle hash should be equal to hash of empty array'); } // If this block had empty withdrawals push an empty array in body if (header.withdrawalsRoot !== undefined) { // Do extra validations for withdrawal before assuming empty withdrawals if (!equalsBytes(header.withdrawalsRoot, KECCAK256_RLP)) { throw EthereumJSErrorWithoutCode('withdrawals root shoot be equal to hash of null when no withdrawals'); } else { body.push([]); } } } const blockData = [header.raw(), ...body]; const opts = { common: this.common, setHardfork: true }; return createBlockFromBytesArray(blockData, opts); } /** * Fetches body of a block given its hash and number. */ async getBody(blockHash, blockNumber) { const body = await this.get(DBTarget.Body, { blockHash, blockNumber }); return body !== undefined ? RLP.decode(body) : undefined; } /** * Fetches header of a block given its hash and number. */ async getHeader(blockHash, blockNumber) { const encodedHeader = await this.get(DBTarget.Header, { blockHash, blockNumber }); const headerValues = RLP.decode(encodedHeader); const opts = { common: this.common, setHardfork: true }; return createBlockHeaderFromBytesArray(headerValues, opts); } /** * Fetches total difficulty for a block given its hash and number. */ async getTotalDifficulty(blockHash, blockNumber) { const td = await this.get(DBTarget.TotalDifficulty, { blockHash, blockNumber }); return bytesToBigInt(RLP.decode(td)); } /** * Performs a block hash to block number lookup. */ async hashToNumber(blockHash) { const value = await this.get(DBTarget.HashToNumber, { blockHash }); if (value === undefined) { throw EthereumJSErrorWithoutCode(`value for ${bytesToHex(blockHash)} not found in DB`); } return value !== undefined ? bytesToBigInt(value) : undefined; } /** * Performs a block number to block hash lookup. */ async numberToHash(blockNumber) { const value = await this.get(DBTarget.NumberToHash, { blockNumber }); return value; } /** * Fetches a key from the db. If `opts.cache` is specified * it first tries to load from cache, and on cache miss will * try to put the fetched item on cache afterwards. */ async get(dbOperationTarget, key) { const dbGetOperation = DBOp.get(dbOperationTarget, key); const cacheString = dbGetOperation.cacheString; const dbKey = dbGetOperation.baseDBOp.key; if (cacheString !== undefined) { if (this._cache[cacheString] === undefined) { throw EthereumJSErrorWithoutCode(`Invalid cache: ${cacheString}`); } let value = this._cache[cacheString].get(dbKey); if (value === undefined) { value = (await this._db.get(dbKey, { keyEncoding: dbGetOperation.baseDBOp.keyEncoding, valueEncoding: dbGetOperation.baseDBOp.valueEncoding, })); if (value !== undefined) { this._cache[cacheString].set(dbKey, value); } } return value; } return this._db.get(dbKey, { keyEncoding: dbGetOperation.baseDBOp.keyEncoding, valueEncoding: dbGetOperation.baseDBOp.valueEncoding, }); } /** * Performs a batch operation on db. */ async batch(ops) { const convertedOps = ops.map((op) => { const type = op.baseDBOp.type ?? (op.baseDBOp.value !== undefined ? 'put' : 'del'); const convertedOp = { key: op.baseDBOp.key, value: op.baseDBOp.value, type, opts: { keyEncoding: op.baseDBOp.keyEncoding, valueEncoding: op.baseDBOp.valueEncoding, }, }; if (type === 'put') return convertedOp; else return convertedOp; }); // update the current cache for each operation ops.map((op) => op.updateCache(this._cache)); return this._db.batch(convertedOps); } } //# sourceMappingURL=manager.js.map