Skip to content

Instantly share code, notes, and snippets.

@M0r13n
Last active December 30, 2023 11:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save M0r13n/f8fc71bc57b475c9841a663da57ec504 to your computer and use it in GitHub Desktop.
Save M0r13n/f8fc71bc57b475c9841a663da57ec504 to your computer and use it in GitHub Desktop.
Yet anonther Hascash implementation in modern Python 3
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