Last active
March 7, 2024 22:39
-
-
Save Rucknium/ce83a26ac99fb8debc0b725941340767 to your computer and use it in GitHub Desktop.
Monero adversarial spammer privacy impact
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# When current.height = 3100144, | |
# start.spam.height = 3097308, and | |
# spam.share <- 0.75 | |
# 59% of the probability mass function of the DSA are outputs created by the suspected spammer | |
# Formula for the exact Monero Decoy Selection Algorithm (DSA) probability mass function is based on: | |
# jeffro256 (2023) "Implementing Monero Decoy Selection" | |
# https://github.com/jeffro256/monero/blob/decoy_selection_md/docs/DECOY_SELECTION.md | |
# and | |
# Rucknium (2023) "Closed-form expression of the wallet2 Decoy Selection Algorithm" | |
# https://www.overleaf.com/read/ndbtkwrbrdjq | |
# Must be able to connect to a monero node. Specify URL below. | |
# Must install these R packages: | |
install.packages(c("RJSONIO", "RCurl", "actuar")) | |
# These variables can be edited: | |
# Input the node's URL here. This should be correct for | |
# a local node using standard ports:' | |
url.rpc <- "http://127.0.0.1:18081/json_rpc" | |
current.height <- 3100144 # About 22:00 UTC March 7, 2024 | |
start.spam.height <- 3097308 # First block of March 4, 2024 UTC | |
spam.share <- 0.75 | |
calculate_average_output_flow <- function(crod) { | |
# 1 | |
num_blocks_to_consider_for_flow = min(c(length(crod), BLOCKS_IN_A_YEAR)) | |
# 2 | |
if (length(crod) > num_blocks_to_consider_for_flow) { | |
num_outputs_to_consider_for_flow = crod[length(crod)] - crod[ length(crod) - num_blocks_to_consider_for_flow ] | |
# R indexes from 1 | |
} else { | |
num_outputs_to_consider_for_flow = crod[length(crod)] # R indexes from 1 | |
} | |
# 3 | |
average_output_flow = DIFFICULTY_TARGET_V2 * num_blocks_to_consider_for_flow / num_outputs_to_consider_for_flow | |
return(average_output_flow) | |
} | |
calculate_num_usable_rct_outputs <- function(crod) { | |
# 1 | |
num_usable_crod_blocks = length(crod) - (CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE - 1) | |
# 2 | |
num_usable_rct_outputs = crod[num_usable_crod_blocks] # R indexes from 1 | |
return(num_usable_rct_outputs) | |
} | |
# Modified from TownforgeR::tf_rpc_curl function | |
xmr.rpc <- function( | |
url.rpc = "http://127.0.0.1:18081/json_rpc", | |
method = "", | |
params = list(), | |
userpwd = "", | |
num.as.string = FALSE, | |
nonce.as.string = FALSE, | |
keep.trying.rpc = FALSE, | |
curl = RCurl::getCurlHandle(), | |
... | |
){ | |
json.ret <- RJSONIO::toJSON( | |
list( | |
jsonrpc = "2.0", | |
id = "0", | |
method = method, | |
params = params | |
), digits = 50 | |
) | |
rcp.ret <- tryCatch(RCurl::postForm(url.rpc, | |
.opts = list( | |
userpwd = userpwd, | |
postfields = json.ret, | |
httpheader = c('Content-Type' = 'application/json', Accept = 'application/json') | |
# https://stackoverflow.com/questions/19267261/timeout-while-reading-csv-file-from-url-in-r | |
), | |
curl = curl | |
), error = function(e) {NULL}) | |
if (keep.trying.rpc && length(rcp.ret) == 0) { | |
while (length(rcp.ret) == 0) { | |
rcp.ret <- tryCatch(RCurl::postForm(url.rpc, | |
.opts = list( | |
userpwd = userpwd, | |
postfields = json.ret, | |
httpheader = c('Content-Type' = 'application/json', Accept = 'application/json') | |
# https://stackoverflow.com/questions/19267261/timeout-while-reading-csv-file-from-url-in-r | |
), | |
curl = curl | |
), error = function(e) {NULL}) | |
} | |
} | |
if (is.null(rcp.ret)) { | |
stop("Cannot connect to monerod. Is monerod running?") | |
} | |
if (num.as.string) { | |
rcp.ret <- gsub("(: )([-0123456789.]+)([,\n\r])", "\\1\"\\2\"\\3", rcp.ret ) | |
} | |
if (nonce.as.string & ! num.as.string) { | |
rcp.ret <- gsub("(\"nonce\": )([-0123456789.]+)([,\n\r])", "\\1\"\\2\"\\3", rcp.ret ) | |
} | |
RJSONIO::fromJSON(rcp.ret, asText = TRUE) # , simplify = FALSE | |
} | |
CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE = 10 | |
DIFFICULTY_TARGET_V2 = 120 | |
DEFAULT_UNLOCK_TIME = CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE * DIFFICULTY_TARGET_V2 | |
RECENT_SPEND_WINDOW = 15 * DIFFICULTY_TARGET_V2 | |
SECONDS_IN_A_YEAR = 60 * 60 * 24 * 365 | |
BLOCKS_IN_A_YEAR = SECONDS_IN_A_YEAR / DIFFICULTY_TARGET_V2 | |
crod <- xmr.rpc(url.rpc = url.rpc, method = "get_output_distribution", | |
params = list(amounts = list(0), from_height = 0, to_height = current.height, binary = FALSE, cumulative = TRUE)) | |
crod <- crod$result$distributions[[1]]$distribution | |
average_output_flow <- calculate_average_output_flow(crod) | |
num_usable_rct_outputs <- calculate_num_usable_rct_outputs(crod) | |
GAMMA_SHAPE = 19.28 | |
GAMMA_RATE = 1.61 | |
# GAMMA_SCALE = 1 / GAMMA_RATE | |
G <- function(x) { | |
actuar::plgamma(x, shapelog = GAMMA_SHAPE, ratelog = GAMMA_RATE) | |
} | |
G_star <- function(x) { | |
(0 <= x*v & x*v <= 1800) * | |
(G(x*v + 1200) - G(1200) + | |
( (x*v)/(1800) ) * G(1200) | |
)/G(z*v + 1200) + | |
(x*v > 1800) * G(x*v + 1200)/G(z*v + 1200) | |
} | |
v <- average_output_flow | |
z <- num_usable_rct_outputs | |
usable.outputs <- 1:num_usable_rct_outputs | |
crod.reversed <- cumsum(abs(diff(rev(crod)))[-(1:9)]) | |
# Remove first 9 blocks before cumsum() since cant spend from those outputs | |
crod.reversed <- c(0, crod.reversed) | |
y_0 <- crod.reversed[-length(crod.reversed)] + 1 | |
y_1 <- crod.reversed[-1] | |
pmf.decoy.crod <- (G_star(y_1 + 1) - G_star(y_0)) / (y_1 + 1 - y_0) | |
pmf.decoy <- rep(pmf.decoy.crod, times = diff(crod.reversed)) | |
start.spam.relative.output.index <- crod.reversed[ (current.height - 9) - start.spam.height] | |
# Final result | |
sum(spam.share * pmf.decoy[1:start.spam.relative.output.index] ) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment