misc-research/Monero-Black-Marble-Flood/code/empirical-effective-ring-size.R
2024-03-27 21:12:26 +00:00

235 lines
7.5 KiB
R

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
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)
}
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)
}
crod <- xmr.rpc(url.rpc = paste0(url.rpc, "/json_rpc"), method = "get_output_distribution",
params = list(amounts = list(0), from_height = 0, to_height = current.height, binary = FALSE, cumulative = TRUE))
start_height <- crod$result$distributions[[1]]$start_height
crod <- crod$result$distributions[[1]]$distribution
crod.full <- crod
spam.output_index <- list()
for (i in seq_along(spam.results)) {
spam.output_index[[i]] <- list(name = spam.types[[i]]$fingerprint.text,
output_index = spam.results[[i]]$spam.fingerprint$output_index)
}
n.workers <- min(floor(parallelly::availableCores()/2), 32L)
future::plan(future::multisession(workers = n.workers))
adversary.owned.dsa.mass <- future.apply::future_lapply((start.spam.height:current.height), function(ring.construction.height) {
crod <- crod.full[1:(ring.construction.height - start_height + 1)]
average_output_flow <- calculate_average_output_flow(crod)
num_usable_rct_outputs <- calculate_num_usable_rct_outputs(crod)
v <- average_output_flow
z <- num_usable_rct_outputs
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)
}
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))
pmf.decoy.reversed <- rev(pmf.decoy)
result <- list()
for (i in seq_along(spam.output_index)) {
estimated.adversary.owned.share <- sum(pmf.decoy.reversed[
spam.output_index[[i]]$output_index[ spam.output_index[[i]]$output_index <= length(pmf.decoy.reversed)] ])
result[[i]] <- data.table(ring.construction.height = ring.construction.height,
estimated.adversary.owned.share = estimated.adversary.owned.share,
type = spam.output_index[[i]]$name)
}
rbindlist(result)
})
adversary.owned.dsa.mass <- rbindlist(adversary.owned.dsa.mass)
adversary.owned.dsa.mass <- merge(adversary.owned.dsa.mass, block.data[, .(height, timestamp.POSIX)],
by.x = "ring.construction.height", by.y = "height")
setorder(adversary.owned.dsa.mass, timestamp.POSIX)
adversary.owned.dsa.mass[, effective.ring.size := 1 + (1 - estimated.adversary.owned.share) * 15]
png("empirical-effective-ring-size.png", width = 800, height = 800)
ggplot(adversary.owned.dsa.mass, aes(x = timestamp.POSIX, y = effective.ring.size, colour = type)) +
geom_line() +
scale_y_continuous(breaks = 1:16, limits = c(0, NA), expand = c(0, 0)) +
scale_x_datetime(date_breaks = "day", guide = guide_axis(angle = 90)) +
ggtitle("Estimated mean effective ring size") +
xlab(" Date github.com/Rucknium") +
ylab("Mean effective ring size") +
labs(colour = "Spam type") +
theme(legend.position = "top", legend.text = element_text(size = 15), legend.title = element_text(size = 15),
plot.title = element_text(size = 20),
plot.subtitle = element_text(size = 15),
axis.text = element_text(size = 15),
axis.title.x = element_text(size = 15, margin = margin(t = 10)),
axis.title.y = element_text(size = 15), strip.text = element_text(size = 15)) +
guides(colour = guide_legend(override.aes = list(linewidth = 5)))
dev.off()
guess.prob <- function(effective.ring.size, nominal.ring.size) {
decoys <- nominal.ring.size - 1
sapply(effective.ring.size, FUN = function(x) {
weighted.mean(1/(1 + 0:decoys),
w = dbinom(0:decoys, size = decoys, prob = (x - 1)/decoys))
})
}
adversary.owned.dsa.mass[, guess.prob := guess.prob(effective.ring.size, nominal.ring.size = 16)]
png("empirical-guessing-probability.png", width = 800, height = 800)
ggplot(adversary.owned.dsa.mass, aes(x = timestamp.POSIX, y = guess.prob, colour = type)) +
geom_line() +
scale_y_continuous( limits = c(0, NA), expand = c(0, 0), labels = scales::label_percent()) +
scale_x_datetime(date_breaks = "day", guide = guide_axis(angle = 90)) +
ggtitle("Estimated probability of correctly guessing the real spend") +
xlab(" Date github.com/Rucknium") +
ylab("Probability") +
labs(colour = "Spam type") +
theme(legend.position = "top", legend.text = element_text(size = 15), legend.title = element_text(size = 15),
plot.title = element_text(size = 20),
plot.subtitle = element_text(size = 15),
axis.text = element_text(size = 15),
axis.title.x = element_text(size = 15, margin = margin(t = 10)),
axis.title.y = element_text(size = 15), strip.text = element_text(size = 15)) +
guides(colour = guide_legend(override.aes = list(linewidth = 5)))
dev.off()
adversary.owned.dsa.mass[, effective.ring.size.one := dbinom(0, size = 15, prob = 1 - estimated.adversary.owned.share)]
png("empirical-ring-size-one.png", width = 800, height = 800)
ggplot(adversary.owned.dsa.mass, aes(x = timestamp.POSIX, y = effective.ring.size.one, colour = type)) +
geom_line() +
scale_y_continuous( limits = c(0, NA), expand = c(0, 0), labels = scales::label_percent()) +
scale_x_datetime(date_breaks = "day", guide = guide_axis(angle = 90)) +
ggtitle("Estimated share of rings with effective ring size of one") +
xlab(" Date github.com/Rucknium") +
ylab("Share of rings") +
labs(colour = "Spam type") +
theme(legend.position = "top", legend.text = element_text(size = 15), legend.title = element_text(size = 15),
plot.title = element_text(size = 20),
plot.subtitle = element_text(size = 15),
axis.text = element_text(size = 15),
axis.title.x = element_text(size = 15, margin = margin(t = 10)),
axis.title.y = element_text(size = 15), strip.text = element_text(size = 15)) +
guides(colour = guide_legend(override.aes = list(linewidth = 5)))
dev.off()
future::plan(future::sequential)
# Reset to remove threaded R sessions to get back RAM