diff --git a/include/balance_sheet.h b/include/balance_sheet.h index 8993f87..b026ee8 100644 --- a/include/balance_sheet.h +++ b/include/balance_sheet.h @@ -9,6 +9,7 @@ #include #include #include +#include typedef struct { uint8_t bytes[32]; @@ -16,7 +17,7 @@ typedef struct { typedef struct { uint8_t address[32]; // For now just the SHA-256 of the public key; allows representation in different encodings (base58, bech32, etc) without changing the underlying data structure - uint64_t balance; + uint256_t balance; // TODO: Additional things } balance_sheet_entry_t; diff --git a/include/uint256.h b/include/uint256.h index 93ab19f..e7d261a 100644 --- a/include/uint256.h +++ b/include/uint256.h @@ -55,6 +55,53 @@ static inline bool uint256_add(uint256_t* a, const uint256_t* b) { return carry > 0; } +static inline bool uint256_subtract_u64(uint256_t* balance, uint64_t amount) { + if (!balance) { + return false; + } + + if (balance->limbs[0] >= amount) { + balance->limbs[0] -= amount; + return false; + } + + uint64_t borrow = amount - balance->limbs[0]; + balance->limbs[0] = UINT64_MAX - borrow + 1ULL; + + for (int i = 1; i < 4; ++i) { + if (balance->limbs[i] > 0) { + balance->limbs[i]--; + return false; + } + + balance->limbs[i] = UINT64_MAX; + } + + return true; // underflow past 256 bits +} + +static inline bool uint256_subtract(uint256_t* a, const uint256_t* b) { + // Check if a < b to prevent underflow + for (int i = 3; i >= 0; i--) { + if (a->limbs[i] > b->limbs[i]) break; + if (a->limbs[i] < b->limbs[i]) return false; // Underflow + } + + uint64_t borrow = 0; + for (int i = 0; i < 4; i++) { + uint64_t old_a = a->limbs[i]; + a->limbs[i] -= b->limbs[i] + borrow; + + // Detect borrow: if we subtracted more than we had, or we were at zero and had a borrow + if (borrow) { + borrow = (a->limbs[i] >= old_a); + } else { + borrow = (a->limbs[i] > old_a); + } + } + return true; +} + /** * Compares two uint256_t values in a greater-than manner. * Returns [-1, 0, 1] if a > b, a < b, or a == b respectively. @@ -67,4 +114,35 @@ static inline int uint256_cmp(const uint256_t* a, const uint256_t* b) { return 0; } +static inline void uint256_serialize(const uint256_t* value, char* out) { + if (!value || !out) { + return; + } + + // Convert into string of decimal digits for easier readability; max 78 digits for 256 bits + char digits[80]; + size_t digitCount = 0; + uint256_t tmp = *value; + while (tmp.limbs[0] != 0 || tmp.limbs[1] != 0 || tmp.limbs[2] != 0 || tmp.limbs[3] != 0) { + uint64_t remainder = 0; + for (int i = 3; i >= 0; --i) { + __uint128_t cur = ((__uint128_t)remainder << 64) | tmp.limbs[i]; + tmp.limbs[i] = (uint64_t)(cur / 10u); + remainder = (uint64_t)(cur % 10u); + } + + if (digitCount < sizeof(digits) - 1) { + digits[digitCount++] = (char)('0' + remainder); + } else { + break; + } + } + digits[digitCount] = '\0'; + + for (size_t i = 0; i < digitCount; ++i) { + out[i] = digits[digitCount - 1 - i]; + } + out[digitCount] = '\0'; +} + #endif diff --git a/src/balance_sheet.c b/src/balance_sheet.c index 5c65082..4d54fd6 100644 --- a/src/balance_sheet.c +++ b/src/balance_sheet.c @@ -63,11 +63,14 @@ void BalanceSheet_Print() { key32_t key = kh_key(sheetMap, k); balance_sheet_entry_t val = kh_val(sheetMap, k); - printf("Sheet entry %llu: mapkey=%02x%02x%02x%02x... address=%02x%02x%02x%02x... balance=%llu\n", + char balanceStr[80]; + uint256_serialize(&val.balance, balanceStr); + + printf("Sheet entry %llu: mapkey=%02x%02x%02x%02x... address=%02x%02x%02x%02x... balance=%s\n", (unsigned long long)(iter), key.bytes[0], key.bytes[1], key.bytes[2], key.bytes[3], val.address[0], val.address[1], val.address[2], val.address[3], - (unsigned long long)(val.balance), + balanceStr, iter++); } } diff --git a/src/block/chain.c b/src/block/chain.c index 6bafb77..b69e5b9 100644 --- a/src/block/chain.c +++ b/src/block/chain.c @@ -33,6 +33,65 @@ static bool BuildPath(char* out, size_t outSize, const char* dirpath, const char 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; @@ -73,51 +132,89 @@ void Chain_Destroy(blockchain_t* chain) { } bool Chain_AddBlock(blockchain_t* chain, block_t* block) { - // Assume the block is pre-verified - - if (chain && block && chain->blocks) { - block_t* blk = (block_t*)DynArr_push_back(chain->blocks, block); - chain->size++; - - if (blk && blk->transactions) { - size_t 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; } - - // Destination 1 - balance_sheet_entry_t entry1; - uint8_t addr1[32]; - memcpy(addr1, tx->transaction.recipientAddress1, 32); - // Assume addr1 is never NULL, since we enforce that - if (BalanceSheet_Lookup(addr1, &entry1)) { - entry1.balance += tx->transaction.amount1; - } else { - memcpy(entry1.address, addr1, 32); - entry1.balance = tx->transaction.amount1; - } - BalanceSheet_Insert(entry1); // Insert/Overwrite - - // Destination 2 - balance_sheet_entry_t entry2; - char ZERO[32] = {0}; - uint8_t addr2[32]; - memcpy(addr2, tx->transaction.recipientAddress2, 32); - if (memcmp(addr2, ZERO, 32) == 0) { continue; } // Destination 2 not specified, continue - if (BalanceSheet_Lookup(addr2, &entry2)) { - entry2.balance += tx->transaction.amount2; - } else { - memcpy(entry2.address, addr2, 32); - entry2.balance = tx->transaction.amount2; - } - BalanceSheet_Insert(entry2); - } - } - - return true; + if (!chain || !block || !chain->blocks) { + return false; } - 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++; + + // 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) { diff --git a/src/block/transaction.c b/src/block/transaction.c index adbc795..9cfe492 100644 --- a/src/block/transaction.c +++ b/src/block/transaction.c @@ -59,7 +59,8 @@ bool Transaction_Verify(const signed_transaction_t* tx) { return false; // Cannot send to coinbase address } - // TODO: Check that the sender has sufficient funds - if not coinbase + // Balance checks are stateful and are handled when a block is added to the chain. + // Transaction_Verify only checks transaction structure, addresses, and signature material. if (tx->transaction.amount2 == 0) { // If amount2 is zero, address2 must be all zeros diff --git a/src/main.c b/src/main.c index ee9aee1..2f6eb39 100644 --- a/src/main.c +++ b/src/main.c @@ -30,6 +30,65 @@ uint32_t difficultyTarget = INITIAL_DIFFICULTY; // extern the currentReward from constants.h so we can update it as we mine blocks and save it to disk extern uint64_t currentReward; +static void AddressFromCompressedPubkey(const uint8_t compressedPubkey[33], uint8_t outAddress[32]) { + if (!compressedPubkey || !outAddress) { + return; + } + + SHA256(compressedPubkey, 33, outAddress); +} + +static bool GenerateTestMinerIdentity(uint8_t privateKey[32], uint8_t compressedPubkey[33], uint8_t address[32]) { + if (!privateKey || !compressedPubkey || !address) { + return false; + } + + secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); + if (!ctx) { + return false; + } + + uint8_t seed[64]; + secp256k1_pubkey pubkey; + + for (uint64_t counter = 0; counter < 1024; ++counter) { + const char* base = "minicoin-test-miner-key"; + size_t baseLen = strlen(base); + memcpy(seed, base, baseLen); + memcpy(seed + baseLen, &counter, sizeof(counter)); + SHA256(seed, baseLen + sizeof(counter), privateKey); + + if (!secp256k1_ec_seckey_verify(ctx, privateKey)) { + continue; + } + + if (!secp256k1_ec_pubkey_create(ctx, &pubkey, privateKey)) { + continue; + } + + size_t pubLen = 33; + if (!secp256k1_ec_pubkey_serialize(ctx, compressedPubkey, &pubLen, &pubkey, SECP256K1_EC_COMPRESSED) || pubLen != 33) { + continue; + } + + AddressFromCompressedPubkey(compressedPubkey, address); + secp256k1_context_destroy(ctx); + return true; + } + + secp256k1_context_destroy(ctx); + return false; +} + +static void MakeTestRecipientAddress(uint8_t outAddress[32]) { + if (!outAddress) { + return; + } + + const char* label = "minicoin-test-recipient-address"; + SHA256((const unsigned char*)label, strlen(label), outAddress); +} + static void Uint256ToDecimal(const uint256_t* value, char* out, size_t outSize) { if (!value || !out || outSize == 0) { return; @@ -161,7 +220,15 @@ int main(int argc, char* argv[]) { printf("Mining %llu blocks with target time %.0fs...\n", (unsigned long long)blocksToMine, targetSeconds); uint8_t minerAddress[32]; - SHA256((const unsigned char*)"minicoin-miner-1", strlen("minicoin-miner-1"), minerAddress); + uint8_t minerPrivateKey[32]; + uint8_t minerCompressedPubkey[33]; + if (!GenerateTestMinerIdentity(minerPrivateKey, minerCompressedPubkey, minerAddress)) { + fprintf(stderr, "failed to generate test miner keypair\n"); + Chain_Destroy(chain); + Block_ShutdownPowContext(); + BalanceSheet_Destroy(); + return 1; + } for (uint64_t mined = 0; mined < blocksToMine; ++mined) { block_t* block = Block_Create(); @@ -266,6 +333,96 @@ int main(int argc, char* argv[]) { isFirstBlockOfLoadedChain = false; } + // Post-loop test: spend 10 coins from the miner address to a different address. + // This validates sender balance checks, transaction signing, merkle root generation, + // and PoW mining for a non-coinbase transaction. + const uint64_t spendAmount = 10ULL * DECIMALS; + uint8_t recipientAddress[32]; + MakeTestRecipientAddress(recipientAddress); + + signed_transaction_t spendTx; + memset(&spendTx, 0, sizeof(spendTx)); + spendTx.transaction.version = 1; + spendTx.transaction.fee = 0; + spendTx.transaction.amount1 = spendAmount; + spendTx.transaction.amount2 = 0; + memcpy(spendTx.transaction.senderAddress, minerAddress, sizeof(minerAddress)); + memcpy(spendTx.transaction.recipientAddress1, recipientAddress, sizeof(recipientAddress)); + memset(spendTx.transaction.recipientAddress2, 0, sizeof(spendTx.transaction.recipientAddress2)); + memcpy(spendTx.transaction.compressedPublicKey, minerCompressedPubkey, sizeof(minerCompressedPubkey)); + + Transaction_Sign(&spendTx, minerPrivateKey); + + block_t* spendBlock = Block_Create(); + if (!spendBlock) { + fprintf(stderr, "failed to create test spend block\n"); + Chain_Destroy(chain); + Block_ShutdownPowContext(); + BalanceSheet_Destroy(); + return 1; + } + + spendBlock->header.version = 1; + spendBlock->header.blockNumber = (uint64_t)Chain_Size(chain); + if (Chain_Size(chain) > 0) { + block_t* lastBlock = Chain_GetBlock(chain, Chain_Size(chain) - 1); + if (lastBlock) { + Block_CalculateHash(lastBlock, spendBlock->header.prevHash); + } else { + memset(spendBlock->header.prevHash, 0, sizeof(spendBlock->header.prevHash)); + } + } else { + memset(spendBlock->header.prevHash, 0, sizeof(spendBlock->header.prevHash)); + } + spendBlock->header.timestamp = (uint64_t)time(NULL); + spendBlock->header.difficultyTarget = difficultyTarget; + spendBlock->header.nonce = 0; + + signed_transaction_t testCoinbaseTx; + memset(&testCoinbaseTx, 0, sizeof(testCoinbaseTx)); + testCoinbaseTx.transaction.version = 1; + testCoinbaseTx.transaction.amount1 = currentReward; + testCoinbaseTx.transaction.fee = 0; + memcpy(testCoinbaseTx.transaction.recipientAddress1, minerAddress, sizeof(minerAddress)); + testCoinbaseTx.transaction.recipientAddress2[0] = 0; + testCoinbaseTx.transaction.amount2 = 0; + memset(testCoinbaseTx.transaction.compressedPublicKey, 0, sizeof(testCoinbaseTx.transaction.compressedPublicKey)); + memset(testCoinbaseTx.transaction.senderAddress, 0xFF, sizeof(testCoinbaseTx.transaction.senderAddress)); + + Block_AddTransaction(spendBlock, &testCoinbaseTx); + Block_AddTransaction(spendBlock, &spendTx); + + uint8_t merkleRoot[32]; + Block_CalculateMerkleRoot(spendBlock, merkleRoot); + memcpy(spendBlock->header.merkleRoot, merkleRoot, sizeof(spendBlock->header.merkleRoot)); + + if (!MineBlock(spendBlock)) { + fprintf(stderr, "failed to mine test spend block\n"); + Block_Destroy(spendBlock); + Chain_Destroy(chain); + Block_ShutdownPowContext(); + BalanceSheet_Destroy(); + return 1; + } + + if (!Chain_AddBlock(chain, spendBlock)) { + fprintf(stderr, "failed to append test spend block to chain\n"); + Block_Destroy(spendBlock); + Chain_Destroy(chain); + Block_ShutdownPowContext(); + BalanceSheet_Destroy(); + return 1; + } + + (void)uint256_add_u64(¤tSupply, testCoinbaseTx.transaction.amount1); + currentReward = CalculateBlockReward(currentSupply, chain); + + printf("Mined test spend block (height=%llu) sending %llu base units to a new address\n", + (unsigned long long)spendBlock->header.blockNumber, + (unsigned long long)spendAmount); + + free(spendBlock); + if (!Chain_SaveToFile(chain, chainDataDir, currentSupply, currentReward)) { fprintf(stderr, "failed to save chain to %s\n", chainDataDir); } else {