diff --git a/docs/egress-node-traffic-analysis.md b/docs/egress-node-traffic-analysis.md index e142607ea..7afe45b50 100644 --- a/docs/egress-node-traffic-analysis.md +++ b/docs/egress-node-traffic-analysis.md @@ -20,7 +20,7 @@ - block size 90,112 bytes (88 KiB) - average transaction size: 1,400 bytes (based on mainnet data) - average transactions per block: ~13.5 (based on mainnet data) -- **131,400 blocks x 90,112 bytes = 11,840,724,480 bytes ~11.84 GiB** +- **131,400 blocks x 90,112 bytes = 11,840,724,480 bytes ~11.03 GiB** > [!Note] > @@ -39,24 +39,24 @@ #### Node traffic ``` -(A) Headers: 131,400 blocks x 1,024 bytes x 20 peers = 2.69 GiB - Bodies: 131,400 blocks x 90,112 bytes x 5 peers = 59.19 GiB - Total: 61.88 GiB +(A) Headers: 131,400 blocks x 1,024 bytes x 20 peers = 2.51 GiB + Bodies: 131,400 blocks x 90,112 bytes x 5 peers = 55.13 GiB + Total: 57.64 GiB -(B) Headers: 131,400 blocks x 1,024 bytes x 35 peers = 4.71 GiB - Bodies: 131,400 blocks x 90,112 bytes x 9 peers = 106.55 GiB - Total: 111.26 GiB +(B) Headers: 131,400 blocks x 1,024 bytes x 35 peers = 4.39 GiB + Bodies: 131,400 blocks x 90,112 bytes x 9 peers = 99.23 GiB + Total: 103.62 GiB ``` - additional traffic from transactions (5-10 GiB?) and consensus (~1-2 GiB?) #### Final total egress per month/ node ``` -(A) Low end: 61.88 GiB + 5 GiB + 1 GiB = 67.88 GiB - High end: 61.88 GiB + 10 GiB + 2 GiB = 73.88 GiB +(A) Low end: 57.64 GiB + 5 GiB + 1 GiB = 63.64 GiB + High end: 57.64 GiB + 10 GiB + 2 GiB = 69.64 GiB -(B) Low end: 111.26 GiB + 5 GiB + 1 GiB = 117.26 GiB - High end: 111.26 GiB + 10 GiB + 2 GiB = 123.26 GiB +(B) Low end: 103.62 GiB + 5 GiB + 1 GiB = 109.62 GiB + High end: 103.62 GiB + 10 GiB + 2 GiB = 115.62 GiB ``` ## Ouroboros Leios @@ -71,7 +71,7 @@ - Body hash: 32 bytes - RB Ref: 32 bytes - Signature: 64 bytes -- Body: 98,304 bytes +- Body: 98,304 bytes (96 KiB) #### Endorsement Blocks (EB) - Header: 240 bytes @@ -87,8 +87,8 @@ - Votes per EB: 600 votes × 1.5 EBs = 900 votes per stage #### Ranking Blocks (RB) -- Header: 1,024 bytes -- Body: 90,112 bytes +- Header: 1,024 bytes (1 KiB) +- Body: 90,112 bytes (88 KiB) ### Monthly Traffic Calculation @@ -100,14 +100,14 @@ #### Example Calculation (1 IB/s, 20 peers, 100% header propagation, 25% body requests) ``` -IB Headers: 2,592,000 seconds × 304 bytes × 20 peers = 15.76 GiB -IB Bodies: 2,592,000 seconds × 98,304 bytes × 5 peers = 1.27 TiB -EB Headers: 194,400 seconds × 240 bytes × 20 peers = 933.12 MiB -EB Bodies: 194,400 seconds × 32 bytes × 20 IBs per stage × 5 peers = 622.08 MiB +IB Headers: 2,592,000 seconds × 304 bytes × 20 peers = 14.69 GiB +IB Bodies: 2,592,000 seconds × 98,304 bytes × 5 peers = 1.18 TiB +EB Headers: 1 IB/s × 240 bytes × 1.5 EBs × 20 peers × 129,600 stages = 869.38 MiB +EB Bodies: 1 IB/s × 32 bytes × 1.5 EBs × 5 peers × 129,600 stages = 579.59 MiB Votes: 194,400 seconds × 150 bytes × 900 votes × 20 peers = 524.88 GiB -RB Headers: 129,600 seconds × 1,024 bytes × 20 peers = 2.65 GiB -RB Bodies: 129,600 seconds × 90,112 bytes × 5 peers = 58.39 GiB -Total: 1.88 TiB +RB Headers: 129,600 seconds × 1,024 bytes × 20 peers = 2.47 GiB +RB Bodies: 129,600 seconds × 90,112 bytes × 5 peers = 54.43 GiB +Total: 1.75 TiB Note: - IB traffic dominates due to larger body size (98,304 bytes = 96 KiB) @@ -120,11 +120,11 @@ Note: | IB/s | IB Headers | IB Bodies | EB Headers | EB Bodies | Votes | RB Headers | RB Bodies | Total | vs Praos (A) | |------|------------|-----------|------------|-----------|-------|------------|-----------|-------|--------------| -| 1 | 15.76 GB | 1.27 TB | 933.12 MB | 622.08 MB | 524.88 GB | 2.65 GB | 58.39 GB | 1.88 TB | +2,670% | -| 5 | 78.80 GB | 6.37 TB | 933.12 MB | 3.11 GB | 2.62 TB | 2.65 GB | 58.39 GB | 9.07 TB | +13,260% | -| 10 | 157.59 GB | 12.74 TB | 933.12 MB | 6.22 GB | 5.25 TB | 2.65 GB | 58.39 GB | 18.11 TB | +26,590% | -| 20 | 315.19 GB | 25.48 TB | 933.12 MB | 12.44 GB | 10.50 TB | 2.65 GB | 58.39 GB | 36.19 TB | +53,130% | -| 30 | 472.78 GB | 38.22 TB | 933.12 MB | 18.66 GB | 15.75 TB | 2.65 GB | 58.39 GB | 54.28 TB | +79,670% | +| 1 | 14.69 GiB | 1.18 TiB | 869.38 MiB | 579.59 MiB | 524.88 GiB | 2.47 GiB | 54.43 GiB | 1.75 TiB | +2,670% | +| 5 | 73.45 GiB | 5.90 TiB | 869.38 MiB | 2.90 GiB | 2.62 TiB | 2.47 GiB | 54.43 GiB | 8.52 TiB | +13,260% | +| 10 | 146.90 GiB | 11.80 TiB | 869.38 MiB | 5.80 GiB | 5.25 TiB | 2.47 GiB | 54.43 GiB | 17.03 TiB | +26,590% | +| 20 | 293.80 GiB | 23.60 TiB | 869.38 MiB | 11.60 GiB | 10.50 TiB | 2.47 GiB | 54.43 GiB | 34.06 TiB | +53,130% | +| 30 | 440.70 GiB | 35.40 TiB | 869.38 MiB | 17.40 GiB | 15.75 TiB | 2.47 GiB | 54.43 GiB | 51.09 TiB | +79,670% | ### Monthly Cost by Cloud Provider ($) @@ -141,7 +141,7 @@ Note: | Hetzner | $0.00108 | 1,024 | $2.07 | $9.80 | $19.50 | $38.92 | $58.35 | +75% | | UpCloud | $0.000 | 1,024–24,576 | $0.00 | $0.00 | $0.00 | $0.00 | $0.00 | 0% | -Note: Percentage increases are calculated against Praos scenario A (20 peers) baseline of 67.88 GiB/month and $7.73/month (using average cost across providers) +Note: Percentage increases are calculated against Praos scenario A (20 peers) baseline of 63.64 GiB/month and $7.73/month (using average cost across providers) ### Data Egress Cost Sources diff --git a/site/docusaurus.config.ts b/site/docusaurus.config.ts index 640658356..228f6e0c4 100644 --- a/site/docusaurus.config.ts +++ b/site/docusaurus.config.ts @@ -102,6 +102,10 @@ const config: Config = { to: "https://leios.cardano-scaling.org/cost-estimator/", label: "Cost Estimator", }, + { + to: "/traffic-estimator", + label: "Traffic Estimator", + }, ], }, { diff --git a/site/src/components/LeiosTrafficCalculator/index.tsx b/site/src/components/LeiosTrafficCalculator/index.tsx new file mode 100644 index 000000000..26690f3af --- /dev/null +++ b/site/src/components/LeiosTrafficCalculator/index.tsx @@ -0,0 +1,887 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import styles from "./styles.module.css"; + +interface CloudProvider { + name: string; + egressCost: number; + freeAllowance: string; +} + +interface SortConfig { + key: string; + direction: "asc" | "desc"; +} + +const cloudProviders: CloudProvider[] = [ + { name: "Google Cloud", egressCost: 0.120, freeAllowance: "0" }, + { name: "Railway", egressCost: 0.100, freeAllowance: "0" }, + { name: "AWS", egressCost: 0.090, freeAllowance: "100" }, + { name: "Microsoft Azure", egressCost: 0.087, freeAllowance: "100" }, + { name: "Alibaba Cloud", egressCost: 0.074, freeAllowance: "10" }, + { name: "DigitalOcean", egressCost: 0.010, freeAllowance: "100–10,000" }, + { name: "Oracle Cloud", egressCost: 0.0085, freeAllowance: "10,240" }, + { name: "Linode", egressCost: 0.005, freeAllowance: "1,024–20,480" }, + { name: "Hetzner", egressCost: 0.00108, freeAllowance: "1,024" }, + { name: "UpCloud", egressCost: 0, freeAllowance: "1,024–24,576" }, +]; + +const IB_RATES = [1, 5, 10, 20, 30]; +const SECONDS_PER_MONTH = 2_592_000; // 30 days + +interface BlockSizes { + ib: { header: number; body: number }; + eb: { header: number; body: number }; + rb: { header: number; body: number }; + vote: { size: number; countPerPipeline: number }; +} + +const blockSizes: BlockSizes = { + ib: { header: 304, body: 98304 }, + eb: { header: 240, body: 32 }, + rb: { header: 1024, body: 90112 }, + vote: { size: 150, countPerPipeline: 900 }, // 600 votes per pipeline * 1.5 EBs per pipeline +}; + +const TX_FEE_PARAMS = { + a: 0.155381, // Fixed fee in ADA + b: 0.000043946, // Fee per byte in ADA + avgTxSize: 1400, // Average size based on mainnet data (Epoch 500+) +}; + +const LeiosTrafficCalculator: React.FC = () => { + const [numPeers, setNumPeers] = useState(20); + const [headerPropagationPercent, setHeaderPropagationPercent] = useState( + 100, + ); + const [bodyRequestPercent, setBodyRequestPercent] = useState(25); + const [blockUtilizationPercent, setBlockUtilizationPercent] = useState(100); + const [adaToUsd, setAdaToUsd] = useState(0.5); + const [totalNodes, setTotalNodes] = useState(10000); + const [treasuryTaxRate, setTreasuryTaxRate] = useState(20); + const [sortConfig, setSortConfig] = useState({ + key: "egressCost", + direction: "desc", + }); + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + const [isToggleVisible, setIsToggleVisible] = useState(false); + const controlsRef = useRef(null); + + useEffect(() => { + const handleScroll = () => { + if (controlsRef.current) { + const controlsBottom = + controlsRef.current.getBoundingClientRect().bottom; + setIsToggleVisible(controlsBottom < 0); + } + }; + + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + const calculateTraffic = (ibRate: number) => { + const ibCount = ibRate * SECONDS_PER_MONTH; + const stagesPerMonth = SECONDS_PER_MONTH / 20; + const ebRate = 1.5; // EBs per pipeline + const ebCount = stagesPerMonth * ebRate; + const rbCount = 0.05 * SECONDS_PER_MONTH; + + const headerPeers = Math.round( + numPeers * (headerPropagationPercent / 100), + ); + const bodyPeers = Math.round(numPeers * (bodyRequestPercent / 100)); + + const traffic = { + ib: { + headers: ibCount * blockSizes.ib.header * headerPeers, + bodies: ibCount * blockSizes.ib.body * + (blockUtilizationPercent / 100) * bodyPeers, + }, + eb: { + headers: ibRate * blockSizes.eb.header * ebRate * headerPeers * + stagesPerMonth, + bodies: ibRate * blockSizes.eb.body * ebRate * bodyPeers * + stagesPerMonth, + }, + rb: { + headers: rbCount * blockSizes.rb.header * headerPeers, + bodies: rbCount * blockSizes.rb.body * bodyPeers, + }, + votes: ebCount * blockSizes.vote.size * + blockSizes.vote.countPerPipeline * headerPeers, + }; + + const totalTraffic = Object.values(traffic).reduce( + (acc, block) => + acc + (block.headers || 0) + (block.bodies || 0) + + (block.votes || 0), + 0, + ); + + // Calculate transaction fees + const txPerIB = Math.floor( + (blockSizes.ib.body * (blockUtilizationPercent / 100)) / + TX_FEE_PARAMS.avgTxSize, + ); + const totalTxs = ibCount * txPerIB; + // Calculate fee per transaction exactly: a + (b * avgTxSize) + const feePerTx = TX_FEE_PARAMS.a + + (TX_FEE_PARAMS.b * TX_FEE_PARAMS.avgTxSize); + // Calculate total fees without any intermediate rounding + const txFeeADA = totalTxs * feePerTx; + + return { + traffic, + totalTraffic, + txFeeADA, + totalTxs, + }; + }; + + const calculateCost = (trafficGB: number, provider: CloudProvider) => { + const freeAllowance = parseInt(provider.freeAllowance.split("–")[0]); + const billableGB = Math.max(0, trafficGB - freeAllowance); + return billableGB * provider.egressCost; + }; + + const handleSort = (key: string) => { + setSortConfig((prevConfig) => ({ + key, + direction: prevConfig.key === key && prevConfig.direction === "asc" + ? "desc" + : "asc", + })); + }; + + const sortedProviders = useMemo(() => { + return [...cloudProviders].sort((a, b) => { + if (sortConfig.key === "name") { + return sortConfig.direction === "asc" + ? a.name.localeCompare(b.name) + : b.name.localeCompare(a.name); + } + if (sortConfig.key === "egressCost") { + return sortConfig.direction === "asc" + ? a.egressCost - b.egressCost + : b.egressCost - a.egressCost; + } + if (sortConfig.key === "freeAllowance") { + const aAllowance = parseInt(a.freeAllowance.split("–")[0]); + const bAllowance = parseInt(b.freeAllowance.split("–")[0]); + return sortConfig.direction === "asc" + ? aAllowance - bAllowance + : bAllowance - aAllowance; + } + if (sortConfig.key.startsWith("ib_")) { + const rate = parseInt(sortConfig.key.split("_")[1]); + const { totalTraffic: aTotal } = calculateTraffic(rate); + const { totalTraffic: bTotal } = calculateTraffic(rate); + const aCost = calculateCost(aTotal / 1e9, a); + const bCost = calculateCost(bTotal / 1e9, b); + return sortConfig.direction === "asc" + ? aCost - bCost + : bCost - aCost; + } + return 0; + }); + }, [sortConfig]); + + const getSortIcon = (key: string) => { + if (sortConfig.key !== key) return "↕️"; + return sortConfig.direction === "asc" ? "↑" : "↓"; + }; + + const formatTraffic = (bytes: number) => { + if (bytes >= 1024 * 1024 * 1024 * 1024) { + return (bytes / (1024 * 1024 * 1024 * 1024)).toFixed(2) + " TiB"; + } else if (bytes >= 1024 * 1024 * 1024) { + return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GiB"; + } else if (bytes >= 1024 * 1024) { + return (bytes / (1024 * 1024)).toFixed(2) + " MiB"; + } else { + return (bytes / 1024).toFixed(2) + " KiB"; + } + }; + + const formatTrafficForTable = (bytes: number) => { + if (bytes >= 1024 * 1024 * 1024 * 1024) { + return (bytes / (1024 * 1024 * 1024 * 1024)).toFixed(2); + } else if (bytes >= 1024 * 1024 * 1024) { + return (bytes / (1024 * 1024 * 1024)).toFixed(2); + } else if (bytes >= 1024 * 1024) { + return (bytes / (1024 * 1024)).toFixed(2); + } else { + return (bytes / 1024).toFixed(2); + } + }; + + const Controls: React.FC<{ idPrefix?: string }> = ({ idPrefix = "" }) => ( +
+
+

