diff --git a/include/block/chain.h b/include/block/chain.h index d95bfba..6606182 100644 --- a/include/block/chain.h +++ b/include/block/chain.h @@ -28,6 +28,33 @@ void Chain_Wipe(blockchain_t* chain); // Returns true on success. bool Chain_RollbackToHeight(blockchain_t* chain, size_t height); +/** + * Atomically replace the blocks at [forkHeight, tip] with `newBlocks` (ascending, `count` of them). + * + * The swap happens only if the candidate branch is properly linked, has strictly more cumulative + * work, and has served its Horizen delayed-submission penalty. `observedAtTipHeight` is the local + * tip height at which the branch was FIRST seen and must not be recomputed as the chain grows -- + * see the comment in the implementation. The initial-block-download exemption is decided inside, + * from local state only, so no caller can switch the penalty off. + * + * On any failure the original chain, balance sheet, supply and reward are restored and false is + * returned. The caller keeps ownership of `newBlocks` in every case: the chain applies copies. +**/ +bool Chain_ReplaceBranch(blockchain_t* chain, + size_t forkHeight, + block_t** newBlocks, + size_t count, + uint64_t observedAtTipHeight); + +// True when this node is catching up rather than following the tip (empty chain, or a median +// block time far in the past). Used to exempt initial sync from the reorg penalty. +bool Chain_IsInitialBlockDownload(blockchain_t* chain); + +// Penalty in blocks of local chain growth before a branch forking `reorgDepth` blocks back may be +// adopted. Thin wrapper over FetchScheduler_ComputeReorgPenaltyBlocks, for callers that only +// want to report it. +uint64_t Chain_ReorgPenaltyForDepth(uint64_t reorgDepth); + // Recompute `currentSupply` and `currentReward` from the in-memory chain blocks. // Returns true on success and updates runtime state globals. bool Chain_RecomputeRuntimeState(blockchain_t* chain); @@ -53,4 +80,15 @@ uint32_t Chain_GetTargetForHeight(blockchain_t* chain, uint64_t height); // Call after any change to the tip. Must NOT be called while holding `chainLock`. void Chain_OnTipAdvanced(blockchain_t* chain); +// Work +// Expected number of hashes to satisfy `difficultyTargetBits`, i.e. 2^256 / (target + 1). +bool Chain_ComputeBlockWork(uint32_t difficultyTargetBits, uint256_t* outWork); + +// Summed work of the chain's blocks over the half-open range [from, to). +// Takes no locks; safe to call while holding `chainLock`. +bool Chain_ComputeWorkRange(blockchain_t* chain, size_t from, size_t to, uint256_t* outWork); + +// Summed work of a candidate branch that is not (yet) part of the chain. +bool Chain_ComputeBranchWork(block_t** blocks, size_t count, uint256_t* outWork); + #endif diff --git a/include/constants.h b/include/constants.h index 248b343..6358779 100644 --- a/include/constants.h +++ b/include/constants.h @@ -43,14 +43,44 @@ static const int MAX_SYNC_RETRIES = 4; // retry attempts per block fetch static const uint64_t SYNC_BACKOFF_BASE_MS = 200ULL; // base backoff in ms (exponential) // Parallelism static const int MAX_PARALLEL_FETCHES = 8; // concurrent block fetches during windowed sync -// Heuristic: if peer is this many blocks ahead, treat as initial sync -static const uint64_t INITIAL_SYNC_HEIGHT_DIFF = 50ULL; +// How far below a detected divergence we ask a peer for blocks, so the orphan pool has enough of +// the competing branch to locate the fork point by prevHash linkage. +static const uint64_t REORG_FETCH_DEPTH = 128ULL; +// How many times one `sync` will probe downwards for a fork point before giving up, so a peer on a +// permanently incompatible chain cannot keep us looping. +static const int MAX_FORK_PROBE_ROUNDS = 3; -// Reorg penalty configuration (used to penalize peers reporting higher heights but with delayed work) +// Reorg penalty configuration (Horizen-style delayed block submission penalty). +// A branch forking B blocks below our tip is held for penalty(B) blocks of local chain growth +// before it may be adopted, so a rented-hashrate attacker has to sustain the attack publicly +// instead of winning by dumping a privately mined branch. +// +// penalty(B) = ceil(FACTOR_NUM/FACTOR_DEN * B^EXPONENT * TARGET_BLOCK_TIME / REF_BLOCK_TIME) +// +// Expressed as integer rationals on purpose: this feeds fork choice, so it must evaluate +// identically on every node. Floating point is not acceptable here. static const uint64_t REORG_PENALTY_GRACE_BLOCKS = 3ULL; // allow small reorgs without penalty -static const double REORG_PENALTY_FACTOR = 1.0; // base scaling factor (theta) -static const double REORG_PENALTY_EXPONENT = 2.0; // exponent p in penalty ~ B^p -static const double REORG_PENALTY_REF_BLOCK_TIME = 150.0; // reference block time in seconds used by original scheme +static const uint64_t REORG_PENALTY_FACTOR_NUM = 1ULL; // base scaling factor (theta), numerator +static const uint64_t REORG_PENALTY_FACTOR_DEN = 1ULL; // base scaling factor (theta), denominator +static const uint32_t REORG_PENALTY_EXPONENT = 2U; // exponent p in penalty ~ B^p +static const uint64_t REORG_PENALTY_REF_BLOCK_TIME = 150ULL; // reference block time in seconds used by original scheme +// Beyond this depth the penalty saturates. At the configured parameters penalty(1000) is already +// ~600k blocks (over a year), so this only exists to keep the arithmetic away from overflow. +static const uint64_t REORG_PENALTY_MAX_DEPTH = 1000ULL; + +// Upper bound on pooled orphan blocks. Orphans are accepted before the chain-derived difficulty +// check (that lives in Chain_AddBlock, which orphans only reach on attach), so without a cap a +// peer can push blocks at an arbitrary height until the node runs out of memory. +static const size_t MAX_ORPHAN_BLOCKS = 512U; + +// A node whose chain tip is older than this many target block times is catching up rather than +// following the tip, and is exempt from the reorg penalty (Horizen does the same via +// IsInitialBlockDownload). Determined purely from local state, so an unverified peer cannot +// trigger the exemption by claiming a large height. +static const uint64_t IBD_TIP_AGE_BLOCKS = 500ULL; +// Number of trailing blocks whose median timestamp is used for the age test above. Using a median +// rather than the tip alone means a single miner cannot backdate one block to fake being in IBD. +static const size_t MEDIAN_TIME_SPAN = 11U; // Reward schedule acceleration: 1 means normal-speed progression. #define EMISSION_ACCELERATION_FACTOR 1ULL @@ -70,9 +100,12 @@ static const double REORG_PENALTY_REF_BLOCK_TIME = 150.0; // reference block tim #define DAG_BASE_GROWTH (1ULL << 30) // 1 GB per epoch, adjusted by acceleration //#define DAG_BASE_SIZE (6ULL << 30) // 6 GB, adjusted per cycle based off DAG_BASE_GROWTH #define DAG_BASE_SIZE (1ULL << 30) // TEMPORARY FOR TESTING -// Swings - calculated as MIN(percentage, absolute GB) to prevent absurd swings from low hashrate or very large DAG growth -#define DAG_MAX_UP_SWING_PERCENTAGE 1.15 // 15% -#define DAG_MAX_DOWN_SWING_PERCENTAGE 0.90 // 10% +// Swings - calculated as MIN(percentage, absolute GB) to prevent absurd swings from low hashrate or very large DAG growth. +// Percentages are integer numerator/denominator pairs, never float literals: DAG size feeds PoW +// verification, so it has to evaluate identically on every node. +#define DAG_MAX_UP_SWING_PERCENT_NUM 15ULL // +15% +#define DAG_MAX_DOWN_SWING_PERCENT_NUM 10ULL // -10% +#define DAG_SWING_PERCENT_DEN 100ULL #define DAG_MAX_UP_SWING_GB (2ULL << 30) // 2 GB #define DAG_MAX_DOWN_SWING_GB (1ULL << 30) // 1 GB #define DAG_GENESIS_SEED 0x00 // Genesis seed is zeroes, every epoch's seed is the hash of the previous block, therefore unpredictable until the block is mined @@ -184,7 +217,6 @@ static inline uint64_t CalculateBlockReward(uint256_t currentSupply, blockchain_ } // Hashing DAG -#include static inline size_t CalculateTargetDAGSize(blockchain_t* chain) { // Base size plus (base growth * difficulty factor), adjusted by acceleration if (!chain || !chain->blocks) { return 0; } // Invalid @@ -213,12 +245,12 @@ static inline size_t CalculateTargetDAGSize(blockchain_t* chain) { // Clamp if (growth > 0) { // Difficulty increased -> Clamp the UPWARD swing - int64_t maxUp = (int64_t)((DAG_BASE_SIZE * 15) / 100); // 15% + int64_t maxUp = (int64_t)((DAG_BASE_SIZE * DAG_MAX_UP_SWING_PERCENT_NUM) / DAG_SWING_PERCENT_DEN); if (growth > maxUp) growth = maxUp; if (growth > (int64_t)DAG_MAX_UP_SWING_GB) growth = DAG_MAX_UP_SWING_GB; } else { // Difficulty decreased -> Clamp the DOWNWARD swing - int64_t maxDown = (int64_t)((DAG_BASE_SIZE * 10) / 100); // 10% + int64_t maxDown = (int64_t)((DAG_BASE_SIZE * DAG_MAX_DOWN_SWING_PERCENT_NUM) / DAG_SWING_PERCENT_DEN); if (-growth > maxDown) growth = -maxDown; if (-growth > (int64_t)DAG_MAX_DOWN_SWING_GB) growth = -(int64_t)DAG_MAX_DOWN_SWING_GB; } diff --git a/include/nets/orphan_pool.h b/include/nets/orphan_pool.h index d609e0f..a221aa6 100644 --- a/include/nets/orphan_pool.h +++ b/include/nets/orphan_pool.h @@ -2,6 +2,7 @@ #define ORPHAN_POOL_H #include +#include #include #include @@ -10,11 +11,21 @@ void OrphanPool_Init(void); void OrphanPool_Destroy(void); // Insert an orphan block into the pool. Ownership of `block` is transferred to the pool. -// `height` is the block number from the header. -void OrphanPool_Insert(block_t* block, uint64_t height); +// `height` is the block number from the header. `observedAtTipHeight` is the local chain tip +// height at the moment the block arrived; it is stamped once and drives the Horizen reorg +// penalty, so it must never be re-derived from a later tip. +// Duplicates (same block hash) are rejected and the block is destroyed. +void OrphanPool_Insert(block_t* block, uint64_t height, uint64_t observedAtTipHeight); -// Attempt to attach any orphans whose parents now exist in `chain`. +// Attempt to attach any orphans whose parents now exist in `chain`, and to adopt a competing +// branch when one is heavier and has served its reorg penalty. // Returns the number of blocks successfully attached. size_t OrphanPool_AttemptAttach(blockchain_t* chain); +// True if a block with this hash is already pooled. +bool OrphanPool_Contains(const uint8_t blockHash[32]); + +// Number of pooled orphans (diagnostics). +size_t OrphanPool_Size(void); + #endif diff --git a/include/uint256.h b/include/uint256.h index e7d261a..e3b1147 100644 --- a/include/uint256.h +++ b/include/uint256.h @@ -114,6 +114,72 @@ static inline int uint256_cmp(const uint256_t* a, const uint256_t* b) { return 0; } +static inline bool uint256_is_zero(const uint256_t* a) { + return a && a->limbs[0] == 0 && a->limbs[1] == 0 && a->limbs[2] == 0 && a->limbs[3] == 0; +} + +/** + * Builds a uint256 from 32 big-endian bytes, the layout used by hashes and by decoded + * difficulty targets (see DecodeCompactTarget). +**/ +static inline uint256_t uint256_from_be_bytes(const uint8_t bytes[32]) { + uint256_t res = {{0, 0, 0, 0}}; + if (!bytes) { + return res; + } + + for (int limb = 0; limb < 4; ++limb) { + // limbs[0] is the least significant, so it holds the LAST eight bytes. + const uint8_t* src = bytes + (3 - limb) * 8; + uint64_t value = 0; + for (int b = 0; b < 8; ++b) { + value = (value << 8) | (uint64_t)src[b]; + } + res.limbs[limb] = value; + } + + return res; +} + +static inline void uint256_bitwise_not(uint256_t* a) { + if (!a) { + return; + } + for (int i = 0; i < 4; ++i) { + a->limbs[i] = ~a->limbs[i]; + } +} + +/** + * Unsigned 256-bit division by restoring binary long division. + * Returns false (leaving *outQuotient untouched) when dividing by zero. +**/ +static inline bool uint256_divide(const uint256_t* numerator, const uint256_t* denominator, uint256_t* outQuotient) { + if (!numerator || !denominator || !outQuotient || uint256_is_zero(denominator)) { + return false; + } + + uint256_t quotient = uint256_from_u64(0); + uint256_t remainder = uint256_from_u64(0); + + for (int bit = 255; bit >= 0; --bit) { + // remainder = (remainder << 1) | bit_of_numerator + for (int i = 3; i > 0; --i) { + remainder.limbs[i] = (remainder.limbs[i] << 1) | (remainder.limbs[i - 1] >> 63); + } + remainder.limbs[0] <<= 1; + remainder.limbs[0] |= (numerator->limbs[bit / 64] >> (bit % 64)) & 1ULL; + + if (uint256_cmp(&remainder, denominator) >= 0) { + (void)uint256_subtract(&remainder, denominator); + quotient.limbs[bit / 64] |= (1ULL << (bit % 64)); + } + } + + *outQuotient = quotient; + return true; +} + static inline void uint256_serialize(const uint256_t* value, char* out) { if (!value || !out) { return; diff --git a/src/block/chain.c b/src/block/chain.c index 0c7013a..a9b899f 100644 --- a/src/block/chain.c +++ b/src/block/chain.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -98,6 +99,51 @@ static bool DebitAddress(const uint8_t address[32], const uint256_t* amount) { return BalanceSheet_Insert(entry) >= 0; } +/** + * Borrow the transaction list of the block at `index`. + * + * In-memory blocks are compacted to headers only once they have been persisted (Chain_SaveToFile) + * or loaded without transactions (Chain_LoadFromFile), so any full replay of the chain has to be + * able to fall back to the on-disk copy. On success `*outLoadedFromDisk` tells the caller whether + * the returned block is a temporary that must be released with Chain_ReturnBlockTransactions. + * Takes no locks; callers are expected to already hold `chainLock`. +**/ +static bool Chain_BorrowBlockTransactions(blockchain_t* chain, size_t index, block_t** outBlock, bool* outLoadedFromDisk) { + if (!chain || !chain->blocks || !outBlock || !outLoadedFromDisk) { + return false; + } + + *outBlock = NULL; + *outLoadedFromDisk = false; + + block_t* blk = (block_t*)DynArr_at(chain->blocks, index); + if (blk && blk->transactions) { + *outBlock = blk; + return true; + } + + block_t* loadedBlk = NULL; + size_t txCount = 0; + if (!Chain_LoadBlockFromFile(chainDataDir, (uint64_t)index, true, &loadedBlk, &txCount) || !loadedBlk) { + return false; + } + + *outBlock = loadedBlk; + *outLoadedFromDisk = true; + return true; +} + +static void Chain_ReturnBlockTransactions(block_t* blk, bool loadedFromDisk) { + if (!loadedFromDisk || !blk) { + return; + } + + if (blk->transactions) { + DynArr_destroy(blk->transactions); + } + free(blk); +} + bool Chain_RecomputeRuntimeState(blockchain_t* chain) { if (!chain) { return false; @@ -105,23 +151,28 @@ bool Chain_RecomputeRuntimeState(blockchain_t* chain) { 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) { + block_t* blk = NULL; + bool loadedFromDisk = false; + if (!Chain_BorrowBlockTransactions(chain, i, &blk, &loadedFromDisk)) { 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) { + Chain_ReturnBlockTransactions(blk, loadedFromDisk); return false; } if (Address_IsCoinbase(tx->transaction.senderAddress)) { if (uint256_add_u64(&rebuiltSupply, tx->transaction.amount1)) { + Chain_ReturnBlockTransactions(blk, loadedFromDisk); return false; } } } + + Chain_ReturnBlockTransactions(blk, loadedFromDisk); } currentSupply = rebuiltSupply; @@ -168,30 +219,45 @@ void Chain_Destroy(blockchain_t* chain) { } } -bool Chain_AddBlock(blockchain_t* chain, block_t* block) { +/** + * Append `block` to the tip. + * + * Caller MUST already hold `chainLock` (write) and `balanceSheetLock`, and MUST call + * Chain_OnTipAdvanced afterwards (outside the locks) if this returns true. Chain_AddBlock is the + * locked wrapper for single appends; Chain_ReplaceBranch drives this directly so that a whole + * branch swap happens under one lock acquisition. +**/ +static bool Chain_AddBlockLocked(blockchain_t* chain, block_t* block) { bool ok = true; - if (!chain || !block || !chain->blocks) { + if (!chain || !block || !chain->blocks || !block->transactions) { return false; } - if (!block->transactions) { - return false; - } - - // Acquire global write locks to protect chain and balance sheet mutations. - pthread_rwlock_wrlock(&chainLock); - pthread_mutex_lock(&balanceSheetLock); - // Ensure the incoming block's header.blockNumber matches the index it will be appended at. size_t expectedIndex = DynArr_size(chain->blocks); if (block->header.blockNumber != expectedIndex) { // Mismatched block number; reject to avoid duplicate indices or inconsistent headers. - pthread_mutex_unlock(&balanceSheetLock); - pthread_rwlock_unlock(&chainLock); return false; } + // Ensure the block actually builds on our tip. Without this, a rollback-then-reapply path can + // splice blocks from two different forks into a chain that no longer links up. + if (expectedIndex > 0) { + block_t* parent = (block_t*)DynArr_at(chain->blocks, expectedIndex - 1); + if (!parent) { + return false; + } + + uint8_t parentHash[32]; + Block_CalculateHash(parent, parentHash); + if (memcmp(parentHash, block->header.prevHash, sizeof(parentHash)) != 0) { + printf("Chain_AddBlock: validation failed: blockIndex=%zu prevHash does not match current tip\n", + expectedIndex); + 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. @@ -201,8 +267,6 @@ bool Chain_AddBlock(blockchain_t* chain, block_t* block) { expectedIndex, (unsigned int)expectedTarget, (unsigned int)block->header.difficultyTarget); - pthread_mutex_unlock(&balanceSheetLock); - pthread_rwlock_unlock(&chainLock); return false; } @@ -349,10 +413,37 @@ bool Chain_AddBlock(blockchain_t* chain, block_t* block) { } } } + + // Advance supply and reward here rather than in each caller. Callers used to do this + // themselves, which meant the orphan-attach and maintenance-thread paths never did it: the + // next block's coinbase was then validated against a stale currentReward and rejected + // forever. It also has to happen per block so that applying a whole branch works. + if (ok) { + (void)uint256_add_u64(¤tSupply, expectedCoinbaseAmount); + currentReward = CalculateBlockReward(currentSupply, chain); + } // ok remains true if no failures } while (0); - // Release locks + if (ok) { + printf("Added new block to chain:\n"); + Block_ShortPrint(block); + } + + return ok; +} + +bool Chain_AddBlock(blockchain_t* chain, block_t* block) { + if (!chain || !block || !chain->blocks || !block->transactions) { + return false; + } + + // Acquire global write locks to protect chain and balance sheet mutations. + pthread_rwlock_wrlock(&chainLock); + pthread_mutex_lock(&balanceSheetLock); + + bool ok = Chain_AddBlockLocked(chain, block); + pthread_mutex_unlock(&balanceSheetLock); pthread_rwlock_unlock(&chainLock); @@ -361,11 +452,6 @@ bool Chain_AddBlock(blockchain_t* chain, block_t* block) { Chain_OnTipAdvanced(chain); } - printf("Added new block to chain:\n"); - Block_ShortPrint(block); - - /* Debug proof removed: coinbase == baseReward + totalFees was printed here during debugging. */ - return ok; } @@ -439,16 +525,17 @@ bool Chain_IsValid(blockchain_t* chain) { return true; } -bool Chain_RollbackToHeight(blockchain_t* chain, size_t height) { +/** + * Truncate the chain to `height` blocks and rebuild the balance sheet and supply from what remains. + * + * Caller MUST already hold `chainLock` (write) and `balanceSheetLock`, and MUST call + * Chain_OnTipAdvanced afterwards (outside the locks). Chain_RollbackToHeight is the locked wrapper. +**/ +static bool Chain_RollbackToHeightLocked(blockchain_t* chain, size_t height) { if (!chain || !chain->blocks) return false; - pthread_rwlock_wrlock(&chainLock); - pthread_mutex_lock(&balanceSheetLock); - size_t cur = DynArr_size(chain->blocks); if (height >= cur) { - pthread_mutex_unlock(&balanceSheetLock); - pthread_rwlock_unlock(&chainLock); return true; // nothing to do } @@ -470,23 +557,18 @@ bool Chain_RollbackToHeight(blockchain_t* chain, size_t height) { BalanceSheet_Destroy(); BalanceSheet_Init(); + // Supply is accumulated in this same pass. Doing it here rather than in a second + // Chain_RecomputeRuntimeState pass is what lets rollback work at all: that function used to + // bail on any header-only block, which is every block once the chain has been saved or loaded. + 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); - block_t* toProcess = blk; + block_t* toProcess = NULL; bool loaded = false; - if (!blk || !blk->transactions) { - // Try to load from disk - block_t* loadedBlk = NULL; - size_t txCount = 0; - if (!Chain_LoadBlockFromFile(chainDataDir, (uint64_t)i, true, &loadedBlk, &txCount)) { - // Can't rebuild without transactions - pthread_mutex_unlock(&balanceSheetLock); - pthread_rwlock_unlock(&chainLock); - return false; - } - toProcess = loadedBlk; - loaded = true; + if (!Chain_BorrowBlockTransactions(chain, i, &toProcess, &loaded)) { + // Can't rebuild without transactions + return false; } // Apply transactions @@ -498,6 +580,7 @@ bool Chain_RollbackToHeight(blockchain_t* chain, size_t height) { // Coinbase credit if (Address_IsCoinbase(tx->transaction.senderAddress)) { + (void)uint256_add_u64(&rebuiltSupply, tx->transaction.amount1); balance_sheet_entry_t entry; if (!BalanceSheet_Lookup(tx->transaction.recipientAddress1, &entry)) { memset(&entry, 0, sizeof(entry)); @@ -552,17 +635,22 @@ bool Chain_RollbackToHeight(blockchain_t* chain, size_t height) { } } - if (loaded && toProcess) { - if (toProcess->transactions) DynArr_destroy(toProcess->transactions); - free(toProcess); - } + Chain_ReturnBlockTransactions(toProcess, loaded); } - if (!Chain_RecomputeRuntimeState(chain)) { - pthread_mutex_unlock(&balanceSheetLock); - pthread_rwlock_unlock(&chainLock); - return false; - } + currentSupply = rebuiltSupply; + currentReward = CalculateBlockReward(currentSupply, chain); + + return true; +} + +bool Chain_RollbackToHeight(blockchain_t* chain, size_t height) { + if (!chain || !chain->blocks) return false; + + pthread_rwlock_wrlock(&chainLock); + pthread_mutex_lock(&balanceSheetLock); + + bool ok = Chain_RollbackToHeightLocked(chain, height); pthread_mutex_unlock(&balanceSheetLock); pthread_rwlock_unlock(&chainLock); @@ -570,9 +658,295 @@ bool Chain_RollbackToHeight(blockchain_t* chain, size_t height) { // A reorg can move the tip back across an adjustment boundary, so the target must come down too. Chain_OnTipAdvanced(chain); + return ok; +} + +static int Chain_CompareTimestamps(const void* lhs, const void* rhs) { + const uint64_t a = *(const uint64_t*)lhs; + const uint64_t b = *(const uint64_t*)rhs; + if (a < b) return -1; + if (a > b) return 1; + return 0; +} + +/** + * Median timestamp of the last MEDIAN_TIME_SPAN blocks. Caller must hold `chainLock`. +**/ +static uint64_t Chain_MedianTimePastLocked(blockchain_t* chain) { + if (!chain || !chain->blocks) { + return 0ULL; + } + + const size_t size = DynArr_size(chain->blocks); + if (size == 0) { + return 0ULL; + } + + size_t span = size < MEDIAN_TIME_SPAN ? size : MEDIAN_TIME_SPAN; + uint64_t samples[MEDIAN_TIME_SPAN]; + size_t taken = 0; + for (size_t i = 0; i < span; ++i) { + block_t* blk = (block_t*)DynArr_at(chain->blocks, size - 1 - i); + if (!blk) { + continue; + } + samples[taken++] = blk->header.timestamp; + } + + if (taken == 0) { + return 0ULL; + } + + qsort(samples, taken, sizeof(uint64_t), Chain_CompareTimestamps); + return samples[taken / 2]; +} + +bool Chain_IsInitialBlockDownload(blockchain_t* chain) { + if (!chain || !chain->blocks) { + return true; + } + + if (DynArr_size(chain->blocks) == 0) { + return true; + } + + const uint64_t medianTime = Chain_MedianTimePastLocked(chain); + const uint64_t now = get_current_time_ms(); + if (medianTime == 0ULL || now <= medianTime) { + return false; // we are at (or ahead of) the current tip time + } + + const uint64_t ageMs = now - medianTime; + const uint64_t thresholdMs = IBD_TIP_AGE_BLOCKS * (uint64_t)TARGET_BLOCK_TIME * 1000ULL; + return ageMs > thresholdMs; +} + +/** + * Verify that a candidate branch is internally linked and attaches to the block below `forkHeight`. + * Caller must hold `chainLock`. +**/ +static bool Chain_BranchIsLinkedLocked(blockchain_t* chain, size_t forkHeight, block_t** blocks, size_t count) { + if (!chain || !chain->blocks || !blocks || count == 0 || forkHeight == 0) { + return false; + } + + block_t* parent = (block_t*)DynArr_at(chain->blocks, forkHeight - 1); + if (!parent) { + return false; + } + + uint8_t expectedPrevHash[32]; + Block_CalculateHash(parent, expectedPrevHash); + + for (size_t i = 0; i < count; ++i) { + if (!blocks[i] || !blocks[i]->transactions) { + return false; + } + if (blocks[i]->header.blockNumber != (uint64_t)(forkHeight + i)) { + return false; + } + if (memcmp(blocks[i]->header.prevHash, expectedPrevHash, sizeof(expectedPrevHash)) != 0) { + return false; + } + Block_CalculateHash(blocks[i], expectedPrevHash); + } + return true; } +static void Chain_FreeBlockArray(block_t** blocks, size_t count, size_t consumedByChain) { + if (!blocks) { + return; + } + + for (size_t i = 0; i < count; ++i) { + if (!blocks[i]) { + continue; + } + if (i < consumedByChain) { + // Chain_AddBlockLocked shallow-copies the struct, so the chain owns the transactions + // now. Free only our wrapper -- Block_Destroy would take the chain's array with it. + free(blocks[i]); + } else { + Block_Destroy(blocks[i]); + } + } + + free(blocks); +} + +uint64_t Chain_ReorgPenaltyForDepth(uint64_t reorgDepth) { + return FetchScheduler_ComputeReorgPenaltyBlocks(reorgDepth); +} + +bool Chain_ReplaceBranch(blockchain_t* chain, + size_t forkHeight, + block_t** newBlocks, + size_t count, + uint64_t observedAtTipHeight) { + if (!chain || !chain->blocks || !newBlocks || count == 0 || forkHeight == 0) { + return false; + } + + pthread_rwlock_wrlock(&chainLock); + pthread_mutex_lock(&balanceSheetLock); + + bool ok = false; + block_t** snapshot = NULL; // deep copies of the blocks we are replacing + size_t snapshotCount = 0; + size_t snapshotConsumed = 0; + block_t** candidate = NULL; // deep copies of the branch we are applying + size_t candidateConsumed = 0; + + do { + const size_t tipCount = DynArr_size(chain->blocks); + if (forkHeight > tipCount) { + break; // fork point is beyond our chain; nothing to replace + } + + if (!Chain_BranchIsLinkedLocked(chain, forkHeight, newBlocks, count)) { + printf("Chain_ReplaceBranch: candidate branch at height %zu is not linked; refusing\n", forkHeight); + break; + } + + // Horizen-style delayed block submission penalty. A branch that forks `depth` blocks below + // our tip is held until our own chain has advanced `penalty(depth)` blocks, so a rented- + // hashrate attacker cannot win by dumping a privately mined branch in one go. The depth is + // measured at FIRST OBSERVATION and never recomputed: if it were re-derived from the moving + // tip, depth and elapsed would both grow by one per block while penalty(depth) grows + // faster, and a penalized branch could never be adopted at all. + // The initial-block-download exemption is decided here from local state only, never handed + // in by a caller: a peer claiming a huge height must not be able to switch the penalty off. + const bool inInitialBlockDownload = Chain_IsInitialBlockDownload(chain); + const uint64_t tipHeight = tipCount > 0 ? (uint64_t)(tipCount - 1) : 0ULL; + if (!inInitialBlockDownload && tipCount > forkHeight) { + const uint64_t observedTip = observedAtTipHeight > tipHeight ? tipHeight : observedAtTipHeight; + const uint64_t depth = observedTip >= (uint64_t)forkHeight + ? (observedTip - (uint64_t)forkHeight + 1ULL) + : 1ULL; + const uint64_t penalty = FetchScheduler_ComputeReorgPenaltyBlocks(depth); + const uint64_t elapsed = tipHeight >= observedTip ? (tipHeight - observedTip) : 0ULL; + + if (elapsed < penalty) { + printf("Chain_ReplaceBranch: deferring reorg at height %zu: depth=%" PRIu64 + " penalty=%" PRIu64 " elapsed=%" PRIu64 "\n", + forkHeight, depth, penalty, elapsed); + break; + } + } + + // Most cumulative work wins, not most blocks. Difficulty varies, so a long low-difficulty + // branch must not beat a short high-difficulty one. Strictly greater, so tied tips do not + // cause the two nodes to keep swapping. + uint256_t incumbentWork; + uint256_t candidateWork; + if (!Chain_ComputeWorkRange(chain, forkHeight, tipCount, &incumbentWork) || + !Chain_ComputeBranchWork(newBlocks, count, &candidateWork)) { + break; + } + if (uint256_cmp(&candidateWork, &incumbentWork) <= 0) { + break; // not heavier; keep what we have + } + + // Snapshot what we are about to discard so a failed apply can be undone. The in-memory + // blocks are freed by the rollback and the on-disk copy is overwritten by the next save, + // so without this a partial apply would be unrecoverable. + snapshotCount = tipCount - forkHeight; + if (snapshotCount > 0) { + snapshot = (block_t**)calloc(snapshotCount, sizeof(block_t*)); + if (!snapshot) { + break; + } + + bool snapshotOk = true; + for (size_t i = 0; i < snapshotCount; ++i) { + block_t* src = NULL; + bool loadedFromDisk = false; + if (!Chain_BorrowBlockTransactions(chain, forkHeight + i, &src, &loadedFromDisk)) { + snapshotOk = false; + break; + } + snapshot[i] = Block_Copy(src); + Chain_ReturnBlockTransactions(src, loadedFromDisk); + if (!snapshot[i]) { + snapshotOk = false; + break; + } + } + if (!snapshotOk) { + break; + } + } + + // Apply copies so the caller's blocks are never aliased by the chain nor destroyed by a + // rollback -- the caller keeps ownership of what it passed in, whatever happens here. + candidate = (block_t**)calloc(count, sizeof(block_t*)); + if (!candidate) { + break; + } + bool copiedAll = true; + for (size_t i = 0; i < count; ++i) { + candidate[i] = Block_Copy(newBlocks[i]); + if (!candidate[i]) { + copiedAll = false; + break; + } + } + if (!copiedAll) { + break; + } + + if (!Chain_RollbackToHeightLocked(chain, forkHeight)) { + printf("Chain_ReplaceBranch: rollback to height %zu failed; chain unchanged\n", forkHeight); + break; + } + + bool applied = true; + for (size_t i = 0; i < count; ++i) { + if (!Chain_AddBlockLocked(chain, candidate[i])) { + applied = false; + break; + } + candidateConsumed++; + } + + if (applied) { + ok = true; + break; + } + + // Undo: drop whatever of the candidate branch made it in and put the original blocks back. + printf("Chain_ReplaceBranch: candidate branch failed to apply at height %zu; restoring previous chain\n", + forkHeight + candidateConsumed); + + if (!Chain_RollbackToHeightLocked(chain, forkHeight)) { + fprintf(stderr, "Chain_ReplaceBranch: FAILED to roll back after a failed apply; chain is truncated to %zu\n", + forkHeight); + break; + } + candidateConsumed = 0; // the rollback freed the copies' transactions + + for (size_t i = 0; i < snapshotCount; ++i) { + if (!Chain_AddBlockLocked(chain, snapshot[i])) { + fprintf(stderr, "Chain_ReplaceBranch: FAILED to restore original block %zu; chain is truncated to %zu\n", + forkHeight + i, forkHeight + i); + break; + } + snapshotConsumed++; + } + } while (0); + + Chain_FreeBlockArray(snapshot, snapshotCount, snapshotConsumed); + Chain_FreeBlockArray(candidate, candidate ? count : 0, candidateConsumed); + + pthread_mutex_unlock(&balanceSheetLock); + pthread_rwlock_unlock(&chainLock); + + Chain_OnTipAdvanced(chain); + + return ok; +} + void Chain_Wipe(blockchain_t* chain) { Chain_ClearBlocks(chain); currentBlockHeight = 0; @@ -1149,13 +1523,16 @@ uint32_t Chain_ComputeTargetAtHeight(blockchain_t* chain, uint64_t height, uint3 } const uint64_t targetTime = (uint64_t)TARGET_BLOCK_TIME * 1000ULL * (uint64_t)DIFFICULTY_ADJUSTMENT_INTERVAL; - double timeRatio = (double)actualTime / (double)targetTime; - // Clamp per-epoch target movement: at most x2 easier or x2 harder. TODO: Check if the clamp should be more aggressive or looser - if (timeRatio > 2.0) { - timeRatio = 2.0; - } else if (timeRatio < 0.5) { - timeRatio = 0.5; + // Clamp per-epoch target movement: at most x2 easier or x2 harder. Clamping the measured span + // is equivalent to clamping the ratio, but stays in integers. + // Everything below is deliberately integer-only: the retarget is consensus-critical, and any + // floating-point rounding difference between nodes would make them disagree on the target. + uint64_t clampedTime = actualTime; + if (clampedTime > targetTime * 2ULL) { + clampedTime = targetTime * 2ULL; + } else if (clampedTime < targetTime / 2ULL) { + clampedTime = targetTime / 2ULL; } uint32_t exponent = currentTarget >> 24; @@ -1164,15 +1541,18 @@ uint32_t Chain_ComputeTargetAtHeight(blockchain_t* chain, uint64_t height, uint3 return INITIAL_DIFFICULTY; } - double newMantissa = (double)mantissa * timeRatio; + // newMantissa = mantissa * clampedTime / targetTime. The mantissa is at most 23 bits and + // clampedTime at most 2 * targetTime (~30 bits at the configured block time), so the product + // cannot overflow 64 bits. + uint64_t newMantissa = ((uint64_t)mantissa * clampedTime) / targetTime; // Normalize to compact format range. - while (newMantissa > 8388607.0) { // 0x007fffff - newMantissa /= 256.0; + while (newMantissa > 0x007fffffULL) { + newMantissa /= 256ULL; exponent++; } - while (newMantissa > 0.0 && newMantissa < 32768.0 && exponent > 3) { // Keep coefficient in normal range - newMantissa *= 256.0; + while (newMantissa > 0ULL && newMantissa < 32768ULL && exponent > 3) { // Keep coefficient in normal range + newMantissa *= 256ULL; exponent--; } @@ -1206,6 +1586,93 @@ uint32_t Chain_GetTargetForHeight(blockchain_t* chain, uint64_t height) { return target; } +bool Chain_ComputeBlockWork(uint32_t difficultyTargetBits, uint256_t* outWork) { + if (!outWork) { + return false; + } + + uint8_t targetBytes[32]; + if (!DecodeCompactTarget(difficultyTargetBits, targetBytes)) { + return false; + } + + const uint256_t target = uint256_from_be_bytes(targetBytes); + + // work = 2^256 / (target + 1). 2^256 is not representable, but since 2^256 >= target + 1 it + // equals (~target / (target + 1)) + 1, which is: all integer, no approximation. + uint256_t denominator = target; + if (uint256_add_u64(&denominator, 1ULL) || uint256_is_zero(&denominator)) { + return false; // target was the maximum representable value + } + + uint256_t numerator = target; + uint256_bitwise_not(&numerator); + + uint256_t work; + if (!uint256_divide(&numerator, &denominator, &work)) { + return false; + } + if (uint256_add_u64(&work, 1ULL)) { + return false; + } + + *outWork = work; + return true; +} + +bool Chain_ComputeWorkRange(blockchain_t* chain, size_t from, size_t to, uint256_t* outWork) { + if (!chain || !chain->blocks || !outWork || from > to) { + return false; + } + + if (to > DynArr_size(chain->blocks)) { + return false; + } + + uint256_t total = uint256_from_u64(0); + for (size_t i = from; i < to; ++i) { + block_t* blk = (block_t*)DynArr_at(chain->blocks, i); + if (!blk) { + return false; + } + + uint256_t work; + if (!Chain_ComputeBlockWork(blk->header.difficultyTarget, &work)) { + return false; + } + if (uint256_add(&total, &work)) { + return false; // 256-bit overflow; not reachable for any real chain + } + } + + *outWork = total; + return true; +} + +bool Chain_ComputeBranchWork(block_t** blocks, size_t count, uint256_t* outWork) { + if (!blocks || !outWork) { + return false; + } + + uint256_t total = uint256_from_u64(0); + for (size_t i = 0; i < count; ++i) { + if (!blocks[i]) { + return false; + } + + uint256_t work; + if (!Chain_ComputeBlockWork(blocks[i]->header.difficultyTarget, &work)) { + return false; + } + if (uint256_add(&total, &work)) { + return false; + } + } + + *outWork = total; + return true; +} + void Chain_OnTipAdvanced(blockchain_t* chain) { if (!chain || !chain->blocks) { return; diff --git a/src/main.c b/src/main.c index 81d1c1e..019626d 100644 --- a/src/main.c +++ b/src/main.c @@ -289,7 +289,7 @@ static bool ComputeEpochDagBytesForHeightFromChain(const blockchain_t* chain, ui int64_t growth = (int64_t)((int64_t)DAG_BASE_GROWTH * difficultyDelta); if (growth > 0) { - int64_t maxUp = (int64_t)((DAG_BASE_SIZE * 15ULL) / 100ULL); + int64_t maxUp = (int64_t)((DAG_BASE_SIZE * DAG_MAX_UP_SWING_PERCENT_NUM) / DAG_SWING_PERCENT_DEN); if (growth > maxUp) { growth = maxUp; } @@ -297,7 +297,7 @@ static bool ComputeEpochDagBytesForHeightFromChain(const blockchain_t* chain, ui growth = (int64_t)DAG_MAX_UP_SWING_GB; } } else { - int64_t maxDown = (int64_t)((DAG_BASE_SIZE * 10ULL) / 100ULL); + int64_t maxDown = (int64_t)((DAG_BASE_SIZE * DAG_MAX_DOWN_SWING_PERCENT_NUM) / DAG_SWING_PERCENT_DEN); if (-growth > maxDown) { growth = -maxDown; } @@ -403,6 +403,36 @@ static bool Block_GetCoinbaseAndFeeTotals(const block_t* block, uint64_t* outCoi return true; } +/** + * Ask `peerConn` for its blocks in [topHeight - REORG_FETCH_DEPTH, topHeight], newest first. + * + * Replies arrive asynchronously as BLOCK_DATA and go through Node_ParseAndAcceptBlock, which routes + * blocks below our tip into the orphan pool rather than dropping them. The pool then locates the + * common ancestor by prevHash linkage and Chain_ReplaceBranch decides whether to adopt. FETCH_BLOCK + * already answers from the peer's own chain, so finding a fork needs no new packet type. +**/ +static void RequestForkWindow(net_node_t* node, tcp_connection_t* peerConn, uint64_t topHeight) { + if (!node || !peerConn) { + return; + } + + const uint64_t from = (topHeight > REORG_FETCH_DEPTH) ? (topHeight - REORG_FETCH_DEPTH) : 0ULL; + printf("Requesting peer blocks %" PRIu64 "..%" PRIu64 " to locate the fork point\n", from, topHeight); + + for (uint64_t hh = topHeight + 1; hh-- > from; ) { + uint64_t req = hh; + if (Node_SendPacket(node, peerConn, PACKET_TYPE_FETCH_BLOCK, &req, sizeof(req)) != 0) { + break; + } + if (hh == 0) { + break; + } + } + + // Give the asynchronous replies time to land in the pool. + sleep_for_milliseconds(SYNC_REQUEST_TIMEOUT_MS); +} + static bool MineAndAppendBlock(blockchain_t* chain, block_t* block, uint256_t* currentSupply, @@ -444,8 +474,6 @@ static bool MineAndAppendBlock(blockchain_t* chain, BalanceSheet_SaveToFile(chainDataDir); } - (void)uint256_add_u64(currentSupply, coinbaseAmount); - uint8_t canonicalHash[32]; uint8_t powHash[32]; Block_CalculateHash(block, canonicalHash); @@ -462,10 +490,8 @@ static bool MineAndAppendBlock(blockchain_t* chain, powHash[0], powHash[1], powHash[2], powHash[3], canonicalHash[0], canonicalHash[1], canonicalHash[2], canonicalHash[3]); - *currentReward = CalculateBlockReward(*currentSupply, chain); - - // 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. + // Supply, reward, the difficulty retarget and the epoch DAG rebuild all happen inside + // Chain_AddBlock, so that blocks we receive from peers advance them exactly like ones we mine. return true; } @@ -1072,36 +1098,25 @@ int main(int argc, char* argv[]) { // Continue syncing in a loop until we've caught up to the peer or no progress is made. bool madeProgressOverall = false; + int forkProbes = 0; while (true) { uint64_t localHeight = (uint64_t)Chain_Size(chain); - // Only penalize small near-tip gaps. Large gaps are treated as normal catch-up, - // because a much taller peer on the same chain is not evidence of a reorg. TODO: Maybe look at this again some other day. - bool isInitialSync = (localHeight == 0) || ((peerHeight > localHeight) && ((peerHeight - localHeight) > INITIAL_SYNC_HEIGHT_DIFF)); + // Whether we are catching up rather than following the tip. Derived from our own chain + // only: this used to key off the peer's advertised height, which let any peer claiming + // localHeight + INITIAL_SYNC_HEIGHT_DIFF switch off reorg handling for the session. + bool isInitialSync = Chain_IsInitialBlockDownload(chain); - // Compute penalty and adjusted peer height. - uint64_t delay = (peerHeight > localHeight) ? (peerHeight - localHeight) : 0ULL; - uint64_t penalty = isInitialSync ? 0ULL : FetchScheduler_ComputeReorgPenaltyBlocks(delay); - uint64_t adjustedPeerHeight = (peerHeight > penalty) ? (peerHeight - penalty) : 0ULL; - - // Ensure we always make forward progress: if the penalty would reduce the - // target below our current height, fetch at least the next block. This - // lets us apply penalties for near-tip reorg risk while still allowing - // normal syncing when the peer is ahead by a small amount. - if (adjustedPeerHeight <= localHeight) { - adjustedPeerHeight = localHeight + 1; - } - - if (adjustedPeerHeight > peerHeight) { - adjustedPeerHeight = peerHeight; - } - - printf("syncing: peerHeight=%" PRIu64 " adjusted=%" PRIu64 " local=%" PRIu64 " penalty=%" PRIu64 "\n", - peerHeight, adjustedPeerHeight, localHeight, penalty); + // The reorg penalty is NOT applied to the fetch window. It is a delay on adopting a + // competing branch (enforced in Chain_ReplaceBranch), not on catching up: penalizing + // the height gap to a peer only throttled honest sync, and for gaps of 4-50 it + // collapsed the window to a single block per pass. + printf("syncing: peerHeight=%" PRIu64 " local=%" PRIu64 " initialSync=%s\n", + peerHeight, localHeight, isInitialSync ? "yes" : "no"); // Windowed parallel fetch uint64_t start = localHeight; - uint64_t end = adjustedPeerHeight; // exclusive target height + uint64_t end = peerHeight; // exclusive target height uint64_t nextReq = start; const int maxInFlight = MAX_PARALLEL_FETCHES; @@ -1176,59 +1191,29 @@ int main(int argc, char* argv[]) { // Check whether this block builds on our expected tip. If not, it's a reorg. if (memcmp(fetched->header.prevHash, expectedPrevHash, sizeof(expectedPrevHash)) != 0) { - // Find matching ancestor in our current chain (if any) - ssize_t matchIndex = -1; - size_t chainSz = Chain_Size(chain); - uint8_t tmpHash[32]; - for (size_t bi = 0; bi < chainSz; ++bi) { - block_t* b = NULL; - if (!Chain_GetBlockCopy(chain, bi, &b) || !b) continue; - Block_CalculateHash(b, tmpHash); - if (memcmp(tmpHash, fetched->header.prevHash, sizeof(tmpHash)) == 0) { - matchIndex = (ssize_t)bi; - Block_Destroy(b); - break; - } - Block_Destroy(b); - } + // Ask the peer for a window of blocks below the divergence so the orphan + // pool can assemble its branch and find the true common ancestor by + // prevHash linkage. FETCH_BLOCK answers from the peer's own chain, and + // Node_ParseAndAcceptBlock now routes sub-tip blocks into the pool + // instead of dropping them, so no protocol change is needed. + // + // We deliberately do NOT roll back here. The swap happens in + // Chain_ReplaceBranch, which compares cumulative work, enforces the + // Horizen reorg penalty, and restores our chain if the branch fails to + // apply. The old code rolled back to height 0 whenever it could not find + // the parent -- a full chain wipe, genesis included, that any peer could + // trigger with a single unlinked block. + printf("Divergence at height %" PRIu64 "; probing for the fork point\n", h); + RequestForkWindow(node, peerConn, h); - uint64_t reorgDepth = 0ULL; - if (matchIndex >= 0) { - reorgDepth = (uint64_t)localHeight - ((uint64_t)matchIndex + 1ULL); + size_t reattached = OrphanPool_AttemptAttach(chain); + if (reattached > 0) { + printf("Reorg attached %zu block(s) from the peer's branch\n", reattached); } else { - // No match found: treat as full reorg depth equal to localHeight - reorgDepth = localHeight; + printf("Reorg candidate not adopted (lighter branch, or still serving its reorg penalty)\n"); } - if (!isInitialSync) { - uint64_t reorgPenalty = FetchScheduler_ComputeReorgPenaltyBlocks(reorgDepth); - printf("Reorg detected at height %" PRIu64 ": depth=%" PRIu64 " penalty=%" PRIu64 "\n", - h, reorgDepth, reorgPenalty); - - // Rollback our chain to the matching ancestor (or to 0 if none) - size_t rollbackTo = (matchIndex >= 0) ? (size_t)(matchIndex + 1) : 0; - if (!Chain_RollbackToHeight(chain, rollbackTo)) { - printf("Failed to rollback to height %zu during reorg handling\n", rollbackTo); - inFlight = 0; // abort sync - break; - } - - size_t reattached = OrphanPool_AttemptAttach(chain); - if (reattached > 0) { - printf("Reorg rollback attached %zu orphan(s)\n", reattached); - } - - // Apply additional penalty by shrinking end and restart window from current Chain_Size - if (peerHeight > reorgPenalty) { - end = peerHeight - reorgPenalty; - } else { - end = start; - } - } else { - printf("Initial sync: reorg-like divergence ignored (height=%" PRIu64 ")\n", h); - } - - // Free fetched block and reset window to pick up new adjusted end and expectedPrevHash + // Free fetched block and reset the window against whatever our tip is now Block_Destroy(fetched); nextReq = Chain_Size(chain); inFlight = 0; @@ -1291,9 +1276,12 @@ int main(int argc, char* argv[]) { } } - // After the window completes, check progress and possibly refresh peer height + // After the window completes, check progress and possibly refresh peer height. + // This flag tracks THIS iteration only: it used to be set once and never cleared, so + // after a single productive pass the "no progress -> stop" guard below could never fire + // again and the outer loop could spin forever holding the REPL. uint64_t newLocal = (uint64_t)Chain_Size(chain); - if (newLocal > localHeight) madeProgressOverall = true; + madeProgressOverall = (newLocal > localHeight); printf("sync complete: localHeight=%" PRIu64 "\n", newLocal); // If we've caught up to the peer, stop. Otherwise refresh peerHeight and loop again. @@ -1309,8 +1297,26 @@ int main(int argc, char* argv[]) { } pthread_mutex_unlock(&node->outboundLock); - // If no progress was made in this iteration, stop to avoid tight loop + // If no progress was made in this iteration, stop to avoid a tight loop -- but first + // consider that the peer may be ahead on a branch that forks BELOW our tip. In that + // case every block we asked for is unappendable and lands in the orphan pool, so the + // window completes having achieved nothing. Probe downwards for the fork point before + // giving up; this is the only trigger that fires for a genuine sub-tip fork, because + // the divergence check above can only see blocks that made it into our chain. if (!madeProgressOverall) { + if (peerHeight > newLocal && forkProbes < MAX_FORK_PROBE_ROUNDS) { + forkProbes++; + printf("No progress but peer is ahead (%" PRIu64 " > %" PRIu64 "); probing for a fork point\n", + peerHeight, newLocal); + RequestForkWindow(node, peerConn, newLocal); + + size_t attached = OrphanPool_AttemptAttach(chain); + if (attached > 0) { + printf("Fork probe adopted %zu block(s) from the peer's branch\n", attached); + continue; + } + printf("Fork probe found nothing adoptable (lighter branch, or still serving its reorg penalty)\n"); + } break; } diff --git a/src/nets/fetch_scheduler.c b/src/nets/fetch_scheduler.c index 3c1b5a3..65d27ec 100644 --- a/src/nets/fetch_scheduler.c +++ b/src/nets/fetch_scheduler.c @@ -1,24 +1,46 @@ #include #include -#include -// Note: floating point is used intentionally here for readability and -// because the final penalty is rounded to whole blocks. This keeps the -// implementation straightforward while avoiding subtle integer overflow -// for large exponents. If desired, replace with fixed-point arithmetic. +// Integer-only on purpose. This penalty gates fork choice (see Chain_ReplaceBranch), so every node +// must compute the exact same number of blocks from the same reorg depth. The previous +// implementation used double/pow/ceil, which is not reproducible across platforms and compilers. uint64_t FetchScheduler_ComputeReorgPenaltyBlocks(uint64_t delayBlocks) { if (delayBlocks <= REORG_PENALTY_GRACE_BLOCKS) { return 0ULL; } - double B = (double)delayBlocks; - double factor = REORG_PENALTY_FACTOR; - double exp = REORG_PENALTY_EXPONENT; - double timeScale = ((double)TARGET_BLOCK_TIME) / REORG_PENALTY_REF_BLOCK_TIME; + uint64_t depth = delayBlocks; + if (depth > REORG_PENALTY_MAX_DEPTH) { + depth = REORG_PENALTY_MAX_DEPTH; + } - double raw = factor * pow(B, exp) * timeScale; - if (raw < 0.0) raw = 0.0; + // depth^EXPONENT, saturating rather than wrapping. + uint64_t raised = 1ULL; + for (uint32_t i = 0; i < REORG_PENALTY_EXPONENT; ++i) { + if (depth != 0ULL && raised > UINT64_MAX / depth) { + return UINT64_MAX; + } + raised *= depth; + } + + // Scale by theta and by the block-time ratio, as one fraction so there is a single rounding + // step: penalty = ceil(raised * FACTOR_NUM * TARGET_BLOCK_TIME / (FACTOR_DEN * REF_BLOCK_TIME)) + const uint64_t numeratorScale = REORG_PENALTY_FACTOR_NUM * (uint64_t)TARGET_BLOCK_TIME; + const uint64_t denominator = REORG_PENALTY_FACTOR_DEN * REORG_PENALTY_REF_BLOCK_TIME; + if (denominator == 0ULL) { + return 0ULL; + } + + if (numeratorScale != 0ULL && raised > UINT64_MAX / numeratorScale) { + return UINT64_MAX; + } + const uint64_t numerator = raised * numeratorScale; + + // Ceiling division without overflowing on the +denominator-1 term. + uint64_t penalty = numerator / denominator; + if (numerator % denominator != 0ULL) { + penalty++; + } - uint64_t penalty = (uint64_t)ceil(raw); return penalty; } diff --git a/src/nets/net_node.c b/src/nets/net_node.c index 496d493..3efd8e5 100644 --- a/src/nets/net_node.c +++ b/src/nets/net_node.c @@ -477,33 +477,54 @@ static node_block_accept_result_t Node_ParseAndAcceptBlock(const unsigned char* return NODE_BLOCK_REJECTED; } + // The orphan pool stamps the local tip height at first sight; that stamp drives the reorg + // penalty and must be taken now, not re-derived later from a moved tip. + uint64_t chainSize = Chain_Size(currentChain); + const uint64_t observedAtTipHeight = chainSize > 0 ? (chainSize - 1) : 0ULL; + // Temporary debug mode: force network-received blocks through the orphan pool to exercise reorg handling. if (forceOrphanReorgEnabled && blk->header.blockNumber > 0) { - OrphanPool_Insert(blk, blockHeight); + OrphanPool_Insert(blk, blockHeight, observedAtTipHeight); printf("Forced orphan BLOCK_DATA at height %" PRIu64 "\n", blockHeight); return NODE_BLOCK_ORPHAN_QUEUED; } // If parent is missing, insert into orphan pool instead of rejecting immediately. - uint64_t chainSize = Chain_Size(currentChain); if (blk->header.blockNumber > chainSize) { // Parent(s) missing; queue as orphan - OrphanPool_Insert(blk, blockHeight); + OrphanPool_Insert(blk, blockHeight, observedAtTipHeight); printf("Queued orphan BLOCK_DATA at height %" PRIu64 "\n", blockHeight); return NODE_BLOCK_ORPHAN_QUEUED; } else if (blk->header.blockNumber < chainSize) { - // Older block than current chain tip: reject - printf("Rejected BLOCK_DATA at height %" PRIu64 ": older than current chain\n", blockHeight); - DynArr_destroy(blk->transactions); - free(blk); - return NODE_BLOCK_REJECTED; + // A block below our tip is either one we already have, or the lower half of a competing + // branch. Dropping both (as this used to) made any fork that diverges below the tip + // impossible to discover: the fork point itself was always thrown away. + block_t* local = NULL; + if (Chain_GetBlockCopy(currentChain, (size_t)blk->header.blockNumber, &local) && local) { + uint8_t localHash[32]; + uint8_t incomingHash[32]; + Block_CalculateHash(local, localHash); + Block_CalculateHash(blk, incomingHash); + Block_Destroy(local); + + if (memcmp(localHash, incomingHash, 32) == 0) { + // Exactly the block we already have. + DynArr_destroy(blk->transactions); + free(blk); + return NODE_BLOCK_REJECTED; + } + } + + OrphanPool_Insert(blk, blockHeight, observedAtTipHeight); + printf("Queued forked BLOCK_DATA at height %" PRIu64 " (below our tip) as orphan\n", blockHeight); + return NODE_BLOCK_ORPHAN_QUEUED; } else { // blk->header.blockNumber == chainSize -> candidate to append. Ensure prevHash matches current tip. if (chainSize > 0) { block_t* last = NULL; if (!Chain_GetBlockCopy(currentChain, (size_t)(chainSize - 1), &last) || !last) { // Can't verify parent; queue as orphan conservatively - OrphanPool_Insert(blk, blockHeight); + OrphanPool_Insert(blk, blockHeight, observedAtTipHeight); printf("Queued orphan BLOCK_DATA at height %" PRIu64 " (unable to verify parent)\n", blockHeight); if (last) Block_Destroy(last); return NODE_BLOCK_ORPHAN_QUEUED; @@ -512,7 +533,7 @@ static node_block_accept_result_t Node_ParseAndAcceptBlock(const unsigned char* Block_CalculateHash(last, lastHash); if (memcmp(lastHash, blk->header.prevHash, 32) != 0) { // Conflicting block at same height; queue as orphan until resolved by a subsequent extension. - OrphanPool_Insert(blk, blockHeight); + OrphanPool_Insert(blk, blockHeight, observedAtTipHeight); Block_Destroy(last); printf("Queued conflicting BLOCK_DATA at same height %" PRIu64 " as orphan\n", blockHeight); return NODE_BLOCK_ORPHAN_QUEUED; @@ -531,19 +552,8 @@ static node_block_accept_result_t Node_ParseAndAcceptBlock(const unsigned char* return NODE_BLOCK_REJECTED; } - uint64_t coinbaseAmount = 0; - if (blk->transactions) { - for (size_t i = 0; i < DynArr_size(blk->transactions); ++i) { - signed_transaction_t* tx = (signed_transaction_t*)DynArr_at(blk->transactions, i); - if (tx && Address_IsCoinbase(tx->transaction.senderAddress)) { - coinbaseAmount = tx->transaction.amount1; - break; - } - } - } - - (void)uint256_add_u64(¤tSupply, coinbaseAmount); - currentReward = CalculateBlockReward(currentSupply, currentChain); + // currentSupply/currentReward are advanced inside Chain_AddBlock, so that every path that + // appends (mining, this one, orphan attach, reorg) keeps them consistent. // Persist on accept if requested if (persist) { @@ -1492,13 +1502,13 @@ void Node_BroadcastChainRange(net_node_t* node, size_t startHeightInclusive, tcp unsigned char hash[32]; Block_CalculateHash(blk, hash); - // Dedupe using seenBlocks + // Dedupe using seenBlocks. The hash is only recorded once the block has actually gone out + // to at least one peer: marking it here unconditionally meant that a block relayed while no + // peer was connected (or while every peer was filtered out below) was never offered again. int seen = 0; pthread_mutex_lock(&node->seenLock); if (DynSet_Contains(node->seenBlocks, hash)) { seen = 1; - } else { - DynSet_Insert(node->seenBlocks, hash); } pthread_mutex_unlock(&node->seenLock); @@ -1526,6 +1536,8 @@ void Node_BroadcastChainRange(net_node_t* node, size_t startHeightInclusive, tcp memcpy(payload + off, tx, sizeof(signed_transaction_t)); off += sizeof(signed_transaction_t); } + size_t delivered = 0; + // Snapshot outbound clients and send pthread_mutex_lock(&node->outboundLock); for (size_t i = 0; i < MAX_CONS; ++i) { @@ -1533,10 +1545,35 @@ void Node_BroadcastChainRange(net_node_t* node, size_t startHeightInclusive, tcp if (!conn) continue; if (conn == sourceConn) continue; if (sourceConn && TcpConnection_PeerAddrEqual(conn, sourceConn)) continue; - Node_SendPacket(node, conn, PACKET_TYPE_BROADCAST_BLOCK, payload, off); + if (Node_SendPacket(node, conn, PACKET_TYPE_BROADCAST_BLOCK, payload, off) == 0) { + delivered++; + } } pthread_mutex_unlock(&node->outboundLock); + // Relay to inbound peers as well. Broadcasting only to outbound connections meant that in a + // two-node setup the node that was dialled never pushed anything back, and the dialer only + // ever learned about new blocks through a manual `sync`. + if (node->server) { + pthread_mutex_lock(&node->server->clientsMutex); + for (size_t i = 0; i < node->server->maxClients; ++i) { + tcp_connection_t* conn = node->server->clientsArrPtr ? node->server->clientsArrPtr[i] : NULL; + if (!conn || TcpConnection_IsDisconnectNotified(conn)) continue; + if (conn == sourceConn) continue; + if (sourceConn && TcpConnection_PeerAddrEqual(conn, sourceConn)) continue; + if (Node_SendPacket(node, conn, PACKET_TYPE_BROADCAST_BLOCK, payload, off) == 0) { + delivered++; + } + } + pthread_mutex_unlock(&node->server->clientsMutex); + } + + if (delivered > 0) { + pthread_mutex_lock(&node->seenLock); + DynSet_Insert(node->seenBlocks, hash); + pthread_mutex_unlock(&node->seenLock); + } + free(payload); Block_Destroy(blk); } diff --git a/src/nets/orphan_pool.c b/src/nets/orphan_pool.c index 79e49ff..172fb88 100644 --- a/src/nets/orphan_pool.c +++ b/src/nets/orphan_pool.c @@ -1,5 +1,7 @@ #include +#include #include +#include #include #include #include @@ -7,202 +9,573 @@ typedef struct { block_t* block; uint64_t height; + uint64_t observedAtTipHeight; // local tip height when first seen; stamped once (reorg penalty) + uint64_t sequence; // insertion order, used to evict the oldest entry when full + uint8_t hash[32]; } orphan_entry_t; static DynArr* g_orphans = NULL; +static uint64_t g_nextSequence = 0; + +// The pool is touched by the maintenance thread, by every per-peer TCP thread and by the REPL +// thread. It used to have no synchronisation at all, so a concurrent Insert could realloc the +// array out from under a scan that was holding a raw element pointer. +// +// Lock ordering: this mutex is never held while calling into chain.c (which takes chainLock). +// Candidate branches are collected under the lock, the lock is dropped, and only then is +// Chain_ReplaceBranch/Chain_AddBlock called. +static pthread_mutex_t g_orphanLock = PTHREAD_MUTEX_INITIALIZER; + +static void OrphanPool_InitLocked(void) { + if (!g_orphans) { + g_orphans = DYNARR_CREATE(orphan_entry_t, 16); + } +} void OrphanPool_Init(void) { - if (g_orphans) return; - g_orphans = DYNARR_CREATE(orphan_entry_t, 16); + pthread_mutex_lock(&g_orphanLock); + OrphanPool_InitLocked(); + pthread_mutex_unlock(&g_orphanLock); } void OrphanPool_Destroy(void) { - if (!g_orphans) return; + pthread_mutex_lock(&g_orphanLock); + if (g_orphans) { + size_t n = DynArr_size(g_orphans); + for (size_t i = 0; i < n; ++i) { + orphan_entry_t* e = (orphan_entry_t*)DynArr_at(g_orphans, i); + if (e && e->block) { + Block_Destroy(e->block); + } + } + DynArr_destroy(g_orphans); + g_orphans = NULL; + } + pthread_mutex_unlock(&g_orphanLock); +} + +static ssize_t OrphanPool_FindByHashLocked(const uint8_t blockHash[32]) { + if (!g_orphans || !blockHash) { + return -1; + } + size_t n = DynArr_size(g_orphans); for (size_t i = 0; i < n; ++i) { orphan_entry_t* e = (orphan_entry_t*)DynArr_at(g_orphans, i); - if (e && e->block) { - Block_Destroy(e->block); + if (e && memcmp(e->hash, blockHash, 32) == 0) { + return (ssize_t)i; } } - DynArr_destroy(g_orphans); - g_orphans = NULL; + + return -1; } -void OrphanPool_Insert(block_t* block, uint64_t height) { - if (!block) return; - if (!g_orphans) OrphanPool_Init(); +// Drop the entry with the lowest sequence number, so a flood of unusable orphans cannot grow +// without bound. Returns true if something was evicted. +static bool OrphanPool_EvictOldestLocked(void) { + if (!g_orphans) { + return false; + } + + size_t n = DynArr_size(g_orphans); + if (n == 0) { + return false; + } + + size_t oldestIndex = 0; + uint64_t oldestSequence = UINT64_MAX; + for (size_t i = 0; i < n; ++i) { + orphan_entry_t* e = (orphan_entry_t*)DynArr_at(g_orphans, i); + if (e && e->sequence < oldestSequence) { + oldestSequence = e->sequence; + oldestIndex = i; + } + } + + orphan_entry_t* victim = (orphan_entry_t*)DynArr_at(g_orphans, oldestIndex); + if (victim && victim->block) { + Block_Destroy(victim->block); + } + DynArr_remove(g_orphans, oldestIndex); + return true; +} + +void OrphanPool_Insert(block_t* block, uint64_t height, uint64_t observedAtTipHeight) { + if (!block) { + return; + } + + uint8_t blockHash[32]; + Block_CalculateHash(block, blockHash); + + pthread_mutex_lock(&g_orphanLock); + OrphanPool_InitLocked(); + if (!g_orphans) { + pthread_mutex_unlock(&g_orphanLock); + Block_Destroy(block); + return; + } + + // Reject duplicates. The same block reaches us from every peer that relays it, and without + // this each copy became its own permanently-resident entry. + if (OrphanPool_FindByHashLocked(blockHash) >= 0) { + pthread_mutex_unlock(&g_orphanLock); + Block_Destroy(block); + return; + } + + while (DynArr_size(g_orphans) >= MAX_ORPHAN_BLOCKS) { + if (!OrphanPool_EvictOldestLocked()) { + break; + } + } + orphan_entry_t e; + memset(&e, 0, sizeof(e)); e.block = block; e.height = height; - (void)DynArr_push_back(g_orphans, &e); + e.observedAtTipHeight = observedAtTipHeight; + e.sequence = g_nextSequence++; + memcpy(e.hash, blockHash, 32); + + if (!DynArr_push_back(g_orphans, &e)) { + pthread_mutex_unlock(&g_orphanLock); + Block_Destroy(block); + return; + } + + pthread_mutex_unlock(&g_orphanLock); } -static size_t OrphanPool_TryAdoptBranch(blockchain_t* chain, uint64_t forkHeight) { - if (!g_orphans || !chain) return 0; +bool OrphanPool_Contains(const uint8_t blockHash[32]) { + pthread_mutex_lock(&g_orphanLock); + bool found = OrphanPool_FindByHashLocked(blockHash) >= 0; + pthread_mutex_unlock(&g_orphanLock); + return found; +} - DynArr* seq = DYNARR_CREATE(block_t*, 8); - if (!seq) return 0; +size_t OrphanPool_Size(void) { + pthread_mutex_lock(&g_orphanLock); + size_t n = g_orphans ? DynArr_size(g_orphans) : 0; + pthread_mutex_unlock(&g_orphanLock); + return n; +} - size_t cursor = forkHeight; - while (1) { - bool found = false; - size_t count = DynArr_size(g_orphans); - for (size_t i = 0; i < count; ++i) { - orphan_entry_t* entry = (orphan_entry_t*)DynArr_at(g_orphans, i); - if (!entry || !entry->block) continue; - if (entry->height == cursor) { - (void)DynArr_push_back(seq, &entry->block); - found = true; - break; - } +// Remove the entry with this hash without freeing the block, and hand the block back. Used once a +// block has been given to the chain, which then owns its transaction array. +static block_t* OrphanPool_TakeByHashLocked(const uint8_t blockHash[32]) { + ssize_t index = OrphanPool_FindByHashLocked(blockHash); + if (index < 0) { + return NULL; + } + + orphan_entry_t* e = (orphan_entry_t*)DynArr_at(g_orphans, (size_t)index); + block_t* blk = e ? e->block : NULL; + DynArr_remove(g_orphans, (size_t)index); + return blk; +} + +static void OrphanPool_DropByHashLocked(const uint8_t blockHash[32]) { + ssize_t index = OrphanPool_FindByHashLocked(blockHash); + if (index < 0) { + return; + } + + orphan_entry_t* e = (orphan_entry_t*)DynArr_at(g_orphans, (size_t)index); + if (e && e->block) { + Block_Destroy(e->block); + } + DynArr_remove(g_orphans, (size_t)index); +} + +/** + * Copy out the orphan that extends `prevHash` at `height`, if any. + * Returns false when there is no such orphan. Caller must hold the pool lock. +**/ +static bool OrphanPool_FindChildLocked(uint64_t height, + const uint8_t prevHash[32], + block_t** outBlock, + uint64_t* outObservedAtTipHeight, + uint8_t outHash[32]) { + if (!g_orphans) { + return false; + } + + size_t n = DynArr_size(g_orphans); + for (size_t i = 0; i < n; ++i) { + orphan_entry_t* e = (orphan_entry_t*)DynArr_at(g_orphans, i); + if (!e || !e->block) { + continue; } - if (!found) break; - cursor++; + if (e->height != height) { + continue; + } + if (memcmp(e->block->header.prevHash, prevHash, 32) != 0) { + continue; + } + + *outBlock = e->block; + *outObservedAtTipHeight = e->observedAtTipHeight; + memcpy(outHash, e->hash, 32); + return true; } - size_t seqCount = DynArr_size(seq); - if (seqCount == 0) { - DynArr_destroy(seq); + return false; +} + +/** + * Follow prevHash links from `forkHeight` to build the longest branch the pool can offer. + * + * The old implementation took the first orphan found at each successive height with no linkage + * check at all, which could splice blocks from two different forks into one incoherent branch. + * Caller must hold the pool lock. The returned array borrows the pooled block pointers; the pool + * still owns them (Chain_ReplaceBranch applies copies). +**/ +static size_t OrphanPool_CollectBranchLocked(uint64_t forkHeight, + const uint8_t forkParentHash[32], + block_t*** outBlocks, + uint8_t** outHashes, + uint64_t* outObservedAtTipHeight) { + *outBlocks = NULL; + *outHashes = NULL; + *outObservedAtTipHeight = 0; + + DynArr* collected = DYNARR_CREATE(block_t*, 8); + DynArr* hashes = DYNARR_CREATE(uint8_t, 8 * 32); + if (!collected || !hashes) { + if (collected) DynArr_destroy(collected); + if (hashes) DynArr_destroy(hashes); return 0; } - size_t currentTipHeight = Chain_Size(chain) == 0 ? 0 : Chain_Size(chain) - 1; - size_t seqTopHeight = forkHeight + seqCount - 1; - if (seqTopHeight <= currentTipHeight) { - DynArr_destroy(seq); - return 0; - } + uint8_t expectedPrevHash[32]; + memcpy(expectedPrevHash, forkParentHash, 32); - size_t rollbackHeight = (forkHeight == 0) ? 0 : (forkHeight - 1); - if (!Chain_RollbackToHeight(chain, rollbackHeight)) { - DynArr_destroy(seq); - return 0; - } + uint64_t earliestObserved = UINT64_MAX; + uint64_t cursor = forkHeight; + size_t count = 0; - size_t attached = 0; - for (size_t i = 0; i < seqCount; ++i) { - block_t* bptr = *(block_t**)DynArr_at(seq, i); - if (!bptr || !Chain_AddBlock(chain, bptr)) { + while (1) { + block_t* child = NULL; + uint64_t observed = 0; + uint8_t childHash[32]; + if (!OrphanPool_FindChildLocked(cursor, expectedPrevHash, &child, &observed, childHash)) { break; } - size_t count = DynArr_size(g_orphans); - for (size_t j = 0; j < count; ++j) { - orphan_entry_t* entry = (orphan_entry_t*)DynArr_at(g_orphans, j); - if (entry && entry->block == bptr) { - DynArr_remove(g_orphans, j); + if (!DynArr_push_back(collected, &child)) { + break; + } + for (size_t b = 0; b < 32; ++b) { + if (!DynArr_push_back(hashes, &childHash[b])) { break; } } + if (observed < earliestObserved) { + earliestObserved = observed; + } + + memcpy(expectedPrevHash, childHash, 32); + cursor++; + count++; + } + + if (count == 0) { + DynArr_destroy(collected); + DynArr_destroy(hashes); + return 0; + } + + block_t** blocks = (block_t**)calloc(count, sizeof(block_t*)); + uint8_t* hashOut = (uint8_t*)calloc(count, 32); + if (!blocks || !hashOut) { + free(blocks); + free(hashOut); + DynArr_destroy(collected); + DynArr_destroy(hashes); + return 0; + } + + for (size_t i = 0; i < count; ++i) { + blocks[i] = *(block_t**)DynArr_at(collected, i); + for (size_t b = 0; b < 32; ++b) { + hashOut[i * 32 + b] = *(uint8_t*)DynArr_at(hashes, i * 32 + b); + } + } + + DynArr_destroy(collected); + DynArr_destroy(hashes); + + *outBlocks = blocks; + *outHashes = hashOut; + *outObservedAtTipHeight = earliestObserved == UINT64_MAX ? 0ULL : earliestObserved; + return count; +} + +// Discard orphans that can no longer ever be applied: anything at or below the current tip whose +// hash does not match the block we actually have there. Without this the pool only ever grew, and +// permanently-invalid entries were retried on every 1 Hz maintenance tick. +static void OrphanPool_PruneStale(blockchain_t* chain) { + if (!chain) { + return; + } + + const size_t chainSize = Chain_Size(chain); + + // Collect the hashes to drop first, so we never call into chain.c while holding the pool lock. + DynArr* doomed = DYNARR_CREATE(uint8_t, 32); + if (!doomed) { + return; + } + + pthread_mutex_lock(&g_orphanLock); + size_t n = g_orphans ? DynArr_size(g_orphans) : 0; + DynArr* candidates = DYNARR_CREATE(uint8_t, 32); + DynArr* candidateHeights = DYNARR_CREATE(uint64_t, 8); + if (candidates && candidateHeights) { + for (size_t i = 0; i < n; ++i) { + orphan_entry_t* e = (orphan_entry_t*)DynArr_at(g_orphans, i); + if (!e || !e->block) { + continue; + } + if (e->height >= (uint64_t)chainSize) { + continue; // still ahead of us; may attach later + } + for (size_t b = 0; b < 32; ++b) { + (void)DynArr_push_back(candidates, &e->hash[b]); + } + (void)DynArr_push_back(candidateHeights, &e->height); + } + } + pthread_mutex_unlock(&g_orphanLock); + + size_t candidateCount = candidateHeights ? DynArr_size(candidateHeights) : 0; + for (size_t i = 0; i < candidateCount; ++i) { + uint64_t height = *(uint64_t*)DynArr_at(candidateHeights, i); + uint8_t orphanHash[32]; + for (size_t b = 0; b < 32; ++b) { + orphanHash[b] = *(uint8_t*)DynArr_at(candidates, i * 32 + b); + } + + block_t* local = NULL; + if (!Chain_GetBlockCopy(chain, (size_t)height, &local) || !local) { + continue; + } + + uint8_t localHash[32]; + Block_CalculateHash(local, localHash); + Block_Destroy(local); + + // Same block we already have: pure duplicate, drop it. A different block at a height we + // have already passed is kept, because it may yet be the base of a heavier branch. + if (memcmp(localHash, orphanHash, 32) == 0) { + for (size_t b = 0; b < 32; ++b) { + (void)DynArr_push_back(doomed, &orphanHash[b]); + } + } + } + + size_t doomedCount = DynArr_size(doomed) / 32; + if (doomedCount > 0) { + pthread_mutex_lock(&g_orphanLock); + for (size_t i = 0; i < doomedCount; ++i) { + uint8_t h[32]; + for (size_t b = 0; b < 32; ++b) { + h[b] = *(uint8_t*)DynArr_at(doomed, i * 32 + b); + } + OrphanPool_DropByHashLocked(h); + } + pthread_mutex_unlock(&g_orphanLock); + } + + if (candidates) DynArr_destroy(candidates); + if (candidateHeights) DynArr_destroy(candidateHeights); + DynArr_destroy(doomed); +} + +/** + * Try to extend the current tip directly with pooled orphans. + * Returns the number of blocks attached. +**/ +static size_t OrphanPool_ExtendTip(blockchain_t* chain) { + size_t attached = 0; + + while (1) { + const size_t chainSize = Chain_Size(chain); + + uint8_t tipHash[32]; + memset(tipHash, 0, sizeof(tipHash)); + if (chainSize > 0) { + block_t* tip = NULL; + if (!Chain_GetBlockCopy(chain, chainSize - 1, &tip) || !tip) { + break; + } + Block_CalculateHash(tip, tipHash); + Block_Destroy(tip); + } + + // Take a copy of the candidate under the lock, then release it before touching the chain. + pthread_mutex_lock(&g_orphanLock); + block_t* pooled = NULL; + uint64_t observed = 0; + uint8_t candidateHash[32]; + bool found = OrphanPool_FindChildLocked((uint64_t)chainSize, tipHash, &pooled, &observed, candidateHash); + block_t* candidate = found ? Block_Copy(pooled) : NULL; + pthread_mutex_unlock(&g_orphanLock); + + if (!found) { + break; + } + if (!candidate) { + break; + } + + if (!Chain_AddBlock(chain, candidate)) { + // Permanent rejection for this block at this height (bad coinbase, wrong difficulty, + // ...). Drop it rather than retrying it on every maintenance tick forever. + Block_Destroy(candidate); + pthread_mutex_lock(&g_orphanLock); + OrphanPool_DropByHashLocked(candidateHash); + pthread_mutex_unlock(&g_orphanLock); + continue; + } + + // The chain took over the copy's transaction array; free only our wrapper. + free(candidate); + + pthread_mutex_lock(&g_orphanLock); + block_t* taken = OrphanPool_TakeByHashLocked(candidateHash); + pthread_mutex_unlock(&g_orphanLock); + if (taken) { + Block_Destroy(taken); // the pool's own copy is independent of the one we applied + } + attached++; } - DynArr_destroy(seq); return attached; } -size_t OrphanPool_AttemptAttach(blockchain_t* chain) { - if (!g_orphans || !chain) return 0; - size_t attached = 0; - bool madeProgress = true; +/** + * Look for a competing branch that forks below our tip and is worth adopting. + * The work comparison, the reorg penalty and the atomicity all live in Chain_ReplaceBranch. +**/ +static size_t OrphanPool_TryAdoptBranch(blockchain_t* chain) { + const size_t chainSize = Chain_Size(chain); + if (chainSize == 0) { + return 0; + } - // Attempt repeatedly while progress is made (to handle chained orphans) - while (madeProgress) { - madeProgress = false; - size_t n = DynArr_size(g_orphans); - for (size_t i = 0; i < n; ++i) { - orphan_entry_t* e = (orphan_entry_t*)DynArr_at(g_orphans, i); - if (!e || !e->block) continue; + // Walk fork points from just below the tip downwards; the shallowest fork wins, which is also + // the one with the smallest reorg penalty. + for (size_t forkHeight = chainSize; forkHeight >= 1; --forkHeight) { + block_t* parent = NULL; + if (!Chain_GetBlockCopy(chain, forkHeight - 1, &parent) || !parent) { + continue; + } + uint8_t parentHash[32]; + Block_CalculateHash(parent, parentHash); + Block_Destroy(parent); - uint64_t parentIndex = (e->height == 0) ? (uint64_t)-1 : (e->height - 1); - bool parentExists = false; - if (e->height == 0) { - // genesis-style block: parent is zero-hash; accept if chain empty - parentExists = (Chain_Size(chain) == 0); - } else if (parentIndex < Chain_Size(chain)) { - block_t* parent = NULL; - if (Chain_GetBlockCopy(chain, (size_t)parentIndex, &parent) && parent) { - parentExists = true; - Block_Destroy(parent); - } else { - parentExists = false; - } - } - - if (parentExists) { - if (e->height < Chain_Size(chain)) { - block_t* local = NULL; - if (Chain_GetBlockCopy(chain, (size_t)e->height, &local) && local) { - uint8_t localHash[32]; - uint8_t orphanHash[32]; - Block_CalculateHash(local, localHash); - Block_CalculateHash(e->block, orphanHash); - Block_Destroy(local); - - if (memcmp(localHash, orphanHash, 32) != 0) { - size_t adopted = OrphanPool_TryAdoptBranch(chain, e->height); - if (adopted > 0) { - attached += adopted; - madeProgress = true; - n = DynArr_size(g_orphans); - i = (size_t)-1; - break; - } - } - } else if (local) { - Block_Destroy(local); - } - } - - // Verify that the parent's hash matches the orphan's prevHash before attaching. - bool parentMatches = false; - if (e->height == 0) { - parentMatches = (Chain_Size(chain) == 0); - } else { - block_t* parent = NULL; - if (Chain_GetBlockCopy(chain, (size_t)parentIndex, &parent) && parent) { - uint8_t parentHash[32]; - Block_CalculateHash(parent, parentHash); - parentMatches = (memcmp(parentHash, e->block->header.prevHash, 32) == 0); - Block_Destroy(parent); - } else { - parentMatches = false; - } - } - - if (!parentMatches) { - // Parent exists but does not match this orphan's prevHash. - size_t adopted = OrphanPool_TryAdoptBranch(chain, e->height); - if (adopted > 0) { - attached += adopted; - madeProgress = true; - n = DynArr_size(g_orphans); - i = (size_t)-1; - break; - } - - continue; - } - - // Try to add to chain - if (Chain_AddBlock(chain, e->block)) { - attached++; - madeProgress = true; - // remove this entry - DynArr_remove(g_orphans, i); - // adjust indices - n = DynArr_size(g_orphans); - i = (size_t)-1; // reset outer loop - break; - } else { - // Keep the orphan around; rejection may be temporary while the local tip is being reorged. - continue; + pthread_mutex_lock(&g_orphanLock); + block_t** branch = NULL; + uint8_t* branchHashes = NULL; + uint64_t observedAtTipHeight = 0; + size_t branchCount = OrphanPool_CollectBranchLocked((uint64_t)forkHeight, parentHash, + &branch, &branchHashes, &observedAtTipHeight); + // Copy the branch so the pool lock can be released before we call into the chain. + block_t** branchCopies = NULL; + if (branchCount > 0) { + branchCopies = (block_t**)calloc(branchCount, sizeof(block_t*)); + if (branchCopies) { + for (size_t i = 0; i < branchCount; ++i) { + branchCopies[i] = Block_Copy(branch[i]); } } } + pthread_mutex_unlock(&g_orphanLock); + + free(branch); + + if (branchCount == 0 || !branchCopies) { + free(branchHashes); + if (branchCopies) { + free(branchCopies); + } + if (forkHeight == 1) break; + continue; + } + + bool copiedAll = true; + for (size_t i = 0; i < branchCount; ++i) { + if (!branchCopies[i]) { + copiedAll = false; + } + } + + bool adopted = false; + if (copiedAll) { + adopted = Chain_ReplaceBranch(chain, forkHeight, branchCopies, branchCount, observedAtTipHeight); + } + + for (size_t i = 0; i < branchCount; ++i) { + if (branchCopies[i]) { + Block_Destroy(branchCopies[i]); // Chain_ReplaceBranch applied its own copies + } + } + free(branchCopies); + + if (adopted) { + printf("Adopted competing branch of %zu block(s) at fork height %zu\n", branchCount, forkHeight); + pthread_mutex_lock(&g_orphanLock); + for (size_t i = 0; i < branchCount; ++i) { + OrphanPool_DropByHashLocked(&branchHashes[i * 32]); + } + pthread_mutex_unlock(&g_orphanLock); + free(branchHashes); + return branchCount; + } + + free(branchHashes); + + if (forkHeight == 1) { + break; + } } + return 0; +} + +size_t OrphanPool_AttemptAttach(blockchain_t* chain) { + if (!chain) { + return 0; + } + + pthread_mutex_lock(&g_orphanLock); + bool empty = (g_orphans == NULL) || (DynArr_size(g_orphans) == 0); + pthread_mutex_unlock(&g_orphanLock); + if (empty) { + return 0; + } + + size_t attached = 0; + + // Extending the tip is always preferable to a reorg, so try that to exhaustion first, and only + // then consider replacing part of our chain with a competing branch. + while (1) { + size_t extended = OrphanPool_ExtendTip(chain); + attached += extended; + + size_t adopted = OrphanPool_TryAdoptBranch(chain); + attached += adopted; + + if (extended == 0 && adopted == 0) { + break; + } + } + + OrphanPool_PruneStale(chain); + return attached; }