Reported: a node at height N+10 on its own short fork, with the peer at N+500,
could never sync. It located the fork point correctly, pooled the branch, and
then refused it forever. At 90s blocks a node is 960 blocks behind after a day
offline, so this is the normal case, not an edge case.
== Why it could never recover ==
The reorg delay was served by LOCAL chain growth alone:
elapsed = tipHeight - observedTip
That only makes sense for a node whose tip is advancing. A node that is behind
has a frozen tip precisely because it is rejecting the branch, so elapsed stays
0 forever while penalty(5) = 42. Both escape hatches also fail: mining out of it
means extending a fork nobody accepts, and the IBD exemption never fires for a
node that is mining, because mining keeps its tip fresh.
Being 490 blocks behind is being behind, not a reorg contest. Refusing to adopt
protects nothing while the node falls a further 40 blocks behind every hour.
The delay is now satisfiable by EITHER side making progress:
localGrowth = tipHeight - observedTip
branchLead = candidateTip - tipHeight (0 if not ahead)
elapsed = max(localGrowth, branchLead)
A branch already extending `penalty` blocks past our tip has demonstrated
exactly what the delay asks for, and every one of those blocks carries PoW we
validated ourselves. Requiring us to independently produce the same amount is
demanding the same proof twice. Only VALIDATED blocks count -- a peer's
advertised height is not evidence and never reaches this code.
It degrades correctly in both directions: a mining node's tip advances, so an
attacker must outpace it by penalty blocks; a node that is only observing
follows the heaviest chain, which is what an observer should do.
== The window has to keep pulling the branch ==
Every forked delivery reset nextReq back to our own tip, so the window
re-requested the same eight heights forever and the pool never grew past the
window size -- branchLead could not rise even in principle. The backward walk
now runs ONCE to establish linkage, then the window marches forward pooling the
branch, retrying adoption per window-full with a final attempt when it drains.
== sync force ==
Operator override that skips the delay for one sync, for a node whose chain is
known to be the wrong one. Threaded explicitly (Chain_ReplaceBranch gains
bypassPenalty, OrphanPool_AttemptAttachForced) rather than through a global, so
nothing a peer sends can reach it. Linkage, work comparison and atomicity still
apply -- it waives only the waiting, and says so loudly in the log.
== Bug found in testing: PoW is branch-relative ==
A block's epoch seed is the last block of the previous epoch ON ITS OWN BRANCH.
Validating a competing branch's block against OUR epoch seed does not merely
fail to resolve: when the chains diverge before the boundary it resolves to the
WRONG seed and rejects a perfectly valid block. Any fork spanning an epoch
boundary was therefore impossible to assemble -- the branch could not grow past
the boundary block, so branchLead stalled one short of it.
The receive path now does self-contained checks only (Block_HasValidStructure:
merkle, transactions, vote, non-empty). Proof of work moved to
Chain_AddBlockLocked, at the point a block joins the chain, where the branch
context is real -- the rollback has put its ancestors in place by then. That is
where the invariant belongs and it removes a duplicate check rather than adding
one. Needs Chain_DagParamsForHeightLocked, because Chain_AddBlockLocked already
holds chainLock for writing and the lock is not recursive.
Consequence worth knowing: the orphan pool can now hold blocks whose work has
not been verified, bounded by MAX_ORPHAN_BLOCKS (512). Each still had to pass
merkle and full transaction/signature validation, and none can reach the chain
unverified.
This bug also affected plain forward sync across block 350000; it was masked
because appending keeps the boundary block in the chain.
== Bug found in testing: stale DAG accepted as current ==
Block_PowHashHeavy matched on epoch index and size but not the seed. A DAG's
content is a function of (seed, size); the epoch index is a label for it. A
reorg is exactly the event that changes the seed while leaving index and size
untouched, so mid-apply the miner's context still held a DAG built from the
PRE-reorg seed, the guard passed, and a valid block was hashed against the wrong
lanes. g_dagSeed now records what each DAG was generated from and both
Block_EnsureAutolykos2Dag and Block_PowHashHeavy compare it.
== Bug found in testing: double free on the failed-apply path ==
SIGABRT in the allocator: free_tiny_botch -> DynArr_destroy -> Block_Destroy ->
Chain_FreeBlockArray -> Chain_ReplaceBranch.
DynArr_push_back stores the struct BY VALUE, so the chain's element and the
caller's block_t share one transactions pointer. Three places free that array
through the chain's copy -- Chain_ClearBlocks, Chain_RollbackToHeightLocked and
Chain_SaveToFile -- and each NULLs only the chain's side, leaving any caller
wrapper dangling. Whether a caller then had to use free() or Block_Destroy() was
a convention carried in comments at every call site plus a consumed-count passed
into Chain_FreeBlockArray. Chain_ReplaceBranch reset that count to 0 after
rolling back a failed apply, which told the cleanup to Block_Destroy exactly the
blocks whose arrays the rollback had just freed.
Rather than fix the count, the aliasing is now safe by construction:
Chain_AddBlockLocked clears the CALLER's transactions pointer immediately after
the push. Since DynArr_destroy(NULL) is a no-op, free(wrapper) and
Block_Destroy(wrapper) become equivalent and both safe regardless of what later
frees the chain's copy. The consumed-count parameter and both counters are gone
-- the thing that could be got wrong no longer exists -- and all call sites are
unified on Block_Destroy.
Placement is deliberate: immediately after the push, not at the end on success.
The ledger pass can fail with the block already in the chain, returning false to
a caller that destroys its wrapper on failure -- OrphanPool_ExtendTip does
exactly that, a third live instance not yet triggered.
Two follow-ons the refactor forced, both improvements anyway: MineAndAppendBlock
read the coinbase for its log line after the add (hoisted above it), and the
success log printed the caller's block rather than the chain's copy.
== Also ==
The deferral line is rate-limited. The maintenance thread retries pooled
branches once a second and elapsed only changes when something moves, so it
printed an identical line every second -- forever, on a node that is not mining.
It now reports each distinct situation once.
== Verification ==
Synthetic fork, node A 5 deep, node B ~60 ahead, EPOCH_LENGTH=8 so the branch
crosses three epoch boundaries:
branchLead climbs 8 -> 16 -> 28 -> 34, crosses penalty(5)=42
Adopted competing branch of 50 block(s) at fork height 20
sync complete: localHeight=70
Chain OK
Repeated under AddressSanitizer: adopted 47 blocks, 0 ASan errors on both nodes.
This matters because the refactor rewrites the exact cleanup path the SIGABRT
came from, and a double free that no longer aborts would otherwise pass silently.
Unit suites pass, including a new assertion "heavy path refuses a DAG built from
a different seed" -- the direct regression for the stale-DAG bug.
Harness note: each node needs its OWN wallet. With a shared one both pay the
same coinbase address, produce identical merkle roots, and at easy difficulty
mine byte-identical blocks -- the fork test silently became a catch-up test.
== Still untested ==
The restore-after-failed-apply path is no longer naturally reachable now that
the two bugs above are fixed, so it needs deliberate corruption to exercise.
Test B (branch only slightly ahead must still DEFER), test C (sync force), and a
TSan pass over the changed paths are outstanding.
587 lines
18 KiB
C
587 lines
18 KiB
C
#include <nets/orphan_pool.h>
|
|
#include <constants.h>
|
|
#include <dynarr.h>
|
|
#include <pthread.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
|
|
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) {
|
|
pthread_mutex_lock(&g_orphanLock);
|
|
OrphanPool_InitLocked();
|
|
pthread_mutex_unlock(&g_orphanLock);
|
|
}
|
|
|
|
void OrphanPool_Destroy(void) {
|
|
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 && memcmp(e->hash, blockHash, 32) == 0) {
|
|
return (ssize_t)i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
// 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;
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// 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 (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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
uint8_t expectedPrevHash[32];
|
|
memcpy(expectedPrevHash, forkParentHash, 32);
|
|
|
|
uint64_t earliestObserved = UINT64_MAX;
|
|
uint64_t cursor = forkHeight;
|
|
size_t count = 0;
|
|
|
|
while (1) {
|
|
block_t* child = NULL;
|
|
uint64_t observed = 0;
|
|
uint8_t childHash[32];
|
|
if (!OrphanPool_FindChildLocked(cursor, expectedPrevHash, &child, &observed, childHash)) {
|
|
break;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// Chain_AddBlock took ownership of the transaction array and cleared our pointer to it.
|
|
Block_Destroy(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++;
|
|
}
|
|
|
|
return attached;
|
|
}
|
|
|
|
/**
|
|
* 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, bool bypassPenalty) {
|
|
const size_t chainSize = Chain_Size(chain);
|
|
if (chainSize == 0) {
|
|
return 0;
|
|
}
|
|
|
|
// 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);
|
|
|
|
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,
|
|
bypassPenalty);
|
|
}
|
|
|
|
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) {
|
|
return OrphanPool_AttemptAttachForced(chain, false);
|
|
}
|
|
|
|
size_t OrphanPool_AttemptAttachForced(blockchain_t* chain, bool bypassPenalty) {
|
|
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, bypassPenalty);
|
|
attached += adopted;
|
|
|
|
if (extended == 0 && adopted == 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
OrphanPool_PruneStale(chain);
|
|
|
|
return attached;
|
|
}
|