Network Parameters

+
+ + setNumPeers(parseInt(e.target.value))} + min="1" + /> +
+
+ + + setTotalNodes(parseInt(e.target.value))} + min="1" + /> +
+
+ +
+

Block Parameters

+
+ + + setHeaderPropagationPercent( + parseInt(e.target.value), + )} + min="0" + max="100" + /> +
+
+ + + setBodyRequestPercent(parseInt(e.target.value))} + min="0" + max="100" + /> +
+
+ + + setBlockUtilizationPercent( + parseInt(e.target.value), + )} + min="0" + max="100" + /> +
+
+ +
+

Economic Parameters

+
+ + + setAdaToUsd(parseFloat(e.target.value))} + min="0" + step="0.01" + /> +
+
+ + + setTreasuryTaxRate(parseInt(e.target.value))} + min="0" + max="100" + /> +
+
+
+ ); + + return ( +
+
+ +
+ + + +
+
+

Parameters

+ +
+
+ +
+
+ +
+

Calculation Breakdown

+
+
+

General Parameters

+
+
+ Time Period +
+
+ Seconds/Month ={" "} + {SECONDS_PER_MONTH.toLocaleString()} +
+
+ Peer Distribution +
+
+ Header Peers = {numPeers} ×{" "} + {headerPropagationPercent}% = {Math.round( + numPeers * (headerPropagationPercent / 100), + )} + {"\n"}Body Peers = {numPeers} ×{" "} + {bodyRequestPercent}% = {Math.round( + numPeers * (bodyRequestPercent / 100), + )} +
+
+ Transaction Parameters +
+
+ Avg Tx Size = {TX_FEE_PARAMS.avgTxSize} bytes + {"\n"}Base Fee = {TX_FEE_PARAMS.a} ₳ + {"\n"}Fee/Byte = {TX_FEE_PARAMS.b} ₳ + {"\n"}Txs/IB = {Math.floor( + (blockSizes.ib.body * + (blockUtilizationPercent / 100)) / + TX_FEE_PARAMS.avgTxSize, + )} Txs + {"\n"}Fee/Tx = {(TX_FEE_PARAMS.a + + TX_FEE_PARAMS.b * TX_FEE_PARAMS.avgTxSize) + .toFixed(6)} ₳ +
+
+
+
+

Block Timing

+
+
+ Endorsement Blocks (EB) +
+
+ Stage Length = 20 secs{"\n"}Stages/Month ={" "} + {(SECONDS_PER_MONTH / 20).toLocaleString()}{" "} + stages{"\n"}EBs/Stage = 1.5{"\n"}EBs/Month = + {" "} + {((SECONDS_PER_MONTH / 20) * 1.5) + .toLocaleString()} EBs +
+
+ Ranking Blocks (RB) +
+
+ RB/sec = 0.05{"\n"}RBs/month ={" "} + {(0.05 * SECONDS_PER_MONTH).toLocaleString()} + {" "} + RBs +
+
+
+
+

Block Sizes

+
+
+ Input Blocks (IB) +
+
+ Header Size = {blockSizes.ib.header} bytes{" "} + {"\n"}Body Size ={" "} + {blockSizes.ib.body.toLocaleString()} bytes +
+
+ Endorsement Blocks (EB) +
+
+ Header Size = {blockSizes.eb.header} bytes{" "} + {"\n"}Body Size = {blockSizes.eb.body}{" "} + bytes/IB ref +
+
+ Votes +
+
+ Vote Size = {blockSizes.vote.size} bytes{" "} + {"\n"}Votes per Pipeline ={" "} + {blockSizes.vote.countPerPipeline}{" "} + votes (600 votes × 1.5 EBs) +
+
+ Ranking Blocks (RB) +
+
+ Header Size = {blockSizes.rb.header} bytes{" "} + {"\n"}Body Size ={" "} + {blockSizes.rb.body.toLocaleString()} bytes +
+
+
+
+
+

Example calculation for 1 IB/s:

+
    +
  • + IB Headers: {SECONDS_PER_MONTH.toLocaleString()} + {" "} + secs × {blockSizes.ib.header} bytes × {Math.round( + numPeers * (headerPropagationPercent / 100), + )} peers = {formatTraffic( + SECONDS_PER_MONTH * blockSizes.ib.header * + Math.round( + numPeers * + (headerPropagationPercent / 100), + ), + )} +
  • +
  • + IB Bodies: {SECONDS_PER_MONTH.toLocaleString()}{" "} + secs × {blockSizes.ib.body.toLocaleString()} bytes × + {" "} + {blockUtilizationPercent}% utilization ×{" "} + {Math.round(numPeers * (bodyRequestPercent / 100))} + {" "} + peers = {formatTraffic( + SECONDS_PER_MONTH * blockSizes.ib.body * + (blockUtilizationPercent / 100) * + Math.round( + numPeers * (bodyRequestPercent / 100), + ), + )} +
  • +
  • + EB Headers:{" "} + {((SECONDS_PER_MONTH / 20) * 1.5).toLocaleString()} + {" "} + seconds × {blockSizes.eb.header} bytes ×{" "} + {Math.round( + numPeers * (headerPropagationPercent / 100), + )} peers = {formatTraffic( + (SECONDS_PER_MONTH / 20) * 1.5 * + blockSizes.eb.header * + Math.round( + numPeers * + (headerPropagationPercent / 100), + ), + )} +
  • +
  • + EB Bodies:{" "} + {((SECONDS_PER_MONTH / 20) * 1.5).toLocaleString()} + {" "} + seconds × {blockSizes.eb.body} bytes × {20}{" "} + IBs/stage ×{" "} + {Math.round(numPeers * (bodyRequestPercent / 100))} + {" "} + peers = {formatTraffic( + (SECONDS_PER_MONTH / 20) * 1.5 * + blockSizes.eb.body * 20 * + Math.round( + numPeers * (bodyRequestPercent / 100), + ), + )} +
  • +
  • + Votes:{" "} + {((SECONDS_PER_MONTH / 20) * 1.5).toLocaleString()} + {" "} + seconds × {blockSizes.vote.size} bytes ×{" "} + {blockSizes.vote.countPerPipeline} votes ×{" "} + {Math.round( + numPeers * (headerPropagationPercent / 100), + )} peers = {formatTraffic( + (SECONDS_PER_MONTH / 20) * 1.5 * + blockSizes.vote.size * + blockSizes.vote.countPerPipeline * + Math.round( + numPeers * + (headerPropagationPercent / 100), + ), + )} +
  • +
  • + RB Headers:{" "} + {(0.05 * SECONDS_PER_MONTH).toLocaleString()}{" "} + seconds × {blockSizes.rb.header} bytes ×{" "} + {Math.round( + numPeers * (headerPropagationPercent / 100), + )} peers = {formatTraffic( + 0.05 * SECONDS_PER_MONTH * + blockSizes.rb.header * + Math.round( + numPeers * + (headerPropagationPercent / 100), + ), + )} +
  • +
  • + RB Bodies:{" "} + {(0.05 * SECONDS_PER_MONTH).toLocaleString()}{" "} + seconds × {blockSizes.rb.body.toLocaleString()}{" "} + bytes ×{" "} + {Math.round(numPeers * (bodyRequestPercent / 100))} + {" "} + peers = {formatTraffic( + 0.05 * SECONDS_PER_MONTH * blockSizes.rb.body * + Math.round( + numPeers * (bodyRequestPercent / 100), + ), + )} +
  • +
  • + Transactions per IB:{" "} + {blockSizes.ib.body.toLocaleString()} bytes ×{" "} + {blockUtilizationPercent}% utilization ÷{" "} + {TX_FEE_PARAMS.avgTxSize} bytes/tx = {Math.floor( + (blockSizes.ib.body * + (blockUtilizationPercent / 100)) / + TX_FEE_PARAMS.avgTxSize, + )} txs +
  • +
  • + Monthly Transactions: {Math.floor( + (blockSizes.ib.body * + (blockUtilizationPercent / 100)) / + TX_FEE_PARAMS.avgTxSize, + )} txs/block × {SECONDS_PER_MONTH.toLocaleString()} + {" "} + blocks = {(SECONDS_PER_MONTH * + Math.floor( + (blockSizes.ib.body * + (blockUtilizationPercent / 100)) / + TX_FEE_PARAMS.avgTxSize, + )).toLocaleString()} txs +
  • +
  • + Fee per Transaction: {TX_FEE_PARAMS.a}{" "} + ₳ + ({TX_FEE_PARAMS.b} ₳/byte ×{" "} + {TX_FEE_PARAMS.avgTxSize} bytes) ={" "} + {(TX_FEE_PARAMS.a + + TX_FEE_PARAMS.b * TX_FEE_PARAMS.avgTxSize) + .toFixed(6)} ₳ +
  • +
  • + Monthly Fee Revenue: {(SECONDS_PER_MONTH * + Math.floor( + (blockSizes.ib.body * + (blockUtilizationPercent / 100)) / + TX_FEE_PARAMS.avgTxSize, + )).toLocaleString()} txs × {(TX_FEE_PARAMS.a + + TX_FEE_PARAMS.b * TX_FEE_PARAMS.avgTxSize) + .toFixed(6)} ₳ = {(SECONDS_PER_MONTH * + Math.floor( + (blockSizes.ib.body * + (blockUtilizationPercent / 100)) / + TX_FEE_PARAMS.avgTxSize, + ) * + (TX_FEE_PARAMS.a + + TX_FEE_PARAMS.b * + TX_FEE_PARAMS.avgTxSize)) + .toLocaleString()} ₳ +
  • +
+
+
+ +

Monthly Traffic and Network Fees

+
+ + + + + + + + + + + + + + + + + {IB_RATES.map((rate) => { + const { + traffic, + totalTraffic, + totalTxs, + } = calculateTraffic(rate); + return ( + + + + + + + + + + + + + ); + })} + +
IB/sIB HeadersIB BodiesEB HeadersEB BodiesVotesRB HeadersRB BodiesTotal TrafficTotal Txs
{rate}{formatTraffic(traffic.ib.headers)}{formatTraffic(traffic.ib.bodies)}{formatTraffic(traffic.eb.headers)}{formatTraffic(traffic.eb.bodies)}{formatTraffic(traffic.votes)}{formatTraffic(traffic.rb.headers)}{formatTraffic(traffic.rb.bodies)}{formatTraffic(totalTraffic)}{totalTxs.toLocaleString()}
+
+
Network Revenue
+
+ Transaction fees shown represent total network revenue + before expenses and taxes. Actual earnings depend on: +
    +
  • Node operator's stake percentage
  • +
  • + SPO's pool configuration (fixed & margin fees) +
  • +
  • Treasury tax (20%)
  • +
  • Other operational costs
  • +
+
+
+
+ +

Monthly Cost by Cloud Provider ($)

+
+ + + + + + + {IB_RATES.map((rate) => ( + + ))} + + + + {sortedProviders.map((provider) => ( + + + + + {IB_RATES.map((rate) => { + const { totalTraffic } = calculateTraffic( + rate, + ); + const egressCost = calculateCost( + totalTraffic / 1e9, + provider, + ); + return ( + + ); + })} + + ))} + +
handleSort("name")} + className={styles.sortable} + > + Provider {getSortIcon("name")} + handleSort("egressCost")} + className={styles.sortable} + > + Price/GiB {getSortIcon("egressCost")} + handleSort("freeAllowance")} + className={styles.sortable} + > + Free Allowance (GiB){" "} + {getSortIcon("freeAllowance")} + handleSort(`ib_${rate}`)} + className={styles.sortable} + > + {rate} IB/s {getSortIcon(`ib_${rate}`)} +
{provider.name}${provider.egressCost.toFixed(3)}{provider.freeAllowance} + ${egressCost.toLocaleString( + undefined, + { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + )} +
+
+
+ Cloud Provider Selection +
+
+ This cost comparison is for informational purposes only. + Beyond cost, consider: +
    +
  • Geographic availability and latency
  • +
  • Service reliability and support
  • +
  • Security and compliance features
  • +
  • Network performance and scalability
  • +
+ Hyperscale providers may offer advantages that justify + higher costs for production operations. +
+
+
+ +

Estimated Monthly Node Income*

+
+ + + + + + + + + + + + + {IB_RATES.map((rate) => { + const { txFeeADA, totalTxs } = calculateTraffic( + rate, + ); + const afterTax = txFeeADA * + (1 - treasuryTaxRate / 100); + const perNode = afterTax / totalNodes; + return ( + + + + + + + + + ); + })} + +
IB/sTotal TxsGross Network Revenue (₳)After Treasury Tax (₳)Per Node (₳)Per Node (USD)
{rate}{totalTxs.toLocaleString()} + {txFeeADA.toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 6, + }).replace(/\.?0+$/, "")} ₳ + + {afterTax.toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 6, + }).replace(/\.?0+$/, "")} ₳ + + {perNode.toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 6, + }).replace(/\.?0+$/, "")} ₳ + + ${(perNode * adaToUsd).toLocaleString( + undefined, + { + minimumFractionDigits: 2, + }, + )} +
+
+
+ Estimated Node Income Disclaimer +
+
+ These estimates are based on several assumptions: +
    +
  • + Equal distribution of rewards across all nodes +
  • +
  • No additional operational costs
  • +
  • No pool fees or other deductions
  • +
  • Constant ADA/USD price
  • +
  • + Network topology: Based on current mainnet + (~3,000 BP nodes), we estimate ~10,000 total + nodes to account for: +
      +
    • BP nodes (behind relays)
    • +
    • + Relay nodes (typically 2+ per BP node) +
    • +
    • + Additional relays for dApps and + infrastructure +
    • +
    • Full node wallets
    • +
    + SPOs can multiply the per-node income by their + total number of nodes (BP + relays) to estimate + their total income. +
  • +
  • + Transaction fee calculation parameters (base fee + and per-byte fee) remain constant, though these + may be adjusted through governance +
  • +
