test send
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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++);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (chain && block && chain->blocks) {
|
||||
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++;
|
||||
|
||||
if (blk && blk->transactions) {
|
||||
size_t txCount = DynArr_size(blk->transactions);
|
||||
for (size_t i = 0; i < txCount; i++) {
|
||||
// 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; }
|
||||
|
||||
// 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;
|
||||
if (!tx) {
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
BalanceSheet_Insert(entry2);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
block_t* Chain_GetBlock(blockchain_t* chain, size_t index) {
|
||||
|
||||
@@ -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
|
||||
|
||||
159
src/main.c
159
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 {
|
||||
|
||||
Reference in New Issue
Block a user