diff --git a/.changeset/rich-owls-count.md b/.changeset/rich-owls-count.md new file mode 100644 index 000000000..56cbb2f03 --- /dev/null +++ b/.changeset/rich-owls-count.md @@ -0,0 +1,5 @@ +--- +"@blobscan/web": minor +--- + +Added support for calculating the blob gas target using Pectra update params diff --git a/apps/docs/src/app/docs/codebase-overview/page.md b/apps/docs/src/app/docs/codebase-overview/page.md index b463ede77..e9f503fb5 100644 --- a/apps/docs/src/app/docs/codebase-overview/page.md +++ b/apps/docs/src/app/docs/codebase-overview/page.md @@ -47,20 +47,21 @@ Blobscan has the following command line interfaces: Here you can find all the shared packages used by the apps: -| Package | Description | -| ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [`@blobscan/api`](https://github.com/Blobscan/blobscan/tree/main/packages/api) | tRPC routers and procedures used by the web app and the REST API | -| [`@blobscan/blob-propagator`](https://github.com/Blobscan/blobscan/tree/main/packages/blob-propagator) | Mechanism for propagating blob data across various storage systems through [bullmq](https://docs.bullmq.io/) sandboxed workers. | +| Package | Description | +| ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`@blobscan/api`](https://github.com/Blobscan/blobscan/tree/main/packages/api) | tRPC routers and procedures used by the web app and the REST API | +| [`@blobscan/blob-propagator`](https://github.com/Blobscan/blobscan/tree/main/packages/blob-propagator) | Mechanism for propagating blob data across various storage systems through [bullmq](https://docs.bullmq.io/) sandboxed workers. | | [`@blobscan/blob-storage-manager`](https://github.com/Blobscan/blobscan/tree/main/packages/blob-storage-manager) | Orchestrates the storage/retrieval of blobs in/from different storage providers. Currently it supports [Google Cloud Storage](https://cloud.google.com/storage), [Swarm](https://www.ethswarm.org), PostgreSQL database and filesystem. | -| [`@blobscan/dayjs`](https://github.com/Blobscan/blobscan/tree/main/packages/dayjs) |  Extended [Day.js](https://day.js.org/) with plugins. | -| [`@blobscan/db`](https://github.com/Blobscan/blobscan/tree/main/packages/db) | Prisma schema and a Prisma client with [extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions) containing custom methods queries. | -| [`@blobscan/logger`](https://github.com/Blobscan/blobscan/tree/main/packages/logger) | Shared logger utilities. | -| [`@blobscan/optimism-decoder`](https://github.com/Blobscan/blobscan/tree/main/packages/optimism-decoder) | Optimism blobs decoder. | -| [`@blobscan/open-telemetry`](https://github.com/Blobscan/blobscan/tree/main/packages/open-telemetry) | [Otel](https://opentelemetry.io/) configuration and sdk setup. | -| [`@blobscan/test`](https://github.com/Blobscan/blobscan/tree/main/packages/test) | Shared test utilities and fixtures. | -| [`@blobscan/zod`](https://github.com/Blobscan/blobscan/tree/main/packages/zod) | Shared [Zod](https://zod.dev) schemas and utilities. | -| [`@blobscan/eth-format`](https://github.com/Blobscan/blobscan/tree/main/packages/eth-format) | Provides utility functions for handling Ethereum value conversions and formatting. | -| [`@blobscan/rollups`](https://github.com/Blobscan/blobscan/tree/main/packages/rollups) | A utility that provides a comprehensive list of all rollups and their associated addresses supported by Blobscan, along with functions to retrieve them easily. | +| [`@blobscan/dayjs`](https://github.com/Blobscan/blobscan/tree/main/packages/dayjs) |  Extended [Day.js](https://day.js.org/) with plugins. | +| [`@blobscan/db`](https://github.com/Blobscan/blobscan/tree/main/packages/db) | Prisma schema and a Prisma client with [extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions) containing custom methods queries. | +| [`@blobscan/logger`](https://github.com/Blobscan/blobscan/tree/main/packages/logger) | Shared logger utilities. | +| [`@blobscan/optimism-decoder`](https://github.com/Blobscan/blobscan/tree/main/packages/optimism-decoder) | Optimism blobs decoder. | +| [`@blobscan/open-telemetry`](https://github.com/Blobscan/blobscan/tree/main/packages/open-telemetry) | [Otel](https://opentelemetry.io/) configuration and sdk setup. | +| [`@blobscan/test`](https://github.com/Blobscan/blobscan/tree/main/packages/test) | Shared test utilities and fixtures. | +| [`@blobscan/zod`](https://github.com/Blobscan/blobscan/tree/main/packages/zod) | Shared [Zod](https://zod.dev) schemas and utilities. | +| [`@blobscan/network-blob-config`](https://github.com/Blobscan/blobscan/tree/main/packages/eth-config) | Provides network blob-related fork configuration parameters. | +| [`@blobscan/eth-format`](https://github.com/Blobscan/blobscan/tree/main/packages/eth-format) | Provides utility functions for handling Ethereum value conversions and formatting. | +| [`@blobscan/rollups`](https://github.com/Blobscan/blobscan/tree/main/packages/rollups) | A utility that provides a comprehensive list of all rollups and their associated addresses supported by Blobscan, along with functions to retrieve them easily. | ### Tooling diff --git a/apps/web/package.json b/apps/web/package.json index 8ec3373ad..3619f560d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -20,7 +20,9 @@ "@blobscan/dates": "workspace:*", "@blobscan/dayjs": "workspace:^0.1.0", "@blobscan/db": "workspace:^0.14.0", + "@blobscan/env": "workspace:^0.1.0", "@blobscan/eth-format": "workspace:^0.1.0", + "@blobscan/network-blob-config": "workspace:^0.1.0", "@blobscan/open-telemetry": "workspace:^0.0.9", "@blobscan/rollups": "workspace:^0.2.3", "@floating-ui/react": "^0.26.23", diff --git a/apps/web/src/components/Displays/BlobGasUsageDisplay.tsx b/apps/web/src/components/Displays/BlobGasUsageDisplay.tsx index 23622dc3f..7cabf434b 100644 --- a/apps/web/src/components/Displays/BlobGasUsageDisplay.tsx +++ b/apps/web/src/components/Displays/BlobGasUsageDisplay.tsx @@ -1,40 +1,63 @@ import type { FC } from "react"; import cn from "classnames"; -import { - BLOB_GAS_LIMIT_PER_BLOCK, - TARGET_BLOB_GAS_PER_BLOCK, - calculateBlobGasTarget, - calculatePercentage, - formatNumber, -} from "~/utils"; +import type { NetworkBlobConfig } from "@blobscan/network-blob-config"; + +import { calculatePercentage, formatNumber, performDiv } from "~/utils"; import { PercentageBar } from "../PercentageBar"; type BlobGasUsageDisplayProps = { + networkBlobConfig: NetworkBlobConfig; blobGasUsed: bigint; compact?: boolean; }; -function getTargetSign(blobGasUsed: bigint): "+" | "-" | undefined { - if (blobGasUsed < TARGET_BLOB_GAS_PER_BLOCK) { +function getTargetSign( + blobGasUsed: bigint, + targetBlobGasPerBlock: bigint +): "+" | "-" | undefined { + if (blobGasUsed < targetBlobGasPerBlock) { return "-"; - } else if (blobGasUsed > TARGET_BLOB_GAS_PER_BLOCK) { + } + + if (blobGasUsed > targetBlobGasPerBlock) { return "+"; - } else { - return; } } +function calculateBlobGasTarget( + blobGasUsed: bigint, + targetBlobsPerBlock: number, + gasPerBlob: bigint +) { + const blobsInBlock = performDiv(blobGasUsed, gasPerBlob); + + return calculatePercentage( + blobsInBlock < targetBlobsPerBlock + ? blobsInBlock + : blobsInBlock - targetBlobsPerBlock, + targetBlobsPerBlock + ); +} + export const BlobGasUsageDisplay: FC = function ({ + networkBlobConfig, blobGasUsed, compact = false, }) { - const blobGasUsedPercentage = calculatePercentage( + const { + gasPerBlob, + blobGasLimit, + targetBlobsPerBlock, + targetBlobGasPerBlock, + } = networkBlobConfig; + const blobGasUsedPercentage = calculatePercentage(blobGasUsed, blobGasLimit); + const blobGasTarget = calculateBlobGasTarget( blobGasUsed, - BigInt(BLOB_GAS_LIMIT_PER_BLOCK) + targetBlobsPerBlock, + gasPerBlob ); - const blobGasTarget = calculateBlobGasTarget(blobGasUsed); - const targetSign = getTargetSign(blobGasUsed); + const targetSign = getTargetSign(blobGasUsed, targetBlobGasPerBlock); const isPositive = targetSign === "+"; const isNegative = targetSign === "-"; diff --git a/apps/web/src/pages/block/[id].tsx b/apps/web/src/pages/block/[id].tsx index 7797bb1e0..ba0dba5ee 100644 --- a/apps/web/src/pages/block/[id].tsx +++ b/apps/web/src/pages/block/[id].tsx @@ -3,6 +3,8 @@ import type { NextPage } from "next"; import { useRouter } from "next/router"; import type { NextRouter } from "next/router"; +import { getNetworkBlobConfigBySlot } from "@blobscan/network-blob-config"; + import { Card } from "~/components/Cards/Card"; import { BlobTransactionCard } from "~/components/Cards/SurfaceCards/BlobTransactionCard"; import { Copyable } from "~/components/Copyable"; @@ -19,16 +21,12 @@ import NextError from "~/pages/_error"; import { useEnv } from "~/providers/Env"; import type { BlockWithExpandedBlobsAndTransactions } from "~/types"; import { - BLOB_GAS_LIMIT_PER_BLOCK, deserializeFullBlock, formatBytes, formatNumber, formatTimestamp, - GAS_PER_BLOB, - MAX_BLOBS_PER_BLOCK, performDiv, pluralize, - TARGET_BLOB_GAS_PER_BLOCK, } from "~/utils"; function performBlockQuery(router: NextRouter) { @@ -58,154 +56,6 @@ const Block: NextPage = function () { const { env } = useEnv(); const networkName = env ? env.PUBLIC_NETWORK_NAME : undefined; - const detailsFields: DetailsLayoutProps["fields"] | undefined = - useMemo(() => { - if (blockData) { - const totalBlockBlobSize = blockData?.transactions.reduce( - (acc, { blobs }) => { - const totalBlobsSize = blobs.reduce( - (blobAcc, { size }) => blobAcc + size, - 0 - ); - - return acc + totalBlobsSize; - }, - 0 - ); - - const firstBlobNumber = networkName - ? getFirstBlobNumber(networkName) - : undefined; - - const previousBlockHref = - firstBlobNumber && blockNumber && firstBlobNumber < blockNumber - ? `/block_neighbor?blockNumber=${blockNumber}&direction=prev` - : undefined; - - return [ - { - name: "Block Height", - helpText: - "Also referred to as the Block Number, the block height represents the length of the blockchain and increases with each newly added block.", - value: ( -
- {blockData.number} - {blockNumber !== undefined && previousBlockHref && ( - - )} -
- ), - }, - { - name: "Status", - helpText: "The finality status of the block.", - value: , - }, - { - name: "Hash", - helpText: "The hash of the block header.", - value: , - }, - { - name: "Timestamp", - helpText: "The time at which the block was created.", - value: ( -
- {formatTimestamp(blockData.timestamp)} -
- ), - }, - { - name: "Slot", - helpText: "The slot number of the block.", - value: ( - - {blockData.slot} - - ), - }, - { - name: "Blob size", - helpText: "Total amount of space used for blobs in this block.", - value: ( -
- {formatBytes(totalBlockBlobSize)} - - ({formatNumber(totalBlockBlobSize / GAS_PER_BLOB)}{" "} - {pluralize("blob", totalBlockBlobSize / GAS_PER_BLOB)}) - -
- ), - }, - { - name: "Blob Gas Price", - helpText: - "The cost per unit of blob gas used by the blobs in this block.", - value: , - }, - { - name: "Blob Gas Used", - helpText: `The total blob gas used by the blobs in this block, along with its percentage relative to both the total blob gas limit and the blob gas target (${( - TARGET_BLOB_GAS_PER_BLOCK / 1024 - ).toFixed(0)} KB).`, - value: , - }, - { - name: "Blob Gas Limit", - helpText: "The maximum blob gas limit for this block.", - value: ( -
- {formatNumber(BLOB_GAS_LIMIT_PER_BLOCK)} - - ({formatNumber(MAX_BLOBS_PER_BLOCK)}{" "} - {pluralize("blob", MAX_BLOBS_PER_BLOCK)} per block) - -
- ), - }, - { - name: "Blob As Calldata Gas", - helpText: - "The total gas that would have been used in this block if the blobs were sent as calldata.", - value: ( -
- {formatNumber(blockData.blobAsCalldataGasUsed)} - - ( - - {formatNumber( - performDiv( - blockData.blobAsCalldataGasUsed, - blockData.blobGasUsed - ), - "standard", - { maximumFractionDigits: 2 } - )} - {" "} - times more expensive) - -
- ), - }, - ]; - } - }, [blockData, networkName, latestBlock, blockNumber, env]); - if (error) { return ( Block not found; } + let detailsFields: DetailsLayoutProps["fields"] | undefined; + + if (blockData && env) { + const networkBlobConfig = getNetworkBlobConfigBySlot( + env.PUBLIC_NETWORK_NAME, + blockData.slot + ); + const { + bytesPerFieldElement, + fieldElementsPerBlob, + blobGasLimit, + maxBlobsPerBlock, + targetBlobGasPerBlock, + } = networkBlobConfig; + const blobSize = bytesPerFieldElement * fieldElementsPerBlob; + + const totalBlockBlobSize = blockData?.transactions.reduce( + (acc, { blobs }) => { + const totalBlobsSize = blobs.reduce( + (blobAcc, { size }) => blobAcc + size, + 0 + ); + + return acc + totalBlobsSize; + }, + 0 + ); + + const firstBlobNumber = networkName + ? getFirstBlobNumber(networkName) + : undefined; + + const previousBlockHref = + firstBlobNumber && blockNumber && firstBlobNumber < blockNumber + ? `/block_neighbor?blockNumber=${blockNumber}&direction=prev` + : undefined; + + detailsFields = [ + { + name: "Block Height", + helpText: + "Also referred to as the Block Number, the block height represents the length of the blockchain and increases with each newly added block.", + value: ( +
+ {blockData.number} + {!!blockNumber && previousBlockHref && ( + + )} +
+ ), + }, + { + name: "Status", + helpText: "The finality status of the block.", + value: , + }, + { + name: "Hash", + helpText: "The hash of the block header.", + value: , + }, + { + name: "Timestamp", + helpText: "The time at which the block was created.", + value: ( +
+ {formatTimestamp(blockData.timestamp)} +
+ ), + }, + { + name: "Slot", + helpText: "The slot number of the block.", + value: ( + + {blockData.slot} + + ), + }, + { + name: "Blob size", + helpText: "Total amount of space used for blobs in this block.", + value: ( +
+ {formatBytes(totalBlockBlobSize)} + + ({formatNumber(totalBlockBlobSize / blobSize)}{" "} + {pluralize("blob", totalBlockBlobSize / blobSize)}) + +
+ ), + }, + { + name: "Blob Gas Price", + helpText: + "The cost per unit of blob gas used by the blobs in this block.", + value: , + }, + { + name: "Blob Gas Used", + helpText: `The total blob gas used by the blobs in this block, along with its percentage relative to both the total blob gas limit and the blob gas target (${ + targetBlobGasPerBlock / BigInt(1024) + } KB).`, + value: ( + + ), + }, + { + name: "Blob Gas Limit", + helpText: "The maximum blob gas limit for this block.", + value: ( +
+ {formatNumber(blobGasLimit)} + + ({formatNumber(maxBlobsPerBlock)}{" "} + {pluralize("blob", maxBlobsPerBlock)} per block) + +
+ ), + }, + { + name: "Blob As Calldata Gas", + helpText: + "The total gas that would have been used in this block if the blobs were sent as calldata.", + value: ( +
+ {formatNumber(blockData.blobAsCalldataGasUsed)} + + ( + + {formatNumber( + performDiv( + blockData.blobAsCalldataGasUsed, + blockData.blobGasUsed + ), + "standard", + { maximumFractionDigits: 2 } + )} + {" "} + times more expensive) + +
+ ), + }, + ]; + } + return ( <> { - return blocks - ? blocks.map((block: DeserializedBlock) => { - const { - blobGasPrice, - blobGasUsed, - number, - slot, - timestamp, - transactions, - } = block; - const blobCount = transactions?.reduce( - (acc, tx) => acc + tx.blobs.length, - 0 - ); + if (!blocks || !env) { + return; + } + + return blocks.map((block: DeserializedBlock) => { + const { + blobGasPrice, + blobGasUsed, + number, + slot, + timestamp, + transactions, + } = block; + const blobCount = transactions?.reduce( + (acc, tx) => acc + tx.blobs.length, + 0 + ); - const getBlocksTableRowExpandItem = ({ - transactions, - }: DeserializedBlock) => { - const headers = [ + const getBlocksTableRowExpandItem = ({ + transactions, + }: DeserializedBlock) => { + const headers = [ + { + cells: [ + { item: "" }, { - cells: [ - { item: "" }, - { - item: "Tx Hash", - }, - { - item: "Blob Versioned Hash", - }, - ], - className: "dark:border-border-dark/20", - sticky: true, + item: "Tx Hash", }, - ]; - - const transactionsCombinedWithInnerBlobs = transactions.flatMap( - (transaction) => - transaction.blobs.map((blob) => ({ - transactionHash: transaction.hash, - blobVersionedHash: blob.versionedHash, - category: transaction.category, - rollup: transaction.rollup, - })) - ); - - const rows = transactionsCombinedWithInnerBlobs.map( - ({ transactionHash, blobVersionedHash, rollup, category }) => ({ - cells: [ - { - item: - category === "rollup" && rollup ? ( - - ) : ( - <> - ), - }, - { - item: ( - - - {transactionHash} - - - ), - }, - { - item: ( - - - {blobVersionedHash} - - - ), - }, - ], - }) - ); + { + item: "Blob Versioned Hash", + }, + ], + className: "dark:border-border-dark/20", + sticky: true, + }, + ]; - return ( - - ); - }; + const transactionsCombinedWithInnerBlobs = transactions.flatMap( + (transaction) => + transaction.blobs.map((blob) => ({ + transactionHash: transaction.hash, + blobVersionedHash: blob.versionedHash, + category: transaction.category, + rollup: transaction.rollup, + })) + ); - return { + const rows = transactionsCombinedWithInnerBlobs.map( + ({ transactionHash, blobVersionedHash, rollup, category }) => ({ cells: [ { - item: ( -
- {[...new Set(transactions.map((tx) => tx.rollup))].map( - (rollup, i) => { - return rollup ? ( -
- -
- ) : ( - <> - ); - } - )} -
- ), + item: + category === "rollup" && rollup ? ( + + ) : ( + <> + ), }, { item: ( - {number} + + {transactionHash} + ), }, - { - item: - timeFormat === "relative" - ? timestamp.fromNow() - : timestamp.format("YYYY-MM-DD HH:mm:ss"), - }, { item: ( - - {slot} - - ), - }, - { - item: ( - - {transactions.length} - - ), - }, - { - item: ( - - {blobCount} - + + {blobVersionedHash} + + ), }, - { - item: , - }, - { - item: , - }, ], - expandItem: getBlocksTableRowExpandItem(block), - }; - }) - : undefined; + }) + ); + + return ( +
+ ); + }; + + return { + cells: [ + { + item: ( +
+ {[...new Set(transactions.map((tx) => tx.rollup))].map( + (rollup, i) => { + return rollup ? ( +
+ +
+ ) : ( + <> + ); + } + )} +
+ ), + }, + { + item: ( + + {number} + + ), + }, + { + item: + timeFormat === "relative" + ? timestamp.fromNow() + : timestamp.format("YYYY-MM-DD HH:mm:ss"), + }, + { + item: ( + + {slot} + + ), + }, + { + item: ( + + {transactions.length} + + ), + }, + { + item: ( + + {blobCount} + + ), + }, + { + item: , + }, + { + item: ( + + ), + }, + ], + expandItem: getBlocksTableRowExpandItem(block), + }; + }); }, [blocks, timeFormat, env]); if (error) { diff --git a/apps/web/src/pages/tx/[hash].tsx b/apps/web/src/pages/tx/[hash].tsx index bb8bebf5c..a98524c32 100644 --- a/apps/web/src/pages/tx/[hash].tsx +++ b/apps/web/src/pages/tx/[hash].tsx @@ -250,7 +250,9 @@ const Tx: NextPage = () => { /> } - externalLink={tx ? `${env}` : undefined} + externalLink={ + tx ? `${env?.PUBLIC_EXPLORER_BASE_URL}/tx/${tx.hash}` : undefined + } fields={detailsFields} /> diff --git a/apps/web/src/utils/ethereum.ts b/apps/web/src/utils/ethereum.ts deleted file mode 100644 index 0d020626f..000000000 --- a/apps/web/src/utils/ethereum.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { calculatePercentage, numberToBigInt, performDiv } from "./number"; - -export const GAS_PER_BLOB = 131_072; // 2 ** 17 -export const BLOB_SIZE = GAS_PER_BLOB; -export const TARGET_BLOB_GAS_PER_BLOCK = 393_216; -export const TARGET_BLOBS_PER_BLOCK = TARGET_BLOB_GAS_PER_BLOCK / GAS_PER_BLOB; -export const BLOB_GAS_LIMIT_PER_BLOCK = 786_432; -export const MAX_BLOBS_PER_BLOCK = BLOB_GAS_LIMIT_PER_BLOCK / GAS_PER_BLOB; - -export function calculateBlobGasTarget(blobGasUsed: bigint) { - const blobsInBlock = performDiv(blobGasUsed, BigInt(GAS_PER_BLOB)); - - if (blobsInBlock < TARGET_BLOBS_PER_BLOCK) { - return calculatePercentage( - numberToBigInt(blobsInBlock), - numberToBigInt(TARGET_BLOBS_PER_BLOCK) - ); - } - - return calculatePercentage( - numberToBigInt(blobsInBlock - TARGET_BLOBS_PER_BLOCK), - numberToBigInt(TARGET_BLOBS_PER_BLOCK) - ); -} - -// This function shortens an Ethereum address by removing characters from the middle. -export function shortenAddress(address: string, length = 4): string { - return `${address.slice(0, length)}…${address.slice(-length)}`; -} - -export function getChainIdByName(chainName: string): number | undefined { - switch (chainName) { - case "mainnet": - return 1; - case "holesky": - return 17000; - case "sepolia": - return 11155111; - case "gnosis": - return 100; - } -} diff --git a/apps/web/src/utils/index.ts b/apps/web/src/utils/index.ts index f43ba4fae..c475e195f 100644 --- a/apps/web/src/utils/index.ts +++ b/apps/web/src/utils/index.ts @@ -2,7 +2,7 @@ export * from "./bytes"; export * from "./charts"; export * from "./blob-decoders"; export * from "./deserializers"; -export * from "./ethereum"; +export * from "./web3"; export * from "./date"; export * from "./routes"; export * from "./search"; diff --git a/apps/web/src/utils/number.ts b/apps/web/src/utils/number.ts index 90020eb87..8fa25785e 100644 --- a/apps/web/src/utils/number.ts +++ b/apps/web/src/utils/number.ts @@ -1,6 +1,6 @@ type FormatMode = "compact" | "standard"; -export function numberToBigInt(value: number): bigint { +export function toBigInt(value: number): bigint { if (Number.isNaN(value) || !Number.isFinite(value)) { return BigInt(0); } @@ -75,21 +75,27 @@ export function parseDecimalNumber(value: string) { } export function calculatePercentage( - numerator: bigint, - denominator: bigint, + numerator: number | bigint, + denominator: number | bigint, opts?: Partial<{ returnComplement: boolean }> ): number { - if (denominator === BigInt(0)) { + if (denominator === 0 || denominator === BigInt(0)) { return 0; } - const pct = performDiv(numerator, denominator) * 100; + let pct: number; - if (opts?.returnComplement) { - return 100 - pct; + if (typeof numerator === "number" && typeof denominator === "number") { + // Perform normal division for numbers + pct = (numerator / denominator) * 100; + } else { + // Convert both to BigInt and perform division + const num = BigInt(numerator); + const den = BigInt(denominator); + pct = Number((num * BigInt(100)) / den); // Convert back to number after computation } - return pct; + return opts?.returnComplement ? 100 - pct : pct; } function isNumberArray(arr: unknown[]): arr is number[] { diff --git a/apps/web/src/utils/web3.ts b/apps/web/src/utils/web3.ts new file mode 100644 index 000000000..23829ae69 --- /dev/null +++ b/apps/web/src/utils/web3.ts @@ -0,0 +1,17 @@ +// This function shortens an Ethereum address by removing characters from the middle. +export function shortenAddress(address: string, length = 4): string { + return `${address.slice(0, length)}…${address.slice(-length)}`; +} + +export function getChainIdByName(chainName: string): number | undefined { + switch (chainName) { + case "mainnet": + return 1; + case "holesky": + return 17000; + case "sepolia": + return 11155111; + case "gnosis": + return 100; + } +} diff --git a/packages/api/package.json b/packages/api/package.json index 27681f6ba..37a31100c 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -21,6 +21,7 @@ "@blobscan/db": "workspace:^0.14.0", "@blobscan/env": "workspace:^0.1.0", "@blobscan/logger": "workspace:^0.1.2", + "@blobscan/network-blob-config": "workspace:^0.1.0", "@blobscan/open-telemetry": "workspace:^0.0.9", "@blobscan/rollups": "workspace:^0.2.3", "@blobscan/zod": "workspace:^0.1.0", diff --git a/packages/api/src/routers/indexer/indexData.utils.ts b/packages/api/src/routers/indexer/indexData.utils.ts index 4ef7694ba..472cd0bad 100644 --- a/packages/api/src/routers/indexer/indexData.utils.ts +++ b/packages/api/src/routers/indexer/indexData.utils.ts @@ -10,14 +10,11 @@ import type { import { Prisma } from "@blobscan/db"; import { Category } from "@blobscan/db/prisma/enums"; import { env } from "@blobscan/env"; +import { getNetworkBlobConfigBySlot } from "@blobscan/network-blob-config"; import { getRollupByAddress } from "@blobscan/rollups"; import type { IndexDataFormattedInput } from "./indexData"; -const MIN_BLOB_BASE_FEE = BigInt(1); -const BLOB_BASE_FEE_UPDATE_FRACTION = BigInt(3_338_477); -const BLOB_GAS_PER_BLOB = BigInt(131_072); - function bigIntToDecimal(bigint: bigint) { return new Prisma.Decimal(bigint.toString()); } @@ -65,13 +62,15 @@ export function calculateBlobSize(blob: string): number { return blob.slice(2).length / 2; } -export function calculateBlobGasPrice(excessBlobGas: bigint): bigint { +export function calculateBlobGasPrice( + slot: number, + excessBlobGas: bigint +): bigint { + const { minBlobBaseFee, blobBaseFeeUpdateFraction } = + getNetworkBlobConfigBySlot(env.CHAIN_ID, slot); + return BigInt( - fakeExponential( - MIN_BLOB_BASE_FEE, - excessBlobGas, - BLOB_BASE_FEE_UPDATE_FRACTION - ) + fakeExponential(minBlobBaseFee, excessBlobGas, blobBaseFeeUpdateFraction) ); } @@ -93,7 +92,14 @@ export function createDBTransactions({ BigInt(0) ); - const blobGasPrice = calculateBlobGasPrice(block.excessBlobGas); + const ethereumConfig = getNetworkBlobConfigBySlot( + env.CHAIN_ID, + block.slot + ); + const blobGasPrice = calculateBlobGasPrice( + block.slot, + block.excessBlobGas + ); const rollup = getRollupByAddress(from, env.CHAIN_ID); const category = rollup ? Category.ROLLUP : Category.OTHER; @@ -107,7 +113,7 @@ export function createDBTransactions({ index, gasPrice: bigIntToDecimal(gasPrice), blobGasUsed: bigIntToDecimal( - BigInt(txBlobs.length) * BLOB_GAS_PER_BLOB + BigInt(txBlobs.length) * ethereumConfig.gasPerBlob ), blobGasPrice: bigIntToDecimal(blobGasPrice), maxFeePerBlobGas: bigIntToDecimal(maxFeePerBlobGas), @@ -130,10 +136,11 @@ export function createDBBlock( (acc, tx) => acc.add(tx.blobAsCalldataGasUsed), new Prisma.Decimal(0) ); + const blobGasPrice = calculateBlobGasPrice(slot, excessBlobGas); - const blobGasPrice = calculateBlobGasPrice(excessBlobGas); return { number, + hash, timestamp: timestampToDate(timestamp), slot, diff --git a/packages/api/test/indexer.test.ts b/packages/api/test/indexer.test.ts index 81296c96b..225b27ca2 100644 --- a/packages/api/test/indexer.test.ts +++ b/packages/api/test/indexer.test.ts @@ -73,6 +73,7 @@ describe("Indexer router", async () => { // 0 // ); const expectedBlobGasPrice = calculateBlobGasPrice( + INPUT.block.slot, BigInt(INPUT.block.excessBlobGas) ); @@ -871,178 +872,4 @@ describe("Indexer router", async () => { unauthorizedRPCCallTest(() => nonAuthorizedCaller.indexer.handleReorg({})); }); - - // describe("handleReorgedSlots", () => { - // describe("when authorized", () => { - // const input: HandleReorgedSlotsInput = { - // reorgedSlots: [106, 107, 108], - // }; - - // it("should mark the transactions contained in the blocks with a slot greater than the new head slot as reorged", async () => { - // const prevTransactionForks = - // await authorizedContext.prisma.transactionFork.findMany(); - // const expectedTransactionForks = fixtures.txs - // .filter(({ blockHash }) => { - // const block = fixtures.blocks.find( - // ({ hash }) => hash === blockHash - // ); - - // return block?.slot && input.reorgedSlots.includes(block.slot); - // }) - // .map(({ hash, blockHash }) => ({ - // hash, - // blockHash, - // })); - - // await authorizedCaller.indexer.handleReorgedSlots(input); - - // const transactionForks = await authorizedContext.prisma.transactionFork - // .findMany() - // .then((txForks) => - // txForks - // .map((txFork) => omitDBTimestampFields(txFork)) - // .filter( - // (txFork) => - // !prevTransactionForks.find( - // (prevTxFork) => prevTxFork.hash === txFork.hash - // ) - // ) - // ); - - // expect(transactionForks).toEqual(expectedTransactionForks); - // }); - - // it("should clean up references to the reorged blocks", async () => { - // const reorgedBlocks = await authorizedContext.prisma.block.findMany({ - // where: { - // slot: { - // in: input.reorgedSlots, - // }, - // }, - // }); - - // const reorgedBlockNumbers = reorgedBlocks.map((block) => block.number); - - // await authorizedCaller.indexer.handleReorgedSlots(input); - - // const reorgedBlocksAddressCategoryInfos = - // await authorizedContext.prisma.addressCategoryInfo.findMany({ - // where: { - // OR: [ - // { - // firstBlockNumberAsSender: { - // in: reorgedBlockNumbers, - // }, - // }, - // { - // firstBlockNumberAsReceiver: { - // in: reorgedBlockNumbers, - // }, - // }, - // ], - // }, - // }); - // const blobsWithReorgedBlocks = - // await authorizedContext.prisma.blob.findMany({ - // where: { - // firstBlockNumber: { - // in: reorgedBlockNumbers, - // }, - // }, - // }); - - // expect( - // reorgedBlocksAddressCategoryInfos, - // "Reorged block references in address category records found" - // ).toEqual([]); - // expect( - // blobsWithReorgedBlocks, - // "Reorged block references in blob records found" - // ).toEqual([]); - // }); - - // it("should return the number of updated slots", async () => { - // const result = await authorizedCaller.indexer.handleReorgedSlots(input); - - // expect(result).toEqual({ - // totalUpdatedSlots: input.reorgedSlots.length, - // }); - // }); - - // it("should ignore non-existent slots and mark the ones that exist as reorged", async () => { - // const reorgedSlots = [106, 107, 99999]; - // const prevTransactionForks = - // await authorizedContext.prisma.transactionFork.findMany(); - // const expectedTransactionForks = fixtures.txs - // .filter(({ blockHash }) => { - // const block = fixtures.blocks.find( - // ({ hash }) => hash === blockHash - // ); - - // return block?.slot && reorgedSlots.includes(block.slot); - // }) - // .map(({ hash, blockHash }) => ({ - // hash, - // blockHash, - // })); - - // const result = await authorizedCaller.indexer.handleReorgedSlots({ - // reorgedSlots: reorgedSlots as [number, ...number[]], - // }); - - // const transactionForks = await authorizedContext.prisma.transactionFork - // .findMany() - // .then((txForks) => - // txForks - // .map((txFork) => omitDBTimestampFields(txFork)) - // .filter( - // (txFork) => - // !prevTransactionForks.find( - // (prevTxFork) => prevTxFork.hash === txFork.hash - // ) - // ) - // ); - - // expect(transactionForks, "Fork transactions mismatch").toEqual( - // expectedTransactionForks - // ); - // expect( - // result.totalUpdatedSlots, - // "Total updated slots mismatch" - // ).toEqual(2); - // }); - // }); - - // it("should not mark any of the provided slots as reorged if all of them are non-existent", async () => { - // const reorgedSlots = [99999, 99998, 99997]; - // const prevTransactionForks = - // await authorizedContext.prisma.transactionFork.findMany(); - - // const result = await authorizedCaller.indexer.handleReorgedSlots({ - // reorgedSlots: reorgedSlots as [number, ...number[]], - // }); - - // const transactionForks = await authorizedContext.prisma.transactionFork - // .findMany() - // .then((txForks) => - // txForks - // .map((txFork) => omitDBTimestampFields(txFork)) - // .filter( - // (txFork) => - // !prevTransactionForks.find( - // (prevTxFork) => prevTxFork.hash === txFork.hash - // ) - // ) - // ); - - // expect(transactionForks, " Fork transactions mismatch").toEqual([]); - // expect(result.totalUpdatedSlots, "Total updated slots mismatch").toEqual( - // 0 - // ); - // }); - - // unauthorizedRPCCallTest(() => - // nonAuthorizedCaller.indexer.handleReorgedSlots({ reorgedSlots: [1000] }) - // ); - // }); }); diff --git a/packages/db/package.json b/packages/db/package.json index 8eff29d23..1096d1253 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -33,6 +33,7 @@ "prisma": "^5.19.1" }, "devDependencies": { + "@blobscan/network-blob-config": "workspace:^0.1.0", "@faker-js/faker": "^8.0.2", "js-sha256": "^0.9.0", "ora": "^8.1.1", diff --git a/packages/db/prisma/seed/DataGenerator.ts b/packages/db/prisma/seed/DataGenerator.ts index b75ed7803..84e26783c 100644 --- a/packages/db/prisma/seed/DataGenerator.ts +++ b/packages/db/prisma/seed/DataGenerator.ts @@ -11,17 +11,19 @@ import { Category, Prisma } from "@prisma/client"; import { sha256 } from "js-sha256"; import dayjs from "@blobscan/dayjs"; +import { FORK_BLOB_CONFIGS } from "@blobscan/network-blob-config"; import { BlobStorage } from "../enums"; import type { SeedParams } from "./params"; import { - BLOB_GAS_PER_BLOB, calculateBlobGasPrice, calculateExcessBlobGas, COMMON_MAX_FEE_PER_BLOB_GAS, ROLLUP_ADDRESSES, } from "./web3"; +const GAS_PER_BLOB = FORK_BLOB_CONFIGS["dencun"].gasPerBlob; + export type FullBlock = Block & { transactions: (Transaction & { blobs: (Blob & { storageRefs: BlobDataStorageReference[] })[]; @@ -85,7 +87,7 @@ export class DataGenerator { }); const proof = faker.string.hexadecimal({ length: 96 }); const versionedHash = `0x01${sha256(commitment).slice(2)}`; - const size = Number(BLOB_GAS_PER_BLOB); + const size = Number(GAS_PER_BLOB); return { commitment, @@ -128,7 +130,7 @@ export class DataGenerator { min: 1, max: 6, }); - const blobGasUsed = BLOB_GAS_PER_BLOB * BigInt(txsCount); + const blobGasUsed = GAS_PER_BLOB * BigInt(txsCount); const excessBlobGas = calculateExcessBlobGas( BigInt(parentBlock.excessBlobGas.toString()), BigInt( @@ -160,7 +162,7 @@ export class DataGenerator { uniqueAddresses: string[] ): Transaction[] { const now = new Date(); - const maxBlobs = Number(block.blobGasUsed) / Number(BLOB_GAS_PER_BLOB); + const maxBlobs = Number(block.blobGasUsed) / Number(GAS_PER_BLOB); const txCount = faker.number.int({ min: 1, @@ -205,9 +207,7 @@ export class DataGenerator { .arrayElement(COMMON_MAX_FEE_PER_BLOB_GAS) .toString(); const extraBlobs = faker.number.int({ min: 0, max: remainingBlobs }); - const blobGasUsed = ( - BigInt(1 + extraBlobs) * BLOB_GAS_PER_BLOB - ).toString(); + const blobGasUsed = (BigInt(1 + extraBlobs) * GAS_PER_BLOB).toString(); remainingBlobs -= extraBlobs; @@ -234,7 +234,7 @@ export class DataGenerator { generateTransactionBlobs(tx: Transaction, prevBlobs: Blob[]): Blob[] { return Array.from({ - length: Number(tx.blobGasUsed) / Number(BLOB_GAS_PER_BLOB), + length: Number(tx.blobGasUsed) / Number(GAS_PER_BLOB), }).map(() => { const isUnique = tx.rollup ? true diff --git a/packages/db/prisma/seed/web3.ts b/packages/db/prisma/seed/web3.ts index f0c17c3ef..a823486f7 100644 --- a/packages/db/prisma/seed/web3.ts +++ b/packages/db/prisma/seed/web3.ts @@ -1,9 +1,6 @@ -import { Rollup } from "../enums"; +import { FORK_BLOB_CONFIGS } from "@blobscan/network-blob-config"; -const MIN_BLOB_BASE_FEE = BigInt(1); -const BLOB_BASE_FEE_UPDATE_FRACTION = BigInt(3_338_477); -export const BLOB_GAS_PER_BLOB = BigInt(131_072); -export const TARGET_BLOB_GAS_PER_BLOCK = BigInt(393216); +import { Rollup } from "../enums"; export const COMMON_MAX_FEE_PER_BLOB_GAS = [ 1000000000, 2, 150000000000, 10, 2000000000, 26000000000, 1, 4000000000, 4, @@ -75,12 +72,11 @@ export function getEIP2028CalldataGas(hexData: string) { } export function calculateBlobGasPrice(excessBlobGas: bigint): bigint { + const { minBlobBaseFee, blobBaseFeeUpdateFraction } = + FORK_BLOB_CONFIGS["pectra"]; + return BigInt( - fakeExponential( - MIN_BLOB_BASE_FEE, - excessBlobGas, - BLOB_BASE_FEE_UPDATE_FRACTION - ) + fakeExponential(minBlobBaseFee, excessBlobGas, blobBaseFeeUpdateFraction) ); } @@ -88,12 +84,14 @@ export function calculateExcessBlobGas( parentExcessBlobGas: bigint, parentBlobGasUsed: bigint ) { + const targetBlobGasPerBlock = + FORK_BLOB_CONFIGS["pectra"].targetBlobGasPerBlock; const excessBlobGas = BigInt(parentExcessBlobGas.toString()); const blobGasUsed = BigInt(parentBlobGasUsed.toString()); - if (excessBlobGas + blobGasUsed < TARGET_BLOB_GAS_PER_BLOCK) { + if (excessBlobGas + blobGasUsed < targetBlobGasPerBlock) { return BigInt(0); } else { - return excessBlobGas + blobGasUsed - TARGET_BLOB_GAS_PER_BLOCK; + return excessBlobGas + blobGasUsed - targetBlobGasPerBlock; } } diff --git a/packages/network-blob-config/index.ts b/packages/network-blob-config/index.ts new file mode 100644 index 000000000..efd4c9678 --- /dev/null +++ b/packages/network-blob-config/index.ts @@ -0,0 +1,89 @@ +export type NetworkFork = "dencun" | "pectra"; + +export type Network = + | "mainnet" + | "holesky" + | "sepolia" + | "gnosis" + | "chiado" + | "devnet"; + +export type NetworkBlobConfig = { + blobBaseFeeUpdateFraction: bigint; + blobGasLimit: bigint; + bytesPerFieldElement: number; + fieldElementsPerBlob: number; + gasPerBlob: bigint; + maxBlobsPerBlock: number; + minBlobBaseFee: bigint; + targetBlobGasPerBlock: bigint; + targetBlobsPerBlock: number; +}; + +const COMMON_NETWORK_BLOB_CONFIG = { + bytesPerFieldElement: 32, + fieldElementsPerBlob: 4096, + gasPerBlob: BigInt(131_072), + blobBaseFeeUpdateFraction: BigInt(3_338_477), + minBlobBaseFee: BigInt(1), +}; + +export const FORK_BLOB_CONFIGS: Record = { + dencun: { + ...COMMON_NETWORK_BLOB_CONFIG, + targetBlobsPerBlock: 3, + maxBlobsPerBlock: 6, + targetBlobGasPerBlock: BigInt(393_216), + blobGasLimit: BigInt(786_432), + }, + pectra: { + ...COMMON_NETWORK_BLOB_CONFIG, + targetBlobsPerBlock: 6, + maxBlobsPerBlock: 9, + targetBlobGasPerBlock: BigInt(786_432), + blobGasLimit: BigInt(1_179_648), + }, +}; + +export function getNetworkForkBySlot( + network: Network, + slot: number +): NetworkFork { + switch (network) { + case "holesky": { + return slot >= 3710976 ? "pectra" : "dencun"; + } + case "sepolia": { + return slot >= 7118848 ? "pectra" : "dencun"; + } + default: + return "dencun"; + } +} + +function getNetworkNameById(networkId: number): Network { + switch (networkId) { + case 1: + return "mainnet"; + case 17000: + return "holesky"; + case 11155111: + return "sepolia"; + case 100: + return "gnosis"; + default: + return "devnet"; + } +} +export function getNetworkBlobConfigBySlot( + networkNameOrId: Network | number, + slot: number +): NetworkBlobConfig { + const network = + typeof networkNameOrId === "number" + ? getNetworkNameById(networkNameOrId) + : networkNameOrId; + const upgrade = getNetworkForkBySlot(network, slot); + + return FORK_BLOB_CONFIGS[upgrade]; +} diff --git a/packages/network-blob-config/package.json b/packages/network-blob-config/package.json new file mode 100644 index 000000000..f65d1c4b8 --- /dev/null +++ b/packages/network-blob-config/package.json @@ -0,0 +1,19 @@ +{ + "name": "@blobscan/network-blob-config", + "version": "0.1.0", + "private": true, + "main": "./index.ts", + "types": "./index.ts", + "license": "MIT", + "scripts": { + "lint": "eslint .", + "lint:fix": "pnpm lint --fix", + "type-check": "tsc --noEmit" + }, + "eslintConfig": { + "root": true, + "extends": [ + "@blobscan/eslint-config/base" + ] + } +} diff --git a/packages/network-blob-config/tsconfig.json b/packages/network-blob-config/tsconfig.json new file mode 100644 index 000000000..8aadd571a --- /dev/null +++ b/packages/network-blob-config/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@blobscan/tsconfig/base.production.json", + "include": ["index.ts", "test/**/*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35f9c3757..4b2a91b62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -244,9 +244,15 @@ importers: '@blobscan/db': specifier: workspace:^0.14.0 version: link:../../packages/db + '@blobscan/env': + specifier: workspace:^0.1.0 + version: link:../../packages/env '@blobscan/eth-format': specifier: workspace:^0.1.0 version: link:../../packages/eth-format + '@blobscan/network-blob-config': + specifier: workspace:^0.1.0 + version: link:../../packages/network-blob-config '@blobscan/open-telemetry': specifier: workspace:^0.0.9 version: link:../../packages/open-telemetry @@ -455,6 +461,9 @@ importers: '@blobscan/logger': specifier: workspace:^0.1.2 version: link:../logger + '@blobscan/network-blob-config': + specifier: workspace:^0.1.0 + version: link:../network-blob-config '@blobscan/open-telemetry': specifier: workspace:^0.0.9 version: link:../open-telemetry @@ -603,6 +612,9 @@ importers: specifier: ^5.19.1 version: 5.19.1 devDependencies: + '@blobscan/network-blob-config': + specifier: workspace:^0.1.0 + version: link:../network-blob-config '@faker-js/faker': specifier: ^8.0.2 version: 8.4.1 @@ -636,6 +648,8 @@ importers: specifier: ^3.10.0 version: 3.13.1 + packages/network-blob-config: {} + packages/open-telemetry: dependencies: '@blobscan/env':