# Fluffy # Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). # at your option. This file may not be copied, modified, or distributed except according to those terms. # Tool to download chain history data from local node, and save it to the json # file or sqlite database. # In case of json: # Block data is stored as it gets transmitted over the wire and as defined here: # https://github.com/ethereum/portal-network-specs/blob/master/history-network.md#content-keys-and-values # # Json file has following format: # { # "hexEncodedBlockHash: { # "header": "the rlp encoded block header as a hex string" # "body": "the SSZ encoded container of transactions and uncles as a hex string" # "receipts: "The SSZ encoded list of the receipts as a hex string" # "number": "block number" # }, # ..., # ..., # } # In case of sqlite: # Data is saved in a format friendly to history network i.e one table with 3 # columns: contentid, contentkey, content. # Such format enables queries to quickly find content in range of some node # which makes it possible to offer content to nodes in bulk. # # When using geth as client to download receipts from, be aware that you will # have to set the number of blocks to maintain the transaction index for to # unlimited if you want access to all transactions/receipts. # e.g: `./build/bin/geth --ws --txlookuplimit=0` # {.push raises: [].} import std/[json, typetraits, strutils, strformat, os, uri], confutils, stew/[byteutils, io2], json_serialization, faststreams, chronicles, eth/[common, rlp], chronos, eth/common/eth_types_json_serialization, json_rpc/rpcclient, snappy, ncli/e2store, ../database/seed_db, ../../premix/[downloader, parser], ../network/history/[history_content, accumulator], ../eth_data/[history_data_json_store, history_data_ssz_e2s, era1], eth_data_exporter/[exporter_conf, exporter_common, cl_data_exporter] # Need to be selective due to the `Block` type conflict from downloader from ../network/history/history_network import encode from ../../nimbus/utils/utils import calcTxRoot, calcReceiptRoot chronicles.formatIt(IoErrorCode): $it proc downloadHeader(client: RpcClient, i: uint64): BlockHeader = let blockNumber = u256(i) try: let jsonHeader = requestHeader(blockNumber, some(client)) parseBlockHeader(jsonHeader) except CatchableError as e: fatal "Error while requesting BlockHeader", error = e.msg, number = i quit 1 proc downloadBlock(i: uint64, client: RpcClient): Block = let num = u256(i) try: return requestBlock(num, flags = {DownloadReceipts}, client = some(client)) except CatchableError as e: fatal "Error while requesting Block", error = e.msg, number = i quit 1 proc writeHeadersToJson(config: ExporterConf, client: RpcClient) = let fh = createAndOpenFile(string config.dataDir, string config.fileName) try: var writer = JsonWriter[DefaultFlavor].init(fh.s, pretty = true) writer.beginRecord() for i in config.startBlock..config.endBlock: let blck = client.downloadHeader(i) writer.writeHeaderRecord(blck) if ((i - config.startBlock) mod 8192) == 0 and i != config.startBlock: info "Downloaded 8192 new block headers", currentHeader = i writer.endRecord() info "File successfully written", path = config.dataDir / config.fileName except IOError as e: fatal "Error occured while writing to file", error = e.msg quit 1 finally: try: fh.close() except IOError as e: fatal "Error occured while closing file", error = e.msg quit 1 proc writeBlocksToJson(config: ExporterConf, client: RpcClient) = let fh = createAndOpenFile(string config.dataDir, string config.fileName) try: var writer = JsonWriter[DefaultFlavor].init(fh.s, pretty = true) writer.beginRecord() for i in config.startBlock..config.endBlock: let blck = downloadBlock(i, client) writer.writeBlockRecord(blck.header, blck.body, blck.receipts) if ((i - config.startBlock) mod 8192) == 0 and i != config.startBlock: info "Downloaded 8192 new blocks", currentBlock = i writer.endRecord() info "File successfully written", path = config.dataDir / config.fileName except IOError as e: fatal "Error occured while writing to file", error = e.msg quit 1 finally: try: fh.close() except IOError as e: fatal "Error occured while closing file", error = e.msg quit 1 proc writeBlocksToDb(config: ExporterConf, client: RpcClient) = let db = SeedDb.new(distinctBase(config.dataDir), config.fileName) defer: db.close() for i in config.startBlock..config.endBlock: let blck = downloadBlock(i, client) blockHash = blck.header.blockHash() contentKeyType = BlockKey(blockHash: blockHash) headerKey = encode(ContentKey( contentType: blockHeader, blockHeaderKey: contentKeyType)) bodyKey = encode(ContentKey( contentType: blockBody, blockBodyKey: contentKeyType)) receiptsKey = encode( ContentKey(contentType: receipts, receiptsKey: contentKeyType)) db.put(headerKey.toContentId(), headerKey.asSeq(), rlp.encode(blck.header)) # No need to seed empty lists into database if len(blck.body.transactions) > 0 or len(blck.body.uncles) > 0: let body = encode(blck.body) db.put(bodyKey.toContentId(), bodyKey.asSeq(), body) if len(blck.receipts) > 0: let receipts = encode(blck.receipts) db.put(receiptsKey.toContentId(), receiptsKey.asSeq(), receipts) info "Data successfuly written to db" proc exportBlocks(config: ExporterConf, client: RpcClient) = case config.storageMode of JsonStorage: if config.headersOnly: writeHeadersToJson(config, client) else: writeBlocksToJson(config, client) of DbStorage: if config.headersOnly: fatal "Db mode not available for headers only" quit 1 else: writeBlocksToDb(config, client) proc newRpcClient(web3Url: Web3Url): RpcClient = # TODO: I don't like this API. I think the creation of the RPC clients should # already include the URL. And then an optional connect may be necessary # depending on the protocol. let client: RpcClient = case web3Url.kind of HttpUrl: newRpcHttpClient() of WsUrl: newRpcWebSocketClient() client proc connectRpcClient( client: RpcClient, web3Url: Web3Url): Future[Result[void, string]] {.async.} = case web3Url.kind of HttpUrl: try: await RpcHttpClient(client).connect(web3Url.url) ok() except CatchableError as e: return err(e.msg) of WsUrl: try: await RpcWebSocketClient(client).connect(web3Url.url) ok() except CatchableError as e: return err(e.msg) proc cmdExportEra1(config: ExporterConf) = let client = newRpcClient(config.web3Url) try: let connectRes = waitFor client.connectRpcClient(config.web3Url) if connectRes.isErr(): fatal "Failed connecting to JSON-RPC client", error = connectRes.error quit 1 except CatchableError as e: # TODO: Add async raises to get rid of this. fatal "Failed connecting to JSON-RPC client", error = e.msg quit 1 var era = Era1(config.era) while config.eraCount == 0 or era < Era1(config.era) + config.eraCount: defer: era += 1 let startNumber = era.startNumber() endNumber = era.endNumber() if startNumber >= mergeBlockNumber: info "Stopping era as it is after the merge" break var accumulatorRoot = default(Digest) let tmpName = era1FileName("mainnet", era, default(Digest)) & ".tmp" info "Writing era1", tmpName var completed = false block writeFileBlock: let e2 = openFile(tmpName, {OpenFlags.Write, OpenFlags.Create, OpenFlags.Truncate}).get() defer: discard closeFile(e2) # TODO: Not checking the result of init, update or finish here, as all # error cases are fatal. But maybe we could throw proper errors still. var group = Era1Group.init(e2, startNumber).get() # Header records to build the accumulator root var headerRecords: seq[accumulator.HeaderRecord] for blockNumber in startNumber..endNumber: let blck = try: # TODO: Not sure about the errors that can occur here. But the whole # block requests over json-rpc should be reworked here (and can be # used in the bridge also then) requestBlock(blockNumber.u256, flags = {DownloadReceipts}, client = some(client)) except CatchableError as e: error "Failed retrieving block, skip creation of era1 file", blockNumber, era, error = e.msg break writeFileBlock var ttd: UInt256 try: blck.jsonData.fromJson "totalDifficulty", ttd except ValueError: break writeFileBlock headerRecords.add(accumulator.HeaderRecord( blockHash: blck.header.blockHash(), totalDifficulty: ttd)) group.update( e2, blockNumber, blck.header, blck.body, blck.receipts, ttd).get() accumulatorRoot = getEpochAccumulatorRoot(headerRecords) group.finish(e2, accumulatorRoot, endNumber).get() completed = true if completed: let name = era1FileName("mainnet", era, accumulatorRoot) # We cannot check for the exact file any earlier as we need to know the # accumulator root. # TODO: Could scan for file with era number in it. if isFile(name): info "Era1 file already exists", era, name if (let e = io2.removeFile(tmpName); e.isErr): warn "Failed to clean up tmp era1 file", tmpName, error = e.error continue try: moveFile(tmpName, name) except Exception as e: # TODO warn "Failed to rename era1 file to its final name", name, tmpName, error = e.msg info "Writing era1 completed", name else: error "Failed creating the era1 file", era if (let e = io2.removeFile(tmpName); e.isErr): warn "Failed to clean up incomplete era1 file", tmpName, error = e.error proc cmdVerifyEra1(config: ExporterConf) = let f = Era1File.open(config.era1FileName).valueOr: warn "Failed to open era file", error = error quit 1 defer: close(f) let root = f.verify.valueOr: warn "Verification of era file failed", error = error quit 1 notice "Era1 file succesfully verified", accumulatorRoot = root.data.to0xHex(), file = config.era1FileName when isMainModule: {.pop.} let config = ExporterConf.load() {.push raises: [].} setLogLevel(config.logLevel) let dataDir = config.dataDir.string if not isDir(dataDir): let res = createPath(dataDir) if res.isErr(): fatal "Error occurred while creating data directory", dir = dataDir, error = ioErrorMsg(res.error) quit 1 case config.cmd of ExporterCmd.history: case config.historyCmd of HistoryCmd.exportBlockData: let client = newRpcClient(config.web3Url) let connectRes = waitFor client.connectRpcClient(config.web3Url) if connectRes.isErr(): fatal "Failed connecting to JSON-RPC client", error = connectRes.error quit 1 if (config.endBlock < config.startBlock): fatal "Initial block number should be smaller than end block number", startBlock = config.startBlock, endBlock = config.endBlock quit 1 try: exportBlocks(config, client) finally: waitFor client.close() of HistoryCmd.exportEpochHeaders: let client = newRpcClient(config.web3Url) let connectRes = waitFor client.connectRpcClient(config.web3Url) if connectRes.isErr(): fatal "Failed connecting to JSON-RPC client", error = connectRes.error quit 1 proc exportEpochHeaders(file: string, epoch: uint64): Result[void, string] = # Downloading headers from JSON RPC endpoint info "Requesting epoch headers", epoch var headers: seq[BlockHeader] for j in 0..