|
| 1 | +import { equalsBytes } from '@ethereumjs/util' |
| 2 | +import * as ssz from 'micro-eth-signer/ssz' |
| 3 | + |
| 4 | +import { EraTypes, parseEntry, readEntry } from './index.js' |
| 5 | + |
| 6 | +import type { SlotIndex } from './index.js' |
| 7 | + |
| 8 | +/** |
| 9 | + * Reads a Slot Index from the end of a bytestring representing an era file |
| 10 | + * @param bytes a Uint8Array bytestring representing a {@link SlotIndex} plus any arbitrary prefixed data |
| 11 | + * @returns a deserialized {@link SlotIndex} |
| 12 | + */ |
| 13 | +export const readSlotIndex = (bytes: Uint8Array): SlotIndex => { |
| 14 | + const recordEnd = bytes.length |
| 15 | + const countBytes = bytes.slice(recordEnd - 8) |
| 16 | + const count = Number(new DataView(countBytes.buffer).getBigInt64(0, true)) |
| 17 | + const recordStart = recordEnd - (8 * count + 24) |
| 18 | + const slotIndexEntry = readEntry(bytes.subarray(recordStart, recordEnd)) |
| 19 | + if (equalsBytes(slotIndexEntry.type, EraTypes.SlotIndex) === false) { |
| 20 | + throw new Error(`expected SlotIndex type, got ${slotIndexEntry.type}`) |
| 21 | + } |
| 22 | + |
| 23 | + const startSlot = Number( |
| 24 | + new DataView(slotIndexEntry.data.slice(0, 8).buffer).getBigInt64(0, true), |
| 25 | + ) |
| 26 | + const slotOffsets = [] |
| 27 | + |
| 28 | + for (let i = 0; i < count; i++) { |
| 29 | + const slotEntry = slotIndexEntry.data.subarray((i + 1) * 8, (i + 2) * 8) |
| 30 | + const slotOffset = Number(new DataView(slotEntry.slice(0, 8).buffer).getBigInt64(0, true)) |
| 31 | + slotOffsets.push(slotOffset) |
| 32 | + } |
| 33 | + return { |
| 34 | + startSlot, |
| 35 | + recordStart, |
| 36 | + slotOffsets, |
| 37 | + } |
| 38 | +} |
| 39 | + |
| 40 | +/** |
| 41 | + * Reads a an era file and extracts the State and Block slot indices |
| 42 | + * @param eraContents a bytestring representing a serialized era file |
| 43 | + * @returns a dictionary containing the State and Block Slot Indices (if present) |
| 44 | + */ |
| 45 | +export const getEraIndexes = ( |
| 46 | + eraContents: Uint8Array, |
| 47 | +): { stateSlotIndex: SlotIndex; blockSlotIndex: SlotIndex | undefined } => { |
| 48 | + const stateSlotIndex = readSlotIndex(eraContents) |
| 49 | + let blockSlotIndex = undefined |
| 50 | + if (stateSlotIndex.startSlot > 0) { |
| 51 | + blockSlotIndex = readSlotIndex(eraContents.slice(0, stateSlotIndex.recordStart)) |
| 52 | + } |
| 53 | + return { stateSlotIndex, blockSlotIndex } |
| 54 | +} |
| 55 | + |
| 56 | +/** |
| 57 | + * |
| 58 | + * @param eraData a bytestring representing an era file |
| 59 | + * @returns a BeaconState object of the same time as returned by {@link ssz.ETH2_TYPES.BeaconState} |
| 60 | + * @throws if BeaconState cannot be found |
| 61 | + */ |
| 62 | +export const readBeaconState = async (eraData: Uint8Array) => { |
| 63 | + const indices = getEraIndexes(eraData) |
| 64 | + const stateEntry = readEntry( |
| 65 | + eraData.slice(indices.stateSlotIndex.recordStart + indices.stateSlotIndex.slotOffsets[0]), |
| 66 | + ) |
| 67 | + const data = await parseEntry(stateEntry) |
| 68 | + if (equalsBytes(stateEntry.type, EraTypes.CompressedBeaconState) === false) { |
| 69 | + throw new Error(`expected CompressedBeaconState type, got ${stateEntry.type}`) |
| 70 | + } |
| 71 | + return ssz.ETH2_TYPES.BeaconState.decode(data.data as Uint8Array) |
| 72 | +} |
| 73 | + |
| 74 | +/** |
| 75 | + * |
| 76 | + * @param eraData a bytestring representing an era file |
| 77 | + * @returns a decompressed SignedBeaconBlock object of the same time as returned by {@link ssz.ETH2_TYPES.SignedBeaconBlock} |
| 78 | + * @throws if SignedBeaconBlock cannot be found |
| 79 | + */ |
| 80 | +export const readBeaconBlock = async (eraData: Uint8Array, offset: number) => { |
| 81 | + const indices = getEraIndexes(eraData) |
| 82 | + const blockEntry = readEntry( |
| 83 | + eraData.slice( |
| 84 | + indices.blockSlotIndex!.recordStart + indices.blockSlotIndex!.slotOffsets[offset], |
| 85 | + ), |
| 86 | + ) |
| 87 | + const data = await parseEntry(blockEntry) |
| 88 | + if (equalsBytes(blockEntry.type, EraTypes.CompressedSignedBeaconBlockType) === false) { |
| 89 | + throw new Error(`expected CompressedSignedBeaconBlockType type, got ${blockEntry.type}`) |
| 90 | + } |
| 91 | + return ssz.ETH2_TYPES.SignedBeaconBlock.decode(data.data as Uint8Array) |
| 92 | +} |
| 93 | + |
| 94 | +/** |
| 95 | + * Reads a an era file and yields a stream of decompressed SignedBeaconBlocks |
| 96 | + * @param eraFile Uint8Array a serialized era file |
| 97 | + * @returns a stream of decompressed SignedBeaconBlocks or undefined if no blocks are present |
| 98 | + */ |
| 99 | +export async function* readBlocksFromEra(eraFile: Uint8Array) { |
| 100 | + const indices = getEraIndexes(eraFile) |
| 101 | + const maxBlocks = indices.blockSlotIndex?.slotOffsets.length |
| 102 | + if (maxBlocks === undefined) { |
| 103 | + // Return early if no blocks are present |
| 104 | + return |
| 105 | + } |
| 106 | + |
| 107 | + for (let x = 0; x < maxBlocks; x++) { |
| 108 | + try { |
| 109 | + const block = await readBeaconBlock(eraFile, x) |
| 110 | + yield block |
| 111 | + } catch { |
| 112 | + // noop - we skip empty slots |
| 113 | + } |
| 114 | + } |
| 115 | +} |
0 commit comments