basic blockchain functionality incl. data hashing

This commit is contained in:
Florian Förster 2025-12-15 16:47:59 +01:00
parent 56be501a79
commit 05fa8548b0
3 changed files with 3447 additions and 2854 deletions

View File

@ -12,6 +12,7 @@ Options.embed_pos_in_docstring = False
Options.annotate = False Options.annotate = False
Options.fast_fail = True Options.fast_fail = True
OPEN_MP = False
DEBUG = bool(os.getenv("DOPT_DEBUG", None)) DEBUG = bool(os.getenv("DOPT_DEBUG", None))
linetrace_opt: bool = False linetrace_opt: bool = False
if DEBUG: if DEBUG:
@ -60,8 +61,11 @@ linker_args: list[str] = []
if sys.platform.startswith("win") and not DEBUG: if sys.platform.startswith("win") and not DEBUG:
c_args = ("/O2", "/GL", "/Gy", "/openmp:llvm") c_args = ["/O2", "/GL", "/Gy"]
l_args = ("/LTCG", "/OPT:REF", "/OPT:ICF", "/openmp:llvm") l_args = ["/LTCG", "/OPT:REF", "/OPT:ICF"]
if OPEN_MP:
c_args.extend(["/openmp:llvm"])
l_args.extend(["/openmp:llvm"])
else: else:
c_args = tuple() c_args = tuple()
l_args = tuple() l_args = tuple()

File diff suppressed because it is too large Load Diff

View File

