analytics.js
1 const fs = require('fs'); 2 const axios = require('axios'); 3 const yargs = require('yargs'); 4 5 // Dim text style escape code 6 const dimStart = "\x1b[2m"; 7 const dimEnd = "\x1b[0m"; 8 9 // Function to get the IP address of a given AWS node 10 async function getIPAddress(awsNodeName) { 11 // Read the ~/.ssh/config file 12 const sshConfigFile = fs.readFileSync(`${process.env.HOME}/.ssh/config`, 'utf8'); 13 14 // Use regular expressions to extract the associated IP address 15 const regex = new RegExp(`Host\\s+${awsNodeName}[\\s\\S]*?HostName\\s+(\\S+)`); 16 const match = sshConfigFile.match(regex); 17 18 if (match && match[1]) { 19 return match[1]; 20 } else { 21 console.error(`No IP address found for ${awsNodeName} in ~/.ssh/config`); 22 } 23 } 24 25 // Function to get the count of AWS nodes based on the naming convention aws-nXX in the SSH config file 26 async function getAWSNodeCount() { 27 // Read the ~/.ssh/config file 28 const sshConfigFile = fs.readFileSync(`${process.env.HOME}/.ssh/config`, 'utf8'); 29 30 // Regular expression to match all aws-nXX formats 31 const regex = /Host\s+(aws-n\d+)/g; 32 let match; 33 let highestNumber = -1; 34 35 // Iterate over all matches and find the highest number 36 while ((match = regex.exec(sshConfigFile)) !== null) { 37 const nodeNumber = parseInt(match[1].replace('aws-n', ''), 10); 38 if (nodeNumber > highestNumber) { 39 highestNumber = nodeNumber; 40 } 41 } 42 43 // Return the count of nodes, adding 1 because it starts from 0 44 return highestNumber >= 0 ? highestNumber + 1 : 0; 45 } 46 47 // Function to fetch block data 48 async function fetchBlockData(baseUrl, height) { 49 try { 50 const response = await axios.get(`${baseUrl}/${height}`); 51 return response.data; 52 } catch (error) { 53 console.error(`Error fetching block at height ${height}:`, error.message); 54 return null; 55 } 56 } 57 58 // Function to calculate the average block time 59 async function calculateAverageBlockTime(baseUrl, latestHeight) { 60 let totalBlockTime = 0; 61 let previousTimestamp = 0; 62 63 for (let height = latestHeight; height >= 1; height--) { 64 const blockData = await fetchBlockData(baseUrl, height); 65 if (!blockData) { 66 continue; 67 } 68 69 const timestamp = blockData.header.metadata.timestamp; 70 71 if (timestamp && timestamp > 0) { 72 if (previousTimestamp > 0) { 73 const deltaTimestamp = Math.abs(timestamp - previousTimestamp); 74 // Skip outliers (to account for stopping the devnet and restarting it) 75 if (deltaTimestamp < 500) { 76 console.log(`Block ${height} - ${deltaTimestamp} seconds`); 77 totalBlockTime += deltaTimestamp; 78 } else { 79 console.log(`Block ${height} - ${deltaTimestamp} seconds (skipped)`); 80 } 81 } 82 previousTimestamp = timestamp; 83 } 84 85 // Calculate and log the average block time thus far 86 const blocksProcessedSoFar = latestHeight - height + 1; 87 if (blocksProcessedSoFar > 1) { 88 const averageBlockTimeSoFar = (totalBlockTime / (blocksProcessedSoFar - 1)).toFixed(1); 89 console.log(`${dimStart}Average Block Time Thus Far - ${averageBlockTimeSoFar} seconds${dimEnd}\n`); 90 } 91 92 // Print the current height every 10 blocks 93 if (height % 10 === 0) { 94 console.log(`Processed ${blocksProcessedSoFar} blocks...\n`); 95 } 96 } 97 98 const averageBlockTime = totalBlockTime / (latestHeight - 1); // Subtract 1 for the first block 99 console.log(`Average Block Time: ${averageBlockTime} seconds`); 100 } 101 102 // Function to calculate the number of rounds in each block 103 async function calculateRoundsInBlocks(baseUrl, latestHeight) { 104 for (let height = latestHeight; height >= 1; height--) { 105 const blockData = await fetchBlockData(baseUrl, height); 106 if (!blockData) { 107 continue; 108 } 109 110 // Extract the subdag object and get the number of keys 111 const subdag = blockData?.authority?.subdag?.subdag; 112 const numRounds = subdag ? Object.keys(subdag).length : 0; 113 114 console.log(`Block ${height} - ${numRounds} rounds`); 115 } 116 } 117 118 async function checkBlockHash(networkName, blockHeight) { 119 const numNodes = await getAWSNodeCount(); 120 console.log(`Detected ${numNodes} AWS nodes... \n`); 121 122 for (let i = 0; i < numNodes; i++) { 123 // Define the AWS node name to search for (e.g., aws-n1) 124 const awsNodeName = `aws-n${i}`; 125 // Get the IP address of the AWS node 126 const ipAddress = await getIPAddress(awsNodeName); 127 // Define the base URL for the node 128 const baseUrl = `http://${ipAddress}:3030/${networkName}/block`; 129 130 // Fetch the block data 131 const blockData = await fetchBlockData(baseUrl, blockHeight); 132 if (blockData && blockData.block_hash) { 133 console.log(`${awsNodeName} - Block ${blockHeight} - ${blockData.block_hash}`); 134 } else { 135 console.log(`${awsNodeName} - Block ${blockHeight} - No block hash found`); 136 } 137 } 138 } 139 140 // Main function to fetch block metrics 141 async function fetchBlockMetrics(metricType, optionalBlockHeight, networkID) { 142 // Derive the network name based on the network ID. 143 let networkName; 144 switch (networkID) { 145 case 0: 146 networkName = "mainnet"; 147 break; 148 case 1: 149 networkName = "testnet"; 150 break; 151 case 2: 152 networkName = "canary"; 153 break; 154 default: 155 throw new Error(`Unknown network ID (${networkID})`); 156 } 157 158 // Function to get the latest block height 159 async function getLatestBlockHeight(baseUrl) { 160 try { 161 const response = await axios.get(`${baseUrl}/height/latest`); 162 const latestHeight = response.data; 163 console.log(`${dimStart}Latest Block Height: ${latestHeight}${dimEnd}`); 164 return latestHeight; 165 } catch (error) { 166 console.error('Error fetching latest block height:', error.message); 167 return null; 168 } 169 } 170 171 // Define the AWS node name to search for (e.g., aws-n1) 172 const awsNodeName = 'aws-n1'; 173 // Get the IP address of the AWS node 174 const ipAddress = await getIPAddress(awsNodeName); 175 // Define the base URL for the node. 176 const baseUrl = `http://${ipAddress}:3030/${networkName}/block`; 177 178 console.log(`${dimStart}IP Address: ${ipAddress}${dimEnd}`); 179 console.log(`${dimStart}Base URL: ${baseUrl}${dimEnd}`); 180 181 const latestHeight = await getLatestBlockHeight(baseUrl); 182 if (latestHeight === null) { 183 console.error('Unable to fetch latest block height, try again...'); 184 return; 185 } else { 186 console.log(``); 187 } 188 189 if (metricType === 'averageBlockTime') { 190 calculateAverageBlockTime(baseUrl, latestHeight); 191 } else if (metricType === 'roundsInBlocks') { 192 calculateRoundsInBlocks(baseUrl, latestHeight); 193 } else if (metricType === 'checkBlockHash' && optionalBlockHeight) { 194 checkBlockHash(networkName, optionalBlockHeight); 195 } else { 196 console.error('Invalid metric type. Supported types: "averageBlockTime" or "roundsInBlocks".'); 197 } 198 } 199 200 async function main() { 201 // Define command-line options 202 const argv = yargs 203 .options({ 204 'metric-type': { 205 alias: 'm', 206 describe: 'Metric type to fetch (averageBlockTime, roundsInBlocks, or checkBlockHash)', 207 demandOption: true, 208 choices: ['averageBlockTime', 'roundsInBlocks', 'checkBlockHash'], 209 }, 210 'block-height': { 211 alias: 'b', 212 describe: 'Block height to examine for checkBlockHash metric', 213 type: 'number', 214 }, 215 'network-id': { 216 alias: 'n', 217 describe: 'Network ID to fetch block metrics from', 218 demandOption: true, 219 type: 'number', 220 choices: [0, 1, 2], 221 } 222 }) 223 .check((argv) => { 224 // Check if metric-type is checkBlockHash and block-height is provided 225 if (argv['metric-type'] === 'checkBlockHash' && (isNaN(argv['block-height']) || argv['block-height'] == null)) { 226 throw new Error('Block height is required when metric-type is checkBlockHash'); 227 } 228 return true; // Indicate that the arguments passed the check 229 }) 230 .argv; 231 232 // Fetch and output the specified block metric 233 fetchBlockMetrics(argv['metric-type'], argv['block-height'], argv['network-id']); 234 } 235 236 // Run the main function 237 main();