7 Miscellaneous Preparation
This script does three things:
Assigns each ring a boolean TRUE/FALSE value for
is.nonstandard.rucknium
andis.nonstandard.isthmus
based on whether its transaction hash was identified as a nonstandard transaction in Chapter 5 .Rarely, ring members can have an age that is younger than the block the transaction was confirmed in. The time recorded by the txpool archive database and the blockchain may disagree because of loose block timestamp rules. In this case, the age of every ring member is shifted so that all ring members have a valid non-negative age.
An
output.index.only.locked
object is created. This is a record of the unlock time of outputs with nonzero unlock time, including coinbase outputs. This is a necessary piece of data to compute the decoy selection probability distribution at every block height, whose support only includes outputs that have a nonzero probability of being selected as decoys, i.e. outputs that are actually spendable at the time of transaction construction.
Again, input this code in the same R session as used in previous chapters.
7.1 Code
:=
xmr.rings[, is.nonstandard.rucknium %chin% tx.hash.nonstandard.fees) |
(tx_hash %chin% tx.hash.nonstandard.unlock.time) |
(tx_hash | is_mordinal_transfer_ring]
is_mordinal_ring
:= tx_hash %chin% isthmus.anomalous.hashes]
xmr.rings[, is.nonstandard.isthmus
rm(tx.hash.nonstandard.fees, tx.hash.nonstandard.unlock.time, isthmus.anomalous.hashes)
<- output.index[ output_amount == 0 | tx_num == 1,
youngest.RingCT youngest.output.index = max(output_index)), by = "block_height"]
.(
# rm(output.index)
# TODO: uncomment this
<- merge(youngest.RingCT,
youngest.RingCT data.table(block_height = min(youngest.RingCT$block_height):max(youngest.RingCT$block_height)),
all = TRUE)
:=
youngest.RingCT[, youngest.output.index ::na.locf(youngest.output.index, na.rm = FALSE)]
zoo# Before RingCT outputs were mandatory, some blocks had zero RingCT-eligible outputs.
# These lines make sure that these blocks are included in the dataset
:= block_height + 9L]
youngest.RingCT[, block_height # Used to be 10L before block_height_ring changed to block_height_ring.at.construction below
<- merge(xmr.rings, youngest.RingCT, all.x = TRUE, by.x = "block_height_ring.at.construction", by.y = "block_height")
xmr.rings # Need all.x = TRUE because will have NAs for the first 10 blocks of youngest.output.index
:= youngest.output.index - output_index]
xmr.rings[, ring_member_age # xmr.rings[, table(ring_member_age < 0) ]
# > 1798220 /357314244
# [1] 0.005032601
:= ring_member_age + 1]
xmr.rings[, ring_member_age # Add 1 so youngest ring member is 1, not zero
<- xmr.rings[ring_member_age <= 0, ]
fetus.rings
<- merge(unique(fetus.rings[, .(tx_hash, input_num)]), xmr.rings, by = c("tx_hash", "input_num"))
fetus.rings setcolorder(fetus.rings, colnames(xmr.rings))
nrow(xmr.rings)
<- fsetdiff(xmr.rings, fetus.rings)
xmr.rings nrow(xmr.rings)
# This is anti-join
<- n.fetus.rings.initial <- nrow(fetus.rings)
n.fetus.rings
<- fetus.rings[FALSE, ]
born.rings
<- copy(youngest.RingCT)
youngest.RingCT.rolling
:= NULL]
fetus.rings[, ring_member_age := NULL]
fetus.rings[, youngest.output.index
<- 0
i while (n.fetus.rings > 0) {
<- i + 1
i := block_height - 1L]
youngest.RingCT.rolling[, block_height <- merge(fetus.rings, youngest.RingCT.rolling, all.x = TRUE, by.x = "block_height_ring.at.construction", by.y = "block_height")
birthing.rings := youngest.output.index - output_index]
birthing.rings[, ring_member_age := any(ring_member_age <= 0), by = c("tx_hash", "input_num")]
birthing.rings[, any.fetus
if (all(birthing.rings$any.fetus)) { next }
<- birthing.rings[(any.fetus), ]
fetus.rings := NULL]
fetus.rings[, ring_member_age := NULL]
fetus.rings[, youngest.output.index <- birthing.rings[, any.fetus]
any.fetus.external := NULL]
birthing.rings[, any.fetus
<- rbind(born.rings, birthing.rings[(! any.fetus.external), ])
born.rings
<- nrow(fetus.rings)
n.fetus.rings
if (i >= 1000) { break }
# Don't go further than 1000 blocks. The few rings
# that cannot be repaired after 1000 blocks will be excluded from the analysis
}# The vast majority are off-by-one-block
stopifnot(nrow(born.rings) + n.fetus.rings == n.fetus.rings.initial)
<- rbind(xmr.rings, born.rings)
xmr.rings
<- output.index[
output.index.only.locked != 0 &
output_unlock_time == 0 | tx_num == 1),
(output_amount
.(output_index, output_unlock_time)]
<- output.index.only.locked[output_unlock_time > 500000000, ]
output.index.only.locked.timestamp # Only about 40 of these have timestamp interpretations instead of block height interpretations
# https://thecharlatan.ch/Monero-Unlock-Time-Vulns/
<- unique(output.index[, .(block_height, block_timestamp)])
block.timestamps
setorder(block.timestamps, block_timestamp)
:= {
output.index.only.locked.timestamp[, output_unlock_time <- findInterval(output_unlock_time, block.timestamps$block_timestamp)
interval.index ifelse(interval.index == 0, 0, 1 + block.timestamps$block_height[interval.index])
# If the time is before the earliest block in the dataset, then findInterval() will return a zero index (invalid)
# So set it to the genesis block
# Technically the genesis block should be unix time zero, but the data gatherer only gets RingCT
# txs, which start well after the genesis block.
}
]
== current.height + 1, output_unlock_time := .Machine$integer.max]
output.index.only.locked.timestamp[output_unlock_time
<- rbind(
output.index.only.locked <= 500000000, ], output.index.only.locked.timestamp)
output.index.only.locked[output_unlock_time
:= as.integer(output_unlock_time)]
output.index.only.locked[, output_unlock_time := as.integer(output_index)]
output.index.only.locked[, output_index
setDF(output.index.only.locked)
# rm(youngest.RingCT)