UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

854 lines (737 loc) 33.6 kB
import {BitArray, deserializeUint8ArrayBitListFromBytes} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; import { BYTES_PER_FIELD_ELEMENT, FIELD_ELEMENTS_PER_BLOB, ForkName, ForkPostDeneb, ForkSeq, MAX_COMMITTEES_PER_SLOT, isForkPostElectra, isForkPostGloas, } from "@lodestar/params"; import {BLSSignature, CommitteeIndex, RootHex, Slot, ValidatorIndex, ssz} from "@lodestar/types"; export type BlockRootHex = RootHex; // pre-electra, AttestationData is used to cache attestations export type AttDataBase64 = string; // electra, CommitteeBits export type CommitteeBitsBase64 = string; /** `attestation.data.index` from gossip-serialized attestations / aggregates */ export type AttDataIndex = number; // pre-electra // class Attestation(Container): // aggregation_bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE] - offset 4 // data: AttestationData - target data - 128 // signature: BLSSignature - 96 // electra // class Attestation(Container): // aggregation_bits: BitList[MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT] - offset 4 // data: AttestationData - target data - 128 // signature: BLSSignature - 96 // committee_bits: BitVector[MAX_COMMITTEES_PER_SLOT] // electra // class SingleAttestation(Container): // committeeIndex: CommitteeIndex - data 8 // attesterIndex: ValidatorIndex - data 8 // data: AttestationData - data 128 // signature: BLSSignature - data 96 // // for all forks // class AttestationData(Container): 128 bytes fixed size // slot: Slot - data 8 // index: CommitteeIndex - data 8 // beacon_block_root: Root - data 32 // source: Checkpoint - data 40 // target: Checkpoint - data 40 const VARIABLE_FIELD_OFFSET = 4; const ATTESTATION_BEACON_BLOCK_ROOT_OFFSET = VARIABLE_FIELD_OFFSET + 8 + 8; export const ROOT_SIZE = 32; const SLOT_SIZE = 8; const COMMITTEE_INDEX_SIZE = 8; const ATTESTATION_DATA_SIZE = 128; // MAX_COMMITTEES_PER_SLOT is in bit, need to convert to byte const COMMITTEE_BITS_SIZE = Math.max(Math.ceil(MAX_COMMITTEES_PER_SLOT / 8), 1); const SIGNATURE_SIZE = 96; const SINGLE_ATTESTATION_ATTDATA_OFFSET = 8 + 8; const SINGLE_ATTESTATION_SLOT_OFFSET = SINGLE_ATTESTATION_ATTDATA_OFFSET; const SINGLE_ATTESTATION_COMMITTEE_INDEX_OFFSET = 0; const SINGLE_ATTESTATION_DATA_INDEX_OFFSET = SINGLE_ATTESTATION_ATTDATA_OFFSET + 8; const SINGLE_ATTESTATION_ATTESTER_INDEX_OFFSET = 8; const SINGLE_ATTESTATION_BEACON_BLOCK_ROOT_OFFSET = SINGLE_ATTESTATION_ATTDATA_OFFSET + 8 + 8; const SINGLE_ATTESTATION_SIGNATURE_OFFSET = SINGLE_ATTESTATION_ATTDATA_OFFSET + ATTESTATION_DATA_SIZE; const SINGLE_ATTESTATION_SIZE = SINGLE_ATTESTATION_SIGNATURE_OFFSET + SIGNATURE_SIZE; // shared Buffers to convert bytes to hex/base64 const blockRootBuf = Buffer.alloc(ROOT_SIZE); const attDataBuf = Buffer.alloc(ATTESTATION_DATA_SIZE); const committeeBitsDataBuf = Buffer.alloc(COMMITTEE_BITS_SIZE); /** * Extract slot from attestation serialized bytes. * Return null if data is not long enough to extract slot. */ export function getSlotFromAttestationSerialized(data: Uint8Array): Slot | null { if (data.length < VARIABLE_FIELD_OFFSET + SLOT_SIZE) { return null; } return getSlotFromOffset(data, VARIABLE_FIELD_OFFSET); } /** * Extract block root from attestation serialized bytes. * Return null if data is not long enough to extract block root. */ export function getBlockRootFromAttestationSerialized(data: Uint8Array): BlockRootHex | null { if (data.length < ATTESTATION_BEACON_BLOCK_ROOT_OFFSET + ROOT_SIZE) { return null; } blockRootBuf.set( data.subarray(ATTESTATION_BEACON_BLOCK_ROOT_OFFSET, ATTESTATION_BEACON_BLOCK_ROOT_OFFSET + ROOT_SIZE) ); return "0x" + blockRootBuf.toString("hex"); } /** * Extract attestation data base64 from all forks' attestation serialized bytes. * Return null if data is not long enough to extract attestation data. */ export function getAttDataFromAttestationSerialized(data: Uint8Array): AttDataBase64 | null { if (data.length < VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE) { return null; } // base64 is a bit efficient than hex attDataBuf.set(data.subarray(VARIABLE_FIELD_OFFSET, VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE)); return attDataBuf.toString("base64"); } /** * Extract AttDataBase64 from `beacon_attestation` gossip message serialized bytes. * This is used for GossipQueue. */ export function getBeaconAttestationGossipIndex(fork: ForkName, data: Uint8Array): AttDataBase64 | null { return ForkSeq[fork] >= ForkSeq.electra ? getAttDataFromSingleAttestationSerialized(data) : getAttDataFromAttestationSerialized(data); } /** * Extract slot from `beacon_attestation` gossip message serialized bytes. */ export function getSlotFromBeaconAttestationSerialized(fork: ForkName, data: Uint8Array): Slot | null { return ForkSeq[fork] >= ForkSeq.electra ? getSlotFromSingleAttestationSerialized(data) : getSlotFromAttestationSerialized(data); } /** * Extract block root from `beacon_attestation` gossip message serialized bytes. */ export function getBlockRootFromBeaconAttestationSerialized(fork: ForkName, data: Uint8Array): BlockRootHex | null { return ForkSeq[fork] >= ForkSeq.electra ? getBlockRootFromSingleAttestationSerialized(data) : getBlockRootFromAttestationSerialized(data); } /** * Extract aggregation bits from attestation serialized bytes. * Return null if data is not long enough to extract aggregation bits. * Pre-electra attestation only */ export function getAggregationBitsFromAttestationSerialized(data: Uint8Array): BitArray | null { const aggregationBitsStartIndex = VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE + SIGNATURE_SIZE; if (data.length < aggregationBitsStartIndex) { return null; } const {uint8Array, bitLen} = deserializeUint8ArrayBitListFromBytes(data, aggregationBitsStartIndex, data.length); return new BitArray(uint8Array, bitLen); } /** * Extract signature from attestation serialized bytes. * Return null if data is not long enough to extract signature. */ export function getSignatureFromAttestationSerialized(data: Uint8Array): BLSSignature | null { const signatureStartIndex = VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE; if (data.length < signatureStartIndex + SIGNATURE_SIZE) { return null; } return data.subarray(signatureStartIndex, signatureStartIndex + SIGNATURE_SIZE); } /** * Extract slot from SingleAttestation serialized bytes. * Return null if data is not long enough to extract slot. */ export function getSlotFromSingleAttestationSerialized(data: Uint8Array): Slot | null { if (data.length !== SINGLE_ATTESTATION_SIZE) { return null; } return getSlotFromOffset(data, SINGLE_ATTESTATION_SLOT_OFFSET); } /** * Extract committee index from SingleAttestation serialized bytes. * Return null if data is not long enough to extract the committee index. */ export function getCommitteeIndexFromSingleAttestationSerialized( fork: ForkName, data: Uint8Array ): CommitteeIndex | null { if (isForkPostElectra(fork)) { if (data.length !== SINGLE_ATTESTATION_SIZE) { return null; } return getIndexFromOffset(data, SINGLE_ATTESTATION_COMMITTEE_INDEX_OFFSET); } if (data.length < VARIABLE_FIELD_OFFSET + SLOT_SIZE + COMMITTEE_INDEX_SIZE) { return null; } return getIndexFromOffset(data, VARIABLE_FIELD_OFFSET + SLOT_SIZE); } /** * Extract data index from SingleAttestation serialized bytes. * Post-gloas, `data.index` field is repurposed: * - 0 - payload was not available (or attestation is same-slot, where availability is not yet known) * - 1 - payload was available * Return null if data is not long enough to extract the index. */ export function getDataIndexFromSingleAttestationSerialized(fork: ForkName, data: Uint8Array): AttDataIndex | null { if (isForkPostElectra(fork)) { if (data.length !== SINGLE_ATTESTATION_SIZE) { return null; } return getIndexFromOffset(data, SINGLE_ATTESTATION_DATA_INDEX_OFFSET); } if (data.length < VARIABLE_FIELD_OFFSET + SLOT_SIZE + COMMITTEE_INDEX_SIZE) { return null; } return getIndexFromOffset(data, VARIABLE_FIELD_OFFSET + SLOT_SIZE); } /** * Extract attester index from SingleAttestation serialized bytes. * Return null if data is not long enough to extract index. */ export function getAttesterIndexFromSingleAttestationSerialized(data: Uint8Array): ValidatorIndex | null { if (data.length !== SINGLE_ATTESTATION_SIZE) { return null; } return getIndexFromOffset(data, SINGLE_ATTESTATION_ATTESTER_INDEX_OFFSET); } /** * Extract block root from SingleAttestation serialized bytes. * Return null if data is not long enough to extract block root. */ export function getBlockRootFromSingleAttestationSerialized(data: Uint8Array): BlockRootHex | null { if (data.length !== SINGLE_ATTESTATION_SIZE) { return null; } blockRootBuf.set( data.subarray(SINGLE_ATTESTATION_BEACON_BLOCK_ROOT_OFFSET, SINGLE_ATTESTATION_BEACON_BLOCK_ROOT_OFFSET + ROOT_SIZE) ); return `0x${blockRootBuf.toString("hex")}`; } /** * Extract attestation data base64 from SingleAttestation serialized bytes. * Return null if data is not long enough to extract attestation data. */ export function getAttDataFromSingleAttestationSerialized(data: Uint8Array): AttDataBase64 | null { if (data.length !== SINGLE_ATTESTATION_SIZE) { return null; } // base64 is a bit efficient than hex attDataBuf.set( data.subarray(SINGLE_ATTESTATION_ATTDATA_OFFSET, SINGLE_ATTESTATION_ATTDATA_OFFSET + ATTESTATION_DATA_SIZE) ); return attDataBuf.toString("base64"); } /** * Extract signature from SingleAttestation serialized bytes. * Return null if data is not long enough to extract signature. */ export function getSignatureFromSingleAttestationSerialized(data: Uint8Array): BLSSignature | null { if (data.length !== SINGLE_ATTESTATION_SIZE) { return null; } return data.subarray(SINGLE_ATTESTATION_SIGNATURE_OFFSET, SINGLE_ATTESTATION_SIGNATURE_OFFSET + SIGNATURE_SIZE); } // // class SignedAggregateAndProof(Container): // message: AggregateAndProof - offset 4 // signature: BLSSignature - data 96 // class AggregateAndProof(Container) // aggregatorIndex: ValidatorIndex - data 8 // aggregate: Attestation - offset 4 // selectionProof: BLSSignature - data 96 const AGGREGATE_AND_PROOF_OFFSET = 4 + 96; const AGGREGATE_OFFSET = AGGREGATE_AND_PROOF_OFFSET + 8 + 4 + 96; const SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET = AGGREGATE_OFFSET + VARIABLE_FIELD_OFFSET; const SIGNED_AGGREGATE_AND_PROOF_ATTESTATION_DATA_INDEX_OFFSET = SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + SLOT_SIZE; const SIGNED_AGGREGATE_AND_PROOF_BLOCK_ROOT_OFFSET = SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + 8 + 8; /** * Extract slot from signed aggregate and proof serialized bytes * Return null if data is not long enough to extract slot * This works for both phase + electra */ export function getSlotFromSignedAggregateAndProofSerialized(data: Uint8Array): Slot | null { if (data.length < SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + SLOT_SIZE) { return null; } return getSlotFromOffset(data, SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET); } /** * Extract block root from signed aggregate and proof serialized bytes * Return null if data is not long enough to extract block root * This works for both phase + electra */ export function getBlockRootFromSignedAggregateAndProofSerialized(data: Uint8Array): BlockRootHex | null { if (data.length < SIGNED_AGGREGATE_AND_PROOF_BLOCK_ROOT_OFFSET + ROOT_SIZE) { return null; } blockRootBuf.set( data.subarray( SIGNED_AGGREGATE_AND_PROOF_BLOCK_ROOT_OFFSET, SIGNED_AGGREGATE_AND_PROOF_BLOCK_ROOT_OFFSET + ROOT_SIZE ) ); return "0x" + blockRootBuf.toString("hex"); } /** * Extract data index from signed aggregate and proof serialized bytes. * Return null if data is not long enough to extract the index. * This works for both phase0 + electra (index is in attestation data at the same offset). */ export function getDataIndexFromSignedAggregateAndProofSerialized(data: Uint8Array): AttDataIndex | null { if (data.length < SIGNED_AGGREGATE_AND_PROOF_ATTESTATION_DATA_INDEX_OFFSET + COMMITTEE_INDEX_SIZE) { return null; } return getIndexFromOffset(data, SIGNED_AGGREGATE_AND_PROOF_ATTESTATION_DATA_INDEX_OFFSET); } /** * Extract AttestationData base64 from SignedAggregateAndProof for electra * Return null if data is not long enough */ export function getAttDataFromSignedAggregateAndProofElectra(data: Uint8Array): AttDataBase64 | null { const startIndex = SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET; const endIndex = startIndex + ATTESTATION_DATA_SIZE; if (data.length < endIndex + SIGNATURE_SIZE + COMMITTEE_BITS_SIZE) { return null; } attDataBuf.set(data.subarray(startIndex, endIndex)); return attDataBuf.toString("base64"); } /** * Extract CommitteeBits base64 from SignedAggregateAndProof for electra * Return null if data is not long enough */ export function getCommitteeBitsFromSignedAggregateAndProofElectra(data: Uint8Array): CommitteeBitsBase64 | null { const startIndex = SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + ATTESTATION_DATA_SIZE + SIGNATURE_SIZE; const endIndex = startIndex + COMMITTEE_BITS_SIZE; if (data.length < endIndex) { return null; } committeeBitsDataBuf.set(data.subarray(startIndex, endIndex)); return committeeBitsDataBuf.toString("base64"); } /** * Extract attestation data base64 from signed aggregate and proof serialized bytes. * Return null if data is not long enough to extract attestation data. */ export function getAttDataFromSignedAggregateAndProofPhase0(data: Uint8Array): AttDataBase64 | null { if (data.length < SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + ATTESTATION_DATA_SIZE) { return null; } // base64 is a bit efficient than hex attDataBuf.set( data.subarray( SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET, SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + ATTESTATION_DATA_SIZE ) ); return attDataBuf.toString("base64"); } /** * 4 + 96 = 100 * ``` * class SignedBeaconBlock(Container): * message: BeaconBlock [offset - 4 bytes] * signature: BLSSignature [fixed - 96 bytes] * * class BeaconBlock(Container): * slot: Slot [fixed - 8 bytes] * proposer_index: ValidatorIndex * parent_root: Root * state_root: Root * body: BeaconBlockBody * ``` */ const SLOT_BYTES_POSITION_IN_SIGNED_BEACON_BLOCK = VARIABLE_FIELD_OFFSET + SIGNATURE_SIZE; // proposer_index is ValidatorIndex = uint64 = 8 bytes const PARENT_ROOT_BYTES_POSITION_IN_SIGNED_BEACON_BLOCK = VARIABLE_FIELD_OFFSET + SIGNATURE_SIZE + SLOT_SIZE + 8; export function getSlotFromSignedBeaconBlockSerialized(data: Uint8Array): Slot | null { if (data.length < SLOT_BYTES_POSITION_IN_SIGNED_BEACON_BLOCK + SLOT_SIZE) { return null; } return getSlotFromOffset(data, SLOT_BYTES_POSITION_IN_SIGNED_BEACON_BLOCK); } export function getParentRootFromSignedBeaconBlockSerialized(data: Uint8Array): RootHex | null { if (data.length < PARENT_ROOT_BYTES_POSITION_IN_SIGNED_BEACON_BLOCK + ROOT_SIZE) { return null; } blockRootBuf.set( data.subarray( PARENT_ROOT_BYTES_POSITION_IN_SIGNED_BEACON_BLOCK, PARENT_ROOT_BYTES_POSITION_IN_SIGNED_BEACON_BLOCK + ROOT_SIZE ) ); return `0x${blockRootBuf.toString("hex")}`; } /** * Extract parentBlockHash from a GLOAS SignedBeaconBlock by navigating the SSZ offset pointer * to the embedded SignedExecutionPayloadBid. * * Layout (bytes from start of SignedBeaconBlock): * [0..4) message offset * [4..100) signature (96 B) * [100..184) BeaconBlock fixed section: slot(8)+proposer_index(8)+parent_root(32)+state_root(32)+body_offset(4) * [184..) BeaconBlockBody * * BeaconBlockBody (GLOAS) fixed section before signedExecutionPayloadBid offset pointer: * randaoReveal(96) + eth1Data(72) + graffiti(32) * + proposerSlashings(4) + attesterSlashings(4) + attestations(4) + deposits(4) + voluntaryExits(4) * + syncAggregate(160) + blsToExecutionChanges(4) = 384 bytes * * The 4-byte pointer at byte 568 (= 184+384) gives the offset of SignedExecutionPayloadBid * within BeaconBlockBody. parentBlockHash is at that bid's byte 100 (after offset+sig). */ // BeaconBlock body starts after: msg_offset(4) + sig(96) + slot(8) + proposer_index(8) + parent_root(32) + state_root(32) + body_offset_ptr(4) const GLOAS_BODY_START_IN_SIGNED_BEACON_BLOCK = VARIABLE_FIELD_OFFSET + SIGNATURE_SIZE + SLOT_SIZE + 8 + ROOT_SIZE + ROOT_SIZE + VARIABLE_FIELD_OFFSET; // = 184 const GLOAS_SIGNED_BID_OFFSET_POINTER_IN_BODY = 96 + 72 + 32 + 4 + 4 + 4 + 4 + 4 + 160 + 4; // = 384 const GLOAS_SIGNED_BID_OFFSET_POINTER_IN_SIGNED_BEACON_BLOCK = GLOAS_BODY_START_IN_SIGNED_BEACON_BLOCK + GLOAS_SIGNED_BID_OFFSET_POINTER_IN_BODY; // = 568 // Within SignedExecutionPayloadBid, parentBlockHash is at byte 100 (msg_offset:4 + sig:96) const PARENT_BLOCK_HASH_OFFSET_IN_SIGNED_BID = VARIABLE_FIELD_OFFSET + SIGNATURE_SIZE; // = 100 // CAUTION: update offsets if BeaconBlockBody fixed fields change after Gloas export function getParentBlockHashFromGloasSignedBeaconBlockSerialized(data: Uint8Array): RootHex | null { if (data.length < GLOAS_SIGNED_BID_OFFSET_POINTER_IN_SIGNED_BEACON_BLOCK + VARIABLE_FIELD_OFFSET) { return null; } const bidOffset = data[GLOAS_SIGNED_BID_OFFSET_POINTER_IN_SIGNED_BEACON_BLOCK] | (data[GLOAS_SIGNED_BID_OFFSET_POINTER_IN_SIGNED_BEACON_BLOCK + 1] << 8) | (data[GLOAS_SIGNED_BID_OFFSET_POINTER_IN_SIGNED_BEACON_BLOCK + 2] << 16) | (data[GLOAS_SIGNED_BID_OFFSET_POINTER_IN_SIGNED_BEACON_BLOCK + 3] << 24); const parentBlockHashStart = GLOAS_BODY_START_IN_SIGNED_BEACON_BLOCK + bidOffset + PARENT_BLOCK_HASH_OFFSET_IN_SIGNED_BID; if (data.length < parentBlockHashStart + ROOT_SIZE) { return null; } blockRootBuf.set(data.subarray(parentBlockHashStart, parentBlockHashStart + ROOT_SIZE)); return `0x${blockRootBuf.toString("hex")}`; } /** * class BlobSidecar(Container): * index: BlobIndex [fixed - 8 bytes ], * blob: Blob, BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB * kzgCommitment: Bytes48, * kzgProof: Bytes48, * signedBlockHeader: * slot: 8 bytes */ const SLOT_BYTES_POSITION_IN_SIGNED_BLOB_SIDECAR = 8 + BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB + 48 + 48; export function getSlotFromBlobSidecarSerialized(data: Uint8Array): Slot | null { if (data.length < SLOT_BYTES_POSITION_IN_SIGNED_BLOB_SIDECAR + SLOT_SIZE) { return null; } return getSlotFromOffset(data, SLOT_BYTES_POSITION_IN_SIGNED_BLOB_SIDECAR); } /** * Pre-Gloas DataColumnSidecar: * { * index: ColumnIndex [fixed - 8 bytes], * column: DataColumn (offset - 4 bytes), * kzgCommitments: (offset - 4 bytes), * kzgProofs: (offset - 4 bytes), * signedBlockHeader: (offset - 4 bytes) -> slot at variable offset after fixed header * kzgCommitmentsInclusionProof: (offset - 4 bytes), * } * Post-Gloas DataColumnSidecar: * { * index: ColumnIndex [8 bytes], * column: DataColumn (offset - 4 bytes), * kzgProofs: (offset - 4 bytes), * slot: Slot [8 bytes] - at offset 16, * beaconBlockRoot: Root [32 bytes] - at offset 24, * } */ const SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR_PRE_GLOAS = 20; const SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR_POST_GLOAS = 16; const BEACON_BLOCK_ROOT_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR = 24; export function getSlotFromDataColumnSidecarSerialized(data: Uint8Array, fork: ForkName): Slot | null { const offset = isForkPostGloas(fork) ? SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR_POST_GLOAS : SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR_PRE_GLOAS; if (data.length < offset + SLOT_SIZE) { return null; } return getSlotFromOffset(data, offset); } export function getBeaconBlockRootFromDataColumnSidecarSerialized(data: Uint8Array): RootHex | null { if (data.length < BEACON_BLOCK_ROOT_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR + ROOT_SIZE) { return null; } blockRootBuf.set( data.subarray( BEACON_BLOCK_ROOT_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR, BEACON_BLOCK_ROOT_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR + ROOT_SIZE ) ); return "0x" + blockRootBuf.toString("hex"); } /** * SignedExecutionPayloadEnvelope SSZ Layout: * ├─ 4 bytes: message offset (points to byte 100) * ├─ 96 bytes: signature * └─ ExecutionPayloadEnvelope (starts at byte 100): * ├─ 4 bytes: payload offset * ├─ 4 bytes: executionRequests offset * ├─ 8 bytes: builderIndex (offset 108-115) * ├─ 32 bytes: beaconBlockRoot (offset 116-147) * ├─ 32 bytes: parentBeaconBlockRoot (offset 148-179) — new in Gloas alpha.6 (consensus-specs#5152) * └─ variable: payload data (starts at envelope + 80) * └─ ExecutionPayload fixed portion includes slotNumber at offset 532 */ const SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MESSAGE_OFFSET = 4; const SIGNED_EXECUTION_PAYLOAD_ENVELOPE_SIGNATURE_SIZE = 96; const EXECUTION_PAYLOAD_ENVELOPE_PAYLOAD_OFFSET = 4; const EXECUTION_PAYLOAD_ENVELOPE_REQUESTS_OFFSET = 4; const EXECUTION_PAYLOAD_ENVELOPE_BUILDER_INDEX_SIZE = 8; const BEACON_BLOCK_ROOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE = SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MESSAGE_OFFSET + SIGNED_EXECUTION_PAYLOAD_ENVELOPE_SIGNATURE_SIZE + EXECUTION_PAYLOAD_ENVELOPE_PAYLOAD_OFFSET + EXECUTION_PAYLOAD_ENVELOPE_REQUESTS_OFFSET + EXECUTION_PAYLOAD_ENVELOPE_BUILDER_INDEX_SIZE; // 116 // Envelope fixed portion: payload_offset(4) + requests_offset(4) + builderIndex(8) + beaconBlockRoot(32) + parentBeaconBlockRoot(32) = 80 const EXECUTION_PAYLOAD_ENVELOPE_FIXED_SIZE = EXECUTION_PAYLOAD_ENVELOPE_PAYLOAD_OFFSET + EXECUTION_PAYLOAD_ENVELOPE_REQUESTS_OFFSET + EXECUTION_PAYLOAD_ENVELOPE_BUILDER_INDEX_SIZE + ROOT_SIZE + ROOT_SIZE; // 80 // slotNumber offset within ExecutionPayload fixed portion: // parentHash(32) + feeRecipient(20) + stateRoot(32) + receiptsRoot(32) + logsBloom(256) + // prevRandao(32) + blockNumber(8) + gasLimit(8) + gasUsed(8) + timestamp(8) + // extraData_offset(4) + baseFeePerGas(32) + blockHash(32) + transactions_offset(4) + // withdrawals_offset(4) + blobGasUsed(8) + excessBlobGas(8) + blockAccessList_offset(4) = 532 const SLOT_NUMBER_OFFSET_IN_EXECUTION_PAYLOAD = 532; // Payload data starts right after the envelope's fixed portion const ENVELOPE_START_IN_SIGNED = SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MESSAGE_OFFSET + SIGNED_EXECUTION_PAYLOAD_ENVELOPE_SIGNATURE_SIZE; // 100 const SLOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE = ENVELOPE_START_IN_SIGNED + EXECUTION_PAYLOAD_ENVELOPE_FIXED_SIZE + SLOT_NUMBER_OFFSET_IN_EXECUTION_PAYLOAD; // 100 + 80 + 532 = 712 export function getSlotFromExecutionPayloadEnvelopeSerialized(data: Uint8Array): Slot | null { if (data.length < SLOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE + SLOT_SIZE) { return null; } return getSlotFromOffset(data, SLOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE); } export function getBeaconBlockRootFromExecutionPayloadEnvelopeSerialized(data: Uint8Array): RootHex | null { if (data.length < BEACON_BLOCK_ROOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE + ROOT_SIZE) { return null; } blockRootBuf.set( data.subarray( BEACON_BLOCK_ROOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE, BEACON_BLOCK_ROOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE + ROOT_SIZE ) ); return "0x" + blockRootBuf.toString("hex"); } /** * BeaconState of all forks (up until Electra, check with new forks) * class BeaconState(Container): * genesis_time: uint64 - 8 bytes * genesis_validators_root: Root - 32 bytes * slot: Slot - 8 bytes * fork: Fork - 16 bytes * latest_block_header: BeaconBlockHeader - fixed size * slot: Slot - 8 bytes * */ const BLOCK_HEADER_SLOT_BYTES_POSITION_IN_BEACON_STATE = 8 + 32 + 8 + 16; export function getLastProcessedSlotFromBeaconStateSerialized(data: Uint8Array): Slot | null { if (data.length < BLOCK_HEADER_SLOT_BYTES_POSITION_IN_BEACON_STATE + SLOT_SIZE) { return null; } return getSlotFromOffset(data, BLOCK_HEADER_SLOT_BYTES_POSITION_IN_BEACON_STATE); } const SLOT_BYTES_POSITION_IN_BEACON_STATE = 8 + 32; export function getSlotFromBeaconStateSerialized(data: Uint8Array): Slot | null { if (data.length < SLOT_BYTES_POSITION_IN_BEACON_STATE) { return null; } return getSlotFromOffset(data, SLOT_BYTES_POSITION_IN_BEACON_STATE); } /** * PayloadAttestationMessage: { * validatorIndex: ValidatorIndex (8 bytes) * data: PayloadAttestationData { * beaconBlockRoot: Root (32 bytes) ← offset 8 * slot: Slot (8 bytes) ← offset 40 * payloadPresent: Boolean (1 byte) * blobDataAvailable: Boolean (1 byte) * } * signature: BLSSignature (96 bytes) * } * Fully fixed-size container, no offset table. */ const PAYLOAD_ATTESTATION_MESSAGE_BEACON_BLOCK_ROOT_OFFSET = 8; const PAYLOAD_ATTESTATION_MESSAGE_SLOT_OFFSET = 8 + ROOT_SIZE; // 40 const PAYLOAD_ATTESTATION_MESSAGE_PAYLOAD_PRESENT_OFFSET = PAYLOAD_ATTESTATION_MESSAGE_SLOT_OFFSET + SLOT_SIZE; // 48 export function getSlotFromPayloadAttestationMessageSerialized(data: Uint8Array): Slot | null { if (data.length < PAYLOAD_ATTESTATION_MESSAGE_SLOT_OFFSET + SLOT_SIZE) { return null; } return getSlotFromOffset(data, PAYLOAD_ATTESTATION_MESSAGE_SLOT_OFFSET); } export function getPayloadPresentFromPayloadAttestationMessageSerialized(data: Uint8Array): boolean | null { if (data.length < PAYLOAD_ATTESTATION_MESSAGE_PAYLOAD_PRESENT_OFFSET + 1) { return null; } return data[PAYLOAD_ATTESTATION_MESSAGE_PAYLOAD_PRESENT_OFFSET] !== 0; } export function getBlockRootFromPayloadAttestationMessageSerialized(data: Uint8Array): RootHex | null { if (data.length < PAYLOAD_ATTESTATION_MESSAGE_BEACON_BLOCK_ROOT_OFFSET + ROOT_SIZE) { return null; } blockRootBuf.set( data.subarray( PAYLOAD_ATTESTATION_MESSAGE_BEACON_BLOCK_ROOT_OFFSET, PAYLOAD_ATTESTATION_MESSAGE_BEACON_BLOCK_ROOT_OFFSET + ROOT_SIZE ) ); return `0x${blockRootBuf.toString("hex")}`; } /** * SignedExecutionPayloadBid: {message: ExecutionPayloadBid (variable), signature: BLSSignature (96 bytes)} * Fixed part: 4-byte offset + 96-byte signature = 100 bytes * message data starts at byte 100 * * ExecutionPayloadBid fixed fields (in order): * parentBlockHash: Bytes32 (32 bytes) * parentBlockRoot: Root (32 bytes) * blockHash: Bytes32 (32 bytes) * prevRandao: Bytes32 (32 bytes) * feeRecipient: ExecutionAddress(20 bytes) * gasLimit: UintBn64 (8 bytes) * builderIndex: BuilderIndex (8 bytes) * slot: Slot (8 bytes) ← absolute offset 264 */ const SIGNED_EXECUTION_PAYLOAD_BID_PARENT_BLOCK_HASH_OFFSET = VARIABLE_FIELD_OFFSET + SIGNATURE_SIZE; // 100 const SIGNED_EXECUTION_PAYLOAD_BID_PARENT_BLOCK_ROOT_OFFSET = SIGNED_EXECUTION_PAYLOAD_BID_PARENT_BLOCK_HASH_OFFSET + ROOT_SIZE; // 132 const SIGNED_EXECUTION_PAYLOAD_BID_SLOT_OFFSET = VARIABLE_FIELD_OFFSET + SIGNATURE_SIZE + 32 + 32 + 32 + 32 + 20 + 8 + 8; // 264 export function getSlotFromSignedExecutionPayloadBidSerialized(data: Uint8Array): Slot | null { if (data.length < SIGNED_EXECUTION_PAYLOAD_BID_SLOT_OFFSET + SLOT_SIZE) { return null; } return getSlotFromOffset(data, SIGNED_EXECUTION_PAYLOAD_BID_SLOT_OFFSET); } export function getParentBlockHashFromSignedExecutionPayloadBidSerialized(data: Uint8Array): RootHex | null { if (data.length < SIGNED_EXECUTION_PAYLOAD_BID_PARENT_BLOCK_HASH_OFFSET + ROOT_SIZE) { return null; } blockRootBuf.set( data.subarray( SIGNED_EXECUTION_PAYLOAD_BID_PARENT_BLOCK_HASH_OFFSET, SIGNED_EXECUTION_PAYLOAD_BID_PARENT_BLOCK_HASH_OFFSET + ROOT_SIZE ) ); return `0x${blockRootBuf.toString("hex")}`; } export function getParentBlockRootFromSignedExecutionPayloadBidSerialized(data: Uint8Array): RootHex | null { if (data.length < SIGNED_EXECUTION_PAYLOAD_BID_PARENT_BLOCK_ROOT_OFFSET + ROOT_SIZE) { return null; } blockRootBuf.set( data.subarray( SIGNED_EXECUTION_PAYLOAD_BID_PARENT_BLOCK_ROOT_OFFSET, SIGNED_EXECUTION_PAYLOAD_BID_PARENT_BLOCK_ROOT_OFFSET + ROOT_SIZE ) ); return `0x${blockRootBuf.toString("hex")}`; } /** * Read only the first 4 bytes of Slot, max value is 4,294,967,295 will be reached 1634 years after genesis * * If the high bytes are not zero, return null */ function getSlotFromOffset(data: Uint8Array, offset: number): Slot | null { return checkSlotHighBytes(data, offset) ? getSlotFromOffsetTrusted(data, offset) : null; } /** * Alias of `getSlotFromOffset` for readability */ function getIndexFromOffset(data: Uint8Array, offset: number): (ValidatorIndex | CommitteeIndex) | null { return getSlotFromOffset(data, offset); } /** * Read only the first 4 bytes of Slot, max value is 4,294,967,295 will be reached 1634 years after genesis */ function getSlotFromOffsetTrusted(data: Uint8Array, offset: number): Slot { return (data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24)) >>> 0; } function checkSlotHighBytes(data: Uint8Array, offset: number): boolean { return (data[offset + 4] | data[offset + 5] | data[offset + 6] | data[offset + 7]) === 0; } export function getBlobKzgCommitmentsCountFromSignedBeaconBlockSerialized( config: ChainForkConfig, blockBytes: Uint8Array ): number { const slot = getSlotFromSignedBeaconBlockSerialized(blockBytes); if (slot === null) throw new Error("Can not parse the slot from block bytes"); if (config.getForkSeq(slot) < ForkSeq.deneb) return 0; const forkName = config.getForkName(slot); if (isForkPostGloas(forkName)) { // Gloas stores commitments under signedExecutionPayloadBid.message.blobKzgCommitments. // Navigate the offset chain: SignedBeaconBlock → message → body → signedExecutionPayloadBid → message → blobKzgCommitments const {SignedBeaconBlock: GloasSignedBlock, BeaconBlock: GloasBlock, BeaconBlockBody: GloasBody} = ssz[forkName]; const {SignedExecutionPayloadBid, ExecutionPayloadBid} = ssz[forkName]; const commitmentSize = ssz.deneb.KZGCommitment.fixedSize; const view = new DataView(blockBytes.buffer, blockBytes.byteOffset, blockBytes.byteLength); const signedBlockRanges = GloasSignedBlock.getFieldRanges(view, 0, blockBytes.length); const messageIdx = Object.keys(GloasSignedBlock.fields).indexOf("message"); const messageRange = signedBlockRanges[messageIdx]; const blockRanges = GloasBlock.getFieldRanges(view, messageRange.start, messageRange.end); const bodyIdx = Object.keys(GloasBlock.fields).indexOf("body"); const bodyRange = blockRanges[bodyIdx]; const bodyStart = messageRange.start + bodyRange.start; const bodyEnd = messageRange.start + bodyRange.end; const bodyRanges = GloasBody.getFieldRanges(view, bodyStart, bodyEnd); const bidIdx = Object.keys(GloasBody.fields).indexOf("signedExecutionPayloadBid"); const bidRange = bodyRanges[bidIdx]; const bidStart = bodyStart + bidRange.start; const bidEnd = bodyStart + bidRange.end; const bidRanges = SignedExecutionPayloadBid.getFieldRanges(view, bidStart, bidEnd); const bidMsgIdx = Object.keys(SignedExecutionPayloadBid.fields).indexOf("message"); const bidMsgRange = bidRanges[bidMsgIdx]; const bidMsgStart = bidStart + bidMsgRange.start; const bidMsgEnd = bidStart + bidMsgRange.end; const execBidRanges = ExecutionPayloadBid.getFieldRanges(view, bidMsgStart, bidMsgEnd); const commitmentsIdx = Object.keys(ExecutionPayloadBid.fields).indexOf("blobKzgCommitments"); const commitmentsRange = execBidRanges[commitmentsIdx]; const start = bidMsgStart + commitmentsRange.start; const end = bidMsgStart + commitmentsRange.end; return Math.round(((end > blockBytes.byteLength ? blockBytes.byteLength : end) - start) / commitmentSize); } const {SignedBeaconBlock, BeaconBlock, BeaconBlockBody, KZGCommitment} = ssz[forkName as ForkPostDeneb]; const view = new DataView(blockBytes.buffer, blockBytes.byteOffset, blockBytes.byteLength); const singedBlockFieldRanges = SignedBeaconBlock.getFieldRanges(view, 0, blockBytes.length); const messageIndex = Object.keys(SignedBeaconBlock.fields).indexOf("message"); const messageRange = singedBlockFieldRanges[messageIndex]; const blockFieldRanges = BeaconBlock.getFieldRanges(view, messageRange.start, messageRange.end); const bodyIndex = Object.keys(BeaconBlock.fields).indexOf("body"); const bodyRange = blockFieldRanges[bodyIndex]; const bodyFieldRanges = BeaconBlockBody.getFieldRanges( view, messageRange.start + bodyRange.start, messageRange.end + bodyRange.end ); const kzgCommitmentsIndex = Object.keys(BeaconBlockBody.fields).indexOf("blobKzgCommitments"); const kzgCommitmentsRange = bodyFieldRanges[kzgCommitmentsIndex]; const commitmentSize = KZGCommitment.fixedSize; const end = messageRange.end + bodyRange.end + kzgCommitmentsRange.end; const start = messageRange.start + bodyRange.start + kzgCommitmentsRange.start; return Math.round(((end > blockBytes.byteLength ? blockBytes.byteLength : end) - start) / commitmentSize); }