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

@@ -9,6 +9,7 @@
#include <khash/khash.h>
#include <crypto/crypto.h>
#include <string.h>
#include <uint256.h>
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;

View File

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

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 {