#include #include #include #include #include #include #include uint64_t currentBlockHeight = 0; static bool EnsureDirectoryExists(const char* dirpath) { if (!dirpath || dirpath[0] == '\0') { return false; } struct stat st; if (stat(dirpath, &st) == 0) { return S_ISDIR(st.st_mode); } if (mkdir(dirpath, 0755) == 0) { return true; } if (errno == EEXIST && stat(dirpath, &st) == 0) { return S_ISDIR(st.st_mode); } return false; } static bool BuildPath(char* out, size_t outSize, const char* dirpath, const char* filename) { if (!out || outSize == 0 || !dirpath || !filename) { return false; } const int written = snprintf(out, outSize, "%s/%s", dirpath, filename); return written > 0 && (size_t)written < outSize; } static bool BuildSpendAmount(const signed_transaction_t* tx, uint256_t* outSpend) { if (!tx || !outSpend) { return false; } *outSpend = uint256_from_u64(0); if (uint256_add_u64(outSpend, tx->transaction.amount1)) { return false; } if (uint256_add_u64(outSpend, tx->transaction.amount2)) { return false; } if (uint256_add_u64(outSpend, tx->transaction.fee)) { return false; } return true; } static bool CreditAddress(const uint8_t address[32], uint64_t amount) { if (!address || amount == 0) { return true; } balance_sheet_entry_t entry; if (BalanceSheet_Lookup((uint8_t*)address, &entry)) { if (uint256_add_u64(&entry.balance, amount)) { return false; } } else { memset(&entry, 0, sizeof(entry)); memcpy(entry.address, address, 32); entry.balance = uint256_from_u64(amount); } return BalanceSheet_Insert(entry) >= 0; } static bool DebitAddress(const uint8_t address[32], const uint256_t* amount) { if (!address || !amount) { return false; } balance_sheet_entry_t entry; if (!BalanceSheet_Lookup((uint8_t*)address, &entry)) { return false; } if (uint256_cmp(&entry.balance, amount) < 0) { return false; } if (!uint256_subtract(&entry.balance, amount)) { return false; } return BalanceSheet_Insert(entry) >= 0; } static void Chain_ClearBlocks(blockchain_t* chain) { if (!chain || !chain->blocks) { return; } for (size_t i = 0; i < DynArr_size(chain->blocks); i++) { block_t* blk = (block_t*)DynArr_at(chain->blocks, i); if (blk && blk->transactions) { DynArr_destroy(blk->transactions); blk->transactions = NULL; } } DynArr_erase(chain->blocks); chain->size = 0; } blockchain_t* Chain_Create() { blockchain_t* ptr = (blockchain_t*)malloc(sizeof(blockchain_t)); if (!ptr) { return NULL; } ptr->blocks = DYNARR_CREATE(block_t, 1); ptr->size = 0; return ptr; } void Chain_Destroy(blockchain_t* chain) { if (chain) { if (chain->blocks) { Chain_ClearBlocks(chain); DynArr_destroy(chain->blocks); } free(chain); } } bool Chain_AddBlock(blockchain_t* chain, block_t* block) { bool ok = true; if (!chain || !block || !chain->blocks) { return false; } if (!block->transactions) { return false; } // Acquire global write locks to protect chain and balance sheet mutations. pthread_rwlock_wrlock(&chainLock); pthread_mutex_lock(&balanceSheetLock); // Ensure the incoming block's header.blockNumber matches the index it will be appended at. size_t expectedIndex = DynArr_size(chain->blocks); if (block->header.blockNumber != expectedIndex) { // Mismatched block number; reject to avoid duplicate indices or inconsistent headers. pthread_mutex_unlock(&balanceSheetLock); pthread_rwlock_unlock(&chainLock); return false; } do { // First pass: ensure all non-coinbase senders can cover the full spend // (amount1 + amount2 + fee) before mutating the chain or balance sheet. size_t txCount = DynArr_size(block->transactions); for (size_t i = 0; i < txCount; ++i) { signed_transaction_t* tx = (signed_transaction_t*)DynArr_at(block->transactions, i); if (!tx) { ok = false; break; } if (Address_IsCoinbase(tx->transaction.senderAddress)) { continue; } uint256_t spend; if (!BuildSpendAmount(tx, &spend)) { ok = false; break; } balance_sheet_entry_t senderEntry; if (!BalanceSheet_Lookup(tx->transaction.senderAddress, &senderEntry)) { fprintf(stderr, "Error: Sender address not found in balance sheet during block addition. Bailing!\n"); ok = false; break; } if (uint256_cmp(&senderEntry.balance, &spend) < 0) { fprintf(stderr, "Error: Sender balance insufficient for block transaction. Bailing!\n"); ok = false; break; } } if (!ok) break; // Push the block only after validation succeeds. block_t* blk = (block_t*)DynArr_push_back(chain->blocks, block); if (!blk) { ok = false; break; } chain->size++; currentBlockHeight = (uint64_t)(chain->size - 1); // Second pass: apply the ledger changes. if (blk->transactions) { txCount = DynArr_size(blk->transactions); for (size_t i = 0; i < txCount; ++i) { signed_transaction_t* tx = (signed_transaction_t*)DynArr_at(blk->transactions, i); if (!tx) { continue; } if (!Address_IsCoinbase(tx->transaction.senderAddress)) { uint256_t spend; if (!BuildSpendAmount(tx, &spend) || !DebitAddress(tx->transaction.senderAddress, &spend)) { fprintf(stderr, "Error: Failed to debit sender balance during block addition. Bailing!\n"); ok = false; break; } } if (!CreditAddress(tx->transaction.recipientAddress1, tx->transaction.amount1)) { fprintf(stderr, "Error: Failed to credit recipient1 balance during block addition. Bailing!\n"); ok = false; break; } if (tx->transaction.amount2 > 0) { uint8_t zeroAddress[32] = {0}; if (memcmp(tx->transaction.recipientAddress2, zeroAddress, 32) == 0) { fprintf(stderr, "Error: amount2 is non-zero but recipient2 is empty during block addition. Bailing!\n"); ok = false; break; } if (!CreditAddress(tx->transaction.recipientAddress2, tx->transaction.amount2)) { fprintf(stderr, "Error: Failed to credit recipient2 balance during block addition. Bailing!\n"); ok = false; break; } } } } // ok remains true if no failures } while (0); // Release locks pthread_mutex_unlock(&balanceSheetLock); pthread_rwlock_unlock(&chainLock); printf("Added new block to chain:\n"); Block_ShortPrint(block); return ok; } block_t* Chain_GetBlock(blockchain_t* chain, size_t index) { if (!chain) return NULL; block_t* blk = NULL; pthread_rwlock_rdlock(&chainLock); blk = (block_t*)DynArr_at(chain->blocks, index); pthread_rwlock_unlock(&chainLock); return blk; } bool Chain_GetBlockCopy(blockchain_t* chain, size_t index, block_t** outCopy) { if (!chain || !outCopy) return false; *outCopy = NULL; pthread_rwlock_rdlock(&chainLock); block_t* src = (block_t*)DynArr_at(chain->blocks, index); if (!src) { pthread_rwlock_unlock(&chainLock); return false; } block_t* copy = Block_Copy(src); pthread_rwlock_unlock(&chainLock); if (!copy) return false; *outCopy = copy; return true; } size_t Chain_Size(blockchain_t* chain) { if (!chain) return 0; size_t sz = 0; pthread_rwlock_rdlock(&chainLock); sz = DynArr_size(chain->blocks); pthread_rwlock_unlock(&chainLock); return sz; } bool Chain_IsValid(blockchain_t* chain) { if (!chain || !chain->blocks) { return false; } const size_t chainSize = DynArr_size(chain->blocks); if (chainSize == 0) { return true; } for (size_t i = 1; i < chainSize; i++) { block_t* blk = (block_t*)DynArr_at(chain->blocks, i); block_t* prevBlk = (block_t*)DynArr_at(chain->blocks, i - 1); if (!blk || !prevBlk || blk->header.blockNumber != i) { return false; } // NULL blocks or blockNumber != order in chain // Verify prevHash is valid uint8_t prevHash[32]; Block_CalculateHash(prevBlk, prevHash); if (memcmp(blk->header.prevHash, prevHash, 32) != 0) { return false; } // A potential issue is verifying PoW, since the chain read might only have header data without transactions. // A potnetial fix is verifying PoW as we go, when getting new blocks from peers, and only accepting blocks // with valid PoW, so that we can assume all blocks in the chain are valid in that regard. // During the initial sync, we can verify the PoW, the validity of each transaction + coinbase, etc. } // Genesis needs special handling because the prevHash is always invalid (no previous block) block_t* genesis = (block_t*)DynArr_at(chain->blocks, 0); if (!genesis || genesis->header.blockNumber != 0) { return false; } return true; } bool Chain_RollbackToHeight(blockchain_t* chain, size_t height) { if (!chain || !chain->blocks) return false; pthread_rwlock_wrlock(&chainLock); pthread_mutex_lock(&balanceSheetLock); size_t cur = DynArr_size(chain->blocks); if (height >= cur) { pthread_mutex_unlock(&balanceSheetLock); pthread_rwlock_unlock(&chainLock); return true; // nothing to do } // Remove blocks above height for (size_t i = cur; i > height; --i) { size_t idx = i - 1; block_t* blk = (block_t*)DynArr_at(chain->blocks, idx); if (blk && blk->transactions) { DynArr_destroy(blk->transactions); blk->transactions = NULL; } DynArr_remove(chain->blocks, idx); } chain->size = DynArr_size(chain->blocks); currentBlockHeight = chain->size ? (uint64_t)(chain->size - 1) : 0ULL; // Rebuild balance sheet from scratch up to current chain size BalanceSheet_Destroy(); BalanceSheet_Init(); for (size_t i = 0; i < chain->size; ++i) { block_t* blk = (block_t*)DynArr_at(chain->blocks, i); block_t* toProcess = blk; bool loaded = false; if (!blk || !blk->transactions) { // Try to load from disk block_t* loadedBlk = NULL; size_t txCount = 0; if (!Chain_LoadBlockFromFile(chainDataDir, (uint64_t)i, true, &loadedBlk, &txCount)) { // Can't rebuild without transactions pthread_mutex_unlock(&balanceSheetLock); pthread_rwlock_unlock(&chainLock); return false; } toProcess = loadedBlk; loaded = true; } // Apply transactions if (toProcess && toProcess->transactions) { size_t txCount = DynArr_size(toProcess->transactions); for (size_t ti = 0; ti < txCount; ++ti) { signed_transaction_t* tx = (signed_transaction_t*)DynArr_at(toProcess->transactions, ti); if (!tx) continue; // Coinbase credit if (Address_IsCoinbase(tx->transaction.senderAddress)) { balance_sheet_entry_t entry; if (!BalanceSheet_Lookup(tx->transaction.recipientAddress1, &entry)) { memset(&entry, 0, sizeof(entry)); memcpy(entry.address, tx->transaction.recipientAddress1, 32); entry.balance = uint256_from_u64(tx->transaction.amount1); } else { (void)uint256_add_u64(&entry.balance, tx->transaction.amount1); } (void)BalanceSheet_Insert(entry); continue; } // Non-coinbase: debit sender uint256_t spend = uint256_from_u64(0); (void)uint256_add_u64(&spend, tx->transaction.amount1); if (tx->transaction.amount2 > 0) (void)uint256_add_u64(&spend, tx->transaction.amount2); if (tx->transaction.fee > 0) (void)uint256_add_u64(&spend, tx->transaction.fee); balance_sheet_entry_t senderEntry; if (!BalanceSheet_Lookup(tx->transaction.senderAddress, &senderEntry)) { // Missing sender; create zero and then subtract (will underflow if invalid) memset(&senderEntry, 0, sizeof(senderEntry)); memcpy(senderEntry.address, tx->transaction.senderAddress, 32); senderEntry.balance = uint256_from_u64(0); } (void)uint256_subtract(&senderEntry.balance, &spend); (void)BalanceSheet_Insert(senderEntry); // Credit recipient1 balance_sheet_entry_t rec1; if (!BalanceSheet_Lookup(tx->transaction.recipientAddress1, &rec1)) { memset(&rec1, 0, sizeof(rec1)); memcpy(rec1.address, tx->transaction.recipientAddress1, 32); rec1.balance = uint256_from_u64(tx->transaction.amount1); } else { (void)uint256_add_u64(&rec1.balance, tx->transaction.amount1); } (void)BalanceSheet_Insert(rec1); // Credit recipient2 if any if (tx->transaction.amount2 > 0) { balance_sheet_entry_t rec2; if (!BalanceSheet_Lookup(tx->transaction.recipientAddress2, &rec2)) { memset(&rec2, 0, sizeof(rec2)); memcpy(rec2.address, tx->transaction.recipientAddress2, 32); rec2.balance = uint256_from_u64(tx->transaction.amount2); } else { (void)uint256_add_u64(&rec2.balance, tx->transaction.amount2); } (void)BalanceSheet_Insert(rec2); } } } if (loaded && toProcess) { if (toProcess->transactions) DynArr_destroy(toProcess->transactions); free(toProcess); } } pthread_mutex_unlock(&balanceSheetLock); pthread_rwlock_unlock(&chainLock); return true; } void Chain_Wipe(blockchain_t* chain) { Chain_ClearBlocks(chain); currentBlockHeight = 0; } bool Chain_SaveToFile(blockchain_t* chain, const char* dirpath, uint256_t currentSupply, uint64_t currentReward) { // To avoid stalling the chain from peers, write after every block addition (THAT IS VERIFIED) // TODO: Check fwrite() and fread() calls if they actually didn't error if (!chain || !chain->blocks || !EnsureDirectoryExists(dirpath)) { return false; } char metaPath[512]; if (!BuildPath(metaPath, sizeof(metaPath), dirpath, "chain.meta")) { return false; } char chainPath[512]; if (!BuildPath(chainPath, sizeof(chainPath), dirpath, "chain.data")) { return false; } char tablePath[512]; if (!BuildPath(tablePath, sizeof(tablePath), dirpath, "chain.table")) { return false; } char metaTmpPath[512]; char chainTmpPath[512]; char tableTmpPath[512]; if (!BuildPath(metaTmpPath, sizeof(metaTmpPath), dirpath, "chain.meta.tmp") || !BuildPath(chainTmpPath, sizeof(chainTmpPath), dirpath, "chain.data.tmp") || !BuildPath(tableTmpPath, sizeof(tableTmpPath), dirpath, "chain.table.tmp")) { return false; } pthread_rwlock_wrlock(&chainLock); FILE* metaFile = fopen(metaTmpPath, "wb+"); FILE* chainFile = fopen(chainTmpPath, "wb+"); FILE* tableFile = fopen(tableTmpPath, "wb+"); if (!metaFile || !chainFile || !tableFile) { if (metaFile) fclose(metaFile); if (chainFile) fclose(chainFile); if (tableFile) fclose(tableFile); pthread_rwlock_unlock(&chainLock); remove(metaTmpPath); remove(chainTmpPath); remove(tableTmpPath); return false; } const size_t chainSize = DynArr_size(chain->blocks); uint64_t byteCount = 0; for (size_t i = 0; i < chainSize; ++i) { block_t* blk = (block_t*)DynArr_at(chain->blocks, i); if (!blk) { fclose(metaFile); fclose(chainFile); fclose(tableFile); pthread_rwlock_unlock(&chainLock); remove(metaTmpPath); remove(chainTmpPath); remove(tableTmpPath); return false; } block_t* diskCopy = blk; bool loadedTemp = false; if (!diskCopy->transactions) { if (!Chain_LoadBlockFromFile(dirpath, (uint64_t)i, true, &diskCopy, NULL) || !diskCopy || !diskCopy->transactions) { if (loadedTemp && diskCopy) { Block_Destroy(diskCopy); } fclose(metaFile); fclose(chainFile); fclose(tableFile); pthread_rwlock_unlock(&chainLock); remove(metaTmpPath); remove(chainTmpPath); remove(tableTmpPath); return false; } loadedTemp = true; } const uint64_t blockStart = byteCount; if (fwrite(&diskCopy->header, sizeof(block_header_t), 1, chainFile) != 1) { if (loadedTemp) Block_Destroy(diskCopy); fclose(metaFile); fclose(chainFile); fclose(tableFile); pthread_rwlock_unlock(&chainLock); remove(metaTmpPath); remove(chainTmpPath); remove(tableTmpPath); return false; } const size_t txSize = DynArr_size(diskCopy->transactions); if (fwrite(&txSize, sizeof(size_t), 1, chainFile) != 1) { if (loadedTemp) Block_Destroy(diskCopy); fclose(metaFile); fclose(chainFile); fclose(tableFile); pthread_rwlock_unlock(&chainLock); remove(metaTmpPath); remove(chainTmpPath); remove(tableTmpPath); return false; } byteCount += sizeof(block_header_t) + sizeof(size_t); for (size_t j = 0; j < txSize; ++j) { signed_transaction_t* tx = (signed_transaction_t*)DynArr_at(diskCopy->transactions, j); if (!tx || fwrite(tx, sizeof(signed_transaction_t), 1, chainFile) != 1) { if (loadedTemp) Block_Destroy(diskCopy); fclose(metaFile); fclose(chainFile); fclose(tableFile); pthread_rwlock_unlock(&chainLock); remove(metaTmpPath); remove(chainTmpPath); remove(tableTmpPath); return false; } byteCount += sizeof(signed_transaction_t); } block_table_entry_t entry; entry.blockNumber = i; entry.byteNumber = blockStart; entry.blockSize = byteCount - blockStart; if (fwrite(&entry, sizeof(block_table_entry_t), 1, tableFile) != 1) { if (loadedTemp) Block_Destroy(diskCopy); fclose(metaFile); fclose(chainFile); fclose(tableFile); pthread_rwlock_unlock(&chainLock); remove(metaTmpPath); remove(chainTmpPath); remove(tableTmpPath); return false; } if (loadedTemp) { Block_Destroy(diskCopy); } else if (blk->transactions) { DynArr_destroy(blk->transactions); blk->transactions = NULL; } } size_t newSize = chainSize; fseek(metaFile, 0, SEEK_SET); fwrite(&newSize, sizeof(size_t), 1, metaFile); uint32_t difficultyTarget = INITIAL_DIFFICULTY; if (newSize > 0) { block_t* lastBlock = (block_t*)DynArr_at(chain->blocks, newSize - 1); uint8_t lastHash[32]; Block_CalculateHash(lastBlock, lastHash); fwrite(lastHash, sizeof(uint8_t), 32, metaFile); difficultyTarget = lastBlock->header.difficultyTarget; } else { uint8_t zeroHash[32] = {0}; fwrite(zeroHash, sizeof(uint8_t), 32, metaFile); } fwrite(¤tSupply, sizeof(uint256_t), 1, metaFile); fwrite(&difficultyTarget, sizeof(uint32_t), 1, metaFile); fwrite(¤tReward, sizeof(uint64_t), 1, metaFile); fflush(metaFile); fflush(chainFile); fflush(tableFile); fclose(metaFile); fclose(chainFile); fclose(tableFile); if (rename(metaTmpPath, metaPath) != 0 || rename(chainTmpPath, chainPath) != 0 || rename(tableTmpPath, tablePath) != 0) { pthread_rwlock_unlock(&chainLock); remove(metaTmpPath); remove(chainTmpPath); remove(tableTmpPath); return false; } pthread_rwlock_unlock(&chainLock); return true; } bool Chain_LoadFromFile(blockchain_t* chain, const char* dirpath, uint256_t* outCurrentSupply, uint32_t* outDifficultyTarget, uint64_t* outCurrentReward, uint8_t* outLastSavedHash, bool loadTransactions) { if (!chain || !chain->blocks || !dirpath || !outCurrentSupply || !outLastSavedHash) { return false; } struct stat st; if (stat(dirpath, &st) != 0 || !S_ISDIR(st.st_mode)) { return false; } char metaPath[512]; if (!BuildPath(metaPath, sizeof(metaPath), dirpath, "chain.meta")) { return false; } char chainPath[512]; if (!BuildPath(chainPath, sizeof(chainPath), dirpath, "chain.data")) { return false; } char tablePath[512]; if (!BuildPath(tablePath, sizeof(tablePath), dirpath, "chain.table")) { return false; } // Read metadata file to get saved chain size (+ other things) FILE* metaFile = fopen(metaPath, "rb+"); FILE* chainFile = fopen(chainPath, "rb+"); FILE* tableFile = fopen(tablePath, "rb+"); if (!metaFile || !chainFile || !tableFile) { if (metaFile) fclose(metaFile); if (chainFile) fclose(chainFile); if (tableFile) fclose(tableFile); return false; } size_t savedSize = 0; if (fread(&savedSize, sizeof(size_t), 1, metaFile) != 1) { fclose(metaFile); fclose(chainFile); fclose(tableFile); return false; } if (fread(outLastSavedHash, sizeof(uint8_t), 32, metaFile) != 32) { fclose(metaFile); fclose(chainFile); fclose(tableFile); return false; } if (fread(outCurrentSupply, sizeof(uint256_t), 1, metaFile) != 1) { fclose(metaFile); fclose(chainFile); fclose(tableFile); return false; } if (fread(outDifficultyTarget, sizeof(uint32_t), 1, metaFile) != 1) { fclose(metaFile); fclose(chainFile); fclose(tableFile); return false; } if (fread(outCurrentReward, sizeof(uint64_t), 1, metaFile) != 1) { fclose(metaFile); fclose(chainFile); fclose(tableFile); return false; } fclose(metaFile); // TODO: Might add a flag to allow reading from a point onward, but just rewrite for now Chain_ClearBlocks(chain); // Clear current chain blocks and free owned transaction buffers before reload. // Load blocks for (size_t i = 0; i < savedSize; i++) { // Get the table entry //fseek(tableFile, sizeof(block_table_entry_t) * i, SEEK_SET); // I think that fread() should take care of this for us block_table_entry_t loc; if (fread(&loc, sizeof(block_table_entry_t), 1, tableFile) != 1) { fclose(chainFile); fclose(tableFile); return false; } if (loc.blockNumber != i) { fclose(chainFile); fclose(tableFile); return false; // Mismatch } // Seek to that position if (fseek(chainFile, loc.byteNumber, SEEK_SET) != 0) { fclose(chainFile); fclose(tableFile); return false; } block_t* blk = (block_t*)calloc(1, sizeof(block_t)); if (!blk) { fclose(chainFile); fclose(tableFile); return false; } // Read block header and transactions if (fread(&blk->header, sizeof(block_header_t), 1, chainFile) != 1) { fclose(chainFile); fclose(tableFile); free(blk); return false; } size_t txSize = 0; if (fread(&txSize, sizeof(size_t), 1, chainFile) != 1) { fclose(chainFile); fclose(tableFile); free(blk); return false; } if (loadTransactions) { blk->transactions = DYNARR_CREATE(signed_transaction_t, txSize == 0 ? 1 : txSize); if (!blk->transactions) { fclose(chainFile); fclose(tableFile); free(blk); return false; } for (size_t j = 0; j < txSize; j++) { signed_transaction_t tx; if (fread(&tx, sizeof(signed_transaction_t), 1, chainFile) != 1) { fclose(chainFile); fclose(tableFile); DynArr_destroy(blk->transactions); free(blk); return false; } if (!DynArr_push_back(blk->transactions, &tx)) { fclose(chainFile); fclose(tableFile); DynArr_destroy(blk->transactions); free(blk); return false; } } } else { if (txSize > 0 && fseek(chainFile, (long)(txSize * sizeof(signed_transaction_t)), SEEK_CUR) != 0) { fclose(chainFile); fclose(tableFile); free(blk); return false; } blk->transactions = NULL; } // Loading from disk currently restores headers only. Do not run Chain_AddBlock, // because it enforces transaction presence and mutates balances. if (!DynArr_push_back(chain->blocks, blk)) { fclose(chainFile); fclose(tableFile); free(blk); return false; } chain->size++; // DynArr_push_back stores blocks by value, so the copied block now owns // blk->transactions. Free wrapper only. free(blk); } chain->size = savedSize; fclose(chainFile); fclose(tableFile); // After read, you SHOULD verify chain validity. We're not doing it here since returning false is a bit unclear if the read failed or if the chain is invalid. return true; } bool Chain_LoadBlockFromFile(const char* dirpath, uint64_t blockNumber, bool loadTransactions, block_t** outBlock, size_t* outTxCount) { if (!dirpath || !outBlock) { return false; } *outBlock = NULL; if (outTxCount) { *outTxCount = 0; } struct stat st; if (stat(dirpath, &st) != 0 || !S_ISDIR(st.st_mode)) { return false; } char metaPath[512]; if (!BuildPath(metaPath, sizeof(metaPath), dirpath, "chain.meta")) { return false; } char chainPath[512]; if (!BuildPath(chainPath, sizeof(chainPath), dirpath, "chain.data")) { return false; } char tablePath[512]; if (!BuildPath(tablePath, sizeof(tablePath), dirpath, "chain.table")) { return false; } FILE* metaFile = fopen(metaPath, "rb"); FILE* chainFile = fopen(chainPath, "rb"); FILE* tableFile = fopen(tablePath, "rb"); if (!metaFile || !chainFile || !tableFile) { if (metaFile) fclose(metaFile); if (chainFile) fclose(chainFile); if (tableFile) fclose(tableFile); return false; } size_t savedSize = 0; if (fread(&savedSize, sizeof(size_t), 1, metaFile) != 1) { fclose(metaFile); fclose(chainFile); fclose(tableFile); return false; } fclose(metaFile); if (blockNumber >= (uint64_t)savedSize) { fclose(chainFile); fclose(tableFile); return false; } uint64_t tableOffset = blockNumber * (uint64_t)sizeof(block_table_entry_t); if (blockNumber != 0 && tableOffset / blockNumber != (uint64_t)sizeof(block_table_entry_t)) { fclose(chainFile); fclose(tableFile); return false; } if (tableOffset > (uint64_t)LONG_MAX || fseek(tableFile, (long)tableOffset, SEEK_SET) != 0) { fclose(chainFile); fclose(tableFile); return false; } block_table_entry_t loc; if (fread(&loc, sizeof(block_table_entry_t), 1, tableFile) != 1) { fclose(chainFile); fclose(tableFile); return false; } if (loc.blockNumber != blockNumber) { fclose(chainFile); fclose(tableFile); return false; } if (loc.byteNumber > (uint64_t)LONG_MAX || fseek(chainFile, (long)loc.byteNumber, SEEK_SET) != 0) { fclose(chainFile); fclose(tableFile); return false; } block_t* blk = (block_t*)calloc(1, sizeof(block_t)); if (!blk) { fclose(chainFile); fclose(tableFile); return false; } if (fread(&blk->header, sizeof(block_header_t), 1, chainFile) != 1) { free(blk); fclose(chainFile); fclose(tableFile); return false; } size_t txSize = 0; if (fread(&txSize, sizeof(size_t), 1, chainFile) != 1) { free(blk); fclose(chainFile); fclose(tableFile); return false; } if (outTxCount) { *outTxCount = txSize; } if (loadTransactions) { blk->transactions = DYNARR_CREATE(signed_transaction_t, txSize == 0 ? 1 : txSize); if (!blk->transactions) { free(blk); fclose(chainFile); fclose(tableFile); return false; } for (size_t i = 0; i < txSize; ++i) { signed_transaction_t tx; if (fread(&tx, sizeof(signed_transaction_t), 1, chainFile) != 1) { DynArr_destroy(blk->transactions); free(blk); fclose(chainFile); fclose(tableFile); return false; } if (!DynArr_push_back(blk->transactions, &tx)) { DynArr_destroy(blk->transactions); free(blk); fclose(chainFile); fclose(tableFile); return false; } } } else { if (txSize > 0) { uint64_t skipBytes = (uint64_t)txSize * (uint64_t)sizeof(signed_transaction_t); if (txSize != 0 && skipBytes / txSize != (uint64_t)sizeof(signed_transaction_t)) { free(blk); fclose(chainFile); fclose(tableFile); return false; } if (skipBytes > (uint64_t)LONG_MAX) { free(blk); fclose(chainFile); fclose(tableFile); return false; } if (fseek(chainFile, (long)skipBytes, SEEK_CUR) != 0) { free(blk); fclose(chainFile); fclose(tableFile); return false; } } blk->transactions = NULL; } fclose(chainFile); fclose(tableFile); *outBlock = blk; return true; } uint32_t Chain_ComputeNextTarget(blockchain_t* chain, uint32_t currentTarget) { if (!chain || !chain->blocks) { return 0x00; // Impossible difficulty, only valid hash is all zeros (practically impossible) } size_t chainSize = DynArr_size(chain->blocks); if (chainSize < DIFFICULTY_ADJUSTMENT_INTERVAL) { // Baby-chain, return initial difficulty return INITIAL_DIFFICULTY; } // Assuming block validation validates timestamps, we can assume they're valid and can just read them block_t* lastBlock = (block_t*)DynArr_at(chain->blocks, chainSize - 1); block_t* adjustmentBlock = (block_t*)DynArr_at(chain->blocks, chainSize - DIFFICULTY_ADJUSTMENT_INTERVAL); if (!lastBlock || !adjustmentBlock) { return 0x00; // Impossible difficulty, only valid hash is all zeros (practically impossible) } // Retarget uses whole-window span. Per-block average is implicit: // (actualTime / interval) / targetBlockTime == actualTime / targetTime. // Block timestamps are stored in milliseconds, so the target window must be ms too. uint64_t actualTime = 0; if (lastBlock->header.timestamp > adjustmentBlock->header.timestamp) { actualTime = lastBlock->header.timestamp - adjustmentBlock->header.timestamp; } if (actualTime == 0) { return currentTarget; // Invalid/non-increasing time window; keep current target } const uint64_t targetTime = (uint64_t)TARGET_BLOCK_TIME * 1000ULL * (uint64_t)DIFFICULTY_ADJUSTMENT_INTERVAL; double timeRatio = (double)actualTime / (double)targetTime; // Clamp per-epoch target movement: at most x2 easier or x2 harder. TODO: Check if the clamp should be more aggressive or looser if (timeRatio > 2.0) { timeRatio = 2.0; } else if (timeRatio < 0.5) { timeRatio = 0.5; } uint32_t exponent = currentTarget >> 24; uint32_t mantissa = currentTarget & 0x007fffff; if (mantissa == 0 || exponent == 0) { return INITIAL_DIFFICULTY; } double newMantissa = (double)mantissa * timeRatio; // Normalize to compact format range. while (newMantissa > 8388607.0) { // 0x007fffff newMantissa /= 256.0; exponent++; } while (newMantissa > 0.0 && newMantissa < 32768.0 && exponent > 3) { // Keep coefficient in normal range newMantissa *= 256.0; exponent--; } if (exponent > 32) { // Easiest representable target in our decoder range. return (32u << 24) | 0x007fffff; } if (exponent < 1) { exponent = 1; } uint32_t newCoeff = (uint32_t)newMantissa; if (newCoeff == 0) { newCoeff = 1; } if (newCoeff > 0x007fffff) { newCoeff = 0x007fffff; } return (exponent << 24) | (newCoeff & 0x007fffff); }