test send

This commit is contained in:
2026-04-02 21:52:59 +02:00
parent df7787ed2d
commit 6800ce2b60
6 changed files with 385 additions and 48 deletions

View File

@@ -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++);
}
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -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(&currentSupply, 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 {