#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) { if (!chain || !block || !chain->blocks) { return false; } if (!block->transactions) { return false; } // 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) { return false; } if (Address_IsCoinbase(tx->transaction.senderAddress)) { continue; } uint256_t spend; if (!BuildSpendAmount(tx, &spend)) { return false; } 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"); return false; } if (uint256_cmp(&senderEntry.balance, &spend) < 0) { fprintf(stderr, "Error: Sender balance insufficient for block transaction. Bailing!\n"); return false; } } // Push the block only after validation succeeds. block_t* blk = (block_t*)DynArr_push_back(chain->blocks, block); if (!blk) { return false; } 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"); return false; } } if (!CreditAddress(tx->transaction.recipientAddress1, tx->transaction.amount1)) { fprintf(stderr, "Error: Failed to credit recipient1 balance during block addition. Bailing!\n"); return false; } 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"); return false; } if (!CreditAddress(tx->transaction.recipientAddress2, tx->transaction.amount2)) { fprintf(stderr, "Error: Failed to credit recipient2 balance during block addition. Bailing!\n"); return false; } } } } return true; } block_t* Chain_GetBlock(blockchain_t* chain, size_t index) { if (chain) { return DynArr_at(chain->blocks, index); } return NULL; } size_t Chain_Size(blockchain_t* chain) { if (chain) { return DynArr_size(chain->blocks); } return 0; } 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; } 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; } // Find metadata file (create if not exists) to get the saved chain size (+ other things) FILE* metaFile = fopen(metaPath, "rb+"); FILE* chainFile = fopen(chainPath, "rb+"); FILE* tableFile = fopen(tablePath, "rb+"); if (!metaFile || !chainFile || !tableFile) { // Just overwrite everything metaFile = fopen(metaPath, "wb+"); if (!metaFile) { return false; } // Initialize metadata with size 0 size_t initialSize = 0; fwrite(&initialSize, sizeof(size_t), 1, metaFile); // Write last block hash (32 bytes of zeros for now) uint8_t zeroHash[32] = {0}; fwrite(zeroHash, sizeof(uint8_t), 32, metaFile); uint256_t zeroSupply = {0}; fwrite(&zeroSupply, sizeof(uint256_t), 1, metaFile); uint32_t initialTarget = INITIAL_DIFFICULTY; fwrite(&initialTarget, sizeof(uint32_t), 1, metaFile); uint64_t initialReward = 0; fwrite(&initialReward, sizeof(uint64_t), 1, metaFile); chainFile = fopen(chainPath, "wb+"); if (!chainFile) { return false; } tableFile = fopen(tablePath, "wb+"); if (!tableFile) { return false; } // TODO: Potentially some other things here, we'll see } // Read size_t savedSize = 0; fread(&savedSize, sizeof(size_t), 1, metaFile); uint8_t lastSavedHash[32]; fread(lastSavedHash, sizeof(uint8_t), 32, metaFile); // Assume chain saved is valid, and that the chain in memory is valid (as LoadFromFile will verify the saved one) if (savedSize > DynArr_size(chain->blocks)) { // Saved chain is longer than current chain, this should not happen if we are always saving the current chain, but just in case, fail to save to avoid overwriting a potentially valid longer chain with a shorter one. fclose(metaFile); fclose(chainFile); fclose(tableFile); return false; } // Filename format: dirpath/chain.data // File format: ([block_header][num_transactions][transactions...])[*length] - since block_header is fixed size, LoadFromFile will only read those by default fseek(chainFile, 0, SEEK_END); // Seek to the end of those files fseek(tableFile, 0, SEEK_END); long pos = ftell(chainFile); if (pos < 0) { fclose(metaFile); fclose(chainFile); fclose(tableFile); return false; } uint64_t byteCount = (uint64_t)pos; // Get the size // Save blocks that are not yet saved for (size_t i = savedSize; i < DynArr_size(chain->blocks); i++) { block_t* blk = (block_t*)DynArr_at(chain->blocks, i); if (!blk) { fclose(metaFile); fclose(chainFile); fclose(tableFile); return false; } uint64_t preIncrementByteSize = byteCount; // Construct file path // Write block header fwrite(&blk->header, sizeof(block_header_t), 1, chainFile); size_t txSize = DynArr_size(blk->transactions); fwrite(&txSize, sizeof(size_t), 1, chainFile); // Write number of transactions byteCount += sizeof(block_header_t) + sizeof(size_t); // Write transactions for (size_t j = 0; j < txSize; j++) { signed_transaction_t* tx = (signed_transaction_t*)DynArr_at(blk->transactions, j); if (fwrite(tx, sizeof(signed_transaction_t), 1, chainFile) != 1) { fclose(chainFile); fclose(metaFile); fclose(tableFile); return false; } byteCount += sizeof(signed_transaction_t); } // Create an entry in the block table block_table_entry_t entry; entry.blockNumber = i; entry.byteNumber = preIncrementByteSize; entry.blockSize = byteCount - preIncrementByteSize; fwrite(&entry, sizeof(block_table_entry_t), 1, tableFile); DynArr_destroy(blk->transactions); blk->transactions = NULL; // Clear transactions to save memory since they're now saved on disk } // Update metadata with new size and last block hash size_t newSize = DynArr_size(chain->blocks); 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); // Safety fflush(metaFile); fflush(chainFile); fflush(tableFile); // Close all pointers fclose(metaFile); fclose(chainFile); fclose(tableFile); 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); }