@ -21,15 +21,21 @@ from libc.stdlib cimport malloc, free
from libc.string cimport memcpy from libc.string cimport memcpy
from cython.operator import postincrement, dereference from cython.operator import postincrement, dereference
cimport polluck_blockchain.openssl_evp as ossl cimport polluck_blockchain.openssl_evp as ossl
from cython.parallel cimport prange # from cython.parallel cimport prange
ctypedef unsigned long ULong ctypedef unsigned long ULong
ctypedef unordered_map[uint64_t, Block*] BcHashmap ctypedef unordered_map[uint64_t, Block*] BcHashmap
cdef const size_t NONCE_OFFSET = <size_t>16
cdef timestamp_to_datetime(uint64_t ts): cdef timestamp_to_datetime(uint64_t ts):
return datetime.datetime.fromtimestamp(float(ts), dopt_basics.datetime.TIMEZONE_UTC) return datetime.datetime.fromtimestamp(float(ts), dopt_basics.datetime.TIMEZONE_UTC)
cdef uint64_t current_timestamp_integer():
cdef uint64_t ts
dt = dopt_basics.datetime.current_time_tz(cut_microseconds=True)
ts = <uint64_t>int(dt.timestamp())
return ts
# cdef float_to_bytes(double num): # cdef float_to_bytes(double num):
# return struct.pack(">d", num) # return struct.pack(">d", num)
@ -50,15 +56,6 @@ cdef int serialize_uint64(unsigned char* out, unsigned long long v) except -1 no
out[6] = (v >> 8) & 0xFF out[6] = (v >> 8) & 0xFF
out[7] = v & 0xFF out[7] = v & 0xFF
# cdef struct SerializeResult:
# unsigned char *ptr
# size_t size
# cdef struct DigestResult:
# unsigned char *ptr
# size_t size
cdef const size_t NONCE_OFFSET = <size_t>16
cdef inline bint has_leading_zero_bits(const unsigned char *digest, int num_bits) nogil: cdef inline bint has_leading_zero_bits(const unsigned char *digest, int num_bits) nogil:
cdef int i, full_bytes = num_bits // 8 cdef int i, full_bytes = num_bits // 8
@ -141,22 +138,6 @@ cdef class PyBlock:
def __str__(self): def __str__(self):
return self.__repr__() return self.__repr__()
# cpdef _perform_hash(self):
# parts = bytearray()
# parts.extend(self._index.to_bytes(8, "big"))
# parts.extend(float_to_bytes(self._timestamp))
# parts.extend(self._data)
# parts.extend(self._prev_hash)
# parts.extend(self._nonce.to_bytes(8, "big"))
# return hashlib.sha256(parts)
# def compute_hash_bytes(self):
# return self._perform_hash().digest()
# def compute_hash(self):
# return self._perform_hash().hexdigest()
# Python public API # Python public API
@property @property
def index(self): def index(self):
@ -178,172 +159,159 @@ cdef class PyBlock:
def nonce(self): def nonce(self):
return self.BlockC.nonce return self.BlockC.nonce
# @nonce.setter
# def nonce(self, value: int):
# self._nonce = value
@property @property
def hash(self): def hash(self):
return self.BlockC.hash.decode("UTF-8") return self.BlockC.hash.decode("UTF-8")
cdef unsigned char* bytes_serialize_c(self, size_t *size) nogil:
cdef:
size_t total_len
unsigned char* buf
size_t pos = 0
# index (8), timestamp (8), nonce (8), data, prev_hash
size[0] = (
<size_t>(8 * 3) +
self.BlockC.data.size() +
self.BlockC.prev_hash.size()
)
buf = <unsigned char*>malloc(size[0] * sizeof(unsigned char))
if buf == NULL:
return NULL
serialize_uint64(buf + pos, self.BlockC.index)
pos += 8
serialize_uint64(buf + pos, self.BlockC.timestamp)
pos += 8
serialize_uint64(buf + pos, self.BlockC.nonce)
pos += 8
# Copy data
memcpy(
buf + pos,
self.BlockC.data.c_str(),
self.BlockC.data.size(),
)
pos += self.BlockC.data.size()
# Copy prev_hash
memcpy(
buf + pos,
self.BlockC.prev_hash.c_str(),
self.BlockC.prev_hash.size(),
)
pos += self.BlockC.prev_hash.size()
return buf
cdef unsigned char* digest(self, unsigned char *data, size_t data_size, size_t *digest_size) nogil:
cdef ossl.EVP_MD_CTX *ctx = ossl.EVP_MD_CTX_new()
if ctx == NULL:
return NULL
cdef const ossl.EVP_MD *algo = ossl.EVP_sha256()
if algo == NULL:
return NULL
cdef:
unsigned char* digest
size_t dig_buff_len
unsigned int digest_len
dig_buff_len = <size_t>ossl.EVP_MD_size(algo)
digest = <unsigned char*>malloc(dig_buff_len * sizeof(unsigned char))
ossl.EVP_DigestInit_ex(ctx, algo, NULL)
ossl.EVP_DigestUpdate(ctx, data, data_size)
ossl.EVP_DigestFinal_ex(ctx, digest, &digest_len)
digest_size[0] = dig_buff_len
ossl.EVP_MD_CTX_free(ctx)
return digest
def bytes_serialize(self): def bytes_serialize(self):
cdef: cdef:
unsigned char *serialize_res unsigned char *serialize_res
size_t serialize_size size_t serialize_size
try: try:
serialize_res = self.bytes_serialize_c(&serialize_size) serialize_res = bytes_serialize_c(self.BlockC, &serialize_size)
return serialize_res[:serialize_size] return serialize_res[:serialize_size]
finally: finally:
free(serialize_res) free(serialize_res)
cpdef mine(self, unsigned int difficulty, unsigned int max_nonce=0xFFFFFFFF):
cdef:
unsigned char *serial_buf
size_t serialize_size
unsigned char *digest
size_t digest_size
bint nonce_found = False
int nonce, nonce_solution = 0
serial_buf = self.bytes_serialize_c(&serialize_size)
with nogil:
for nonce in range(max_nonce):
serialize_uint64(serial_buf + NONCE_OFFSET, <uint64_t>nonce)
digest = self.digest(serial_buf, serialize_size, &digest_size)
if has_leading_zero_bits(digest, difficulty): def perform_hash(self):
nonce_found = True
nonce_solution = nonce
break
free(digest)
if not nonce_found:
raise RuntimeError("No valid nonce found")
self.BlockC.nonce = <uint64_t>nonce_solution
cdef unsigned char* perform_hash_c(self, size_t *digest_size) nogil:
cdef:
unsigned char *serialize_res
size_t serialize_size
unsigned char *digest
serialize_res = self.bytes_serialize_c(&serialize_size)
if serialize_res == NULL:
return NULL
digest = self.digest(serialize_res, serialize_size, digest_size)
if digest == NULL:
return NULL
free(serialize_res)
return digest
cpdef perform_hash(self):
cdef: cdef:
unsigned char *digest unsigned char *digest
size_t digest_size size_t digest_size
try: try:
digest = self.perform_hash_c(&digest_size) digest = perform_hash_c(self.BlockC, &digest_size)
if digest == NULL: if digest == NULL:
raise MemoryError() raise MemoryError()
# TODO out: hash assignment in blockchain
self.BlockC.hash = bytes(digest[:digest_size]).hex().encode("UTF-8") self.BlockC.hash = bytes(digest[:digest_size]).hex().encode("UTF-8")
finally: finally:
free(digest) free(digest)
# TODO rework
return self.hash return self.hash
cdef unsigned char* bytes_serialize_c(Block *block, size_t *size) nogil:
cdef:
size_t total_len
unsigned char* buf
size_t pos = 0
# index (8), timestamp (8), nonce (8), data, prev_hash
size[0] = (
<size_t>(8 * 3) +
block.data.size() +
block.prev_hash.size()
)
buf = <unsigned char*>malloc(size[0] * sizeof(unsigned char))
if buf == NULL:
return NULL
serialize_uint64(buf + pos, block.index)
pos += 8
serialize_uint64(buf + pos, block.timestamp)
pos += 8
serialize_uint64(buf + pos, block.nonce)
pos += 8
# Copy data
memcpy(
buf + pos,
block.data.c_str(),
block.data.size(),
)
pos += block.data.size()
# Copy prev_hash
memcpy(
buf + pos,
block.prev_hash.c_str(),
block.prev_hash.size(),
)
pos += block.prev_hash.size()
return buf
cdef unsigned char* SHA256_digest(const void *data, size_t data_size, size_t *digest_size) nogil:
cdef ossl.EVP_MD_CTX *ctx = ossl.EVP_MD_CTX_new()
if ctx == NULL:
return NULL
cdef const ossl.EVP_MD *algo = ossl.EVP_sha256()
if algo == NULL:
return NULL
cdef:
unsigned char* digest
size_t dig_buff_len
unsigned int digest_len
dig_buff_len = <size_t>ossl.EVP_MD_size(algo)
digest_size[0] = dig_buff_len
digest = <unsigned char*>malloc(dig_buff_len * sizeof(unsigned char))
ossl.EVP_DigestInit_ex(ctx, algo, NULL)
ossl.EVP_DigestUpdate(ctx, data, data_size)
ossl.EVP_DigestFinal_ex(ctx, digest, &digest_len)
ossl.EVP_MD_CTX_free(ctx)
return digest
cdef unsigned char* perform_hash_c(Block *block, size_t *digest_size) nogil:
cdef:
unsigned char *serialize_res
size_t serialize_size
unsigned char *digest
serialize_res = bytes_serialize_c(block, &serialize_size)
if serialize_res == NULL:
return NULL
digest = SHA256_digest(serialize_res, serialize_size, digest_size)
free(serialize_res)
if digest == NULL:
return NULL
return digest
# @hash.setter cdef int mine_block(Block *block, unsigned int difficulty, uint64_t *nonce_solution, unsigned int max_nonce=0xFFFFFFFF) nogil:
# def hash(self, value): cdef:
# if not isinstance(value, str): unsigned char *serial_buf
# raise TypeError("No string") size_t serialize_size
# self._hash = value.encode("UTF-8") unsigned char *digest
size_t digest_size
bint nonce_found = False
int nonce
# cdef void add_block(BcHashmap *map, Block *block): serial_buf = bytes_serialize_c(block, &serialize_size)
# map[block.index] = block
with nogil:
for nonce in range(max_nonce):
serialize_uint64(serial_buf + NONCE_OFFSET, <uint64_t>nonce)
digest = SHA256_digest(serial_buf, serialize_size, &digest_size)
if has_leading_zero_bits(digest, difficulty):
nonce_found = True
nonce_solution[0] = nonce
break
free(digest)
free(serial_buf)
if not nonce_found:
return 1
return 0
cdef class Blockchain: cdef class Blockchain:
cdef int _difficulty cdef unsigned int _difficulty
cdef uint64_t _index cdef uint64_t _index
cdef BcHashmap *_chain cdef BcHashmap *_chain
cdef bint _genesis_done cdef bint _genesis_done
def __cinit__(self): def __cinit__(self):
self._difficulty = 1 self._difficulty = 26
self._index = <uint64_t>0 self._index = <uint64_t>0
self._genesis_done = <bint>0 self._genesis_done = <bint>0
self._chain = new unordered_map[uint64_t, Block*]() self._chain = new unordered_map[uint64_t, Block*]()
@ -351,8 +319,9 @@ cdef class Blockchain:
raise MemoryError("Could not allocate hasmap") raise MemoryError("Could not allocate hasmap")
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# self.db_path = db_path
pass pass
def __dealloc__(self): def __dealloc__(self):
# ownership is typically not transferred from the Blockchain extension class # ownership is typically not transferred from the Blockchain extension class
cdef BcHashmap.iterator it = self._chain.begin() cdef BcHashmap.iterator it = self._chain.begin()
@ -367,6 +336,26 @@ cdef class Blockchain:
def __len__(self): def __len__(self):
return self._index + 1 return self._index + 1
@property
def difficulty(self):
return self._difficulty
@difficulty.setter
def difficulty(self, value):
if not isinstance(value, int):
raise TypeError("Difficulty must be integer value")
if value <= 0:
raise ValueError("Difficulty must be greater than 0")
self._difficulty = value
@property
def genesis_done(self):
return self._genesis_done
@property
def index(self):
return self._index
def print_key_value_pair(self): def print_key_value_pair(self):
cdef BcHashmap.iterator it = self._chain.begin() cdef BcHashmap.iterator it = self._chain.begin()
cdef Block *block cdef Block *block
@ -377,109 +366,101 @@ cdef class Blockchain:
print(py_block) print(py_block)
postincrement(it) postincrement(it)
@property cdef Block* get_block_c(self, uint64_t idx) nogil:
def genesis_done(self): if idx > self._index:
return self._genesis_done return NULL
@property
def index(self):
return self._index
# TODO error handling
cdef Block* get_block_c(self, uint64_t idx):
return self._chain[0][idx] return self._chain[0][idx]
cdef int add_block(self, Block *block) nogil:
cdef:
uint64_t mined_nonce
size_t digest_size
unsigned char *sha256_digest
# mine block
if mine_block(block, self._difficulty, &mined_nonce) != 0:
return 1
block.nonce = mined_nonce
# hash block, add hash to block, add block to blockchain hashmap
sha256_digest = perform_hash_c(block, &digest_size)
with gil:
block.hash = bytes(sha256_digest[:digest_size]).hex().encode("UTF-8")
free(sha256_digest)
self._chain[0][block.index] = block
if self._genesis_done:
self._index += 1
return 0
# // Python public API
def get_block(self, idx): def get_block(self, idx):
if idx < 0 or idx > self._index: if idx < 0 or idx > self._index:
raise IndexError("Index value is out of bounds") raise IndexError("Index value is out of bounds")
cdef Block *block = self.get_block_c(idx) cdef Block *block = self.get_block_c(idx)
if block == NULL:
raise IndexError("Provided index not found")
return PyBlock.from_ptr(block, owner=False) return PyBlock.from_ptr(block, owner=False)
cdef void add_block(self, Block *block): cdef string hash_data(self, data):
self._chain[0][block.index] = block cdef:
if self._genesis_done: string data_str
self._index += 1 unsigned char *data_digest
size_t digest_size
data_str = data.encode("UTF-8")
data_digest = SHA256_digest(data_str.c_str(), data_str.size(), &digest_size)
if data_digest == NULL:
raise RuntimeError("Failed to hash data")
data_str = bytes(data_digest[:digest_size]).hex().encode("UTF-8")
free(data_digest)
return data_str
def create_genesis_block(self): def create_genesis_block(self):
genesis_prev_hash = ("0" * 64).encode("UTF-8")
cdef string data_str = self.hash_data("Genesis Block")
cdef Block *block = new Block( cdef Block *block = new Block(
self._index, self._index,
int(datetime.datetime(2025, 12, 1, 12, 0, 0).timestamp()), int(datetime.datetime(2025, 12, 1, 12, 0, 0).timestamp()),
0, 0,
"Genesis Block".encode("UTF-8"), data_str,
"0".encode("UTF-8"), genesis_prev_hash,
"".encode("UTF-8"), "".encode("UTF-8"),
) )
block.hash = "dummy hash".encode("UTF-8") cdef int res = self.add_block(block)
self.add_block(block) if res != 0:
raise RuntimeError("Could not mine block. No nonce found")
self._genesis_done = True self._genesis_done = True
def new_block(self, data): def new_block(self, data):
cdef:
Block *prev_block
string prev_hash
uint64_t new_idx
string data_str
unsigned char *data_digest
size_t digest_size
if not self._genesis_done: if not self._genesis_done:
raise RuntimeError("Create a genesis block first.") raise RuntimeError("Create a genesis block first.")
if not isinstance(data, str):
raise TypeError("Data must be a string")
cdef Block *prev_block = self.get_block_c(self._index) data_str = self.hash_data(data)
cdef string prev_hash = prev_block.prev_hash prev_block = self.get_block_c(self._index)
cdef uint64_t new_idx = self._index + 1 prev_hash = prev_block.prev_hash
new_idx = self._index + 1
cdef Block *block = new Block( cdef Block *block = new Block(
new_idx, new_idx,
int(datetime.datetime(2025, 12, 1, 12, 0, 0).timestamp()), int(datetime.datetime(2025, 12, 1, 12, 0, 0).timestamp()),
0, 0,
data.encode("UTF-8"), data_str,
prev_hash, prev_hash,
"".encode("UTF-8"), "".encode("UTF-8"),
) )
self.add_block(block) cdef int res = self.add_block(block)
if res != 0:
raise RuntimeError("Could not mine block. No nonce found")
# def __init__(
# self,
# difficulty: int = 1,
# ) -> None:
# self._difficulty = difficulty
# @property
# def index(self):
# return self._index
# @property
# def difficulty(self):
# return self._difficulty
# cdef create_genesis_block(self):
# genesis = Block(
# index=0,
# data="Genesis Block",
# previous_hash="0",
# nonce=0,
# )
# genesis.hash = genesis.compute_hash()
# self.chain[self._index] = genesis
# cdef proof_of_work(self, block: Block):
# prefix = "0" * self._difficulty
# while True:
# block_hash = block.compute_hash()
# if block_hash.startswith(prefix):
# return block_hash
# block._nonce += 1
# cdef add_block(self, data: str):
# prev_hash = self.chain[self._index].hash
# new_block = Block(
# index=(self._index + 1),
# data=data,
# previous_hash=prev_hash,
# nonce=0,
# )
# # start = time.perf_counter()
# new_block.hash = self.proof_of_work(new_block)
# # elapsed = time.perf_counter() - start
# self.chain.append(new_block)
# self._index += 1
# print(f"Mined block {new_block.index} with nonce={new_block.nonce}")
# # print(f"Mined block {new_block.index} in {elapsed:.3f}s with nonce={new_block.nonce}")
# return new_block