Files
skalacoin/src/nets/orphan_pool.c
T

585 lines
19 KiB
C

#include <nets/orphan_pool.h>
#include <constants.h>
#include <dlibc/vector.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 vector_t* 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 g_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;
// Element destructor for `g_orphans`: an entry owns its block, so dropping or evicting an entry
// frees the block along with it. OrphanPool_TakeByHashLocked hands a block out through
// vector_take_at(), which deliberately skips this. Block_Destroy accepts NULL.
static void OrphanPool_EntryDestructor(void* element) {
orphan_entry_t* e = (orphan_entry_t*)element;
Block_Destroy(e->block);
}
static void OrphanPool_InitLocked(void) {
if (!g_orphans) {
g_orphans = vector_create(sizeof(orphan_entry_t));
vector_set_destructor(g_orphans, OrphanPool_EntryDestructor);
}
}
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);
vector_destroy(&g_orphans); // The element destructor frees every pooled block
pthread_mutex_unlock(&g_orphanLock);
}
static ssize_t OrphanPool_FindByHashLocked(const uint8_t blockHash[32]) {
if (!g_orphans || !blockHash) {
return -1;
}
size_t n = vector_size(g_orphans);
for (size_t i = 0; i < n; ++i) {
orphan_entry_t* e = (orphan_entry_t*)vector_get(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 = vector_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*)vector_get(g_orphans, i);
if (e && e->sequence < oldestSequence) {
oldestSequence = e->sequence;
oldestIndex = i;
}
}
vector_pop_at(g_orphans, oldestIndex); // The element destructor frees the victim's block
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 (vector_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 (vector_push_back(g_orphans, &e) != 0) {
pthread_mutex_unlock(&g_orphanLock);
Block_Destroy(block); // never pooled, so the pool does not own it
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 = vector_size(g_orphans);
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. vector_take_at() skips
// the element destructor, which is what moves ownership of the block to the caller.
static block_t* OrphanPool_TakeByHashLocked(const uint8_t blockHash[32]) {
ssize_t index = OrphanPool_FindByHashLocked(blockHash);
if (index < 0) {
return NULL;
}
orphan_entry_t entry;
if (vector_take_at(g_orphans, (size_t)index, &entry) != 0) {
return NULL;
}
return entry.block;
}
static void OrphanPool_DropByHashLocked(const uint8_t blockHash[32]) {
ssize_t index = OrphanPool_FindByHashLocked(blockHash);
if (index < 0) {
return;
}
vector_pop_at(g_orphans, (size_t)index); // The element destructor frees the block
}
/**
* 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 = vector_size(g_orphans);
for (size_t i = 0; i < n; ++i) {
orphan_entry_t* e = (orphan_entry_t*)vector_get(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;
vector_t* collected = vector_create(sizeof(block_t*));
vector_t* hashes = vector_create(32); // one whole 32-byte block hash per element, index-aligned with collected
if (!collected || !hashes) {
vector_destroy(&collected);
vector_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;
}
// A block only counts once both pushes have landed, so the two vectors never drift out of
// step: the copy-out below reads index i from both.
if (vector_push_back(collected, &child) != 0) {
break;
}
if (vector_push_back(hashes, childHash) != 0) {
break;
}
if (observed < earliestObserved) {
earliestObserved = observed;
}
memcpy(expectedPrevHash, childHash, 32);
cursor++;
count++;
}
if (count == 0) {
vector_destroy(&collected);
vector_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);
vector_destroy(&collected);
vector_destroy(&hashes);
return 0;
}
for (size_t i = 0; i < count; ++i) {
block_t** blockSlot = (block_t**)vector_get(collected, i);
const uint8_t* hashSlot = (const uint8_t*)vector_get_const(hashes, i);
if (!blockSlot || !hashSlot) {
count = i; // never hand back a branch with a hole in it
break;
}
blocks[i] = *blockSlot;
memcpy(&hashOut[i * 32], hashSlot, 32);
}
vector_destroy(&collected);
vector_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.
vector_t* doomed = vector_create(32); // one whole 32-byte block hash per element
if (!doomed) {
return;
}
pthread_mutex_lock(&g_orphanLock);
size_t n = vector_size(g_orphans);
vector_t* candidates = vector_create(32); // index-aligned with candidateHeights
vector_t* candidateHeights = vector_create(sizeof(uint64_t));
if (candidates && candidateHeights) {
for (size_t i = 0; i < n; ++i) {
orphan_entry_t* e = (orphan_entry_t*)vector_get(g_orphans, i);
if (!e || !e->block) {
continue;
}
if (e->height >= (uint64_t)chainSize) {
continue; // still ahead of us; may attach later
}
if (vector_push_back(candidates, e->hash) != 0) {
break;
}
if (vector_push_back(candidateHeights, &e->height) != 0) {
vector_pop_back(candidates); // keep the two index-aligned
break;
}
}
}
pthread_mutex_unlock(&g_orphanLock);
size_t candidateCount = vector_size(candidateHeights);
for (size_t i = 0; i < candidateCount; ++i) {
const uint64_t* heightSlot = (const uint64_t*)vector_get_const(candidateHeights, i);
const uint8_t* orphanHash = (const uint8_t*)vector_get_const(candidates, i);
if (!heightSlot || !orphanHash) {
continue;
}
uint64_t height = *heightSlot;
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) {
(void)vector_push_back(doomed, orphanHash);
}
}
size_t doomedCount = vector_size(doomed);
if (doomedCount > 0) {
pthread_mutex_lock(&g_orphanLock);
for (size_t i = 0; i < doomedCount; ++i) {
const uint8_t* h = (const uint8_t*)vector_get_const(doomed, i);
if (h) {
OrphanPool_DropByHashLocked(h);
}
}
pthread_mutex_unlock(&g_orphanLock);
}
vector_destroy(&candidates);
vector_destroy(&candidateHeights);
vector_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 = vector_is_empty(g_orphans); // also true while the pool is not initialised
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;
}