+
+
+
+
+ ); +}; + +export default LeiosTrafficCalculator; diff --git a/site/src/components/LeiosTrafficCalculator/styles.module.css b/site/src/components/LeiosTrafficCalculator/styles.module.css new file mode 100644 index 000000000..d1261ff2e --- /dev/null +++ b/site/src/components/LeiosTrafficCalculator/styles.module.css @@ -0,0 +1,401 @@ +.container { + padding: 2rem; + width: 100%; + max-width: 1200px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + position: relative; + transition: margin-right 0.3s ease; +} + +.container.sidebarOpen { + margin-left: -100px; + width: calc(100% - 200px); +} + +@media (max-width: 768px) { + .container.sidebarOpen { + margin-right: 0; + } +} + +.description { + width: 100%; + margin-bottom: 2rem; + padding: 1rem; + background-color: var(--ifm-color-emphasis-50); + border-radius: 8px; + border: 1px solid var(--ifm-color-emphasis-200); +} + +.description p { + margin: 0; + font-size: 0.95rem; + line-height: 1.5; +} + +.description a { + color: var(--ifm-color-primary); + text-decoration: none; +} + +.description a:hover { + text-decoration: underline; +} + +.controls { + display: flex; + flex-wrap: wrap; + gap: 2rem; + margin-bottom: 2rem; +} + +.controlGroup { + flex: 1; + min-width: 300px; + background-color: #ffffff; + padding: 1.5rem; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06); + border: 1px solid #e1e4e8; +} + +.controlGroup h4 { + margin: 0 0 1rem 0; + color: #24292f; + font-size: 1.1rem; + font-weight: 600; + padding-bottom: 0.5rem; + border-bottom: 1px solid #e1e4e8; +} + +.control { + margin-bottom: 1rem; +} + +.control:last-child { + margin-bottom: 0; +} + +.control label { + display: block; + margin-bottom: 0.5rem; + color: #24292f; + font-size: 0.9rem; + font-weight: 500; +} + +.control input { + width: 100%; + padding: 0.5rem; + border: 1px solid #e1e4e8; + border-radius: 4px; + background: #ffffff; + color: #24292f; + font-size: 0.9rem; +} + +.control input:focus { + outline: none; + border-color: #0969da; + box-shadow: 0 0 0 2px rgba(9, 105, 218, 0.2); +} + +.calculationBreakdown { + width: 100%; + margin: 2rem 0; + padding: 2rem; + background-color: #ffffff; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06); + border: 1px solid #e1e4e8; +} + +.calculationBreakdown h3 { + color: #24292f; + margin-bottom: 1.5rem; + font-weight: 600; + font-size: 1.25rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid #e1e4e8; +} + +.breakdownGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; + margin: 1rem 0; +} + +.calculationSection { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.calculationTitle { + font-weight: 600; + color: #24292f; + font-size: 0.9rem; + margin-top: 0.25rem; +} + +.calculationTitle:first-child { + margin-top: 0; +} + +.calculationCode { + font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; + font-size: 0.85rem; + line-height: 1.6; + color: #24292f; + white-space: pre-wrap; + word-break: break-word; + padding: 0.5rem; + background-color: #ffffff; + border-radius: 4px; + border: 1px solid #e1e4e8; +} + +.breakdownItem { + background-color: #f6f8fa; + padding: 1.25rem; + border-radius: 6px; + border: 1px solid #e1e4e8; +} + +.breakdownItem h4 { + margin-bottom: 0.75rem; + color: #24292f; + font-weight: 600; + font-size: 1.1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid #e1e4e8; +} + +.breakdownItem ul { + margin: 0; + padding-left: 1.5rem; +} + +.breakdownItem ul ul { + margin-top: 0.5rem; + padding-left: 1.5rem; + list-style-type: circle; +} + +.breakdownItem li { + margin: 0.5rem 0; + font-size: 0.9rem; + color: #24292f; +} + +.calculationNote { + margin-top: 2rem; + padding-top: 1.5rem; + border-top: 1px solid #e1e4e8; +} + +.calculationNote p { + font-weight: 500; + margin-bottom: 0.75rem; + color: #24292f; +} + +.calculationNote ul { + margin: 0; + padding-left: 1.5rem; +} + +.calculationNote li { + margin: 0.25rem 0; + font-size: 0.9rem; + color: #24292f; +} + +.tableContainer { + width: 100%; + margin: 1rem 0; + overflow-x: auto; +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; + min-width: 800px; +} + +.table th, +.table td { + padding: 0.75rem; + text-align: right; + border: 1px solid var(--ifm-color-emphasis-300); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.table th:first-child, +.table td:first-child { + text-align: left; + width: 200px; +} + +.table th:nth-child(2), +.table td:nth-child(2) { + width: 100px; +} + +.table th:nth-child(3), +.table td:nth-child(3) { + width: 150px; +} + +.table th { + background-color: #f6f8fa; + font-weight: 500; + color: #24292f; +} + +.sortable { + cursor: pointer; + user-select: none; + position: relative; +} + +.sortable:hover { + background-color: #e1e4e8; +} + +.table tbody tr { + background-color: #ffffff; +} + +.table tbody tr:nth-child(even) { + background-color: #f8f9fa; +} + +.table tbody tr:hover { + background-color: #f1f3f5; +} + +.note { + margin: 1rem 0; + padding: 1rem; + border-left: 4px solid #0969da; + background-color: #f6f8fa; + border-radius: 0 6px 6px 0; +} + +.noteTitle { + font-weight: 600; + margin-bottom: 0.5rem; + color: #0969da; +} + +.noteContent { + margin: 0; + color: #24292f; +} + +.noteContent ul { + margin: 0.5rem 0 0.5rem 1.5rem; + padding: 0; +} + +.noteContent li { + margin: 0.25rem 0; +} + +.sidebarToggle { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 1000; + background: #ffffff; + border: 1px solid #e1e4e8; + border-radius: 4px; + padding: 8px 12px; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + opacity: 0; + transition: opacity 0.3s ease; +} + +.sidebarToggle.visible { + opacity: 1; +} + +.sidebarToggle:hover { + background: #f6f8fa; +} + +.sidebar { + position: fixed; + top: 0; + right: -400px; + width: 400px; + height: 100vh; + background: #ffffff; + box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1); + transition: right 0.3s ease; + z-index: 999; + overflow-y: auto; + padding: 2rem; +} + +.sidebar.open { + right: 0; +} + +.sidebarControls { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.sidebarHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid #e1e4e8; +} + +.sidebarHeader h3 { + margin: 0; + color: #24292f; + font-size: 1.25rem; +} + +.closeButton { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: #24292f; + padding: 4px; + line-height: 1; +} + +.closeButton:hover { + color: #0969da; +} + +@media (max-width: 768px) { + .sidebar { + width: 100%; + right: -100%; + } + + .sidebar.open { + right: 0; + } +} \ No newline at end of file diff --git a/site/src/pages/traffic-estimator/index.tsx b/site/src/pages/traffic-estimator/index.tsx new file mode 100644 index 000000000..2eeda410d --- /dev/null +++ b/site/src/pages/traffic-estimator/index.tsx @@ -0,0 +1,31 @@ +import LeiosTrafficCalculator from "@site/src/components/LeiosTrafficCalculator"; +import Layout from "@theme/Layout"; +import React from "react"; + +export default function TrafficEstimator(): React.ReactElement { + return ( + +
+
+
+

Leios Node Traffic Estimator

+

+ Use this calculator to estimate monthly egress + traffic and costs for running an Ouroboros Leios + node. Based on the default configuration with a + stage length of 20 slots, each stage (pipeline) + generates 1.5 Endorsement Blocks (EBs) that + reference Input Blocks (IBs) from that stage. Adjust + the number of peers and propagation parameters to + see how they affect your monthly traffic. +

+ +
+
+
+
+ ); +}