k98kurz icon

difficulty.py

k98kurz | PRO | 01/11/22 08:57:38 PM UTC | 0 ⭐ | 1526 👁️ | Never ⏰ | []
Python |

6.41 KB

|

None

|

0 👍

/

0 👎

"""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)
 

Comments