add fee-aware mining, coinbase validation, and reorg-safe orphan handling

Mining: blocks now include mempool txs, select spendable txs by fee, and pay coinbase as base reward + fees in main.c.
 - Consensus: block validation now enforces coinbase accounting and rejects invalid coinbase placement, including coinbase on amount2, in block.c and transaction.c.
 - Chain state: rollback now rebuilds currentSupply/currentReward, and block addition preflights spendability before mutating balances in chain.c.
 - Orphans/reorgs: orphan retry is safer, rollback-triggered sync reattaches orphans immediately, and transient orphan failures no longer drop blocks in orphan_pool.c and main.c.
 - Networking/mempool: node lifecycle now initializes the mempool, broadcasts can exclude one peer, and mempool snapshotting supports mining selection in net_node.c and txmempool.c.
 - Ledger simulation: added non-mutating spendable-transaction selection for block assembly in balance_sheet.c.
This commit is contained in:
2026-05-29 13:44:15 +02:00
parent 41a154a9fd
commit 763aeb648f
13 changed files with 634 additions and 45 deletions
+71 -6
View File
@@ -214,23 +214,88 @@ 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 (tx && Address_IsCoinbase(tx->transaction.senderAddress)) {
if (hasCoinbase) {
return false; // More than one coinbase transaction
return false;
}
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;
}
}
return true && hasCoinbase && DynArr_size(block->transactions) > 0; // Every block must have at least one transaction (the coinbase)
}
bool Block_ValidateCoinbaseAndFees(const block_t* block, uint64_t expectedCoinbaseAmount, uint64_t* outTotalFees) {
if (!block || !block->transactions) {
return false;
}
bool hasCoinbase = false;
uint64_t totalFees = 0;
uint8_t zeroAddress[32] = {0};
for (size_t i = 0; i < DynArr_size(block->transactions); ++i) {
signed_transaction_t* tx = (signed_transaction_t*)DynArr_at(block->transactions, i);
if (!tx) {
return false;
}
if (Address_IsCoinbase(tx->transaction.senderAddress)) {
if (hasCoinbase) {
return false;
}
hasCoinbase = true;
if (!Transaction_Verify(tx)) {
return false;
}
if (tx->transaction.fee != 0 || tx->transaction.amount2 != 0) {
return false;
}
if (tx->transaction.amount1 != expectedCoinbaseAmount) {
return false;
}
if (Address_IsCoinbase(tx->transaction.recipientAddress1)) {
return false;
}
if (memcmp(tx->transaction.recipientAddress2, zeroAddress, sizeof(zeroAddress)) != 0) {
return false;
}
continue;
}
if (!Transaction_Verify(tx)) {
return false;
}
if (UINT64_MAX - totalFees < tx->transaction.fee) {
return false;
}
totalFees += tx->transaction.fee;
}
if (!hasCoinbase) {
return false;
}
if (outTotalFees) {
*outTotalFees = totalFees;
}
return true;
}
bool Block_IsFullyValid(const block_t* block) {
bool merkleValid = false;
uint8_t calculatedMerkleRoot[32];
+88 -20
View File
@@ -97,6 +97,37 @@ static bool DebitAddress(const uint8_t address[32], const uint256_t* amount) {
return BalanceSheet_Insert(entry) >= 0;
}
static bool Chain_RecomputeRuntimeState(blockchain_t* chain) {
if (!chain) {
return false;
}
uint256_t rebuiltSupply = uint256_from_u64(0);
for (size_t i = 0; i < chain->size; ++i) {
block_t* blk = (block_t*)DynArr_at(chain->blocks, i);
if (!blk || !blk->transactions) {
return false;
}
for (size_t j = 0; j < DynArr_size(blk->transactions); ++j) {
signed_transaction_t* tx = (signed_transaction_t*)DynArr_at(blk->transactions, j);
if (!tx) {
return false;
}
if (Address_IsCoinbase(tx->transaction.senderAddress)) {
if (uint256_add_u64(&rebuiltSupply, tx->transaction.amount1)) {
return false;
}
}
}
}
currentSupply = rebuiltSupply;
currentReward = CalculateBlockReward(currentSupply, chain);
return true;
}
static void Chain_ClearBlocks(blockchain_t* chain) {
if (!chain || !chain->blocks) {
return;
@@ -161,34 +192,65 @@ bool Chain_AddBlock(blockchain_t* chain, block_t* block) {
}
do {
// 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);
signed_transaction_t* candidateTxs = (signed_transaction_t*)calloc(txCount, sizeof(signed_transaction_t));
if (!candidateTxs) {
ok = false;
break;
}
size_t nonCoinbaseCount = 0;
for (size_t i = 0; i < txCount; ++i) {
signed_transaction_t* tx = (signed_transaction_t*)DynArr_at(block->transactions, i);
if (!tx) {
ok = false; break;
ok = false;
break;
}
if (Address_IsCoinbase(tx->transaction.senderAddress)) {
continue;
}
uint256_t spend;
if (!BuildSpendAmount(tx, &spend)) { ok = false; break; }
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");
ok = false; break;
}
if (uint256_cmp(&senderEntry.balance, &spend) < 0) {
fprintf(stderr, "Error: Sender balance insufficient for block transaction. Bailing!\n");
ok = false; break;
candidateTxs[i] = *tx;
if (!Address_IsCoinbase(tx->transaction.senderAddress)) {
++nonCoinbaseCount;
}
}
if (!ok) break;
if (!ok) {
free(candidateTxs);
break;
}
signed_transaction_t* spendableTxs = NULL;
size_t spendableCount = 0;
uint64_t totalFees = 0;
if (!BalanceSheet_SelectSpendableTransactions(candidateTxs, txCount, &spendableTxs, &spendableCount, &totalFees)) {
free(candidateTxs);
ok = false;
break;
}
free(candidateTxs);
if (spendableCount != nonCoinbaseCount) {
free(spendableTxs);
ok = false;
break;
}
uint64_t expectedCoinbaseAmount = currentReward;
if (UINT64_MAX - expectedCoinbaseAmount < totalFees) {
free(spendableTxs);
ok = false;
break;
}
expectedCoinbaseAmount += totalFees;
uint64_t observedFees = 0;
if (!Block_ValidateCoinbaseAndFees(block, expectedCoinbaseAmount, &observedFees) || observedFees != totalFees) {
free(spendableTxs);
ok = false;
break;
}
free(spendableTxs);
// Push the block only after validation succeeds.
block_t* blk = (block_t*)DynArr_push_back(chain->blocks, block);
@@ -434,6 +496,12 @@ bool Chain_RollbackToHeight(blockchain_t* chain, size_t height) {
}
}
if (!Chain_RecomputeRuntimeState(chain)) {
pthread_mutex_unlock(&balanceSheetLock);
pthread_rwlock_unlock(&chainLock);
return false;
}
pthread_mutex_unlock(&balanceSheetLock);
pthread_rwlock_unlock(&chainLock);
+17 -1
View File
@@ -42,7 +42,23 @@ bool Transaction_Verify(const signed_transaction_t* tx) {
}
if (Address_IsCoinbase(tx->transaction.senderAddress)) {
// Coinbase transactions are valid if the signature is correct for the block (handled in Block_Verify)
if (tx->transaction.amount1 == 0) {
return false;
}
if (tx->transaction.amount2 != 0) {
return false;
}
if (Address_IsCoinbase(tx->transaction.recipientAddress1) || Address_IsCoinbase(tx->transaction.recipientAddress2)) {
return false;
}
uint8_t zeroAddress[32] = {0};
if (memcmp(tx->transaction.recipientAddress2, zeroAddress, 32) != 0) {
return false;
}
return true;
}