Last active
December 30, 2023 11:28
-
-
Save M0r13n/f8fc71bc57b475c9841a663da57ec504 to your computer and use it in GitHub Desktop.
Yet anonther Hascash implementation in modern Python 3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import base64 | |
import datetime | |
import hashlib | |
import string | |
import random | |
YYMMDDhhmm = '%y%m%d%H%M' | |
YYMMDDhhmmss = '%y%m%d%H%M%S' | |
ASCII_LETTERS = string.ascii_letters | |
DEFAULT_SALT_LENGTH = 8 | |
def to_base64(v: object) -> bytes: | |
"""Encode any object as base64""" | |
return base64.b64encode(str(v).encode()) | |
def normalize_challenge(challenge: str) -> str: | |
"""Suffix the challenge with a trailing ':'""" | |
if not challenge.endswith(':'): | |
challenge += ':' | |
return challenge | |
def sha160(challenge: bytes) -> bytes: | |
"""Compute the SHA1 160-bit hash""" | |
return hashlib.sha1(challenge).digest() | |
def are_first_n_bits_zero(byte_string: bytes, n: int) -> bool: | |
byte_index = (n // 8) # Calculate the byte index of the Nth bit | |
bit_offset = 8 - (n % 8) # Calculate the bit offset within that byte | |
# Check if all the bits before the Nth bit are zero | |
if any(byte_string[:byte_index]): | |
return False | |
# Check if the bits within the Nth byte before the Nth bit are zero | |
if (byte_string[byte_index] >> bit_offset) == 0: | |
return True | |
return False | |
def mint(challenge: str, bits: int) -> str: | |
"""Generic mint function that works for any proof of work algorithm. | |
Searches for a SHA1 hash with N trailing zeros where N is the number of bits. | |
Returns the counter that was found while searching encoded in base64.""" | |
counter = 0 | |
challenge = normalize_challenge(challenge) | |
encoded_challenge = challenge.encode() | |
while True: | |
digest = sha160(encoded_challenge + to_base64(counter)) | |
if are_first_n_bits_zero(digest, bits): | |
return to_base64(counter).decode() | |
counter += 1 | |
def check( | |
challenge: str, date_callback=None, resource_callback=None, | |
double_spent_callback=None) -> bool: | |
"""Check a hashcash challenge. | |
Pass callbacks to verify date, resource, and double-spent tokens. | |
By default, it is only verified that the first N bits are zero.""" | |
_, bits, date, resource, _, rand, _ = challenge.split(':') | |
digest = hashlib.sha1(challenge.encode()).digest() | |
ok = are_first_n_bits_zero(digest, int(bits)) | |
if date_callback: | |
ok &= date_callback(date) | |
if resource_callback: | |
ok &= resource_callback(resource) | |
if double_spent_callback: | |
ok &= double_spent_callback(rand) | |
return ok | |
def salt(l: int) -> str: | |
"""Return a random string of length 'l'""" | |
return ''.join([random.choice(ASCII_LETTERS) for _ in range(l)]) | |
def hashcash( | |
ver: int, bits: int, resource: str, ts: datetime.datetime | None, | |
ext: str | None, rand: str | None, with_seconds: bool = True | |
): | |
"""Create a hashcash header for a given resource. | |
ts defaults to datetime.now(). | |
Dates are formatted as YYMMDDhhmmss by default. | |
Extensions are omitted by default. | |
A random salt of 8 chars is computed by default.""" | |
if ts is None: | |
ts = datetime.datetime.now() | |
if with_seconds: | |
date = ts.strftime(YYMMDDhhmmss) | |
else: | |
date = ts.strftime(YYMMDDhhmm) | |
if rand is None: | |
rand = salt(DEFAULT_SALT_LENGTH) | |
if ext is None: | |
ext = '' | |
challenge = ":".join(map(str, (ver, bits, date, resource, ext, rand))) | |
return ":".join((challenge, mint(challenge, bits))) | |
if __name__ == '__main__': | |
ok = check('1:24:040806:foo::511801694b4cd6b0:1e7297a') | |
print(ok) | |
ok = check('1:20:231223:foo::oga8OEEzi2prfwG6:000000000000000nQk') | |
print(ok) | |
result = hashcash( | |
1, 20, 'anni@cypherspace.org', datetime.datetime.strptime('130303060000', YYMMDDhhmmss), | |
None, 'McMybZIhxKXu57jd', with_seconds=False | |
) | |
ok = check(result) | |
print(ok, result) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment