Skip to content

Instantly share code, notes, and snippets.

@Rucknium
Last active March 7, 2024 22:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Rucknium/ce83a26ac99fb8debc0b725941340767 to your computer and use it in GitHub Desktop.
Save Rucknium/ce83a26ac99fb8debc0b725941340767 to your computer and use it in GitHub Desktop.
Monero adversarial spammer privacy impact
# 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