TT, I am trying to determine if the Sybil amplification I outlined was in your paper or not and whether or not that elevates to a much more serious threat.
Sure,
Try my python code, which outlines a similar attacker output-saturation attack. This is a worst case scenario, in which the attacker is generating outpoints from the beginning of the chain onwards, and depends on coins not breaking onto different denominations but rather staying the same denomination. So, it's a little contrived. The success of the attacker with totally random output selection of inputs by all users becomes exponentially more difficult for the attacker the later he begins spamming outputs.
import math
import random
# cryptonote transactions
# mixin = mixin level (outputs mixed in that are not yours)
# mixedIn = outputs by index (simulates ref by hash)
# these are stored as a list of ints.
# unrevealed = number of revealed outputs mixed in.
# these are stored as a list of ints.
# this assumes that all outputs are of the same size
# or at least equally mixable (gmaxwell/andytoshi scheme).
class transaction():
def __init__(self, mixin, mixedIn, unrevealed):
self.mixin = mixin
self.mixedIn = mixedIn
self.unrevealed = unrevealed
def revealAllOutputs(self):
while len(self.unrevealed) > 0:
self.unrevealed.pop()
# Remove outputs that have been revealed
def revealOutput(self, outputIndex):
if(self.unrevealed.count(outputIndex) > 0):
self.unrevealed.remove(outputIndex)
# Count number of unrevealed outputs
def unrevealedOutputs(self):
return len(self.unrevealed)
class ledger():
def __init__(self,
transactionsTotalPerTrial,
numberOfTrials,
maximumMixinTested,
revealPercentage):
self.transactionsTotalPerTrial = transactionsTotalPerTrial
self.numberOfTrials = numberOfTrials
self.mixin = mixin
self.revealPercentage = revealPercentage
self.ledger = []
self.knownRevealedOutputs = []
for numberOfTransactions in range (0, transactionsTotalPerTrial):
# if there aren't enough elements to list, then
# just mixin as many elements as possible.
if len(self.ledger) < self.mixin+1:
mixedIn = []
revealed = []
for i in range(0, len(self.ledger)):
mixedIn.append(i)
revealed.append(i)
self.ledger.append(transaction(len(self.ledger), mixedIn, revealed))
# otherwise, pick some random elements to mix into
# the ring signature and make a new tx.
else:
mixedIn = []
revealed = []
for i in range(0, self.mixin):
randomOutput = random.randint(0, len(self.ledger)-2)
# can't remix existing elements, so find an
# output we haven't mixed yet.
while (mixedIn.count(randomOutput) > 0):
randomOutput = random.randint(0, len(self.ledger)-2)
mixedIn.append(randomOutput)
revealed.append(randomOutput)
self.ledger.append(transaction(mixin, mixedIn, revealed))
# choose your outputs to reveal.
outputsToReveal = []
for i in range(0, int(revealPercentage * transactionsTotalPerTrial)):
randomOutput = random.randint(mixin, transactionsTotalPerTrial-1)
while (outputsToReveal.count(randomOutput) > 0):
randomOutput = random.randint(mixin, transactionsTotalPerTrial-1)
outputsToReveal.append(randomOutput)
# reveal the outputs by calling the recursive recursiveReveal
# function.
self.recursiveReveal(outputsToReveal)
def recursiveReveal(self, outputsToReveal):
while len(outputsToReveal) > 0:
revealedOutput = outputsToReveal.pop()
# reveal all outputs for this output.
self.ledger[revealedOutput].revealAllOutputs()
# if it's been mixed somewhere, remove it
# from that list.
for i in range(0, transactionsTotalPerTrial):
self.ledger[i].revealOutput(revealedOutput)
self.knownRevealedOutputs.append(revealedOutput)
# diff the ledger and outputsToRevealOriginal to uncover any
# newly revealed outputs via chain reactions.
newlyRevealedOutputCount = 0
newlyRevealedOutputs = []
for i in range(mixin, transactionsTotalPerTrial):
if self.ledger[i].unrevealedOutputs() == 0:
if self.knownRevealedOutputs.count(i) == 0:
newlyRevealedOutputs.append(i)
newlyRevealedOutputCount += 1
if newlyRevealedOutputCount == 0:
return
else:
self.recursiveReveal(newlyRevealedOutputs)
# count the number of totally revealed outputs and return them.
def getTotallyRevealedOutputs(self):
totallyRevealedOutputs = 0
for i in range(mixin, transactionsTotalPerTrial):
if self.ledger[i].unrevealedOutputs() == 0:
totallyRevealedOutputs += 1
return totallyRevealedOutputs
def getVariance(yourList, mean):
length = float(len(yourList))
sum = 0.0
while len(yourList) > 0:
x = yourList.pop()
xDiffSquared = math.pow(x - mean, 2)
sum += xDiffSquared
return (sum / (length - 1))
transactionsTotalPerTrial = 2000
numberOfTrials = 25
maximumMixinTested = 7
revealPercentage = 0.50
# open file to write the results to disk.
f = open("results.txt","w")
f.write("Transactions per trial: " + str(transactionsTotalPerTrial) + "\n")
f.write("Number of trials : " + str(numberOfTrials) + "\n")
f.write("Maximum mixin tested: " + str(maximumMixinTested) + "\n")
f.write("Reveal percentage: " + str(revealPercentage * 100) + "%\n\n")
for mixin in range (1, maximumMixinTested+1):
f.write("mixin = " + str(mixin) + "\n")
allTrialResults = []
for trial in range (0, numberOfTrials):
# ledger is the list of all transactions
trialLedger = ledger(transactionsTotalPerTrial,
numberOfTrials,
mixin,
revealPercentage)
totallyRevealedOutputs = float(trialLedger.getTotallyRevealedOutputs())
# determine the ratio of revealed outputs.
revealedOutputRatio = totallyRevealedOutputs / float(transactionsTotalPerTrial)
# store this ratio.
f.write(str(revealedOutputRatio) + ", ")
allTrialResults.append(revealedOutputRatio)
f.write("\n")
averageOfAllTrials = reduce(lambda x, y: x + y, allTrialResults) / len(allTrialResults)
varianceAllTrials = getVariance(allTrialResults, averageOfAllTrials)
revealsFromChainReaction = averageOfAllTrials - revealPercentage
nonAttackerRevealPercent = revealsFromChainReaction / (1-revealPercentage)
f.write("Average revealed output ratio: " + str(averageOfAllTrials * 100) + "%\n")
f.write("Reveals resulting from chain reaction: " + str(revealsFromChainReaction * 100) + "% +/- " + str(varianceAllTrials * 100) + "%\n")
f.write("Percentage of non-attacker outputs revealed: " + str(nonAttackerRevealPercent * 100) + "%\n\n")
f.close()
We've known about this for a long time, I'm just wrapping up my work on completing the fix for it now.
There are two other non-trivial de-anonymizing attacks that I'm writing proposals to mitigate now too, can you find them?