/ .devnet / .analytics / analytics.js
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();