diff --git a/CMakeLists.txt b/CMakeLists.txt index 6c41ac2..d26121a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -124,5 +124,7 @@ target_compile_options(node PRIVATE -Wpedantic -g ) -target_compile_definitions(node PRIVATE) +target_compile_definitions(node PRIVATE + CHAIN_DATA_DIR="${CMAKE_BINARY_DIR}/chain_data" +) set_target_properties(node PROPERTIES OUTPUT_NAME "minicoin_node") diff --git a/include/block/block.h b/include/block/block.h index 79bbd3c..1bdda77 100644 --- a/include/block/block.h +++ b/include/block/block.h @@ -10,13 +10,14 @@ #include typedef struct { - uint8_t version; - uint32_t blockNumber; + uint64_t blockNumber; + uint64_t timestamp; + uint64_t nonce; uint8_t prevHash[32]; uint8_t merkleRoot[32]; - uint64_t timestamp; uint32_t difficultyTarget; // Encoding: [1 byte exponent][3 byte coefficient]; Target = coefficient * 256^(exponent-3) - uint64_t nonce; // Higher nonce for RandomX + uint8_t version; + uint8_t reserved[3]; // 3 bytes (Explicit padding for 8-byte alignment) } block_header_t; typedef struct { diff --git a/include/block/chain.h b/include/block/chain.h index 3adecf9..72de91e 100644 --- a/include/block/chain.h +++ b/include/block/chain.h @@ -4,6 +4,9 @@ #include #include #include +#include +#include +#include typedef struct { DynArr* blocks; @@ -16,5 +19,10 @@ bool Chain_AddBlock(blockchain_t* chain, block_t* block); block_t* Chain_GetBlock(blockchain_t* chain, size_t index); size_t Chain_Size(blockchain_t* chain); bool Chain_IsValid(blockchain_t* chain); +void Chain_Wipe(blockchain_t* chain); + +// I/O +bool Chain_SaveToFile(blockchain_t* chain, const char* dirpath); +bool Chain_LoadFromFile(blockchain_t* chain, const char* dirpath); #endif diff --git a/include/constants.h b/include/constants.h new file mode 100644 index 0000000..5dac3a1 --- /dev/null +++ b/include/constants.h @@ -0,0 +1,37 @@ +#ifndef CONSTANTS_H +#define CONSTANTS_H + +#include +#include +#include + +#define DECIMALS 1000000000000ULL +#define EMISSION_SPEED_FACTOR 20 +const uint64_t M_CAP = 18446744073709551615ULL; // Max uint64 +const uint64_t TAIL_EMISSION = (uint64_t)(1.0 * DECIMALS); // Emission floor is 1.0 coins per block +// No max supply. Instead of halving, it'll follow a more gradual, Monero-like emission curve. + +static inline uint64_t CalculateBlockReward(uint256_t currentSupply, uint64_t height) { + // Inclusive of block 0 + + if (current_supply.limbs[1] > 0 || + current_supply.limbs[2] > 0 || + current_supply.limbs[3] > 0 || + current_supply.limbs[0] >= M_CAP) { + return TAIL_EMISSION; + } + + uint64_t supply_64 = current_supply.limbs[0]; + + // Formula: (M - Supply) >> 2^k - lifted from Monero's codebase (thanks guys!) + uint64_t reward = (M_CAP - supply_64) >> EMISSION_SPEED_FACTOR; + + // Check if the calculated reward has fallen below the floor + if (reward < TAIL_EMISSION) { + return TAIL_EMISSION; + } + + return reward; +} + +#endif \ No newline at end of file diff --git a/include/randomx/librx_wrapper.h b/include/randomx/librx_wrapper.h index 0af8262..fdf7258 100644 --- a/include/randomx/librx_wrapper.h +++ b/include/randomx/librx_wrapper.h @@ -10,7 +10,7 @@ extern "C" { #endif -bool RandomX_Init(const char* key); +bool RandomX_Init(const char* key, bool preferFullMemory); void RandomX_Destroy(); void RandomX_CalculateHash(const uint8_t* input, size_t inputLen, uint8_t* output); diff --git a/include/uint256.h b/include/uint256.h new file mode 100644 index 0000000..20dba76 --- /dev/null +++ b/include/uint256.h @@ -0,0 +1,58 @@ +#ifndef UINT256_H +#define UINT256_H + +#include +#include +#include + +typedef struct { + uint64_t limbs[4]; // 4 * 64 = 256 bits +} uint256_t; + +// Initialize a uint256 with a standard 64-bit value +static inline uint256_t uint256_from_u64(uint64_t val) { + uint256_t res = {{val, 0, 0, 0}}; + return res; +} + +/** + * Adds a uint64_t (transaction amount) to a uint256_t (balance). + * Returns true if an overflow occurred (total supply exceeded 256 bits). +**/ +static inline bool uint256_add_u64(uint256_t* balance, uint64_t amount) { + uint64_t old = balance->limbs[0]; + balance->limbs[0] += amount; + + // Check for carry: if the new value is less than the old, it wrapped around + if (balance->limbs[0] < old) { + for (int i = 1; i < 4; i++) { + balance->limbs[i]++; + // If the limb didn't wrap to 0, the carry is fully absorbed + if (balance->limbs[i] != 0) return false; + } + return true; // Overflowed all 256 bits + } + return false; +} + +/** + * Adds two uint256_t values together. + * Standard full addition logic. +**/ +static inline bool uint256_add(uint256_t* a, const uint256_t* b) { + uint64_t carry = 0; + for (int i = 0; i < 4; i++) { + uint64_t old_a = a->limbs[i]; + a->limbs[i] += b->limbs[i] + carry; + + // Detect carry: current is less than what we added, or we were at max and had a carry + if (carry) { + carry = (a->limbs[i] <= old_a); + } else { + carry = (a->limbs[i] < old_a); + } + } + return carry > 0; +} + +#endif \ No newline at end of file diff --git a/src/block/block.c b/src/block/block.c index 30fd7fc..b7812a4 100644 --- a/src/block/block.c +++ b/src/block/block.c @@ -140,16 +140,17 @@ bool Block_AllTransactionsValid(const block_t* block) { for (size_t i = 0; i < DynArr_size(block->transactions); i++) { signed_transaction_t* tx = (signed_transaction_t*)DynArr_at(block->transactions, i); - if (!Transaction_Verify(tx)) { - return false; - } - - if (Address_IsCoinbase(tx->transaction.senderAddress)) { + if (tx && Address_IsCoinbase(tx->transaction.senderAddress)) { if (hasCoinbase) { return false; // More than one coinbase transaction } hasCoinbase = true; + continue; // Coinbase transactions are valid since the miner has the right to create coins. Only rule is one per block. + } + + if (!Transaction_Verify(tx)) { + return false; } } diff --git a/src/block/chain.c b/src/block/chain.c index e511b6c..3b10391 100644 --- a/src/block/chain.c +++ b/src/block/chain.c @@ -1,4 +1,36 @@ #include +#include +#include + +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; +} blockchain_t* Chain_Create() { blockchain_t* ptr = (blockchain_t*)malloc(sizeof(blockchain_t)); @@ -24,6 +56,7 @@ void Chain_Destroy(blockchain_t* chain) { bool Chain_AddBlock(blockchain_t* chain, block_t* block) { if (chain && block && chain->blocks) { DynArr_push_back(chain->blocks, block); + chain->size++; return true; } @@ -48,6 +81,217 @@ bool Chain_IsValid(blockchain_t* chain) { if (!chain || !chain->blocks) { return false; } - // Add validation logic here + + 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. + } + + // 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) { + if (chain && chain->blocks) { + DynArr_erase(chain->blocks); + chain->size = 0; + } +} + +bool Chain_SaveToFile(blockchain_t* chain, const char* dirpath) { + // To avoid stalling the chain from peers, write after every block addition (THAT IS VERIFIED) + + if (!chain || !chain->blocks || !EnsureDirectoryExists(dirpath)) { + return false; + } + + char metaPath[512]; + if (!BuildPath(metaPath, sizeof(metaPath), dirpath, "chain.meta")) { + return false; + } + + // Find metadata file (create if not exists) to get the saved chain size (+ other things) + FILE* metaFile = fopen(metaPath, "rb+"); + if (!metaFile) { + 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); + + // 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); + return false; + } + + // Filename formart: dirpath/block_{index}.dat + // File format: [block_header][num_transactions][transactions...] - since block_header is fixed size, LoadFromFile will only read those by default + + // 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); + return false; + } + + // Construct file path + char filePath[256]; + snprintf(filePath, sizeof(filePath), "%s/block_%zu.dat", dirpath, i); + + FILE* blockFile = fopen(filePath, "wb"); + if (!blockFile) { + fclose(metaFile); + return false; + } + + // Write block header + fwrite(&blk->header, sizeof(block_header_t), 1, blockFile); + size_t txSize = DynArr_size(blk->transactions); + fwrite(&txSize, sizeof(size_t), 1, blockFile); // Write number of transactions + // 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, blockFile) != 1) { + fclose(blockFile); + fclose(metaFile); + return false; + } + } + + fclose(blockFile); + } + + // 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); + 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); + } + fclose(metaFile); + + return true; +} + +bool Chain_LoadFromFile(blockchain_t* chain, const char* dirpath) { + if (!chain || !chain->blocks || !dirpath) { + 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; + } + + // Read metadata file to get saved chain size (+ other things) + FILE* metaFile = fopen(metaPath, "rb"); + if (!metaFile) { + return false; + } + + size_t savedSize = 0; + fread(&savedSize, sizeof(size_t), 1, metaFile); + uint8_t lastSavedHash[32]; + fread(lastSavedHash, sizeof(uint8_t), 32, metaFile); + fclose(metaFile); + + // TODO: Might add a flag to allow reading from a point onward, but just rewrite for now + DynArr_erase(chain->blocks); // Clear current chain blocks, but keep allocated memory for efficiency, since we will likely be loading a similar number of blocks as currently in memory. + + // Load blocks + for (size_t i = 0; i < savedSize; i++) { + // Construct file path + char filePath[256]; + snprintf(filePath, sizeof(filePath), "%s/block_%zu.dat", dirpath, i); + + block_t* blk = Block_Create(); + if (!blk) { + return false; + } + + FILE* blockFile = fopen(filePath, "rb"); + if (!blockFile) { + Block_Destroy(blk); + return false; + } + + // Read block header and transactions + if (fread(&blk->header, sizeof(block_header_t), 1, blockFile) != 1) { + fclose(blockFile); + Block_Destroy(blk); + return false; + } + + size_t txSize = 0; + if (fread(&txSize, sizeof(size_t), 1, blockFile) != 1) { + fclose(blockFile); + Block_Destroy(blk); + return false; + } + + for (size_t j = 0; j < txSize; j++) { + signed_transaction_t tx; + if (fread(&tx, sizeof(signed_transaction_t), 1, blockFile) != 1) { + fclose(blockFile); + Block_Destroy(blk); + return false; + } + Block_AddTransaction(blk, &tx); + } + fclose(blockFile); + + Chain_AddBlock(chain, blk); + } + + chain->size = savedSize; + + // 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; } diff --git a/src/main.c b/src/main.c index de8845a..6199977 100644 --- a/src/main.c +++ b/src/main.c @@ -11,6 +11,10 @@ #include #include +#ifndef CHAIN_DATA_DIR +#define CHAIN_DATA_DIR "chain_data" +#endif + void handle_sigint(int sig) { printf("Caught signal %d, exiting...\n", sig); RandomX_Destroy(); @@ -121,8 +125,10 @@ static bool MineBlock(block_t* block) { int main(void) { signal(SIGINT, handle_sigint); + const char* chainDataDir = CHAIN_DATA_DIR; + // Init RandomX - if (!RandomX_Init("minicoin")) { // TODO: Use a key that is not hardcoded; E.g. hash of the last block, every thousand blocks, etc. + if (!RandomX_Init("minicoin", false)) { // TODO: Use a key that is not hardcoded; E.g. hash of the last block, every thousand blocks, difficulty recalibration, etc. fprintf(stderr, "failed to initialize RandomX\n"); return 1; } @@ -133,6 +139,33 @@ int main(void) { return 1; } + // Attempt read + if (!Chain_LoadFromFile(chain, chainDataDir)) { + printf("No existing chain loaded from %s\n", chainDataDir); + } + + if (Chain_Size(chain) > 0) { + if (Chain_IsValid(chain)) { + printf("Loaded chain with %zu blocks from disk\n", Chain_Size(chain)); + } else { + fprintf(stderr, "loaded chain is invalid, scrapping, resyncing.\n"); // TODO: Actually implement resyncing from peers instead of just scrapping the chain + const size_t badSize = Chain_Size(chain); + + // Delete files (wipe dir) + for (size_t i = 0; i < badSize; i++) { + char filePath[256]; + snprintf(filePath, sizeof(filePath), "%s/block_%zu.dat", chainDataDir, i); + remove(filePath); + } + + char metaPath[256]; + snprintf(metaPath, sizeof(metaPath), "%s/chain.meta", chainDataDir); + remove(metaPath); + + Chain_Wipe(chain); + } + } + secp256k1_context* secpCtx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY); if (!secpCtx) { fprintf(stderr, "failed to create secp256k1 context\n"); @@ -153,6 +186,17 @@ int main(void) { return 1; } + // Coinbase TX - no signature needed, one per block + signed_transaction_t coinbaseTx; + memset(&coinbaseTx, 0, sizeof(coinbaseTx)); + coinbaseTx.transaction.version = 1; + coinbaseTx.transaction.amount = 50; // Block reward + coinbaseTx.transaction.fee = 0; + SHA256(receiverCompressedPublicKey, 33, coinbaseTx.transaction.recipientAddress); + memset(coinbaseTx.transaction.compressedPublicKey, 0x00, 33); // No public key for coinbase + memset(coinbaseTx.transaction.senderAddress, 0xFF, 32); // Coinbase marker + + // Test TX signed_transaction_t tx; memset(&tx, 0, sizeof(tx)); tx.transaction.version = 1; @@ -182,22 +226,36 @@ int main(void) { } block->header.version = 1; - block->header.blockNumber = (uint32_t)Chain_Size(chain); - memset(block->header.prevHash, 0, sizeof(block->header.prevHash)); + block->header.blockNumber = (uint64_t)Chain_Size(chain); + // Get prevHash from last block if exists + if (Chain_Size(chain) > 0) { + block_t* lastBlock = Chain_GetBlock(chain, Chain_Size(chain) - 1); + if (lastBlock) { + Block_CalculateHash(lastBlock, block->header.prevHash); + } else { + memset(block->header.prevHash, 0, sizeof(block->header.prevHash)); + } + } else { + memset(block->header.prevHash, 0, sizeof(block->header.prevHash)); + } memset(block->header.merkleRoot, 0, sizeof(block->header.merkleRoot)); block->header.timestamp = (uint64_t)time(NULL); - const double hps = MeasureRandomXHashrate(); - const double targetSeconds = 60.0; - const double expectedHashes = (hps > 0.0) ? (hps * targetSeconds) : 65536.0; - block->header.difficultyTarget = CompactTargetForExpectedHashes(expectedHashes); + const double hps = MeasureRandomXHashrate(); + const double targetSeconds = 10.0; + const double expectedHashes = (hps > 0.0) ? (hps * targetSeconds) : 65536.0; + block->header.difficultyTarget = CompactTargetForExpectedHashes(expectedHashes); block->header.nonce = 0; - printf("RandomX benchmark: %.2f H/s, target %.0fs, nBits=0x%08x\n", - hps, - targetSeconds, - block->header.difficultyTarget); + printf("RandomX benchmark: %.2f H/s, target %.0fs, nBits=0x%08x\n", + hps, + targetSeconds, + block->header.difficultyTarget); + Block_AddTransaction(block, &coinbaseTx); + printf("Added coinbase transaction to block: recipient %02x... -> amount %lu\n", + coinbaseTx.transaction.recipientAddress[0], coinbaseTx.transaction.recipientAddress[31], + coinbaseTx.transaction.amount); Block_AddTransaction(block, &tx); printf("Added transaction to block: sender %02x... -> recipient %02x..., amount %lu, fee %lu\n", tx.transaction.senderAddress[0], tx.transaction.senderAddress[31], @@ -222,10 +280,10 @@ int main(void) { return 1; } - printf("Mined block %u with nonce %llu and chain size %zu\n", - block->header.blockNumber, - (unsigned long long)block->header.nonce, - Chain_Size(chain)); + printf("Mined block %llu with nonce %llu and chain size %zu\n", + (unsigned long long)block->header.blockNumber, + (unsigned long long)block->header.nonce, + Chain_Size(chain)); printf("Block hash (SHA256): "); uint8_t blockHash[32]; @@ -241,6 +299,12 @@ int main(void) { } printf("\n"); + if (!Chain_SaveToFile(chain, chainDataDir)) { + fprintf(stderr, "failed to save chain to %s\n", chainDataDir); + } else { + printf("Saved chain with %zu blocks to %s\n", Chain_Size(chain), chainDataDir); + } + // Chain currently stores a copy of block_t that references the same tx array pointer, // so we do not destroy `block` here to avoid invalidating chain data. secp256k1_context_destroy(secpCtx); diff --git a/src/randomx/librx_wrapper.c b/src/randomx/librx_wrapper.c index b8d9e76..b086660 100644 --- a/src/randomx/librx_wrapper.c +++ b/src/randomx/librx_wrapper.c @@ -8,7 +8,7 @@ static randomx_cache* rxCache = NULL; static randomx_dataset* rxDataset = NULL; static randomx_vm* rxVm = NULL; -bool RandomX_Init(const char* key) { +bool RandomX_Init(const char* key, bool preferFullMemory) { if (!key || rxCache || rxVm) { return false; } @@ -24,18 +24,20 @@ bool RandomX_Init(const char* key) { randomx_init_cache(rxCache, key, strlen(key)); // Prefer full-memory mode. If dataset allocation fails, fall back to light mode. - rxDataset = randomx_alloc_dataset(vmFlags); - if (rxDataset) { - const unsigned long datasetItems = randomx_dataset_item_count(); - randomx_init_dataset(rxDataset, rxCache, 0, datasetItems); - rxVm = randomx_create_vm(vmFlags, NULL, rxDataset); - if (rxVm) { - printf("RandomX initialized in full-memory mode\n"); - return true; - } + if (preferFullMemory) { + rxDataset = randomx_alloc_dataset(vmFlags); + if (rxDataset) { + const unsigned long datasetItems = randomx_dataset_item_count(); + randomx_init_dataset(rxDataset, rxCache, 0, datasetItems); + rxVm = randomx_create_vm(vmFlags, NULL, rxDataset); + if (rxVm) { + printf("RandomX initialized in full-memory mode\n"); + return true; + } - randomx_release_dataset(rxDataset); - rxDataset = NULL; + randomx_release_dataset(rxDataset); + rxDataset = NULL; + } } vmFlags = baseFlags;