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:
@@ -28,6 +28,33 @@ void Chain_Wipe(blockchain_t* chain);
|
|||||||
// Returns true on success.
|
// Returns true on success.
|
||||||
bool Chain_RollbackToHeight(blockchain_t* chain, size_t height);
|
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.
|
// Recompute `currentSupply` and `currentReward` from the in-memory chain blocks.
|
||||||
// Returns true on success and updates runtime state globals.
|
// Returns true on success and updates runtime state globals.
|
||||||
bool Chain_RecomputeRuntimeState(blockchain_t* chain);
|
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`.
|
// Call after any change to the tip. Must NOT be called while holding `chainLock`.
|
||||||
void Chain_OnTipAdvanced(blockchain_t* chain);
|
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
|
#endif
|
||||||
|
|||||||
+44
-12
@@ -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)
|
static const uint64_t SYNC_BACKOFF_BASE_MS = 200ULL; // base backoff in ms (exponential)
|
||||||
// Parallelism
|
// Parallelism
|
||||||
static const int MAX_PARALLEL_FETCHES = 8; // concurrent block fetches during windowed sync
|
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
|
// How far below a detected divergence we ask a peer for blocks, so the orphan pool has enough of
|
||||||
static const uint64_t INITIAL_SYNC_HEIGHT_DIFF = 50ULL;
|
// 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 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 uint64_t REORG_PENALTY_FACTOR_NUM = 1ULL; // base scaling factor (theta), numerator
|
||||||
static const double REORG_PENALTY_EXPONENT = 2.0; // exponent p in penalty ~ B^p
|
static const uint64_t REORG_PENALTY_FACTOR_DEN = 1ULL; // base scaling factor (theta), denominator
|
||||||
static const double REORG_PENALTY_REF_BLOCK_TIME = 150.0; // reference block time in seconds used by original scheme
|
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.
|
// Reward schedule acceleration: 1 means normal-speed progression.
|
||||||
#define EMISSION_ACCELERATION_FACTOR 1ULL
|
#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_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 (6ULL << 30) // 6 GB, adjusted per cycle based off DAG_BASE_GROWTH
|
||||||
#define DAG_BASE_SIZE (1ULL << 30) // TEMPORARY FOR TESTING
|
#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
|
// 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%
|
// Percentages are integer numerator/denominator pairs, never float literals: DAG size feeds PoW
|
||||||
#define DAG_MAX_DOWN_SWING_PERCENTAGE 0.90 // 10%
|
// 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_UP_SWING_GB (2ULL << 30) // 2 GB
|
||||||
#define DAG_MAX_DOWN_SWING_GB (1ULL << 30) // 1 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
|
#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
|
// Hashing DAG
|
||||||
#include <math.h>
|
|
||||||
static inline size_t CalculateTargetDAGSize(blockchain_t* chain) {
|
static inline size_t CalculateTargetDAGSize(blockchain_t* chain) {
|
||||||
// Base size plus (base growth * difficulty factor), adjusted by acceleration
|
// Base size plus (base growth * difficulty factor), adjusted by acceleration
|
||||||
if (!chain || !chain->blocks) { return 0; } // Invalid
|
if (!chain || !chain->blocks) { return 0; } // Invalid
|
||||||
@@ -213,12 +245,12 @@ static inline size_t CalculateTargetDAGSize(blockchain_t* chain) {
|
|||||||
// Clamp
|
// Clamp
|
||||||
if (growth > 0) {
|
if (growth > 0) {
|
||||||
// Difficulty increased -> Clamp the UPWARD swing
|
// 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 > maxUp) growth = maxUp;
|
||||||
if (growth > (int64_t)DAG_MAX_UP_SWING_GB) growth = DAG_MAX_UP_SWING_GB;
|
if (growth > (int64_t)DAG_MAX_UP_SWING_GB) growth = DAG_MAX_UP_SWING_GB;
|
||||||
} else {
|
} else {
|
||||||
// Difficulty decreased -> Clamp the DOWNWARD swing
|
// 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 > maxDown) growth = -maxDown;
|
||||||
if (-growth > (int64_t)DAG_MAX_DOWN_SWING_GB) growth = -(int64_t)DAG_MAX_DOWN_SWING_GB;
|
if (-growth > (int64_t)DAG_MAX_DOWN_SWING_GB) growth = -(int64_t)DAG_MAX_DOWN_SWING_GB;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#define ORPHAN_POOL_H
|
#define ORPHAN_POOL_H
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
#include <block/block.h>
|
#include <block/block.h>
|
||||||
#include <block/chain.h>
|
#include <block/chain.h>
|
||||||
|
|
||||||
@@ -10,11 +11,21 @@ void OrphanPool_Init(void);
|
|||||||
void OrphanPool_Destroy(void);
|
void OrphanPool_Destroy(void);
|
||||||
|
|
||||||
// Insert an orphan block into the pool. Ownership of `block` is transferred to the pool.
|
// Insert an orphan block into the pool. Ownership of `block` is transferred to the pool.
|
||||||
// `height` is the block number from the header.
|
// `height` is the block number from the header. `observedAtTipHeight` is the local chain tip
|
||||||
void OrphanPool_Insert(block_t* block, uint64_t height);
|
// 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.
|
// Returns the number of blocks successfully attached.
|
||||||
size_t OrphanPool_AttemptAttach(blockchain_t* chain);
|
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
|
#endif
|
||||||
|
|||||||
@@ -114,6 +114,72 @@ static inline int uint256_cmp(const uint256_t* a, const uint256_t* b) {
|
|||||||
return 0;
|
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) {
|
static inline void uint256_serialize(const uint256_t* value, char* out) {
|
||||||
if (!value || !out) {
|
if (!value || !out) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
+526
-59
@@ -2,6 +2,7 @@
|
|||||||
#include <constants.h>
|
#include <constants.h>
|
||||||
#include <runtime_state.h>
|
#include <runtime_state.h>
|
||||||
#include <txmempool.h>
|
#include <txmempool.h>
|
||||||
|
#include <nets/fetch_scheduler.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
@@ -98,6 +99,51 @@ static bool DebitAddress(const uint8_t address[32], const uint256_t* amount) {
|
|||||||
return BalanceSheet_Insert(entry) >= 0;
|
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) {
|
bool Chain_RecomputeRuntimeState(blockchain_t* chain) {
|
||||||
if (!chain) {
|
if (!chain) {
|
||||||
return false;
|
return false;
|
||||||
@@ -105,23 +151,28 @@ bool Chain_RecomputeRuntimeState(blockchain_t* chain) {
|
|||||||
|
|
||||||
uint256_t rebuiltSupply = uint256_from_u64(0);
|
uint256_t rebuiltSupply = uint256_from_u64(0);
|
||||||
for (size_t i = 0; i < chain->size; ++i) {
|
for (size_t i = 0; i < chain->size; ++i) {
|
||||||
block_t* blk = (block_t*)DynArr_at(chain->blocks, i);
|
block_t* blk = NULL;
|
||||||
if (!blk || !blk->transactions) {
|
bool loadedFromDisk = false;
|
||||||
|
if (!Chain_BorrowBlockTransactions(chain, i, &blk, &loadedFromDisk)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (size_t j = 0; j < DynArr_size(blk->transactions); ++j) {
|
for (size_t j = 0; j < DynArr_size(blk->transactions); ++j) {
|
||||||
signed_transaction_t* tx = (signed_transaction_t*)DynArr_at(blk->transactions, j);
|
signed_transaction_t* tx = (signed_transaction_t*)DynArr_at(blk->transactions, j);
|
||||||
if (!tx) {
|
if (!tx) {
|
||||||
|
Chain_ReturnBlockTransactions(blk, loadedFromDisk);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Address_IsCoinbase(tx->transaction.senderAddress)) {
|
if (Address_IsCoinbase(tx->transaction.senderAddress)) {
|
||||||
if (uint256_add_u64(&rebuiltSupply, tx->transaction.amount1)) {
|
if (uint256_add_u64(&rebuiltSupply, tx->transaction.amount1)) {
|
||||||
|
Chain_ReturnBlockTransactions(blk, loadedFromDisk);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Chain_ReturnBlockTransactions(blk, loadedFromDisk);
|
||||||
}
|
}
|
||||||
|
|
||||||
currentSupply = rebuiltSupply;
|
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;
|
bool ok = true;
|
||||||
|
|
||||||
if (!chain || !block || !chain->blocks) {
|
if (!chain || !block || !chain->blocks || !block->transactions) {
|
||||||
return false;
|
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.
|
// Ensure the incoming block's header.blockNumber matches the index it will be appended at.
|
||||||
size_t expectedIndex = DynArr_size(chain->blocks);
|
size_t expectedIndex = DynArr_size(chain->blocks);
|
||||||
if (block->header.blockNumber != expectedIndex) {
|
if (block->header.blockNumber != expectedIndex) {
|
||||||
// Mismatched block number; reject to avoid duplicate indices or inconsistent headers.
|
// Mismatched block number; reject to avoid duplicate indices or inconsistent headers.
|
||||||
pthread_mutex_unlock(&balanceSheetLock);
|
|
||||||
pthread_rwlock_unlock(&chainLock);
|
|
||||||
return false;
|
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
|
// 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
|
// 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.
|
// 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,
|
expectedIndex,
|
||||||
(unsigned int)expectedTarget,
|
(unsigned int)expectedTarget,
|
||||||
(unsigned int)block->header.difficultyTarget);
|
(unsigned int)block->header.difficultyTarget);
|
||||||
pthread_mutex_unlock(&balanceSheetLock);
|
|
||||||
pthread_rwlock_unlock(&chainLock);
|
|
||||||
return false;
|
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
|
// ok remains true if no failures
|
||||||
} while (0);
|
} 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_mutex_unlock(&balanceSheetLock);
|
||||||
pthread_rwlock_unlock(&chainLock);
|
pthread_rwlock_unlock(&chainLock);
|
||||||
|
|
||||||
@@ -361,11 +452,6 @@ bool Chain_AddBlock(blockchain_t* chain, block_t* block) {
|
|||||||
Chain_OnTipAdvanced(chain);
|
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;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,16 +525,17 @@ bool Chain_IsValid(blockchain_t* chain) {
|
|||||||
return true;
|
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;
|
if (!chain || !chain->blocks) return false;
|
||||||
|
|
||||||
pthread_rwlock_wrlock(&chainLock);
|
|
||||||
pthread_mutex_lock(&balanceSheetLock);
|
|
||||||
|
|
||||||
size_t cur = DynArr_size(chain->blocks);
|
size_t cur = DynArr_size(chain->blocks);
|
||||||
if (height >= cur) {
|
if (height >= cur) {
|
||||||
pthread_mutex_unlock(&balanceSheetLock);
|
|
||||||
pthread_rwlock_unlock(&chainLock);
|
|
||||||
return true; // nothing to do
|
return true; // nothing to do
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,24 +557,19 @@ bool Chain_RollbackToHeight(blockchain_t* chain, size_t height) {
|
|||||||
BalanceSheet_Destroy();
|
BalanceSheet_Destroy();
|
||||||
BalanceSheet_Init();
|
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) {
|
for (size_t i = 0; i < chain->size; ++i) {
|
||||||
block_t* blk = (block_t*)DynArr_at(chain->blocks, i);
|
block_t* toProcess = NULL;
|
||||||
block_t* toProcess = blk;
|
|
||||||
bool loaded = false;
|
bool loaded = false;
|
||||||
|
|
||||||
if (!blk || !blk->transactions) {
|
if (!Chain_BorrowBlockTransactions(chain, i, &toProcess, &loaded)) {
|
||||||
// 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
|
// Can't rebuild without transactions
|
||||||
pthread_mutex_unlock(&balanceSheetLock);
|
|
||||||
pthread_rwlock_unlock(&chainLock);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
toProcess = loadedBlk;
|
|
||||||
loaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply transactions
|
// Apply transactions
|
||||||
if (toProcess && toProcess->transactions) {
|
if (toProcess && toProcess->transactions) {
|
||||||
@@ -498,6 +580,7 @@ bool Chain_RollbackToHeight(blockchain_t* chain, size_t height) {
|
|||||||
|
|
||||||
// Coinbase credit
|
// Coinbase credit
|
||||||
if (Address_IsCoinbase(tx->transaction.senderAddress)) {
|
if (Address_IsCoinbase(tx->transaction.senderAddress)) {
|
||||||
|
(void)uint256_add_u64(&rebuiltSupply, tx->transaction.amount1);
|
||||||
balance_sheet_entry_t entry;
|
balance_sheet_entry_t entry;
|
||||||
if (!BalanceSheet_Lookup(tx->transaction.recipientAddress1, &entry)) {
|
if (!BalanceSheet_Lookup(tx->transaction.recipientAddress1, &entry)) {
|
||||||
memset(&entry, 0, sizeof(entry));
|
memset(&entry, 0, sizeof(entry));
|
||||||
@@ -552,27 +635,318 @@ bool Chain_RollbackToHeight(blockchain_t* chain, size_t height) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loaded && toProcess) {
|
Chain_ReturnBlockTransactions(toProcess, loaded);
|
||||||
if (toProcess->transactions) DynArr_destroy(toProcess->transactions);
|
|
||||||
free(toProcess);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Chain_RecomputeRuntimeState(chain)) {
|
currentSupply = rebuiltSupply;
|
||||||
pthread_mutex_unlock(&balanceSheetLock);
|
currentReward = CalculateBlockReward(currentSupply, chain);
|
||||||
pthread_rwlock_unlock(&chainLock);
|
|
||||||
return false;
|
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_mutex_unlock(&balanceSheetLock);
|
||||||
pthread_rwlock_unlock(&chainLock);
|
pthread_rwlock_unlock(&chainLock);
|
||||||
|
|
||||||
// A reorg can move the tip back across an adjustment boundary, so the target must come down too.
|
// A reorg can move the tip back across an adjustment boundary, so the target must come down too.
|
||||||
Chain_OnTipAdvanced(chain);
|
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;
|
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) {
|
void Chain_Wipe(blockchain_t* chain) {
|
||||||
Chain_ClearBlocks(chain);
|
Chain_ClearBlocks(chain);
|
||||||
currentBlockHeight = 0;
|
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;
|
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
|
// Clamp per-epoch target movement: at most x2 easier or x2 harder. Clamping the measured span
|
||||||
if (timeRatio > 2.0) {
|
// is equivalent to clamping the ratio, but stays in integers.
|
||||||
timeRatio = 2.0;
|
// Everything below is deliberately integer-only: the retarget is consensus-critical, and any
|
||||||
} else if (timeRatio < 0.5) {
|
// floating-point rounding difference between nodes would make them disagree on the target.
|
||||||
timeRatio = 0.5;
|
uint64_t clampedTime = actualTime;
|
||||||
|
if (clampedTime > targetTime * 2ULL) {
|
||||||
|
clampedTime = targetTime * 2ULL;
|
||||||
|
} else if (clampedTime < targetTime / 2ULL) {
|
||||||
|
clampedTime = targetTime / 2ULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t exponent = currentTarget >> 24;
|
uint32_t exponent = currentTarget >> 24;
|
||||||
@@ -1164,15 +1541,18 @@ uint32_t Chain_ComputeTargetAtHeight(blockchain_t* chain, uint64_t height, uint3
|
|||||||
return INITIAL_DIFFICULTY;
|
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.
|
// Normalize to compact format range.
|
||||||
while (newMantissa > 8388607.0) { // 0x007fffff
|
while (newMantissa > 0x007fffffULL) {
|
||||||
newMantissa /= 256.0;
|
newMantissa /= 256ULL;
|
||||||
exponent++;
|
exponent++;
|
||||||
}
|
}
|
||||||
while (newMantissa > 0.0 && newMantissa < 32768.0 && exponent > 3) { // Keep coefficient in normal range
|
while (newMantissa > 0ULL && newMantissa < 32768ULL && exponent > 3) { // Keep coefficient in normal range
|
||||||
newMantissa *= 256.0;
|
newMantissa *= 256ULL;
|
||||||
exponent--;
|
exponent--;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1206,6 +1586,93 @@ uint32_t Chain_GetTargetForHeight(blockchain_t* chain, uint64_t height) {
|
|||||||
return target;
|
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) {
|
void Chain_OnTipAdvanced(blockchain_t* chain) {
|
||||||
if (!chain || !chain->blocks) {
|
if (!chain || !chain->blocks) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
+88
-82
@@ -289,7 +289,7 @@ static bool ComputeEpochDagBytesForHeightFromChain(const blockchain_t* chain, ui
|
|||||||
int64_t growth = (int64_t)((int64_t)DAG_BASE_GROWTH * difficultyDelta);
|
int64_t growth = (int64_t)((int64_t)DAG_BASE_GROWTH * difficultyDelta);
|
||||||
|
|
||||||
if (growth > 0) {
|
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) {
|
if (growth > maxUp) {
|
||||||
growth = maxUp;
|
growth = maxUp;
|
||||||
}
|
}
|
||||||
@@ -297,7 +297,7 @@ static bool ComputeEpochDagBytesForHeightFromChain(const blockchain_t* chain, ui
|
|||||||
growth = (int64_t)DAG_MAX_UP_SWING_GB;
|
growth = (int64_t)DAG_MAX_UP_SWING_GB;
|
||||||
}
|
}
|
||||||
} else {
|
} 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) {
|
if (-growth > maxDown) {
|
||||||
growth = -maxDown;
|
growth = -maxDown;
|
||||||
}
|
}
|
||||||
@@ -403,6 +403,36 @@ static bool Block_GetCoinbaseAndFeeTotals(const block_t* block, uint64_t* outCoi
|
|||||||
return true;
|
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,
|
static bool MineAndAppendBlock(blockchain_t* chain,
|
||||||
block_t* block,
|
block_t* block,
|
||||||
uint256_t* currentSupply,
|
uint256_t* currentSupply,
|
||||||
@@ -444,8 +474,6 @@ static bool MineAndAppendBlock(blockchain_t* chain,
|
|||||||
BalanceSheet_SaveToFile(chainDataDir);
|
BalanceSheet_SaveToFile(chainDataDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
(void)uint256_add_u64(currentSupply, coinbaseAmount);
|
|
||||||
|
|
||||||
uint8_t canonicalHash[32];
|
uint8_t canonicalHash[32];
|
||||||
uint8_t powHash[32];
|
uint8_t powHash[32];
|
||||||
Block_CalculateHash(block, canonicalHash);
|
Block_CalculateHash(block, canonicalHash);
|
||||||
@@ -462,10 +490,8 @@ static bool MineAndAppendBlock(blockchain_t* chain,
|
|||||||
powHash[0], powHash[1], powHash[2], powHash[3],
|
powHash[0], powHash[1], powHash[2], powHash[3],
|
||||||
canonicalHash[0], canonicalHash[1], canonicalHash[2], canonicalHash[3]);
|
canonicalHash[0], canonicalHash[1], canonicalHash[2], canonicalHash[3]);
|
||||||
|
|
||||||
*currentReward = CalculateBlockReward(*currentSupply, chain);
|
// 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.
|
||||||
// The difficulty retarget and epoch DAG rebuild happen in Chain_AddBlock, so that blocks we
|
|
||||||
// receive from peers advance them exactly like blocks we mine ourselves.
|
|
||||||
|
|
||||||
return true;
|
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.
|
// Continue syncing in a loop until we've caught up to the peer or no progress is made.
|
||||||
bool madeProgressOverall = false;
|
bool madeProgressOverall = false;
|
||||||
|
int forkProbes = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
uint64_t localHeight = (uint64_t)Chain_Size(chain);
|
uint64_t localHeight = (uint64_t)Chain_Size(chain);
|
||||||
|
|
||||||
// Only penalize small near-tip gaps. Large gaps are treated as normal catch-up,
|
// Whether we are catching up rather than following the tip. Derived from our own chain
|
||||||
// because a much taller peer on the same chain is not evidence of a reorg. TODO: Maybe look at this again some other day.
|
// only: this used to key off the peer's advertised height, which let any peer claiming
|
||||||
bool isInitialSync = (localHeight == 0) || ((peerHeight > localHeight) && ((peerHeight - localHeight) > INITIAL_SYNC_HEIGHT_DIFF));
|
// localHeight + INITIAL_SYNC_HEIGHT_DIFF switch off reorg handling for the session.
|
||||||
|
bool isInitialSync = Chain_IsInitialBlockDownload(chain);
|
||||||
|
|
||||||
// Compute penalty and adjusted peer height.
|
// The reorg penalty is NOT applied to the fetch window. It is a delay on adopting a
|
||||||
uint64_t delay = (peerHeight > localHeight) ? (peerHeight - localHeight) : 0ULL;
|
// competing branch (enforced in Chain_ReplaceBranch), not on catching up: penalizing
|
||||||
uint64_t penalty = isInitialSync ? 0ULL : FetchScheduler_ComputeReorgPenaltyBlocks(delay);
|
// the height gap to a peer only throttled honest sync, and for gaps of 4-50 it
|
||||||
uint64_t adjustedPeerHeight = (peerHeight > penalty) ? (peerHeight - penalty) : 0ULL;
|
// collapsed the window to a single block per pass.
|
||||||
|
printf("syncing: peerHeight=%" PRIu64 " local=%" PRIu64 " initialSync=%s\n",
|
||||||
// Ensure we always make forward progress: if the penalty would reduce the
|
peerHeight, localHeight, isInitialSync ? "yes" : "no");
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Windowed parallel fetch
|
// Windowed parallel fetch
|
||||||
uint64_t start = localHeight;
|
uint64_t start = localHeight;
|
||||||
uint64_t end = adjustedPeerHeight; // exclusive target height
|
uint64_t end = peerHeight; // exclusive target height
|
||||||
uint64_t nextReq = start;
|
uint64_t nextReq = start;
|
||||||
|
|
||||||
const int maxInFlight = MAX_PARALLEL_FETCHES;
|
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.
|
// Check whether this block builds on our expected tip. If not, it's a reorg.
|
||||||
if (memcmp(fetched->header.prevHash, expectedPrevHash, sizeof(expectedPrevHash)) != 0) {
|
if (memcmp(fetched->header.prevHash, expectedPrevHash, sizeof(expectedPrevHash)) != 0) {
|
||||||
// Find matching ancestor in our current chain (if any)
|
// Ask the peer for a window of blocks below the divergence so the orphan
|
||||||
ssize_t matchIndex = -1;
|
// pool can assemble its branch and find the true common ancestor by
|
||||||
size_t chainSz = Chain_Size(chain);
|
// prevHash linkage. FETCH_BLOCK answers from the peer's own chain, and
|
||||||
uint8_t tmpHash[32];
|
// Node_ParseAndAcceptBlock now routes sub-tip blocks into the pool
|
||||||
for (size_t bi = 0; bi < chainSz; ++bi) {
|
// instead of dropping them, so no protocol change is needed.
|
||||||
block_t* b = NULL;
|
//
|
||||||
if (!Chain_GetBlockCopy(chain, bi, &b) || !b) continue;
|
// We deliberately do NOT roll back here. The swap happens in
|
||||||
Block_CalculateHash(b, tmpHash);
|
// Chain_ReplaceBranch, which compares cumulative work, enforces the
|
||||||
if (memcmp(tmpHash, fetched->header.prevHash, sizeof(tmpHash)) == 0) {
|
// Horizen reorg penalty, and restores our chain if the branch fails to
|
||||||
matchIndex = (ssize_t)bi;
|
// apply. The old code rolled back to height 0 whenever it could not find
|
||||||
Block_Destroy(b);
|
// the parent -- a full chain wipe, genesis included, that any peer could
|
||||||
break;
|
// trigger with a single unlinked block.
|
||||||
}
|
printf("Divergence at height %" PRIu64 "; probing for the fork point\n", h);
|
||||||
Block_Destroy(b);
|
RequestForkWindow(node, peerConn, h);
|
||||||
}
|
|
||||||
|
|
||||||
uint64_t reorgDepth = 0ULL;
|
|
||||||
if (matchIndex >= 0) {
|
|
||||||
reorgDepth = (uint64_t)localHeight - ((uint64_t)matchIndex + 1ULL);
|
|
||||||
} else {
|
|
||||||
// No match found: treat as full reorg depth equal to localHeight
|
|
||||||
reorgDepth = localHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
size_t reattached = OrphanPool_AttemptAttach(chain);
|
||||||
if (reattached > 0) {
|
if (reattached > 0) {
|
||||||
printf("Reorg rollback attached %zu orphan(s)\n", reattached);
|
printf("Reorg attached %zu block(s) from the peer's branch\n", reattached);
|
||||||
|
} else {
|
||||||
|
printf("Reorg candidate not adopted (lighter branch, or still serving its reorg penalty)\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply additional penalty by shrinking end and restart window from current Chain_Size
|
// Free fetched block and reset the window against whatever our tip is now
|
||||||
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
|
|
||||||
Block_Destroy(fetched);
|
Block_Destroy(fetched);
|
||||||
nextReq = Chain_Size(chain);
|
nextReq = Chain_Size(chain);
|
||||||
inFlight = 0;
|
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);
|
uint64_t newLocal = (uint64_t)Chain_Size(chain);
|
||||||
if (newLocal > localHeight) madeProgressOverall = true;
|
madeProgressOverall = (newLocal > localHeight);
|
||||||
printf("sync complete: localHeight=%" PRIu64 "\n", newLocal);
|
printf("sync complete: localHeight=%" PRIu64 "\n", newLocal);
|
||||||
|
|
||||||
// If we've caught up to the peer, stop. Otherwise refresh peerHeight and loop again.
|
// 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);
|
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 (!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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+34
-12
@@ -1,24 +1,46 @@
|
|||||||
#include <nets/fetch_scheduler.h>
|
#include <nets/fetch_scheduler.h>
|
||||||
#include <constants.h>
|
#include <constants.h>
|
||||||
#include <math.h>
|
|
||||||
|
|
||||||
// Note: floating point is used intentionally here for readability and
|
// Integer-only on purpose. This penalty gates fork choice (see Chain_ReplaceBranch), so every node
|
||||||
// because the final penalty is rounded to whole blocks. This keeps the
|
// must compute the exact same number of blocks from the same reorg depth. The previous
|
||||||
// implementation straightforward while avoiding subtle integer overflow
|
// implementation used double/pow/ceil, which is not reproducible across platforms and compilers.
|
||||||
// for large exponents. If desired, replace with fixed-point arithmetic.
|
|
||||||
uint64_t FetchScheduler_ComputeReorgPenaltyBlocks(uint64_t delayBlocks) {
|
uint64_t FetchScheduler_ComputeReorgPenaltyBlocks(uint64_t delayBlocks) {
|
||||||
if (delayBlocks <= REORG_PENALTY_GRACE_BLOCKS) {
|
if (delayBlocks <= REORG_PENALTY_GRACE_BLOCKS) {
|
||||||
return 0ULL;
|
return 0ULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
double B = (double)delayBlocks;
|
uint64_t depth = delayBlocks;
|
||||||
double factor = REORG_PENALTY_FACTOR;
|
if (depth > REORG_PENALTY_MAX_DEPTH) {
|
||||||
double exp = REORG_PENALTY_EXPONENT;
|
depth = REORG_PENALTY_MAX_DEPTH;
|
||||||
double timeScale = ((double)TARGET_BLOCK_TIME) / REORG_PENALTY_REF_BLOCK_TIME;
|
}
|
||||||
|
|
||||||
double raw = factor * pow(B, exp) * timeScale;
|
// depth^EXPONENT, saturating rather than wrapping.
|
||||||
if (raw < 0.0) raw = 0.0;
|
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;
|
return penalty;
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-24
@@ -477,33 +477,54 @@ static node_block_accept_result_t Node_ParseAndAcceptBlock(const unsigned char*
|
|||||||
return NODE_BLOCK_REJECTED;
|
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.
|
// Temporary debug mode: force network-received blocks through the orphan pool to exercise reorg handling.
|
||||||
if (forceOrphanReorgEnabled && blk->header.blockNumber > 0) {
|
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);
|
printf("Forced orphan BLOCK_DATA at height %" PRIu64 "\n", blockHeight);
|
||||||
return NODE_BLOCK_ORPHAN_QUEUED;
|
return NODE_BLOCK_ORPHAN_QUEUED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If parent is missing, insert into orphan pool instead of rejecting immediately.
|
// If parent is missing, insert into orphan pool instead of rejecting immediately.
|
||||||
uint64_t chainSize = Chain_Size(currentChain);
|
|
||||||
if (blk->header.blockNumber > chainSize) {
|
if (blk->header.blockNumber > chainSize) {
|
||||||
// Parent(s) missing; queue as orphan
|
// 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);
|
printf("Queued orphan BLOCK_DATA at height %" PRIu64 "\n", blockHeight);
|
||||||
return NODE_BLOCK_ORPHAN_QUEUED;
|
return NODE_BLOCK_ORPHAN_QUEUED;
|
||||||
} else if (blk->header.blockNumber < chainSize) {
|
} else if (blk->header.blockNumber < chainSize) {
|
||||||
// Older block than current chain tip: reject
|
// A block below our tip is either one we already have, or the lower half of a competing
|
||||||
printf("Rejected BLOCK_DATA at height %" PRIu64 ": older than current chain\n", blockHeight);
|
// 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);
|
DynArr_destroy(blk->transactions);
|
||||||
free(blk);
|
free(blk);
|
||||||
return NODE_BLOCK_REJECTED;
|
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 {
|
} else {
|
||||||
// blk->header.blockNumber == chainSize -> candidate to append. Ensure prevHash matches current tip.
|
// blk->header.blockNumber == chainSize -> candidate to append. Ensure prevHash matches current tip.
|
||||||
if (chainSize > 0) {
|
if (chainSize > 0) {
|
||||||
block_t* last = NULL;
|
block_t* last = NULL;
|
||||||
if (!Chain_GetBlockCopy(currentChain, (size_t)(chainSize - 1), &last) || !last) {
|
if (!Chain_GetBlockCopy(currentChain, (size_t)(chainSize - 1), &last) || !last) {
|
||||||
// Can't verify parent; queue as orphan conservatively
|
// 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);
|
printf("Queued orphan BLOCK_DATA at height %" PRIu64 " (unable to verify parent)\n", blockHeight);
|
||||||
if (last) Block_Destroy(last);
|
if (last) Block_Destroy(last);
|
||||||
return NODE_BLOCK_ORPHAN_QUEUED;
|
return NODE_BLOCK_ORPHAN_QUEUED;
|
||||||
@@ -512,7 +533,7 @@ static node_block_accept_result_t Node_ParseAndAcceptBlock(const unsigned char*
|
|||||||
Block_CalculateHash(last, lastHash);
|
Block_CalculateHash(last, lastHash);
|
||||||
if (memcmp(lastHash, blk->header.prevHash, 32) != 0) {
|
if (memcmp(lastHash, blk->header.prevHash, 32) != 0) {
|
||||||
// Conflicting block at same height; queue as orphan until resolved by a subsequent extension.
|
// 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);
|
Block_Destroy(last);
|
||||||
printf("Queued conflicting BLOCK_DATA at same height %" PRIu64 " as orphan\n", blockHeight);
|
printf("Queued conflicting BLOCK_DATA at same height %" PRIu64 " as orphan\n", blockHeight);
|
||||||
return NODE_BLOCK_ORPHAN_QUEUED;
|
return NODE_BLOCK_ORPHAN_QUEUED;
|
||||||
@@ -531,19 +552,8 @@ static node_block_accept_result_t Node_ParseAndAcceptBlock(const unsigned char*
|
|||||||
return NODE_BLOCK_REJECTED;
|
return NODE_BLOCK_REJECTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t coinbaseAmount = 0;
|
// currentSupply/currentReward are advanced inside Chain_AddBlock, so that every path that
|
||||||
if (blk->transactions) {
|
// appends (mining, this one, orphan attach, reorg) keeps them consistent.
|
||||||
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);
|
|
||||||
|
|
||||||
// Persist on accept if requested
|
// Persist on accept if requested
|
||||||
if (persist) {
|
if (persist) {
|
||||||
@@ -1492,13 +1502,13 @@ void Node_BroadcastChainRange(net_node_t* node, size_t startHeightInclusive, tcp
|
|||||||
unsigned char hash[32];
|
unsigned char hash[32];
|
||||||
Block_CalculateHash(blk, hash);
|
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;
|
int seen = 0;
|
||||||
pthread_mutex_lock(&node->seenLock);
|
pthread_mutex_lock(&node->seenLock);
|
||||||
if (DynSet_Contains(node->seenBlocks, hash)) {
|
if (DynSet_Contains(node->seenBlocks, hash)) {
|
||||||
seen = 1;
|
seen = 1;
|
||||||
} else {
|
|
||||||
DynSet_Insert(node->seenBlocks, hash);
|
|
||||||
}
|
}
|
||||||
pthread_mutex_unlock(&node->seenLock);
|
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);
|
memcpy(payload + off, tx, sizeof(signed_transaction_t)); off += sizeof(signed_transaction_t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t delivered = 0;
|
||||||
|
|
||||||
// Snapshot outbound clients and send
|
// Snapshot outbound clients and send
|
||||||
pthread_mutex_lock(&node->outboundLock);
|
pthread_mutex_lock(&node->outboundLock);
|
||||||
for (size_t i = 0; i < MAX_CONS; ++i) {
|
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) continue;
|
||||||
if (conn == sourceConn) continue;
|
if (conn == sourceConn) continue;
|
||||||
if (sourceConn && TcpConnection_PeerAddrEqual(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);
|
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);
|
free(payload);
|
||||||
Block_Destroy(blk);
|
Block_Destroy(blk);
|
||||||
}
|
}
|
||||||
|
|||||||
+524
-151
@@ -1,5 +1,7 @@
|
|||||||
#include <nets/orphan_pool.h>
|
#include <nets/orphan_pool.h>
|
||||||
|
#include <constants.h>
|
||||||
#include <dynarr.h>
|
#include <dynarr.h>
|
||||||
|
#include <pthread.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@@ -7,17 +9,38 @@
|
|||||||
typedef struct {
|
typedef struct {
|
||||||
block_t* block;
|
block_t* block;
|
||||||
uint64_t height;
|
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;
|
} orphan_entry_t;
|
||||||
|
|
||||||
static DynArr* g_orphans = NULL;
|
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) {
|
void OrphanPool_Init(void) {
|
||||||
if (g_orphans) return;
|
pthread_mutex_lock(&g_orphanLock);
|
||||||
g_orphans = DYNARR_CREATE(orphan_entry_t, 16);
|
OrphanPool_InitLocked();
|
||||||
|
pthread_mutex_unlock(&g_orphanLock);
|
||||||
}
|
}
|
||||||
|
|
||||||
void OrphanPool_Destroy(void) {
|
void OrphanPool_Destroy(void) {
|
||||||
if (!g_orphans) return;
|
pthread_mutex_lock(&g_orphanLock);
|
||||||
|
if (g_orphans) {
|
||||||
size_t n = DynArr_size(g_orphans);
|
size_t n = DynArr_size(g_orphans);
|
||||||
for (size_t i = 0; i < n; ++i) {
|
for (size_t i = 0; i < n; ++i) {
|
||||||
orphan_entry_t* e = (orphan_entry_t*)DynArr_at(g_orphans, i);
|
orphan_entry_t* e = (orphan_entry_t*)DynArr_at(g_orphans, i);
|
||||||
@@ -28,181 +51,531 @@ void OrphanPool_Destroy(void) {
|
|||||||
DynArr_destroy(g_orphans);
|
DynArr_destroy(g_orphans);
|
||||||
g_orphans = NULL;
|
g_orphans = NULL;
|
||||||
}
|
}
|
||||||
|
pthread_mutex_unlock(&g_orphanLock);
|
||||||
void OrphanPool_Insert(block_t* block, uint64_t height) {
|
|
||||||
if (!block) return;
|
|
||||||
if (!g_orphans) OrphanPool_Init();
|
|
||||||
orphan_entry_t e;
|
|
||||||
e.block = block;
|
|
||||||
e.height = height;
|
|
||||||
(void)DynArr_push_back(g_orphans, &e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static size_t OrphanPool_TryAdoptBranch(blockchain_t* chain, uint64_t forkHeight) {
|
static ssize_t OrphanPool_FindByHashLocked(const uint8_t blockHash[32]) {
|
||||||
if (!g_orphans || !chain) return 0;
|
if (!g_orphans || !blockHash) {
|
||||||
|
return -1;
|
||||||
DynArr* seq = DYNARR_CREATE(block_t*, 8);
|
|
||||||
if (!seq) return 0;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) break;
|
|
||||||
cursor++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t seqCount = DynArr_size(seq);
|
|
||||||
if (seqCount == 0) {
|
|
||||||
DynArr_destroy(seq);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t rollbackHeight = (forkHeight == 0) ? 0 : (forkHeight - 1);
|
|
||||||
if (!Chain_RollbackToHeight(chain, rollbackHeight)) {
|
|
||||||
DynArr_destroy(seq);
|
|
||||||
return 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)) {
|
|
||||||
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);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Attempt repeatedly while progress is made (to handle chained orphans)
|
|
||||||
while (madeProgress) {
|
|
||||||
madeProgress = false;
|
|
||||||
size_t n = DynArr_size(g_orphans);
|
size_t n = DynArr_size(g_orphans);
|
||||||
for (size_t i = 0; i < n; ++i) {
|
for (size_t i = 0; i < n; ++i) {
|
||||||
orphan_entry_t* e = (orphan_entry_t*)DynArr_at(g_orphans, i);
|
orphan_entry_t* e = (orphan_entry_t*)DynArr_at(g_orphans, i);
|
||||||
if (!e || !e->block) continue;
|
if (e && memcmp(e->hash, blockHash, 32) == 0) {
|
||||||
|
return (ssize_t)i;
|
||||||
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) {
|
return -1;
|
||||||
if (e->height < Chain_Size(chain)) {
|
}
|
||||||
block_t* local = NULL;
|
|
||||||
if (Chain_GetBlockCopy(chain, (size_t)e->height, &local) && local) {
|
// Drop the entry with the lowest sequence number, so a flood of unusable orphans cannot grow
|
||||||
uint8_t localHash[32];
|
// 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];
|
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_CalculateHash(local, localHash);
|
||||||
Block_CalculateHash(e->block, orphanHash);
|
|
||||||
Block_Destroy(local);
|
Block_Destroy(local);
|
||||||
|
|
||||||
if (memcmp(localHash, orphanHash, 32) != 0) {
|
// Same block we already have: pure duplicate, drop it. A different block at a height we
|
||||||
size_t adopted = OrphanPool_TryAdoptBranch(chain, e->height);
|
// have already passed is kept, because it may yet be the base of a heavier branch.
|
||||||
if (adopted > 0) {
|
if (memcmp(localHash, orphanHash, 32) == 0) {
|
||||||
attached += adopted;
|
for (size_t b = 0; b < 32; ++b) {
|
||||||
madeProgress = true;
|
(void)DynArr_push_back(doomed, &orphanHash[b]);
|
||||||
n = DynArr_size(g_orphans);
|
}
|
||||||
i = (size_t)-1;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
Block_CalculateHash(tip, tipHash);
|
||||||
} else if (local) {
|
Block_Destroy(tip);
|
||||||
Block_Destroy(local);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that the parent's hash matches the orphan's prevHash before attaching.
|
// Take a copy of the candidate under the lock, then release it before touching the chain.
|
||||||
bool parentMatches = false;
|
pthread_mutex_lock(&g_orphanLock);
|
||||||
if (e->height == 0) {
|
block_t* pooled = NULL;
|
||||||
parentMatches = (Chain_Size(chain) == 0);
|
uint64_t observed = 0;
|
||||||
} else {
|
uint8_t candidateHash[32];
|
||||||
block_t* parent = NULL;
|
bool found = OrphanPool_FindChildLocked((uint64_t)chainSize, tipHash, &pooled, &observed, candidateHash);
|
||||||
if (Chain_GetBlockCopy(chain, (size_t)parentIndex, &parent) && parent) {
|
block_t* candidate = found ? Block_Copy(pooled) : NULL;
|
||||||
uint8_t parentHash[32];
|
pthread_mutex_unlock(&g_orphanLock);
|
||||||
Block_CalculateHash(parent, parentHash);
|
|
||||||
parentMatches = (memcmp(parentHash, e->block->header.prevHash, 32) == 0);
|
|
||||||
Block_Destroy(parent);
|
|
||||||
} else {
|
|
||||||
parentMatches = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!parentMatches) {
|
if (!found) {
|
||||||
// Parent exists but does not match this orphan's prevHash.
|
break;
|
||||||
size_t adopted = OrphanPool_TryAdoptBranch(chain, e->height);
|
}
|
||||||
if (adopted > 0) {
|
if (!candidate) {
|
||||||
attached += adopted;
|
|
||||||
madeProgress = true;
|
|
||||||
n = DynArr_size(g_orphans);
|
|
||||||
i = (size_t)-1;
|
|
||||||
break;
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to add to chain
|
// The chain took over the copy's transaction array; free only our wrapper.
|
||||||
if (Chain_AddBlock(chain, e->block)) {
|
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++;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 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) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user