diff --git a/DESCRIPTION b/DESCRIPTION index 14832f0..8f8e8be 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -25,7 +25,9 @@ Imports: parallelly, future, future.apply, - qs + qs, + RCurl, + RJSONIO Suggests: archive, testthat (>= 3.0.0), diff --git a/NAMESPACE b/NAMESPACE index 6cc3b29..59cf300 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,5 +2,6 @@ export("ping.peers") export("get.p2p.log") export("compress.log") +export("newborn.nodes") importFrom("utils", "read.csv", "untar", "installed.packages") -importFrom("stats", "complete.cases") +importFrom("stats", "complete.cases", "quantile") diff --git a/R/newborn.R b/R/newborn.R new file mode 100644 index 0000000..8bf2f31 --- /dev/null +++ b/R/newborn.R @@ -0,0 +1,136 @@ + + +#' Print likely newborn nodes to the console +#' +#' @description When a node connects to our own node, it could be a newborn +#' node that is syncing from the genesis block. `newborn.nodes` queries the +#' local Monero node about node connections with a `get_peers` call and prints +#' information about likely newborn nodes to the console. The unrestricted RPC +#' port must be reachable by R. This function is an infinite loop. `ctrl + c` +#' to interrupt the function and print all IP addresses of likely newborn nodes +#' recorded so far. Each peer will only be printed once during a specific run of +#' `newborn.nodes`, even if the node disconnects and reconnects later. +#' +#' @param unrestricted.rpc.url URL and port of the monerod unrestricted RPC. +#' Default is `http://127.0.0.1:18081` +#' @param poll.time How often, in seconds, to check for a newborn node +#' connection. Default is 30 seconds. +#' @param sync.height.lag Criteria to consider a node as "newborn". Nodes that +#' have a height greater than current network height minus `sync.height.lag` +#' will not be considered a newborn node. Default is 3 months. +#' @param avg_upload.limit Criteria to consider a node as "newborn". The +#' `avg_upload` from the `get_peers` call must be greater than or equal to +#' `avg_upload.limit` OR `current_upload` must be greater than or equal to +#' `current_upload.limit` to consider the node as "newborn". Default is 10 for +#' both limits. Sometimes a node gets stuck at a low height, so it isn't +#' actually new or syncing. This criteria makes sure that the peer is actually +#' actively syncing data. +#' @param current_upload.limit Criteria to consider a node as "newborn". +#' +#' @return +#' NULL (invisible) +#' @export +#' +#' @examples +#' \dontrun{ +#' newborn.nodes() +#' } + +newborn.nodes <- function(unrestricted.rpc.url = "http://127.0.0.1:18081", + poll.time = 30, sync.height.lag = 30 * 24 * 90, avg_upload.limit = 10, + current_upload.limit = 10) { + + json.post <- RJSONIO::toJSON( + list( + jsonrpc = "2.0", + id = "0", + method = "get_connections", + params = "" + ) + ) + + already.seen.newborn.address <- c() + already.seen.newborn.peer_id <- c() + + on.exit({ + session.duration <- Sys.time() - session.start.time + cat(paste0(format(Sys.time(), "%Y-%m-%d %T"), + " ", length(already.seen.newborn.address), + " likely newborn nodes seen during this session that lasted ", + round(session.duration, 1), " ", units(session.duration), + ": \n", + paste0(already.seen.newborn.address, collapse = " "), "\n")) + }) + + session.start.time <- Sys.time() + + cat(paste0(format(session.start.time, "%Y-%m-%d %T"), " Start polling monerod for newborn nodes...\n")) + + while (TRUE) { + + peers <- RJSONIO::fromJSON( + RCurl::postForm(paste0(unrestricted.rpc.url, "/json_rpc"), + .opts = list( + userpwd = "", + postfields = json.post, + httpheader = c('Content-Type' = 'application/json', Accept = 'application/json') + ) + ), asText = TRUE + ) + + if(length(peers$result$connections) == 0 ) { + cat("Unexpected results from node's RPC response. Check that monerod's unrestricted RPC port is at ", + unrestricted.rpc.url, "\n") + Sys.sleep(poll.time) + next + } + + peers <- peers$result$connections + + peer.heights <- sapply(peers, FUN = function(x) {x$height}) + peer.address <- sapply(peers, FUN = function(x) {x$address}) + peer.peer_id <- sapply(peers, FUN = function(x) {x$peer_id}) + peer.avg_upload <- sapply(peers, FUN = function(x) {x$avg_upload}) + peer.current_upload <- sapply(peers, FUN = function(x) {x$current_upload}) + + consensus.height <- floor(quantile(peer.heights, prob = 0.9)) + + new.nodes <- which( + peer.heights > 1 & + (peer.heights <= consensus.height - sync.height.lag) & + (! peer.address %in% already.seen.newborn.address) & + (! peer.peer_id %in% already.seen.newborn.peer_id) & + (peer.avg_upload > avg_upload.limit | peer.current_upload > current_upload.limit) + ) + + for (i in new.nodes) { + + cat( + paste0(format(Sys.time(), "%Y-%m-%d %T"), " ", + "Likely newborn node: ", + formatC(peers[[i]]$address, width = 21), ", ", + # "Peer ID: ", peers[[i]]$peer_id, ", ", + # "Connection ID: ", peers[[i]]$connection_id, ", ", + "Height: ", + formatC(peers[[i]]$height, width = 7, format = "d"), "/", consensus.height, ", ", + ifelse(peers[[i]]$pruning_seed > 0, " Pruned", "Unpruned"), ", ", + ifelse(peers[[i]]$incoming, "Incoming connection", "Outgoing connection"), ", ", + "avg_upload: ", formatC(peers[[i]]$avg_upload, width = 4), + # ", ", "current_upload: ", formatC(peers[[i]]$current_upload, width = 4), + # ", ", "send_count: ", formatC(peers[[i]]$send_count, width = 8, format = "d"), + "\n" + ) + ) + + already.seen.newborn.address <- c(already.seen.newborn.address, peers[[i]]$address) + already.seen.newborn.peer_id <- c(already.seen.newborn.peer_id, peers[[i]]$peer_id) + + } + + Sys.sleep(poll.time) + + } + + return(invisible(NULL)) + +} diff --git a/man/newborn.nodes.Rd b/man/newborn.nodes.Rd new file mode 100644 index 0000000..8c31abe --- /dev/null +++ b/man/newborn.nodes.Rd @@ -0,0 +1,53 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/newborn.R +\name{newborn.nodes} +\alias{newborn.nodes} +\title{Print likely newborn nodes to the console} +\usage{ +newborn.nodes( + unrestricted.rpc.url = "http://127.0.0.1:18081", + poll.time = 30, + sync.height.lag = 30 * 24 * 90, + avg_upload.limit = 10, + current_upload.limit = 10 +) +} +\arguments{ +\item{unrestricted.rpc.url}{URL and port of the monerod unrestricted RPC. +Default is \verb{http://127.0.0.1:18081}} + +\item{poll.time}{How often, in seconds, to check for a newborn node +connection. Default is 30 seconds.} + +\item{sync.height.lag}{Criteria to consider a node as "newborn". Nodes that +have a height greater than current network height minus \code{sync.height.lag} +will not be considered a newborn node. Default is 3 months.} + +\item{avg_upload.limit}{Criteria to consider a node as "newborn". The +\code{avg_upload} from the \code{get_peers} call must be greater than or equal to +\code{avg_upload.limit} OR \code{current_upload} must be greater than or equal to +\code{current_upload.limit} to consider the node as "newborn". Default is 10 for +both limits. Sometimes a node gets stuck at a low height, so it isn't +actually new or syncing. This criteria makes sure that the peer is actually +actively syncing data.} + +\item{current_upload.limit}{Criteria to consider a node as "newborn".} +} +\value{ +NULL (invisible) +} +\description{ +When a node connects to our own node, it could be a newborn +node that is syncing from the genesis block. \code{newborn.nodes} queries the +local Monero node about node connections with a \code{get_peers} call and prints +information about likely newborn nodes to the console. The unrestricted RPC +port must be reachable by R. This function is an infinite loop. \code{ctrl + c} +to interrupt the function and print all IP addresses of likely newborn nodes +recorded so far. Each peer will only be printed once during a specific run of +\code{newborn.nodes}, even if the node disconnects and reconnects later. +} +\examples{ +\dontrun{ +newborn.nodes() +} +}