import crypto from "node:crypto";
import { BLOB_TX_TYPE, BYTES_PER_FIELD_ELEMENT, FIELD_ELEMENTS_PER_BLOB, ForkName, ForkSeq, SLOTS_PER_EPOCH, } from "@lodestar/params";
import { computeTimeAtSlot } from "@lodestar/state-transition";
import { ssz } from "@lodestar/types";
import { fromHex, toRootHex } from "@lodestar/utils";
import { ZERO_HASH_HEX } from "../../constants/index.js";
import { INTEROP_BLOCK_HASH } from "../../node/utils/interop/state.js";
import { kzgCommitmentToVersionedHash } from "../../util/blobs.js";
import { kzg } from "../../util/kzg.js";
import { ClientCode, ExecutionPayloadStatus, PayloadIdCache } from "./interface.js";
import { deserializePayloadAttributes, serializeBlobsBundle, serializeExecutionPayload, serializeExecutionRequests, } from "./types.js";
import { quantityToNum } from "./utils.js";
const INTEROP_GAS_LIMIT = 30e6;
const PRUNE_PAYLOAD_ID_AFTER_MS = 5000;
const TX_TYPE_EIP1559 = 2;
/**
 * Mock ExecutionEngine for fast prototyping and unit testing
 */
export class ExecutionEngineMockBackend {
    opts;
    // Public state to check if notifyForkchoiceUpdate() is called properly
    headBlockHash = ZERO_HASH_HEX;
    safeBlockHash = ZERO_HASH_HEX;
    finalizedBlockHash = ZERO_HASH_HEX;
    payloadIdCache = new PayloadIdCache();
    /** Known valid blocks */
    validBlocks = new Map();
    /** Preparing payloads to be retrieved via engine_getPayloadV1 */
    preparingPayloads = new Map();
    payloadsForDeletion = new Map();
    predefinedPayloadStatuses = new Map();
    payloadId = 0;
    capellaForkTimestamp;
    denebForkTimestamp;
    electraForkTimestamp;
    fuluForkTimestamp;
    gloasForkTimestamp;
    handlers;
    constructor(opts) {
        this.opts = opts;
        this.validBlocks.set(opts.genesisBlockHash ?? ZERO_HASH_HEX, {
            parentHash: ZERO_HASH_HEX,
            blockHash: ZERO_HASH_HEX,
            timestamp: 0,
            blockNumber: 0,
        });
        const eth1BlockHash = opts.eth1BlockHash ?? toRootHex(INTEROP_BLOCK_HASH);
        this.validBlocks.set(eth1BlockHash, {
            parentHash: ZERO_HASH_HEX,
            blockHash: eth1BlockHash,
            timestamp: 0,
            blockNumber: 1,
        });
        const { config } = opts;
        this.capellaForkTimestamp =
            opts.genesisTime && config
                ? computeTimeAtSlot(config, config.CAPELLA_FORK_EPOCH * SLOTS_PER_EPOCH, opts.genesisTime)
                : Infinity;
        this.denebForkTimestamp =
            opts.genesisTime && config
                ? computeTimeAtSlot(config, config.DENEB_FORK_EPOCH * SLOTS_PER_EPOCH, opts.genesisTime)
                : Infinity;
        this.electraForkTimestamp =
            opts.genesisTime && config
                ? computeTimeAtSlot(config, config.ELECTRA_FORK_EPOCH * SLOTS_PER_EPOCH, opts.genesisTime)
                : Infinity;
        this.fuluForkTimestamp =
            opts.genesisTime && config
                ? computeTimeAtSlot(config, config.FULU_FORK_EPOCH * SLOTS_PER_EPOCH, opts.genesisTime)
                : Infinity;
        this.gloasForkTimestamp =
            opts.genesisTime && config
                ? computeTimeAtSlot(config, config.GLOAS_FORK_EPOCH * SLOTS_PER_EPOCH, opts.genesisTime)
                : Infinity;
        this.handlers = {
            engine_newPayloadV1: this.notifyNewPayload.bind(this),
            engine_newPayloadV2: this.notifyNewPayload.bind(this),
            engine_newPayloadV3: this.notifyNewPayload.bind(this),
            engine_newPayloadV4: this.notifyNewPayload.bind(this),
            engine_forkchoiceUpdatedV1: this.notifyForkchoiceUpdate.bind(this),
            engine_forkchoiceUpdatedV2: this.notifyForkchoiceUpdate.bind(this),
            engine_forkchoiceUpdatedV3: this.notifyForkchoiceUpdate.bind(this),
            engine_getPayloadV1: this.getPayloadV1.bind(this),
            engine_getPayloadV2: this.getPayloadV5.bind(this),
            engine_getPayloadV3: this.getPayloadV5.bind(this),
            engine_getPayloadV4: this.getPayloadV5.bind(this),
            engine_getPayloadV5: this.getPayloadV5.bind(this),
            engine_getPayloadBodiesByHashV1: this.getPayloadBodiesByHash.bind(this),
            engine_getPayloadBodiesByRangeV1: this.getPayloadBodiesByRange.bind(this),
            engine_getClientVersionV1: this.getClientVersionV1.bind(this),
            engine_getBlobsV1: this.getBlobs.bind(this),
            engine_getBlobsV2: this.getBlobsV2.bind(this),
        };
    }
    getPayloadBodiesByHash(_blockHex) {
        return [];
    }
    getPayloadBodiesByRange(_start, _count) {
        return [];
    }
    /**
     * Mock manipulator to add predefined responses before execution engine client calls
     */
    addPredefinedPayloadStatus(blockHash, payloadStatus) {
        this.predefinedPayloadStatuses.set(blockHash, payloadStatus);
    }
    /**
     * `engine_newPayloadV1`
     */
    notifyNewPayload(executionPayloadRpc, 
    // add versionedHashes validation later if required
    _versionedHashes) {
        const blockHash = executionPayloadRpc.blockHash;
        const parentHash = executionPayloadRpc.parentHash;
        // For optimistic sync spec tests, allow to define responses ahead of time
        const predefinedResponse = this.predefinedPayloadStatuses.get(blockHash);
        if (predefinedResponse) {
            return predefinedResponse;
        }
        if (this.opts.onlyPredefinedResponses) {
            throw Error(`No predefined response for blockHash ${blockHash}`);
        }
        // 1. Client software MUST validate blockHash value as being equivalent to Keccak256(RLP(ExecutionBlockHeader)),
        //    where ExecutionBlockHeader is the execution layer block header (the former PoW block header structure).
        //    Fields of this object are set to the corresponding payload values and constant values according to the Block
        //    structure section of EIP-3675, extended with the corresponding section of EIP-4399. Client software MUST run
        //    this validation in all cases even if this branch or any other branches of the block tree are in an active sync
        //    process.
        //
        // > Mock does not do this validation
        // 2. Client software MAY initiate a sync process if requisite data for payload validation is missing. Sync process
        // is specified in the Sync section.
        //
        // > N/A: Mock can't sync
        // 3. Client software MUST validate the payload if it extends the canonical chain and requisite data for the
        //    validation is locally available. The validation process is specified in the Payload validation section.
        //
        // > Mock only validates that parent is known
        if (!this.validBlocks.has(parentHash)) {
            // if requisite data for the payload's acceptance or validation is missing
            // return {status: SYNCING, latestValidHash: null, validationError: null}
            return { status: ExecutionPayloadStatus.SYNCING, latestValidHash: null, validationError: null };
        }
        // 4. Client software MAY NOT validate the payload if the payload doesn't belong to the canonical chain.
        //
        // > N/A: Mock does not track the chain dag
        // Mock logic: persist valid payload as part of canonical chain
        this.validBlocks.set(blockHash, {
            parentHash,
            blockHash,
            timestamp: quantityToNum(executionPayloadRpc.timestamp),
            blockNumber: quantityToNum(executionPayloadRpc.blockNumber),
        });
        // IF the payload has been fully validated while processing the call
        // RETURN payload status from the Payload validation process
        // If validation succeeds, the response MUST contain {status: VALID, latestValidHash: payload.blockHash}
        return { status: ExecutionPayloadStatus.VALID, latestValidHash: blockHash, validationError: null };
    }
    /**
     * `engine_forkchoiceUpdatedV1`
     */
    notifyForkchoiceUpdate(forkChoiceData, payloadAttributesRpc) {
        const { headBlockHash, safeBlockHash, finalizedBlockHash } = forkChoiceData;
        // For optimistic sync spec tests, allow to define responses ahead of time
        const predefinedResponse = this.predefinedPayloadStatuses.get(headBlockHash);
        if (predefinedResponse) {
            return {
                payloadStatus: predefinedResponse,
                payloadId: null,
            };
        }
        if (this.opts.onlyPredefinedResponses) {
            throw Error(`No predefined response for headBlockHash ${headBlockHash}`);
        }
        // 1. Client software MAY initiate a sync process if forkchoiceState.headBlockHash references an unknown payload or
        //    a payload that can't be validated because data that are requisite for the validation is missing. The sync
        //    process is specified in the Sync section.
        //
        // > N/A: Mock can't sync
        // 2. Client software MAY skip an update of the forkchoice state and MUST NOT begin a payload build process if
        //    forkchoiceState.headBlockHash references an ancestor of the head of canonical chain. In the case of such an
        //    event, client software MUST return {payloadStatus:
        //    {status: VALID, latestValidHash: forkchoiceState.headBlockHash, validationError: null}, payloadId: null}.
        //
        // > TODO
        // 3. If forkchoiceState.headBlockHash references a PoW block, client software MUST validate this block with
        //    respect to terminal block conditions according to EIP-3675. This check maps to the transition block validity
        //    section of the EIP. Additionally, if this validation fails, client software MUST NOT update the forkchoice
        //    state and MUST NOT begin a payload build process.
        //
        // > N/A: All networks have completed the merge transition
        // 4. Before updating the forkchoice state, client software MUST ensure the validity of the payload referenced by
        //    forkchoiceState.headBlockHash, and MAY validate the payload while processing the call. The validation process
        //    is specified in the Payload validation section. If the validation process fails, client software MUST NOT
        //    update the forkchoice state and MUST NOT begin a payload build process.
        //
        // > N/A payload already validated
        const headBlock = this.validBlocks.get(headBlockHash);
        if (!headBlock) {
            // IF references an unknown payload or a payload that can't be validated because requisite data is missing
            // RETURN {payloadStatus: {status: SYNCING, latestValidHash: null, validationError: null}, payloadId: null}
            //
            return {
                payloadStatus: { status: ExecutionPayloadStatus.SYNCING, latestValidHash: null, validationError: null },
                payloadId: null,
            };
        }
        // 5. Client software MUST update its forkchoice state if payloads referenced by forkchoiceState.headBlockHash and
        //    forkchoiceState.finalizedBlockHash are VALID.
        if (!this.validBlocks.has(finalizedBlockHash)) {
            throw Error(`Unknown finalizedBlockHash ${finalizedBlockHash}`);
        }
        this.headBlockHash = headBlockHash;
        this.safeBlockHash = safeBlockHash;
        this.finalizedBlockHash = finalizedBlockHash;
        // 6. Client software MUST return -38002: Invalid forkchoice state error if the payload referenced by
        //    forkchoiceState.headBlockHash is VALID and a payload referenced by either forkchoiceState.finalizedBlockHash
        //    or forkchoiceState.safeBlockHash does not belong to the chain defined by forkchoiceState.headBlockHash.
        //
        // > N/A: Mock does not track the chain dag
        if (payloadAttributesRpc) {
            const payloadAttributes = deserializePayloadAttributes(payloadAttributesRpc);
            // 7. Client software MUST ensure that payloadAttributes.timestamp is greater than timestamp of a block referenced
            //    by forkchoiceState.headBlockHash. If this condition isn't held client software MUST respond with
            //   `-38003: Invalid payload attributes` and MUST NOT begin a payload build process.
            //    In such an event, the forkchoiceState update MUST NOT be rolled back.
            if (headBlock.timestamp > payloadAttributes.timestamp) {
                throw Error("Invalid payload attributes");
            }
            // 8. Client software MUST begin a payload build process building on top of forkchoiceState.headBlockHash and
            //    identified via buildProcessId value if payloadAttributes is not null and the forkchoice state has been
            //    updated successfully. The build process is specified in the Payload building section.
            const payloadId = this.payloadId++;
            // Generate empty payload first to be correct with respect to fork
            const fork = this.timestampToFork(payloadAttributes.timestamp);
            const executionPayload = ssz[fork].ExecutionPayload.defaultValue();
            // Make executionPayload valid
            executionPayload.parentHash = fromHex(headBlockHash);
            executionPayload.feeRecipient = fromHex(payloadAttributes.suggestedFeeRecipient);
            executionPayload.prevRandao = payloadAttributes.prevRandao;
            executionPayload.blockNumber = headBlock.blockNumber + 1;
            executionPayload.gasLimit = INTEROP_GAS_LIMIT;
            executionPayload.gasUsed = Math.floor(0.5 * INTEROP_GAS_LIMIT);
            executionPayload.timestamp = payloadAttributes.timestamp;
            executionPayload.blockHash = crypto.randomBytes(32);
            executionPayload.transactions = [];
            // Between 0 and 4 transactions for all forks
            const eip1559TxCount = Math.round(4 * Math.random());
            for (let i = 0; i < eip1559TxCount; i++) {
                const tx = crypto.randomBytes(512);
                tx[0] = TX_TYPE_EIP1559;
                executionPayload.transactions.push(tx);
            }
            const commitments = [];
            const blobs = [];
            const proofs = [];
            if (ForkSeq[fork] >= ForkSeq.fulu) {
                // if post fulu, add between 0 and 3 data column transactions based on slot with BlobsBundleV2
                const fuluTxCount = executionPayload.blockNumber % 4;
                for (let i = 0; i < fuluTxCount; i++) {
                    const blob = generateRandomBlob();
                    const commitment = kzg.blobToKzgCommitment(blob);
                    const { proofs: cellProofs } = kzg.computeCellsAndKzgProofs(blob);
                    executionPayload.transactions.push(transactionForKzgCommitment(commitment));
                    commitments.push(commitment);
                    blobs.push(blob);
                    proofs.push(...cellProofs);
                }
            }
            else if (ForkSeq[fork] >= ForkSeq.deneb && ForkSeq[fork] < ForkSeq.fulu) {
                // if post deneb, add between 0 and 2 blob transactions
                const denebTxCount = executionPayload.blockNumber % 3;
                for (let i = 0; i < denebTxCount; i++) {
                    const blob = generateRandomBlob();
                    const commitment = kzg.blobToKzgCommitment(blob);
                    const proof = kzg.computeBlobKzgProof(blob, commitment);
                    executionPayload.transactions.push(transactionForKzgCommitment(commitment));
                    commitments.push(commitment);
                    blobs.push(blob);
                    proofs.push(proof);
                }
            }
            if (ForkSeq[fork] >= ForkSeq.capella) {
                executionPayload.withdrawals = ssz.capella.Withdrawals.defaultValue();
            }
            this.preparingPayloads.set(payloadId, {
                executionPayload: serializeExecutionPayload(fork, executionPayload),
                blobsBundle: serializeBlobsBundle({
                    commitments,
                    blobs,
                    proofs,
                }),
                executionRequests: serializeExecutionRequests({
                    deposits: ssz.electra.DepositRequests.defaultValue(),
                    withdrawals: ssz.electra.WithdrawalRequests.defaultValue(),
                    consolidations: ssz.electra.ConsolidationRequests.defaultValue(),
                }),
            });
            // IF the payload is deemed VALID and the build process has begun
            // {payloadStatus: {status: VALID, latestValidHash: forkchoiceState.headBlockHash, validationError: null}, payloadId: buildProcessId}
            return {
                payloadStatus: { status: ExecutionPayloadStatus.VALID, latestValidHash: null, validationError: null },
                payloadId: String(payloadId),
            };
        }
        // Don't start build process
        // IF the payload is deemed VALID and a build process hasn't been started
        // {payloadStatus: {status: VALID, latestValidHash: forkchoiceState.headBlockHash, validationError: null}, payloadId: null}
        return {
            payloadStatus: { status: ExecutionPayloadStatus.VALID, latestValidHash: null, validationError: null },
            payloadId: null,
        };
    }
    /**
     * `engine_getPayloadV1`
     *
     * 1. Given the payloadId client software MUST respond with the most recent version of the payload that is available in the corresponding building process at the time of receiving the call.
     * 2. The call MUST be responded with 5: Unavailable payload error if the building process identified by the payloadId doesn't exist.
     * 3. Client software MAY stop the corresponding building process after serving this call.
     */
    getPayloadV1(payloadId) {
        return this.getPayloadV5(payloadId).executionPayload;
    }
    getPayloadV5(payloadId) {
        // 1. Given the payloadId client software MUST return the most recent version of the payload that is available in
        //    the corresponding build process at the time of receiving the call.
        const payloadIdNbr = Number(payloadId);
        const payload = this.preparingPayloads.get(payloadIdNbr);
        // 2. The call MUST return -38001: Unknown payload error if the build process identified by the payloadId does not
        //    exist.
        if (!payload) {
            throw Error(`Unknown payloadId ${payloadId}`);
        }
        // 3. Client software MAY stop the corresponding build process after serving this call.
        // Do after a while to allow getBlobsBundle()
        const now = Date.now();
        for (const [oldPayloadId, addedTimestampMs] of this.payloadsForDeletion.entries()) {
            if (addedTimestampMs < now - PRUNE_PAYLOAD_ID_AFTER_MS) {
                this.preparingPayloads.delete(oldPayloadId);
                this.payloadsForDeletion.delete(oldPayloadId);
            }
        }
        this.payloadsForDeletion.set(payloadIdNbr, now);
        return {
            executionPayload: payload.executionPayload,
            blockValue: String(1e9),
            blobsBundle: payload.blobsBundle,
            executionRequests: payload.executionRequests,
        };
    }
    getClientVersionV1(_clientVersion) {
        return [{ code: ClientCode.XX, name: "mock", version: "", commit: "" }];
    }
    getBlobs(versionedHashes) {
        return versionedHashes.map((_vh) => null);
    }
    getBlobsV2(_versionedHashes) {
        return null;
    }
    timestampToFork(timestamp) {
        if (timestamp >= this.gloasForkTimestamp)
            return ForkName.gloas;
        if (timestamp >= this.fuluForkTimestamp)
            return ForkName.fulu;
        if (timestamp >= this.electraForkTimestamp)
            return ForkName.electra;
        if (timestamp >= this.denebForkTimestamp)
            return ForkName.deneb;
        if (timestamp >= this.capellaForkTimestamp)
            return ForkName.capella;
        return ForkName.bellatrix;
    }
}
function transactionForKzgCommitment(kzgCommitment) {
    // Just use versionedHash as the transaction encoding to mock newPayloadV3 verification
    // prefixed with BLOB_TX_TYPE
    const transaction = new Uint8Array(33);
    const versionedHash = kzgCommitmentToVersionedHash(kzgCommitment);
    transaction[0] = BLOB_TX_TYPE;
    transaction.set(versionedHash, 1);
    return transaction;
}
/**
 * Generate random blob of sequential integers such that each element is < BLS_MODULUS
 */
function generateRandomBlob() {
    const blob = new Uint8Array(FIELD_ELEMENTS_PER_BLOB * BYTES_PER_FIELD_ELEMENT);
    const dv = new DataView(blob.buffer, blob.byteOffset, blob.byteLength);
    for (let i = 0; i < FIELD_ELEMENTS_PER_BLOB; i++) {
        dv.setUint32(i * BYTES_PER_FIELD_ELEMENT, i);
    }
    return blob;
}
//# sourceMappingURL=mock.js.map