Fix the reorg system: make rollback survivable, adopt by cumulative work, and enforce the reorg penalty everywhere

(This is a big one, get ready - I told Claude to write the commit message cause I couldn't be bothered)

Root cause: reorg was broken at every layer and the failures compounded. Verified with the node's own
SKALACOIN_FORCE_ORPHAN_REORG debug mode, which stalled permanently at height 2 on 114 consecutive
coinbase-validation failures. A binary built at HEAD behaves identically, so none of this is a regression —
the reorg path had simply never worked.

Rollback (the keystone):
 - Chain_RollbackToHeight always returned false on any node that had ever saved or loaded its chain, and
   only after it had already truncated the chain and destroyed the balance sheet. Chain_RecomputeRuntimeState
   bails on any header-only block, and Chain_SaveToFile nulls transactions on every in-memory block once
   persisted, so the failure was universal in practice. Both callers treated the false as "nothing happened"
 - Added Chain_BorrowBlockTransactions / Chain_ReturnBlockTransactions, which fall back to the on-disk copy
   when the in-memory block has been compacted to headers
 - Supply is now accumulated in the rollback's existing balance-sheet replay pass instead of a second
   Chain_RecomputeRuntimeState pass
 - Gave Chain_RecomputeRuntimeState the same disk fallback: it had been failing on every restart with an
   existing chain, silently leaving currentSupply/currentReward at whatever came out of chain.meta

Fork choice is now cumulative work, not height:
 - Added Chain_ComputeBlockWork / Chain_ComputeWorkRange / Chain_ComputeBranchWork, computing
   2^256 / (target + 1) per block and summing over a range. Derived on demand from headers — no header,
   chain.meta or wire-format change
 - uint256 only had add/sub/cmp, so added uint256_divide (restoring binary long division),
   uint256_from_be_bytes, uint256_bitwise_not and uint256_is_zero
 - Comparison is strictly greater, so tied tips do not cause two nodes to keep swapping
 - Height-based choice was wrong now that difficulty actually varies: a long low-difficulty branch beat a
   short high-difficulty one

Atomic branch replacement:
 - Added Chain_ReplaceBranch: validate linkage, apply the reorg penalty, compare work, snapshot the outgoing
   blocks, roll back, apply. On any failure the original chain, balance sheet, supply, reward and difficulty
   target are restored. The caller keeps ownership of its blocks in every case — the chain applies copies
 - Split Chain_AddBlock and Chain_RollbackToHeight into locked public wrappers over unlocked internals, so a
   whole branch swap happens under one lock acquisition and Chain_OnTipAdvanced runs once per reorg rather
   than once per block
 - Chain_AddBlock now validates header.prevHash against the tip. It never did — that check lived only in
   Chain_IsValid and the network path, which is exactly why a rollback-then-reapply could splice blocks from
   two different forks into a chain that no longer links up
 - Moved the currentSupply/currentReward update into Chain_AddBlock. Each caller used to do it separately, so
   the orphan-attach and maintenance-thread paths never did, and the next block's coinbase was then validated
   against a stale currentReward and rejected forever. This was the height-2 stall

Reorg penalty (Horizen-style delayed block submission):
 - The penalty was only ever reachable from the manual sync command. The P2P broadcast -> orphan pool ->
   branch adoption path, which is the path an attacker actually uses, had none at all and picked the winner
   by raw height. It is now enforced inside Chain_ReplaceBranch, the single choke point every adoption
   passes through
 - Removed its application to the sync fetch window. The height gap to a peer is not a reorg depth;
   penalising it only throttled honest catch-up, and for gaps of 4-50 it collapsed the window to one block
   per pass, defeating MAX_PARALLEL_FETCHES
 - The depth is stamped once when a branch is first observed (orphan_entry_t.observedAtTipHeight) and never
   recomputed. Re-deriving it from a moving tip never converges: depth and elapsed both grow by one per block
   while penalty(depth) grows faster, so a penalized branch could never be adopted at all
 - The initial-sync exemption now comes from Chain_IsInitialBlockDownload, which uses the local median time
   past over MEDIAN_TIME_SPAN blocks. It used to key off the peer's advertised height, so any peer claiming
   localHeight + INITIAL_SYNC_HEIGHT_DIFF could switch reorg handling off for the whole session. A median
   rather than the tip alone means one backdated block cannot fake it either

Orphan pool (largely rewritten):
 - Added a pool mutex. It had no synchronisation whatsoever while being mutated from the 1 Hz maintenance
   thread, every per-peer TCP thread and the REPL thread; a concurrent insert could realloc the array while a
   scan held a raw element pointer. The lock is never held across a call into chain.c
 - Dedup by block hash, a MAX_ORPHAN_BLOCKS cap with oldest-first eviction, and pruning of entries that can
   no longer apply. Nothing was ever reaped before, and orphans are reachable before the chain-derived
   difficulty check, so this is also the memory-exhaustion fix
 - Candidate branches are now assembled by following prevHash from the fork point. Taking the first orphan
   found at each successive height could interleave blocks from two competing forks into one incoherent branch
 - Fixed rollbackHeight = forkHeight - 1. Chain_RollbackToHeight is exclusive, so every non-genesis adoption
   amputated one block too many and then failed Chain_AddBlock's index check
 - Fixed a block_t wrapper leak on every successful attach (free the wrapper, not Block_Destroy — the chain
   owns the transactions after a shallow copy)
 - Permanently invalid orphans are dropped instead of being retried on every maintenance tick forever

Forks below the tip are now discoverable:
 - A block at blockNumber < chainSize was rejected and freed, so the fork point and the lower half of any
   competing branch were always thrown away and a sub-tip fork could never be learned. Now the hash is
   compared: identical means a duplicate and is ignored, different means it goes to the orphan pool
 - The sync loop probes downwards (RequestForkWindow, bounded by REORG_FETCH_DEPTH and MAX_FORK_PROBE_ROUNDS)
   when it makes no progress while the peer is ahead. That is the only trigger that fires for a genuine
   sub-tip fork, because the old divergence check could only see blocks that had already entered our chain.
   FETCH_BLOCK already answers from the peer's own chain, so no protocol change was needed
 - Removed the rollback-to-height-0 path. "Could not find the parent" used to wipe the entire local chain,
   genesis included, and any peer could trigger it with a single unlinked block

Floating point removed from consensus and network math:
 - Chain_ComputeTargetAtHeight (the difficulty retarget) used double ratio arithmetic, and
   FetchScheduler_ComputeReorgPenaltyBlocks used double/pow/ceil. Both are consensus-critical and are now
   integer only; float results are not reproducible across platforms and compilers, and a single last-digit
   difference in a target or a penalty splits the network
 - The penalty constants became integer rationals (REORG_PENALTY_FACTOR_NUM/DEN, integer EXPONENT and
   REF_BLOCK_TIME) with saturating exponentiation and explicit ceiling division. Output is unchanged:
   penalty(4)=10, penalty(8)=39, penalty(10)=60, penalty(50)=1500, penalty(100)=6000
 - Removed the unused float macros DAG_MAX_UP/DOWN_SWING_PERCENTAGE and the now-dead math.h include from
   constants.h. Both were latent: DAG size feeds PoW verification. Replaced with integer numerator/denominator
   constants and used them at both clamp sites (values verified identical)

Other fixes that were blocking fork propagation:
 - madeProgressOverall was set but never reset, so after one productive pass the "no progress -> stop" guard
   could never fire again and the sync loop could spin forever holding the REPL
 - seenBlocks was inserted before/regardless of a successful send, so a block broadcast while no peer was
   connected was never offered again. It is now recorded only after the block actually goes out
 - Broadcasts relayed to outbound connections only, so in a two-node setup the dialled node never pushed
   anything back and the dialer learned of new blocks only via a manual sync. Inbound peers are now relayed to

Verified with two-node harnesses at a shortened adjustment interval:
 - forced-orphan regression: was height 2 with 114 coinbase rejections, now reaches the peer's height with
   zero rejections and both nodes report Chain OK
 - sub-tip fork at depth 3: the node discovers the fork below its own tip, discards its three blocks, adopts
   the heavier five, and both nodes converge on an identical tip hash
 - deep fork at depth 8: the strictly heavier branch is correctly refused with depth=8 penalty=39 elapsed=0
 - uint256 work arithmetic covered by a standalone test (division, big-endian conversion, monotonicity,
   halved target doubles work)

One issue found along the way: the chain in build/chain_data does not pass fullverify. Block 7683 reverts to
INITIAL_DIFFICULTY where it should carry 0x1f06df14, i.e. it contains blocks mined before 1288a64 landed. A
binary built at HEAD rejects it too, so this is stale data rather than a regression — it needs a wipechain
and a re-mine.
This commit is contained in:
2026-07-28 00:24:45 +02:00
parent 1288a64977
commit 1ff2890c0f
9 changed files with 1398 additions and 346 deletions
+520 -147
View File
@@ -1,5 +1,7 @@
#include <nets/orphan_pool.h>
#include <constants.h>
#include <dynarr.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
@@ -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;
}