UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

335 lines • 15 kB
import { BYTES_PER_FIELD_ELEMENT, BYTES_PER_LOGS_BLOOM, CELLS_PER_EXT_BLOB, CONSOLIDATION_REQUEST_TYPE, DEPOSIT_REQUEST_TYPE, FIELD_ELEMENTS_PER_BLOB, ForkSeq, WITHDRAWAL_REQUEST_TYPE, } from "@lodestar/params"; import { ssz, } from "@lodestar/types"; import { isExecutionRequestType, } from "./interface.js"; import { bytesToData, dataIntoBytes, dataToBytes, numToQuantity, quantityToBigint, quantityToNum, } from "./utils.js"; const BLOB_BYTES = BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB; const PROOF_BYTES = 48; export const BLOB_AND_PROOF_V2_RPC_BYTES = BLOB_BYTES + PROOF_BYTES * CELLS_PER_EXT_BLOB; export function serializeExecutionPayload(fork, data) { const payload = { parentHash: bytesToData(data.parentHash), feeRecipient: bytesToData(data.feeRecipient), stateRoot: bytesToData(data.stateRoot), receiptsRoot: bytesToData(data.receiptsRoot), logsBloom: bytesToData(data.logsBloom), prevRandao: bytesToData(data.prevRandao), blockNumber: numToQuantity(data.blockNumber), gasLimit: numToQuantity(data.gasLimit), gasUsed: numToQuantity(data.gasUsed), timestamp: numToQuantity(data.timestamp), extraData: bytesToData(data.extraData), baseFeePerGas: numToQuantity(data.baseFeePerGas), blockHash: bytesToData(data.blockHash), transactions: data.transactions.map((tran) => bytesToData(tran)), }; // Capella adds withdrawals to the ExecutionPayload if (ForkSeq[fork] >= ForkSeq.capella) { const { withdrawals } = data; payload.withdrawals = withdrawals.map(serializeWithdrawal); } // DENEB adds blobGasUsed & excessBlobGas to the ExecutionPayload if (ForkSeq[fork] >= ForkSeq.deneb) { const { blobGasUsed, excessBlobGas } = data; payload.blobGasUsed = numToQuantity(blobGasUsed); payload.excessBlobGas = numToQuantity(excessBlobGas); } // No changes in Electra if (ForkSeq[fork] >= ForkSeq.gloas) { const { blockAccessList, slotNumber } = data; payload.blockAccessList = bytesToData(blockAccessList); payload.slotNumber = numToQuantity(slotNumber); } return payload; } export function serializeVersionedHashes(vHashes) { return vHashes.map(bytesToData); } export function hasPayloadValue(response) { return response.blockValue !== undefined; } export function parseExecutionPayload(fork, response) { let data; let executionPayloadValue; let blobsBundle; let executionRequests; let shouldOverrideBuilder; if (hasPayloadValue(response)) { executionPayloadValue = quantityToBigint(response.blockValue); data = response.executionPayload; blobsBundle = response.blobsBundle ? parseBlobsBundle(response.blobsBundle) : undefined; executionRequests = response.executionRequests ? deserializeExecutionRequests(response.executionRequests) : undefined; shouldOverrideBuilder = response.shouldOverrideBuilder ?? false; } else { data = response; // Just set it to zero as default executionPayloadValue = BigInt(0); blobsBundle = undefined; executionRequests = undefined; shouldOverrideBuilder = false; } const executionPayload = { parentHash: dataToBytes(data.parentHash, 32), feeRecipient: dataToBytes(data.feeRecipient, 20), stateRoot: dataToBytes(data.stateRoot, 32), receiptsRoot: dataToBytes(data.receiptsRoot, 32), logsBloom: dataToBytes(data.logsBloom, BYTES_PER_LOGS_BLOOM), prevRandao: dataToBytes(data.prevRandao, 32), blockNumber: quantityToNum(data.blockNumber), gasLimit: quantityToNum(data.gasLimit), gasUsed: quantityToNum(data.gasUsed), timestamp: quantityToNum(data.timestamp), extraData: dataToBytes(data.extraData, null), baseFeePerGas: quantityToBigint(data.baseFeePerGas), blockHash: dataToBytes(data.blockHash, 32), transactions: data.transactions.map((tran) => dataToBytes(tran, null)), }; if (ForkSeq[fork] >= ForkSeq.capella) { const { withdrawals } = data; // Geth can also reply with null if (withdrawals == null) { throw Error(`withdrawals missing for ${fork} >= capella executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}`); } executionPayload.withdrawals = withdrawals.map((w) => deserializeWithdrawal(w)); } // DENEB adds excessBlobGas to the ExecutionPayload if (ForkSeq[fork] >= ForkSeq.deneb) { const { blobGasUsed, excessBlobGas } = data; if (blobGasUsed == null) { throw Error(`blobGasUsed missing for ${fork} >= deneb executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}`); } if (excessBlobGas == null) { throw Error(`excessBlobGas missing for ${fork} >= deneb executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}`); } executionPayload.blobGasUsed = quantityToBigint(blobGasUsed); executionPayload.excessBlobGas = quantityToBigint(excessBlobGas); } // No changes in Electra if (ForkSeq[fork] >= ForkSeq.gloas) { const { blockAccessList, slotNumber } = data; if (blockAccessList == null) { throw Error(`blockAccessList missing for ${fork} >= gloas executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}`); } if (slotNumber == null) { throw Error(`slotNumber missing for ${fork} >= gloas executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}`); } executionPayload.blockAccessList = dataToBytes(blockAccessList, null); executionPayload.slotNumber = quantityToNum(slotNumber); } return { executionPayload, executionPayloadValue, blobsBundle, executionRequests, shouldOverrideBuilder }; } export function serializePayloadAttributes(data) { return { timestamp: numToQuantity(data.timestamp), prevRandao: bytesToData(data.prevRandao), suggestedFeeRecipient: data.suggestedFeeRecipient, withdrawals: data.withdrawals?.map(serializeWithdrawal), parentBeaconBlockRoot: data.parentBeaconBlockRoot ? bytesToData(data.parentBeaconBlockRoot) : undefined, slotNumber: data.slotNumber !== undefined ? numToQuantity(data.slotNumber) : undefined, }; } export function serializeBeaconBlockRoot(data) { return bytesToData(data); } export function deserializePayloadAttributes(data) { return { timestamp: quantityToNum(data.timestamp), prevRandao: dataToBytes(data.prevRandao, 32), // DATA is anyway a hex string, so we can just track it as a hex string to // avoid any conversions suggestedFeeRecipient: data.suggestedFeeRecipient, withdrawals: data.withdrawals?.map((withdrawal) => deserializeWithdrawal(withdrawal)), parentBeaconBlockRoot: data.parentBeaconBlockRoot ? dataToBytes(data.parentBeaconBlockRoot, 32) : undefined, slotNumber: data.slotNumber !== undefined ? quantityToNum(data.slotNumber) : undefined, }; } export function parseBlobsBundle(data) { return { // As of Nov 17th 2022 according to Dan's tests Geth returns null if no blobs in block commitments: (data.commitments ?? []).map((kzg) => dataToBytes(kzg, 48)), blobs: (data.blobs ?? []).map((blob) => dataToBytes(blob, BLOB_BYTES)), proofs: (data.proofs ?? []).map((kzg) => dataToBytes(kzg, PROOF_BYTES)), }; } export function serializeBlobsBundle(data) { return { commitments: data.commitments.map((kzg) => bytesToData(kzg)), blobs: data.blobs.map((blob) => bytesToData(blob)), proofs: data.proofs.map((proof) => bytesToData(proof)), }; } export function serializeWithdrawal(withdrawal) { return { index: numToQuantity(withdrawal.index), validatorIndex: numToQuantity(withdrawal.validatorIndex), address: bytesToData(withdrawal.address), // Both CL and EL now deal in Gwei, just little-endian to big-endian conversion required amount: numToQuantity(withdrawal.amount), }; } export function deserializeWithdrawal(serialized) { return { index: quantityToNum(serialized.index), validatorIndex: quantityToNum(serialized.validatorIndex), address: dataToBytes(serialized.address, 20), // Both CL and EL now deal in Gwei, just big-endian to little-endian conversion required amount: quantityToBigint(serialized.amount), }; } /** * Prepend a single-byte requestType to requestsBytes */ function prefixRequests(requestsBytes, requestType) { const prefixedRequests = new Uint8Array(1 + requestsBytes.length); prefixedRequests[0] = requestType; prefixedRequests.set(requestsBytes, 1); return prefixedRequests; } function serializeDepositRequests(depositRequests) { const requestsBytes = ssz.electra.DepositRequests.serialize(depositRequests); return bytesToData(prefixRequests(requestsBytes, DEPOSIT_REQUEST_TYPE)); } function deserializeDepositRequests(serialized) { return ssz.electra.DepositRequests.deserialize(dataToBytes(serialized, null)); } function serializeWithdrawalRequests(withdrawalRequests) { const requestsBytes = ssz.electra.WithdrawalRequests.serialize(withdrawalRequests); return bytesToData(prefixRequests(requestsBytes, WITHDRAWAL_REQUEST_TYPE)); } function deserializeWithdrawalRequests(serialized) { return ssz.electra.WithdrawalRequests.deserialize(dataToBytes(serialized, null)); } function serializeConsolidationRequests(consolidationRequests) { const requestsBytes = ssz.electra.ConsolidationRequests.serialize(consolidationRequests); return bytesToData(prefixRequests(requestsBytes, CONSOLIDATION_REQUEST_TYPE)); } function deserializeConsolidationRequests(serialized) { return ssz.electra.ConsolidationRequests.deserialize(dataToBytes(serialized, null)); } /** * This is identical to get_execution_requests_list in * https://github.com/ethereum/consensus-specs/blob/v1.5.0-alpha.8/specs/electra/beacon-chain.md#new-get_execution_requests_list */ export function serializeExecutionRequests(executionRequests) { const { deposits, withdrawals, consolidations } = executionRequests; const result = []; if (deposits.length !== 0) { result.push(serializeDepositRequests(deposits)); } if (withdrawals.length !== 0) { result.push(serializeWithdrawalRequests(withdrawals)); } if (consolidations.length !== 0) { result.push(serializeConsolidationRequests(consolidations)); } return result; } export function deserializeExecutionRequests(serialized) { const result = { deposits: [], withdrawals: [], consolidations: [], }; if (serialized.length === 0) { return result; } let prevRequestType; for (let prefixedRequests of serialized) { // Slice out 0x so it is easier to extract request type if (prefixedRequests.startsWith("0x")) { prefixedRequests = prefixedRequests.slice(2); } const currentRequestType = parseInt(prefixedRequests.substring(0, 2), 16); if (!isExecutionRequestType(currentRequestType)) { throw Error(`Invalid request type currentRequestType=${prefixedRequests.substring(0, 2)}`); } const requests = prefixedRequests.slice(2); if (requests.length === 0) { throw Error(`Request with empty data must be excluded from execution requests currentRequestType=${currentRequestType}`); } if (prevRequestType !== undefined && prevRequestType >= currentRequestType) { throw Error(`Current request type must be larger than previous request type prevRequestType=${prevRequestType} currentRequestType=${currentRequestType}`); } switch (currentRequestType) { case DEPOSIT_REQUEST_TYPE: { result.deposits = deserializeDepositRequests(requests); break; } case WITHDRAWAL_REQUEST_TYPE: { result.withdrawals = deserializeWithdrawalRequests(requests); break; } case CONSOLIDATION_REQUEST_TYPE: { result.consolidations = deserializeConsolidationRequests(requests); break; } } prevRequestType = currentRequestType; } return result; } export function deserializeExecutionPayloadBody(data) { return data ? { transactions: data.transactions.map((tran) => dataToBytes(tran, null)), withdrawals: data.withdrawals ? data.withdrawals.map(deserializeWithdrawal) : null, } : null; } export function serializeExecutionPayloadBody(data) { return data ? { transactions: data.transactions.map((tran) => bytesToData(tran)), withdrawals: data.withdrawals ? data.withdrawals.map(serializeWithdrawal) : null, } : null; } export function deserializeBlobAndProofs(data) { return data ? { blob: dataToBytes(data.blob, BLOB_BYTES), proof: dataToBytes(data.proof, PROOF_BYTES), } : null; } export function deserializeBlobAndProofsV2(data) { return { blob: dataToBytes(data.blob, BLOB_BYTES), proofs: data.proofs.map((proof) => dataToBytes(proof, PROOF_BYTES)), }; } /** * The same to deserializeBlobAndProofsV2 but using preallocated buffers since BlobAndProofV2Rpc is fixed size */ export function deserializeBlobAndProofsV2IntoBytes(data, buffer) { if (buffer.length !== BLOB_AND_PROOF_V2_RPC_BYTES) { throw Error(`Invalid buffer length ${buffer.length}, expected ${BLOB_AND_PROOF_V2_RPC_BYTES} to hold BlobAndProofV2Rpc`); } // https://github.com/ethereum/execution-apis/blob/main/src/engine/osaka.md#blobandproofv2 // proofs MUST contain exactly CELLS_PER_EXT_BLOB cell proofs. if (data.proofs.length !== CELLS_PER_EXT_BLOB) { throw Error(`Invalid proofs length ${data.proofs.length}, expected ${CELLS_PER_EXT_BLOB}`); } const blob = dataIntoBytes(data.blob, buffer.subarray(0, BLOB_BYTES)); const proofs = []; for (let i = 0; i < CELLS_PER_EXT_BLOB; i++) { const proof = dataIntoBytes(data.proofs[i], buffer.subarray(BLOB_BYTES + i * PROOF_BYTES, BLOB_BYTES + (i + 1) * PROOF_BYTES)); if (proof.length !== PROOF_BYTES) { throw Error(`Invalid proof length ${proof.length}, expected ${PROOF_BYTES}`); } proofs.push(proof); } return { blob, proofs, }; } export function assertReqSizeLimit(blockHashesReqCount, count) { if (blockHashesReqCount > count) { throw new Error(`Requested blocks must not be > ${count}`); } return; } //# sourceMappingURL=types.js.map