"""Difficulty: calculate or check the PoW difficulty (preceding null bits) of an input""" from hashlib import new from sys import argv def calculate(hash: bytes, bigendian: bool = True) -> int: """Calculate the difficulty (number of preceding null bits).""" # prepare variables number = int.from_bytes(hash, byteorder = 'big' if bigendian else 'little') length = len(hash) trailing_bits = 0 # shift off bits until a null result is achieved while number != 0 and trailing_bits < length * 8: number = number >> 1 trailing_bits += 1 # difficulty achieved is the preceeding null bits return length * 8 - trailing_bits def calculate_linear(hash: bytes, bigendian: bool = True) -> int: """Calculates the difficulty as the expected average number of tries.""" return 2**calculate(hash, bigendian) def calculate_query_bytes(target: bytes, hash_len: int) -> int: """Calculates the expected number of tries to find a hash containing the target sequence of bytes. """ target_len = len(target) permutations = hash_len - target_len + 1 return 2**(target_len*8)/permutations def check(diff: int, hash: bytes, bigendian: bool = True) -> bool: """Checks if a hash meets the minimum difficulty threshold (preceeding null bits).""" # prepare variables number = int.from_bytes(hash, byteorder='big' if bigendian else 'little') length = len(hash) # raise exception if difficulty is impossible to achieve if diff > length * 8: raise Exception('Difficulty cannot exceed bit length of input') # shift off all but {diff} preceding bits preceding_bits = number >> (length * 8 - diff) # it meets the difficulty if preceding bits are 0 return preceding_bits == 0 def license(): """Copyleft (c) 2022 k98kurz Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyleft notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """ return license.__doc__ def usage(name): print('Calculate difficulty of a given hash/value or check if it reaches a threshold.') print(f'usage: {name} [check:{{number}}|calc|linear|query] [hash:{{hashlib_algo}}|hex|str] [input_string] [bigendian=True]') print(f'\toperators:') print(f'\t\tcalc\t\tcalculate the number of preceding null bits of the given hash') print(f'\t\tlinear\t\tcalculate the average expected number of tries of the given hash') print(f'\t\tquery\t\tcalculate the average expected number of tries for a 32 byte hash containing the input_string') print(f'\tmodes:') print(f'\t\thash:{{hashlib_algo}}\thash the input_string before running the operation') print(f'\t\t\thash mode is not available for the query operation') print(f'\t\thex\t\tinterpret the input_string as hexadecimal') print(f'\t\tstr\t\tinterpret the input_string as a utf-8 string') print(f'\tbigendian\t(bool) the endianess for byte interpretation is big or little') print(f'\texamples:') print(f'\t\t`{name} check:8 hex 0001` checks if the hex string 0001 has 8 preceding null bits; should return `pass`') print(f'\t\t`{name} check:1 hash:sha256 1234` checks if the hex string 0001 has 8 preceding null bits; should return `pass`') print(f'\t\t`{name} calc hex 0f` calculates the number of preceding null bits in the hex string 0f; should return 4') print(f'\t\t`{name} linear hex 0f` calculates the expected number of tries to get the number of preceding null bits in the hex string 0f; should return 16') print(f'\t\t`{name} query str X` calculates the expected number of tries to find a hash containing the hex string 0f; should return 8.0') def main(args): """Main function for invoking as cli tool.""" if len(args) < 4: return usage(args[0]) # parse arguments op = args[1].split(':') mode = args[2].split(':') input = args[3] bigendian = True if len(args) < 5 or args[4] in ('True', 'true', '1') else False if len(op) == 1: # calculate or query if op[0] in ('calc', 'linear'): if len(mode) == 1: # default mode, i.e. no hashing input = bytearray.fromhex(input) if mode[0] == 'hex' else bytes(input, 'utf-8') print(calculate_linear(input, bigendian) if op[0] == 'linear' else calculate(input, bigendian)) else: # hashing mode if 'shake' in mode[1]: input = new(mode[1], bytes(input, 'utf-8')).digest(len(input)) else: input = new(mode[1], bytes(input, 'utf-8')).digest() print(calculate_linear(input, bigendian) if op[0] == 'linear' else calculate(input, bigendian)) elif op[0] == 'query': if len(mode) == 1: # default mode, i.e. no hashing input = bytearray.fromhex(input) if mode[0] == 'hex' else bytes(input, 'utf-8') print(calculate_query_bytes(input, 32)) else: print('error: hashing mode not available for query operation') else: # difficulty checking op if len(mode) == 1: # default mode, i.e. no hashing input = bytearray.fromhex(input) if mode[0] == 'hex' else bytes(input, 'utf-8') print('pass' if check(int(op[1]), input, bigendian) else 'fail') else: # hashing mode if 'shake' in mode[1]: input = new(mode[1], bytes(input, 'utf-8')).digest(len(input)) else: input = new(mode[1], bytes(input, 'utf-8')).digest() print('pass' if check(int(op[1]), input, bigendian) else 'fail') # cli if __name__ == '__main__': main(argv)