diff --git a/include/block/chain.h b/include/block/chain.h index 35f9d60..d95bfba 100644 --- a/include/block/chain.h +++ b/include/block/chain.h @@ -41,6 +41,16 @@ bool Chain_LoadFromFile(blockchain_t* chain, const char* dirpath, uint256_t* out bool Chain_LoadBlockFromFile(const char* dirpath, uint64_t blockNumber, bool loadTransactions, block_t** outBlock, size_t* outTxCount); // Difficulty -uint32_t Chain_ComputeNextTarget(blockchain_t* chain, uint32_t currentTarget); +// Retarget for the block at `height`, measured over the window [height - INTERVAL, height - 1]. +// `chain` must hold blocks 0..height-1. Takes no locks; safe to call while holding `chainLock`. +uint32_t Chain_ComputeTargetAtHeight(blockchain_t* chain, uint64_t height, uint32_t currentTarget); + +// The consensus-required difficultyTarget for the block at `height`, derived from the chain alone. +// Takes no locks; safe to call while holding `chainLock`. +uint32_t Chain_GetTargetForHeight(blockchain_t* chain, uint64_t height); + +// Refresh runtime state derived from the chain tip (difficulty target, epoch DAG). +// Call after any change to the tip. Must NOT be called while holding `chainLock`. +void Chain_OnTipAdvanced(blockchain_t* chain); #endif diff --git a/src/block/chain.c b/src/block/chain.c index 7841128..0c7013a 100644 --- a/src/block/chain.c +++ b/src/block/chain.c @@ -192,6 +192,20 @@ bool Chain_AddBlock(blockchain_t* chain, block_t* block) { return false; } + // Ensure the block was mined at the difficulty this chain requires at that height. Without this + // a peer whose difficulty went stale (or a malicious one) can hand us a block mined at an + // easier target, which Block_HasValidProofOfWork accepts because it checks the header's own value. + uint32_t expectedTarget = Chain_GetTargetForHeight(chain, (uint64_t)expectedIndex); + if (block->header.difficultyTarget != expectedTarget) { + printf("Chain_AddBlock: validation failed: blockIndex=%zu expectedDifficulty=%#x observedDifficulty=%#x\n", + expectedIndex, + (unsigned int)expectedTarget, + (unsigned int)block->header.difficultyTarget); + pthread_mutex_unlock(&balanceSheetLock); + pthread_rwlock_unlock(&chainLock); + return false; + } + do { size_t txCount = DynArr_size(block->transactions); signed_transaction_t* candidateTxs = (signed_transaction_t*)calloc(txCount, sizeof(signed_transaction_t)); @@ -342,6 +356,11 @@ bool Chain_AddBlock(blockchain_t* chain, block_t* block) { pthread_mutex_unlock(&balanceSheetLock); pthread_rwlock_unlock(&chainLock); + if (ok) { + // Every path that appends comes through here, so this is where difficulty/DAG catch up. + Chain_OnTipAdvanced(chain); + } + printf("Added new block to chain:\n"); Block_ShortPrint(block); @@ -548,12 +567,16 @@ bool Chain_RollbackToHeight(blockchain_t* chain, size_t height) { pthread_mutex_unlock(&balanceSheetLock); pthread_rwlock_unlock(&chainLock); + // A reorg can move the tip back across an adjustment boundary, so the target must come down too. + Chain_OnTipAdvanced(chain); + return true; } void Chain_Wipe(blockchain_t* chain) { Chain_ClearBlocks(chain); currentBlockHeight = 0; + difficultyTarget = INITIAL_DIFFICULTY; } bool Chain_SaveToFile(blockchain_t* chain, const char* dirpath, uint256_t currentSupply, uint64_t currentReward) { @@ -1093,20 +1116,23 @@ bool Chain_LoadBlockFromFile(const char* dirpath, uint64_t blockNumber, bool loa return true; } -uint32_t Chain_ComputeNextTarget(blockchain_t* chain, uint32_t currentTarget) { +uint32_t Chain_ComputeTargetAtHeight(blockchain_t* chain, uint64_t height, uint32_t currentTarget) { if (!chain || !chain->blocks) { return 0x00; // Impossible difficulty, only valid hash is all zeros (practically impossible) } - size_t chainSize = DynArr_size(chain->blocks); - if (chainSize < DIFFICULTY_ADJUSTMENT_INTERVAL) { + if (height < DIFFICULTY_ADJUSTMENT_INTERVAL) { // Baby-chain, return initial difficulty return INITIAL_DIFFICULTY; } + if (height > (uint64_t)DynArr_size(chain->blocks)) { + return 0x00; // Retarget window is not fully present in this chain + } + // Assuming block validation validates timestamps, we can assume they're valid and can just read them - block_t* lastBlock = (block_t*)DynArr_at(chain->blocks, chainSize - 1); - block_t* adjustmentBlock = (block_t*)DynArr_at(chain->blocks, chainSize - DIFFICULTY_ADJUSTMENT_INTERVAL); + block_t* lastBlock = (block_t*)DynArr_at(chain->blocks, (size_t)(height - 1)); + block_t* adjustmentBlock = (block_t*)DynArr_at(chain->blocks, (size_t)(height - DIFFICULTY_ADJUSTMENT_INTERVAL)); if (!lastBlock || !adjustmentBlock) { return 0x00; // Impossible difficulty, only valid hash is all zeros (practically impossible) } @@ -1168,3 +1194,32 @@ uint32_t Chain_ComputeNextTarget(blockchain_t* chain, uint32_t currentTarget) { return (exponent << 24) | (newCoeff & 0x007fffff); } + +uint32_t Chain_GetTargetForHeight(blockchain_t* chain, uint64_t height) { + // The target is a pure function of the chain: replay every adjustment boundary at or below + // `height`, starting from the genesis difficulty. Never trust a cached or peer-supplied value. + uint32_t target = INITIAL_DIFFICULTY; + for (uint64_t h = DIFFICULTY_ADJUSTMENT_INTERVAL; h <= height; h += DIFFICULTY_ADJUSTMENT_INTERVAL) { + target = Chain_ComputeTargetAtHeight(chain, h, target); + } + + return target; +} + +void Chain_OnTipAdvanced(blockchain_t* chain) { + if (!chain || !chain->blocks) { + return; + } + + size_t chainSize = Chain_Size(chain); + + // Refresh the cached target for the block that comes next, so every path that moves the tip + // (mining, P2P accept, sync, orphan attach, reorg) stays on the same difficulty as its peers. + difficultyTarget = Chain_GetTargetForHeight(chain, (uint64_t)chainSize); + + if (chainSize % EPOCH_LENGTH == 0 && chainSize > 0) { + uint8_t dagSeed[32]; + GetNextDAGSeed(chain, dagSeed); + (void)Block_RebuildAutolykos2Dag(CalculateTargetDAGSize(chain), dagSeed); + } +} diff --git a/src/main.c b/src/main.c index ba16c72..81d1c1e 100644 --- a/src/main.c +++ b/src/main.c @@ -406,9 +406,8 @@ static bool Block_GetCoinbaseAndFeeTotals(const block_t* block, uint64_t* outCoi static bool MineAndAppendBlock(blockchain_t* chain, block_t* block, uint256_t* currentSupply, - uint64_t* currentReward, - uint32_t* difficultyTarget) { - if (!chain || !block || !currentSupply || !currentReward || !difficultyTarget) { + uint64_t* currentReward) { + if (!chain || !block || !currentSupply || !currentReward) { return false; } @@ -465,15 +464,8 @@ static bool MineAndAppendBlock(blockchain_t* chain, *currentReward = CalculateBlockReward(*currentSupply, chain); - if (Chain_Size(chain) % DIFFICULTY_ADJUSTMENT_INTERVAL == 0) { - *difficultyTarget = Chain_ComputeNextTarget(chain, *difficultyTarget); - } - - if (Chain_Size(chain) % EPOCH_LENGTH == 0 && Chain_Size(chain) > 0) { - uint8_t dagSeed[32]; - GetNextDAGSeed(chain, dagSeed); - (void)Block_RebuildAutolykos2Dag(CalculateTargetDAGSize(chain), dagSeed); - } + // The difficulty retarget and epoch DAG rebuild happen in Chain_AddBlock, so that blocks we + // receive from peers advance them exactly like blocks we mine ourselves. return true; } @@ -551,12 +543,12 @@ static bool VerifyChainFully(blockchain_t* chain) { Block_Destroy(prevBlk); } - // Determine expected difficulty for this block. TODO: Optimize to recompute at adjustment intervals only instead of every block. + // Determine expected difficulty for this block. The retarget window (i-INTERVAL..i-1) is + // already present in `chain`, so it reads from there rather than the replay copy. if (i < DIFFICULTY_ADJUSTMENT_INTERVAL) { expectedDifficulty = INITIAL_DIFFICULTY; } else if ((i % DIFFICULTY_ADJUSTMENT_INTERVAL) == 0) { - // Compute target using previous blocks only (0..i-1) - expectedDifficulty = Chain_ComputeNextTarget(prevChain, expectedDifficulty); + expectedDifficulty = Chain_ComputeTargetAtHeight(chain, (uint64_t)i, expectedDifficulty); } // Ensure the block's header difficulty matches the expected difficulty (can't cheat easier) @@ -723,6 +715,10 @@ int main(int argc, char* argv[]) { if (!Chain_RecomputeRuntimeState(chain)) { fprintf(stderr, "Failed to recompute runtime state from loaded chain\n"); } + + // chain.meta stores the tip's own target, which is not the next block's target when the tip + // sits on an adjustment boundary. Derive it from the chain instead. + Chain_OnTipAdvanced(chain); } if (!BalanceSheet_LoadFromFile(chainDataDir)) { @@ -899,7 +895,7 @@ int main(int argc, char* argv[]) { break; } - block_t* block = BuildNextBlock(chain, difficultyTarget); + block_t* block = BuildNextBlock(chain, Chain_GetTargetForHeight(chain, (uint64_t)Chain_Size(chain))); if (!block) { fprintf(stderr, "failed to create block\n"); free(acceptedTxs); @@ -923,7 +919,7 @@ int main(int argc, char* argv[]) { } free(acceptedTxs); - if (!MineAndAppendBlock(chain, block, ¤tSupply, ¤tReward, &difficultyTarget)) { + if (!MineAndAppendBlock(chain, block, ¤tSupply, ¤tReward)) { Block_Destroy(block); minedAll = false; break; @@ -1000,7 +996,7 @@ int main(int argc, char* argv[]) { continue; } - block_t* block = BuildNextBlock(chain, difficultyTarget); + block_t* block = BuildNextBlock(chain, Chain_GetTargetForHeight(chain, (uint64_t)Chain_Size(chain))); if (!block) { fprintf(stderr, "failed to create block\n"); continue; @@ -1029,7 +1025,7 @@ int main(int argc, char* argv[]) { AddressToHexString(recipientAddress, recipientHex); printf("%s\n\nMining block...\n", recipientHex); - if (!MineAndAppendBlock(chain, block, ¤tSupply, ¤tReward, &difficultyTarget)) { + if (!MineAndAppendBlock(chain, block, ¤tSupply, ¤tReward)) { Block_Destroy(block); continue; }