Files
skalacoin/src/nets/net_node.c
T
dcrubro 0e721ca389 Fix reorg penalty scaling direction and rebuild DAG sizing around a miner-signalled band
Two consensus-parameter bugs, both latent today and both guaranteed to become
live splits later. Neither is a regression; both are cheap to fix now and
expensive to fix after launch.

== 1. The reorg penalty scaled with block time the wrong way ==

FetchScheduler_ComputeReorgPenaltyBlocks had TARGET_BLOCK_TIME in the numerator
and REORG_PENALTY_REF_BLOCK_TIME in the denominator. The penalty is counted in
BLOCKS, so the wall-clock protection it actually buys was

    penalty(d) * TARGET_BLOCK_TIME  ~  d^2 * T^2 / REF

i.e. quadratic in block time, when the stated intent in constants.h is
wall-clock equivalence to the reference scheme defined at REF_BLOCK_TIME = 150.
At T = 90 the chain got 54*d^2 seconds of protection instead of 150*d^2 --
2.78x weaker than intended -- and any future block-time reduction would have
weakened it further, silently.

Swapping the two makes T cancel out of the wall-clock figure:

    penalty(d) = ceil(d^2 * REF / T)   ->   protection ~ d^2 * REF seconds

Penalty in blocks, depth -> before/after: 4 -> 10/27, 8 -> 39/107,
10 -> 60/167, 50 -> 1500/4167, 100 -> 6000/16667.

Verified by rebuilding at TARGET_BLOCK_TIME = 60: wall-clock protection is now
identical at both block times (2400s, 9600s, 15000s, ...), ratio 1.0000. Under
the old formula 60s blocks would have been 56% weaker.

Still integer-only: the ceiling division and saturation guards are unchanged,
and numeratorScale only moves 90 -> 150 while `raised` stays bounded by
REORG_PENALTY_MAX_DEPTH^2 = 1e6, nowhere near overflow.

This is fork choice, not block validity, so mixed-version nodes converge once
the longer penalty expires and no block ever becomes invalid.

== 2. DAG sizing was a fail-open consensus split waiting on block 350000 ==

Fixed, in rough order of severity:

  * FAIL-OPEN PoW. Block_CalculateAutolykos2Hash memset the hash to zero on any
    failure, and zero compares below every target -- so a DAG that was missing,
    mis-sized or had failed to build made *every* block pass PoW validation
    instead of rejecting it. PoW checks now fail closed.

  * TWO DIVERGENT IMPLEMENTATIONS of the same rule: CalculateTargetDAGSize in
    constants.h (mining) and ComputeEpochDagBytesForHeightFromChain in main.c
    (verification). They already disagreed at exactly height == EPOCH_LENGTH,
    where the constants.h copy underflowed size_t computing
    `Chain_Size - 1 - EPOCH_LENGTH` and returned 0.

  * A MEANINGLESS DIFFICULTY COUPLING. difficultyTarget is compact-encoded
    [1B exponent][3B coefficient]; subtracting two compact values mixes
    exponent and mantissa. The first real retarget yields a delta of 900649,
    which times DAG_BASE_GROWTH is ~967 TB, so the result was always clamped
    and the "proportional" term degenerated to a binary base+maxUp/base-maxDown
    switch. The size also never accumulated -- it was always recomputed from
    the constant base -- so the documented 1 GB/epoch growth could not happen.

  * A NON-EPOCH-ALIGNED SEED. GetNextDAGSeed returned hash(tip) while the
    verifier correctly used hash(block[epochIndex * EPOCH_LENGTH - 1]). The
    `chainSize % EPOCH_LENGTH == 0` rebuild guard masked this while running,
    but startup called it at an arbitrary height -- so a node restarted
    mid-epoch built a DAG from a different seed than one that had run straight
    through the boundary, and its blocks were rejected.

  * UNSIGNED PROMOTION. `DAG_BASE_GROWTH * difficultyDelta` promoted the signed
    delta to unsigned long long and wrapped before being assigned back to
    int64_t; the main.c copy cast first, so the two also differed in overflow
    behaviour. Both are gone.

  * A LEAK on the `targetSize <= 0` path in main.c (both block copies).

  * A USE-AFTER-FREE at every epoch boundary: Block_RebuildAutolykos2Dag ran
    DagClear -> DagAllocate -> DagGenerate from whichever thread advanced the
    tip, freeing ctx->dag.buf while miners read it.

  * GetAutolykos2Ctx called DagAllocate without DagGenerate, leaving
    dag.len == 0 so every heavy hash failed -- which, combined with the
    fail-open above, meant "everything is valid". It also memset 1 GiB that
    DagGenerate immediately overwrote.

  * TOCTOU / lock reentrancy: CalculateTargetDAGSize called Chain_Size 4x and
    Chain_GetBlockCopy 2x, each taking chainLock for reading, making it unsafe
    to call from a write-locked section.

--- New sizing rule: default-grow inside a hard band ---

DAG size now follows a recurrence gated by a miner signal in the block header,
clamped to [DAG_MIN_SIZE, DAG_MAX_SIZE]:

    brake  = (hold + down) * DAG_BRAKE_DEN > EPOCH_LENGTH * DAG_BRAKE_NUM
    downQ  =          down * DAG_DOWN_DEN  > EPOCH_LENGTH * DAG_DOWN_NUM

    downQ(k) && downQ(k-1) -> size -= DAG_EPOCH_STEP   (floored at DAG_MIN_SIZE)
    brake                  -> size unchanged
    otherwise              -> size += DAG_EPOCH_STEP   (capped at DAG_MAX_SIZE)

Growth is the default and there is deliberately NO up-vote: every signal a
miner can express only slows the walk or reverses it. Under stratum-style
pooled mining the pool builds the header and therefore controls its share of
the vote, so the mechanism has to be safe under pool capture -- and it is,
because the lever a pool would want (grow the DAG to price out smaller miners)
does not exist. This is NOT because upward capture would be self-defeating: it
would in fact be profitable, since the fixed block reward redistributes to
whoever survives and difficulty retargets down. The protection is the absence
of the lever. The whole upward trajectory is therefore governance
(DAG_EPOCH_STEP, DAG_MAX_SIZE), not signalling.

Braking keeps the DAG small, which helps old hardware and only costs ASIC
resistance -- bounded by DAG_MIN_SIZE, which is the constant that actually
secures the property. Shrinking needs a supermajority sustained across two
consecutive epochs; that gates the onset only, so miners genuinely being
squeezed get relief every epoch rather than every other one.

Thresholds are cross-multiplied rather than divided, so there is no rounding
for nodes to disagree on, and the denominator is the constant epoch length
rather than blocks-observed, so a partial epoch cannot read as a stronger
signal than it is. No floating point anywhere on this path.

--- Header vote field ---

reserved[0] carries the vote: 0 = grow (default), 1 = hold, 2 = down.
reserved[1..2] must be zero. All three already sat inside the packed, hashed
header, so the vote is committed to by both the canonical hash and the PoW hash
and cannot be altered after mining -- no wire-format or hash-layout change.

0 must mean grow, because the point of this shape is that inaction produces
growth; it also means a miner that knows nothing about the vote contributes to
the intended default rather than silently freezing the schedule.

Rejecting unrecognised vote values and non-zero spare bytes is a new validity
rule. It closes 24 bits of undefined-meaning malleable header space.

--- Validation moves to the light path ---

Autolykos2_DagGenerate fills lane i with exactly what ReadDagLaneFromSeed
recomputes for lane i -- Blake2b(seed || (i/2)_LE64), half i&1 -- so the DAG is
a pure cache and the two hashing paths are bit-for-bit equivalent. Validation
therefore uses the light path: no allocation, correct for any epoch rather than
only whichever one the global DAG happens to hold, and the DAG band becomes a
miner requirement rather than a full-node memory requirement.

Chain_OnTipAdvanced no longer rebuilds the DAG at all. MineBlock builds it on
demand for the height it is working on, so a node that does not mine never
allocates one, generation stays off the tip-advance path, and the buffer is
only ever touched by the miner (the ctx mutex remains as a backstop).

--- API changes ---

  + Chain_DagParamsForHeight(chain, height, &dagBytes, seed) -- single source
    of truth for both the size and the epoch seed, backed by a memoised
    per-epoch table on blockchain_t. The table is a pure cache of a function of
    the headers, extended lazily and dropped whenever anything at or below the
    tip changes; it lives on the chain rather than in a global because a
    second, header-only chain is built to re-verify historical PoW. Guarded by
    a per-chain mutex, always taken after chainLock.
  + Block_EnsureAutolykos2Dag / Block_PowHashHeavy / Block_PowHashLight.
    The heavy variant verifies the epoch and size itself, so it can never
    answer from a DAG built for another epoch.
  + Block_HasValidProofOfWorkWithParams -- resolve-once form. MineBlock called
    Block_HasValidProofOfWork inside its nonce loop, so resolving from the
    chain per attempt would have taken chainLock millions of times per block.
  + Block_HasValidVote.
  ~ Block_HasValidProofOfWork / Block_IsFullyValid now take the chain, because
    PoW validity genuinely is chain-relative. blockchain_t gained a struct tag
    so block.h can forward-declare it.
  - CalculateTargetDAGSize, GetNextDAGSeed, ComputeEpochDagBytesForHeightFromChain,
    ComputeEpochSeedForHeightFromChain, Block_CalculateAutolykos2Hash,
    Block_RebuildAutolykos2Dag, Autolykos2_LightHash (dead).
  - DAG_BASE_GROWTH and the five DAG_MAX_*_SWING_* / DAG_SWING_PERCENT_DEN
    macros, all now unreachable.

Also: Block_CalculateAutolykos2Hash truncated the height to uint32 while the
light path takes uint64, so the two would have diverged above block 2^32; the
full width is now passed. Added a `dagvote <grow|hold|down>` REPL command and a
progress line during DAG generation, which is tens of seconds at production
sizes.

static_asserts now enforce DAG_MIN_SIZE <= DAG_BASE_SIZE <= DAG_MAX_SIZE and
32-byte alignment, so a misconfigured band fails the build instead of being
silently clamped.

NOTE: DAG_MIN_SIZE (2 GiB), DAG_BASE_SIZE (2 GiB), DAG_MAX_SIZE (8 GiB) and
DAG_EPOCH_STEP (1 GiB) are economic judgements, not derivations, and since the
vote cannot accelerate growth they are the entire upward story. Sanity-check
them before launch.

== Verification ==

  * Penalty: exact table match at d = 4/8/10/50/100/1000, zero within grace,
    saturation at MAX_DEPTH, and identical wall-clock protection when rebuilt
    at TARGET_BLOCK_TIME = 60.
  * DAG recurrence (30 assertions against the real objects): default growth,
    legacy all-zero headers read as grow, strict inequality at exactly 1/2 and
    exactly 7/8, one qualifying epoch freezes but does not shrink, two
    consecutive shrink, sustained shrink repeats, both clamps saturate.
  * Epoch seed: constant across an epoch, equal to hash(block[k*EL - 1]),
    genesis seed in epoch 0, and resolvable at exactly height == EPOCH_LENGTH.
  * Fail-closed: unresolvable params and a zero-byte DAG both reject.
  * Heavy/light equivalence across 3 epochs; heavy refuses a DAG built for the
    wrong epoch or size.
  * Two nodes across 3 epoch boundaries: B reached height 30 purely by
    receiving, then mined blocks A accepted; both fullverify Chain OK, zero
    rejections.
  * Restart mid-epoch: B restarted at height 28 derived epoch 3's seed as
    hash(block[23]) -- the boundary block, not the tip -- and kept producing
    blocks A accepted. This fails before the change.
  * Divergent votes: A voting hold and B voting grow computed identical size
    and seed, confirming the tally is chain-derived, not config-derived.
  * ThreadSanitizer across epoch rollovers under load: no new races.
2026-07-30 19:32:10 +02:00

1682 lines
71 KiB
C

#include <nets/net_node.h>
#include <nets/nodediscovery.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <runtime_state.h>
#include <balance_sheet.h>
#include <inttypes.h>
#include <nets/orphan_pool.h>
#include <inttypes.h>
#include <pthread.h>
#include <unistd.h>
#include <txmempool.h>
#include <netinet/in.h>
#include <arpa/inet.h>
static net_node_t* Node_FromConnection(tcp_connection_t* conn) {
if (!conn) {
return NULL;
}
return (net_node_t*)conn->owner;
}
static uint64_t Node_GetCurrentBlockHeight(void) {
if (currentChain) {
return (uint64_t)Chain_Size(currentChain);
}
return currentBlockHeight;
}
// Compares two listen endpoints (family + IP + port).
static int NetNode_EndpointEqual(const struct sockaddr_storage* a, const struct sockaddr_storage* b) {
if (a->ss_family != b->ss_family) return 0;
if (a->ss_family == AF_INET) {
const struct sockaddr_in* x = (const struct sockaddr_in*)a;
const struct sockaddr_in* y = (const struct sockaddr_in*)b;
return x->sin_port == y->sin_port &&
memcmp(&x->sin_addr, &y->sin_addr, sizeof(struct in_addr)) == 0;
}
if (a->ss_family == AF_INET6) {
const struct sockaddr_in6* x = (const struct sockaddr_in6*)a;
const struct sockaddr_in6* y = (const struct sockaddr_in6*)b;
return x->sin6_port == y->sin6_port &&
memcmp(&x->sin6_addr, &y->sin6_addr, sizeof(struct in6_addr)) == 0;
}
return 0;
}
// Builds a listen endpoint from an IP string + port, normalising IPv4-mapped IPv6 to plain IPv4
// so it compares equal to Node_ConnListenEndpoint output. Returns non-zero on success.
static int NetNode_MakeEndpoint(const char* ip, unsigned short port, struct sockaddr_storage* out) {
if (!ip || !out) return 0;
memset(out, 0, sizeof(*out));
struct in_addr a4;
if (inet_pton(AF_INET, ip, &a4) == 1) {
struct sockaddr_in* o = (struct sockaddr_in*)out;
o->sin_family = AF_INET;
o->sin_addr = a4;
o->sin_port = htons(port);
return 1;
}
struct in6_addr a6;
if (inet_pton(AF_INET6, ip, &a6) == 1) {
if (IN6_IS_ADDR_V4MAPPED(&a6)) {
struct sockaddr_in* o = (struct sockaddr_in*)out;
o->sin_family = AF_INET;
memcpy(&o->sin_addr, ((const uint8_t*)&a6) + 12, sizeof(struct in_addr));
o->sin_port = htons(port);
} else {
struct sockaddr_in6* o = (struct sockaddr_in6*)out;
o->sin6_family = AF_INET6;
o->sin6_addr = a6;
o->sin6_port = htons(port);
}
return 1;
}
return 0;
}
// Returns non-zero if we already hold an outbound connection to the given listen endpoint.
static int Node_HasOutboundTo(net_node_t* node, const struct sockaddr_storage* endpoint) {
int found = 0;
pthread_mutex_lock(&node->outboundLock);
for (size_t i = 0; i < MAX_CONS; ++i) {
tcp_connection_t* c = node->outboundClients[i].connection;
if (!c) continue;
struct sockaddr_storage ep;
if (Node_ConnListenEndpoint(c, &ep) && NetNode_EndpointEqual(&ep, endpoint)) {
found = 1;
break;
}
}
pthread_mutex_unlock(&node->outboundLock);
return found;
}
// Returns non-zero if some inbound connection OTHER than `self` already has the given listen
// endpoint (used to reject a duplicate inbound once we learn the peer's advertised listen port).
// Connections that are already tearing down do not count - otherwise a peer reconnecting from the
// same endpoint gets its fresh inbound rejected by the corpse of the previous one.
static int Node_HasOtherInboundFrom(net_node_t* node, const tcp_connection_t* self, const struct sockaddr_storage* endpoint) {
if (!node->server) return 0;
int found = 0;
pthread_mutex_lock(&node->server->clientsMutex);
for (size_t i = 0; i < node->server->maxClients; ++i) {
tcp_connection_t* other = node->server->clientsArrPtr ? node->server->clientsArrPtr[i] : NULL;
if (!other || other == self || TcpConnection_IsDisconnectNotified(other)) continue;
struct sockaddr_storage ep;
if (Node_ConnListenEndpoint(other, &ep) && NetNode_EndpointEqual(&ep, endpoint)) {
found = 1;
break;
}
}
pthread_mutex_unlock(&node->server->clientsMutex);
return found;
}
// Returns non-zero if a live connection OTHER than `self` with the same role already belongs to the
// node identified by nodeId. This is the endpoint-independent duplicate check: a multi-homed peer
// reaches us from several addresses, so comparing endpoints alone lets the same node in twice.
static int Node_HasOtherConnectionToNode(net_node_t* node, const tcp_connection_t* self, uint64_t nodeId) {
if (nodeId == 0) return 0;
int found = 0;
pthread_mutex_lock(&node->outboundLock);
for (size_t i = 0; i < MAX_CONS && !found; ++i) {
tcp_connection_t* c = node->outboundClients[i].connection;
if (!c || c == self || TcpConnection_IsDisconnectNotified(c)) continue;
if (c->role == self->role && c->peerNodeId == nodeId) found = 1;
}
pthread_mutex_unlock(&node->outboundLock);
if (found) return 1;
if (node->server) {
pthread_mutex_lock(&node->server->clientsMutex);
for (size_t i = 0; i < node->server->maxClients && !found; ++i) {
tcp_connection_t* c = node->server->clientsArrPtr ? node->server->clientsArrPtr[i] : NULL;
if (!c || c == self || TcpConnection_IsDisconnectNotified(c)) continue;
if (c->role == self->role && c->peerNodeId == nodeId) found = 1;
}
pthread_mutex_unlock(&node->server->clientsMutex);
}
return found;
}
// Returns non-zero if a connection OTHER than `exclude` to the same peer is still live - matched
// either on the listen endpoint or, when known, on the peer's identity (which also covers its other
// addresses). A connection that is itself mid-disconnect (disconnectedNotified) does not count as
// live - this is what lets us decide a peer is fully gone even when both its inbound and outbound
// drop simultaneously.
static int Node_HasLiveConnectionTo(net_node_t* node, const struct sockaddr_storage* endpoint, uint64_t nodeId, const tcp_connection_t* exclude) {
int found = 0;
pthread_mutex_lock(&node->outboundLock);
for (size_t i = 0; i < MAX_CONS && !found; ++i) {
tcp_connection_t* c = node->outboundClients[i].connection;
if (!c || c == exclude || TcpConnection_IsDisconnectNotified(c)) continue;
if (nodeId != 0 && c->peerNodeId == nodeId) { found = 1; break; }
struct sockaddr_storage ep;
if (Node_ConnListenEndpoint(c, &ep) && NetNode_EndpointEqual(&ep, endpoint)) found = 1;
}
pthread_mutex_unlock(&node->outboundLock);
if (found) return 1;
if (node->server) {
pthread_mutex_lock(&node->server->clientsMutex);
for (size_t i = 0; i < node->server->maxClients && !found; ++i) {
tcp_connection_t* c = node->server->clientsArrPtr ? node->server->clientsArrPtr[i] : NULL;
if (!c || c == exclude || TcpConnection_IsDisconnectNotified(c)) continue;
if (nodeId != 0 && c->peerNodeId == nodeId) { found = 1; break; }
struct sockaddr_storage ep;
if (Node_ConnListenEndpoint(c, &ep) && NetNode_EndpointEqual(&ep, endpoint)) found = 1;
}
pthread_mutex_unlock(&node->server->clientsMutex);
}
return found;
}
// Called when a connection to a peer drops. Strikes the peer from the discovery table, but only
// once it is logically disconnected - i.e. no other live connection (inbound or outbound) to the
// same node remains. Must be called from the disconnect callback while `conn` is still valid and
// outside outboundLock/clientsMutex.
static void Node_HandlePeerDisconnect(net_node_t* node, tcp_connection_t* conn) {
if (!node || !node->discovery || !conn) return;
struct sockaddr_storage ep;
if (!Node_ConnListenEndpoint(conn, &ep)) return; // never advertised an endpoint -> not tracked
// Still reachable via another connection (possibly on one of its other addresses).
if (Node_HasLiveConnectionTo(node, &ep, conn->peerNodeId, conn)) return;
NodeDiscovery_RemovePeer(node->discovery, &ep);
}
int Node_ConnListenEndpoint(const tcp_connection_t* conn, struct sockaddr_storage* out) {
if (!conn || !out) return 0;
memset(out, 0, sizeof(*out));
// Determine the peer's listen port. For an outbound connection the port we dialed already
// is the peer's listen port; for an inbound one it is the port advertised in HELLO.
unsigned short listenP;
if (conn->role == TCP_CONNECTION_ROLE_OUTBOUND) {
listenP = (conn->addrFamily == AF_INET6)
? ntohs(((const struct sockaddr_in6*)&conn->peerAddr)->sin6_port)
: ntohs(((const struct sockaddr_in*)&conn->peerAddr)->sin_port);
} else {
listenP = conn->peerListenPort;
}
if (listenP == 0) return 0; // unknown listen port -> not a usable endpoint
if (conn->addrFamily == AF_INET) {
const struct sockaddr_in* a = (const struct sockaddr_in*)&conn->peerAddr;
struct sockaddr_in* o = (struct sockaddr_in*)out;
o->sin_family = AF_INET;
o->sin_addr = a->sin_addr;
o->sin_port = htons(listenP);
return 1;
}
if (conn->addrFamily == AF_INET6) {
const struct sockaddr_in6* a = (const struct sockaddr_in6*)&conn->peerAddr;
if (IN6_IS_ADDR_V4MAPPED(&a->sin6_addr)) {
// Normalise IPv4-mapped IPv6 to plain IPv4.
struct sockaddr_in* o = (struct sockaddr_in*)out;
o->sin_family = AF_INET;
memcpy(&o->sin_addr, ((const uint8_t*)&a->sin6_addr) + 12, sizeof(struct in_addr));
o->sin_port = htons(listenP);
} else {
struct sockaddr_in6* o = (struct sockaddr_in6*)out;
o->sin6_family = AF_INET6;
o->sin6_addr = a->sin6_addr;
o->sin6_port = htons(listenP);
}
return 1;
}
return 0;
}
uint64_t Node_ConnPeerNodeId(const tcp_connection_t* conn) {
return conn ? conn->peerNodeId : 0;
}
size_t Node_GetPeerEndpoints(net_node_t* node, struct sockaddr_storage* outEndpoints, uint64_t* outNodeIds, size_t maxOut) {
if (!node || !outEndpoints || maxOut == 0) return 0;
size_t count = 0;
// Outbound connections
pthread_mutex_lock(&node->outboundLock);
for (size_t i = 0; i < MAX_CONS && count < maxOut; ++i) {
tcp_connection_t* c = node->outboundClients[i].connection;
if (!c || TcpConnection_IsDisconnectNotified(c)) continue; // ignore connections that are tearing down
struct sockaddr_storage ep;
if (!Node_ConnListenEndpoint(c, &ep)) continue;
int dup = 0;
for (size_t k = 0; k < count; ++k) {
if (NetNode_EndpointEqual(&outEndpoints[k], &ep)) { dup = 1; break; }
}
if (dup) continue;
if (outNodeIds) outNodeIds[count] = c->peerNodeId;
outEndpoints[count++] = ep;
}
pthread_mutex_unlock(&node->outboundLock);
// Inbound connections
if (node->server) {
pthread_mutex_lock(&node->server->clientsMutex);
for (size_t i = 0; i < node->server->maxClients && count < maxOut; ++i) {
tcp_connection_t* c = node->server->clientsArrPtr ? node->server->clientsArrPtr[i] : NULL;
if (!c || TcpConnection_IsDisconnectNotified(c)) continue; // ignore connections that are tearing down
struct sockaddr_storage ep;
if (!Node_ConnListenEndpoint(c, &ep)) continue;
int dup = 0;
for (size_t k = 0; k < count; ++k) {
if (NetNode_EndpointEqual(&outEndpoints[k], &ep)) { dup = 1; break; }
}
if (dup) continue;
if (outNodeIds) outNodeIds[count] = c->peerNodeId;
outEndpoints[count++] = ep;
}
pthread_mutex_unlock(&node->server->clientsMutex);
}
return count;
}
// Outcome of the identity check run once a connection's HELLO/ACK_HELLO has been parsed.
typedef enum {
NODE_IDENTITY_OK = 0,
NODE_IDENTITY_SELF, // the peer is this very node, reached through one of its own addresses
NODE_IDENTITY_DUPLICATE // we already hold a connection of this role to that node
} node_identity_result_t;
// Records the identity a peer advertised and decides whether the connection should survive.
// `conn->peerNodeId` and `conn->peerListenPort` must already be set from the handshake.
static node_identity_result_t Node_CheckPeerIdentity(net_node_t* node, tcp_connection_t* conn) {
if (!node || !conn || conn->peerNodeId == 0) return NODE_IDENTITY_OK; // peer too old to advertise one
struct sockaddr_storage ep;
int haveEp = Node_ConnListenEndpoint(conn, &ep);
if (conn->peerNodeId == localNodeId) {
// We dialled ourselves (or accepted our own dial). Remember the endpoint as our own so
// discovery stops offering it back to us, and drop the connection.
if (haveEp && node->discovery) {
NodeDiscovery_MarkSelfEndpoint(node->discovery, &ep);
}
return NODE_IDENTITY_SELF;
}
// Record the identity behind this endpoint even when the connection is about to be dropped as a
// duplicate: that is what lets discovery skip the peer's other addresses while we are connected
// to it, instead of dialling each of them in turn.
if (haveEp && node->discovery) {
NodeDiscovery_NoteIdentity(node->discovery, &ep, conn->peerNodeId);
}
if (Node_HasOtherConnectionToNode(node, conn, conn->peerNodeId)) {
return NODE_IDENTITY_DUPLICATE;
}
return NODE_IDENTITY_OK;
}
// Thunks routing UDP ping/pong events into the discovery state.
static void Node_OnPongThunk(udp_node_t* udp, const struct sockaddr_storage* from,
uint64_t nonce, int protoVersion, uint64_t rttMs, void* user) {
(void)udp; (void)protoVersion;
net_node_t* node = (net_node_t*)user;
if (node && node->discovery) {
NodeDiscovery_OnPong(node->discovery, from, nonce, rttMs);
}
}
static void Node_OnPingTimeoutThunk(udp_node_t* udp, const struct sockaddr_storage* dest,
uint64_t nonce, void* user) {
(void)udp;
net_node_t* node = (net_node_t*)user;
if (node && node->discovery) {
NodeDiscovery_OnPingTimeout(node->discovery, dest, nonce);
}
}
typedef enum {
NODE_BLOCK_REJECTED = 0,
NODE_BLOCK_ORPHAN_QUEUED = 1,
NODE_BLOCK_ACCEPTED = 2,
NODE_BLOCK_DUPLICATE = 3 // already on our chain; not a fault, do not log it as a rejection
} node_block_accept_result_t;
// Reclaims outbound slots whose peer has disconnected. Mirrors the inbound self-reclaim in
// TcpServer_clientthreadprocess: detach dead connections from their slots under outboundLock, then
// join their io threads and destroy/free them outside the lock. Pinned connections (a raw pointer
// is still held elsewhere, e.g. by an in-progress sync) are skipped and retried on a later tick.
static void Node_ReapDeadOutbound(net_node_t* node) {
if (!node) return;
tcp_connection_t* dead[MAX_CONS];
size_t deadCount = 0;
pthread_mutex_lock(&node->outboundLock);
for (size_t i = 0; i < MAX_CONS; ++i) {
tcp_connection_t* c = node->outboundClients[i].connection;
if (!c) continue;
if (!TcpConnection_IsDisconnectNotified(c)) continue; // still live
if (atomic_load(&c->pinCount) != 0) continue; // someone holds a raw pointer; retry later
// Detach the dead connection from its slot and reset the slot to a clean free state.
node->outboundClients[i].connection = NULL;
node->outboundClients[i].peerBlockHeight = 0;
dead[deadCount++] = c;
}
pthread_mutex_unlock(&node->outboundLock);
// Join + destroy outside the lock: the io thread's on_disconnect callback itself takes
// outboundLock, so joining under it would deadlock.
for (size_t i = 0; i < deadCount; ++i) {
tcp_connection_t* c = dead[i];
if (!pthread_equal(c->ioThread, pthread_self())) {
pthread_join(c->ioThread, NULL);
}
TcpConnection_Destroy(c);
free(c);
}
}
static void* Node_MaintenanceThread(void* arg) {
net_node_t* n = (net_node_t*)arg;
if (!n) return NULL;
while (n->maintenanceRunning) {
if (currentChain) {
size_t attached = OrphanPool_AttemptAttach(currentChain);
if (attached > 0) {
printf("Maintenance: attached %zu orphan(s)\n", attached);
Chain_SaveToFile(currentChain, chainDataDir, currentSupply, currentReward);
BalanceSheet_SaveToFile(chainDataDir);
}
}
// Reclaim outbound slots whose peer has disconnected so they can be reused.
Node_ReapDeadOutbound(n);
// Peer discovery tick: ping/query connected peers and connect to the best-ping discoveries.
if (n->discovery) {
NodeDiscovery_Iterate(n->discovery);
}
sleep_for_milliseconds((uint64_t)n->maintenanceIntervalMs);
}
return NULL;
}
static int Node_DecodePacket(const tcp_connection_t* conn, packet_type_t* outType, const unsigned char** outPayload, size_t* outPayloadLen) {
if (!conn || !outType || !outPayload || !outPayloadLen || conn->dataBufLen < 1 || !conn->dataBuf) {
return -1;
}
uint8_t packetType = conn->dataBuf[0];
if (!PacketType_IsValid(packetType)) {
return -1;
}
*outType = (packet_type_t)packetType;
*outPayload = conn->dataBuf + 1;
*outPayloadLen = conn->dataBufLen - 1;
return 0;
}
static node_block_accept_result_t Node_ParseAndAcceptBlock(const unsigned char* payload, size_t payloadLen, bool persist) {
if (!payload) { return NODE_BLOCK_REJECTED; }
size_t offset = 0;
if (payloadLen < sizeof(uint64_t) + sizeof(block_header_t) + sizeof(uint64_t)) { return NODE_BLOCK_REJECTED; }
uint64_t blockHeight = 0;
memcpy(&blockHeight, payload + offset, sizeof(blockHeight));
offset += sizeof(blockHeight);
block_t* blk = (block_t*)calloc(1, sizeof(block_t));
if (!blk) { return NODE_BLOCK_REJECTED; }
memcpy(&blk->header, payload + offset, sizeof(blk->header));
blk->header.blockNumber = blockHeight;
offset += sizeof(blk->header);
uint64_t txCount = 0;
memcpy(&txCount, payload + offset, sizeof(txCount));
offset += sizeof(txCount);
blk->transactions = DYNARR_CREATE(signed_transaction_t, txCount == 0 ? 1 : (size_t)txCount);
if (!blk->transactions) { free(blk); return NODE_BLOCK_REJECTED; }
for (uint64_t i = 0; i < txCount; ++i) {
if (offset + sizeof(signed_transaction_t) > payloadLen) {
DynArr_destroy(blk->transactions);
free(blk);
return NODE_BLOCK_REJECTED;
}
signed_transaction_t tx;
memcpy(&tx, payload + offset, sizeof(tx));
offset += sizeof(tx);
if (!DynArr_push_back(blk->transactions, &tx)) {
DynArr_destroy(blk->transactions);
free(blk);
return NODE_BLOCK_REJECTED;
}
}
// The chain check has to come first now: PoW validity is chain-relative (the epoch DAG size and
// seed are derived from it), so there is nothing to validate against without a chain.
if (!currentChain) {
printf("Rejected BLOCK_DATA at height %" PRIu64 ": no active chain\n", blockHeight);
DynArr_destroy(blk->transactions);
free(blk);
return NODE_BLOCK_REJECTED;
}
// Validate block
if (!Block_IsFullyValid(blk, currentChain)) {
printf("Rejected BLOCK_DATA at height %" PRIu64 " during validation\n", blockHeight);
DynArr_destroy(blk->transactions);
free(blk);
return NODE_BLOCK_REJECTED;
}
// The orphan pool stamps the local tip height at first sight; that stamp drives the reorg
// penalty and must be taken now, not re-derived later from a moved tip.
uint64_t chainSize = Chain_Size(currentChain);
const uint64_t observedAtTipHeight = chainSize > 0 ? (chainSize - 1) : 0ULL;
// Temporary debug mode: force network-received blocks through the orphan pool to exercise reorg handling.
if (forceOrphanReorgEnabled && blk->header.blockNumber > 0) {
OrphanPool_Insert(blk, blockHeight, observedAtTipHeight);
printf("Forced orphan BLOCK_DATA at height %" PRIu64 "\n", blockHeight);
return NODE_BLOCK_ORPHAN_QUEUED;
}
// If parent is missing, insert into orphan pool instead of rejecting immediately.
if (blk->header.blockNumber > chainSize) {
// Parent(s) missing; queue as orphan
OrphanPool_Insert(blk, blockHeight, observedAtTipHeight);
printf("Queued orphan BLOCK_DATA at height %" PRIu64 "\n", blockHeight);
return NODE_BLOCK_ORPHAN_QUEUED;
} else if (blk->header.blockNumber < chainSize) {
// A block below our tip is either one we already have, or the lower half of a competing
// branch. Dropping both (as this used to) made any fork that diverges below the tip
// impossible to discover: the fork point itself was always thrown away.
block_t* local = NULL;
if (Chain_GetBlockCopy(currentChain, (size_t)blk->header.blockNumber, &local) && local) {
uint8_t localHash[32];
uint8_t incomingHash[32];
Block_CalculateHash(local, localHash);
Block_CalculateHash(blk, incomingHash);
Block_Destroy(local);
if (memcmp(localHash, incomingHash, 32) == 0) {
// Exactly the block we already have.
DynArr_destroy(blk->transactions);
free(blk);
return NODE_BLOCK_DUPLICATE;
}
}
OrphanPool_Insert(blk, blockHeight, observedAtTipHeight);
printf("Queued forked BLOCK_DATA at height %" PRIu64 " (below our tip) as orphan\n", blockHeight);
return NODE_BLOCK_ORPHAN_QUEUED;
} else {
// blk->header.blockNumber == chainSize -> candidate to append. Ensure prevHash matches current tip.
if (chainSize > 0) {
block_t* last = NULL;
if (!Chain_GetBlockCopy(currentChain, (size_t)(chainSize - 1), &last) || !last) {
// Can't verify parent; queue as orphan conservatively
OrphanPool_Insert(blk, blockHeight, observedAtTipHeight);
printf("Queued orphan BLOCK_DATA at height %" PRIu64 " (unable to verify parent)\n", blockHeight);
if (last) Block_Destroy(last);
return NODE_BLOCK_ORPHAN_QUEUED;
}
uint8_t lastHash[32];
Block_CalculateHash(last, lastHash);
if (memcmp(lastHash, blk->header.prevHash, 32) != 0) {
// Conflicting block at same height; queue as orphan until resolved by a subsequent extension.
OrphanPool_Insert(blk, blockHeight, observedAtTipHeight);
Block_Destroy(last);
printf("Queued conflicting BLOCK_DATA at same height %" PRIu64 " as orphan\n", blockHeight);
return NODE_BLOCK_ORPHAN_QUEUED;
}
Block_Destroy(last);
}
}
if (!Chain_AddBlock(currentChain, blk)) {
// Chain_AddBlock failed; cleanup
printf("Rejected BLOCK_DATA at height %" PRIu64 " during chain add\n", blockHeight);
if (blk->transactions) {
DynArr_destroy(blk->transactions);
}
free(blk);
return NODE_BLOCK_REJECTED;
}
// currentSupply/currentReward are advanced inside Chain_AddBlock, so that every path that
// appends (mining, this one, orphan attach, reorg) keeps them consistent.
// Persist on accept if requested
if (persist) {
Chain_SaveToFile(currentChain, chainDataDir, currentSupply, currentReward);
BalanceSheet_SaveToFile(chainDataDir);
}
// Chain_AddBlock copied the block into the chain; free our temporary wrapper but do NOT destroy transactions (they are freed by Chain_SaveToFile when persisted)
free(blk);
// Attempt to attach any orphans that may now have their parents present.
size_t attached = OrphanPool_AttemptAttach(currentChain);
if (attached > 0) {
printf("Attached %zu orphan(s) after accepting block\n", attached);
// Persist after attaching orphans
Chain_SaveToFile(currentChain, chainDataDir, currentSupply, currentReward);
BalanceSheet_SaveToFile(chainDataDir);
}
return NODE_BLOCK_ACCEPTED;
}
static void Node_ForwardConnect(net_node_t* node, tcp_connection_t* conn) {
if (node && node->on_connect) {
node->on_connect(conn, node->callbackUser);
}
}
static void Node_ForwardDisconnect(net_node_t* node, tcp_connection_t* conn) {
if (node && node->on_disconnect) {
node->on_disconnect(conn, node->callbackUser);
}
}
static void Node_ForwardData(net_node_t* node, tcp_connection_t* conn, const unsigned char* payload, size_t payloadLen) {
if (node && node->on_data) {
node->on_data(conn, payload, payloadLen, node->callbackUser);
}
}
net_node_t* Node_Create() {
net_node_t* node = (net_node_t*)malloc(sizeof(net_node_t));
if (!node) {
return NULL;
}
memset(node, 0, sizeof(*node));
node->server = TcpServer_Create();
if (!node->server) {
free(node);
return NULL;
}
for (size_t i = 0; i < MAX_CONS; ++i) {
if (TcpClient_Init(&node->outboundClients[i]) != 0) {
Node_Destroy(node);
return NULL;
}
}
// Initialize outbound lock and seen-block cache
pthread_mutex_init(&node->seenLock, NULL);
pthread_mutex_init(&node->outboundLock, NULL);
node->seenBlocks = DynSet_Create(32); // 32-byte canonical hashes
TxMempool_Init();
TcpServer_Init(node->server, listenPort, "::");
node->server->owner = node;
node->server->on_connect = Node_Server_OnConnect;
node->server->on_data = Node_Server_OnData;
node->server->on_disconnect = Node_Server_OnDisconnect;
TcpServer_Start(node->server, MAX_CONS);
OrphanPool_Init();
// Start the UDP ping/pong daemon (latency oracle) and peer discovery. Non-fatal on failure;
// the node still works without discovery, it just won't crawl for new peers.
node->udpNode = (udp_node_t*)malloc(sizeof(udp_node_t));
if (node->udpNode) {
if (UdpNode_Init(node->udpNode, (uint16_t)listenPort) == 0) {
UdpNode_SetCallbacks(node->udpNode, Node_OnPongThunk, Node_OnPingTimeoutThunk, node);
if (UdpNode_Start(node->udpNode) == 0) {
node->discovery = NodeDiscovery_Create(node, node->udpNode);
} else {
UdpNode_Destroy(node->udpNode);
free(node->udpNode);
node->udpNode = NULL;
}
} else {
free(node->udpNode);
node->udpNode = NULL;
}
}
// Start maintenance thread
node->maintenanceRunning = 1;
node->maintenanceIntervalMs = 1000; // 1s
if (pthread_create(&node->maintenanceThread, NULL, Node_MaintenanceThread, node) != 0) {
// Failed to start maintenance thread; continue without it
node->maintenanceRunning = 0;
}
return node;
}
void Node_Destroy(net_node_t* node) {
if (!node) {
return;
}
// Stop the maintenance thread first: it runs the outbound reaper (which touches outboundClients
// and outboundLock) and the discovery tick, so it must not run concurrently with the teardown
// below or against soon-to-be-destroyed state.
if (node->maintenanceRunning) {
node->maintenanceRunning = 0;
pthread_join(node->maintenanceThread, NULL);
}
// Detach every outbound connection from its slot under outboundLock, then tear the connections
// down outside it -- the same pattern Node_ReapDeadOutbound uses, and for the same two reasons.
//
// Calling TcpClient_Destroy directly here instead raced with still-running inbound client
// threads: those read outboundClients[i].connection under outboundLock (via
// Node_HasLiveConnectionTo), while TcpClient_Disconnect cleared the same field with no lock
// held. The lock cannot simply be held across the destroy, because that path joins the io
// thread whose on_disconnect callback takes outboundLock itself.
tcp_connection_t* outbound[MAX_CONS];
size_t outboundToClose = 0;
pthread_mutex_lock(&node->outboundLock);
for (size_t i = 0; i < MAX_CONS; ++i) {
tcp_connection_t* conn = node->outboundClients[i].connection;
if (!conn) continue;
node->outboundClients[i].connection = NULL;
node->outboundClients[i].peerBlockHeight = 0;
outbound[outboundToClose++] = conn;
}
node->outboundCount = 0;
pthread_mutex_unlock(&node->outboundLock);
for (size_t i = 0; i < outboundToClose; ++i) {
tcp_connection_t* conn = outbound[i];
TcpConnection_RequestClose(conn);
if (!pthread_equal(conn->ioThread, pthread_self())) {
pthread_join(conn->ioThread, NULL);
}
if (!TcpConnection_IsDisconnectNotified(conn) && conn->on_disconnect) {
TcpConnection_MarkDisconnectNotified(conn);
conn->on_disconnect(conn);
}
TcpConnection_Destroy(conn);
free(conn);
}
if (node->server) {
TcpServer_Stop(node->server);
TcpServer_Destroy(node->server);
}
// Tear down UDP + discovery. Stop UDP first so no pong/timeout callback races the destroy.
if (node->udpNode) {
UdpNode_Stop(node->udpNode);
}
if (node->discovery) {
NodeDiscovery_Destroy(node->discovery);
node->discovery = NULL;
}
if (node->udpNode) {
UdpNode_Destroy(node->udpNode);
free(node->udpNode);
node->udpNode = NULL;
}
OrphanPool_Destroy();
TxMempool_Destroy();
if (node->seenBlocks) {
DynSet_Destroy(node->seenBlocks);
node->seenBlocks = NULL;
}
pthread_mutex_destroy(&node->seenLock);
pthread_mutex_destroy(&node->outboundLock);
free(node);
}
void Node_SetCallbacks(
net_node_t* node,
void (*on_connect)(tcp_connection_t* conn, void* user),
void (*on_data)(tcp_connection_t* conn, const unsigned char* data, size_t len, void* user),
void (*on_disconnect)(tcp_connection_t* conn, void* user),
void* user
) {
if (!node) {
return;
}
node->on_connect = on_connect;
node->on_data = on_data;
node->on_disconnect = on_disconnect;
node->callbackUser = user;
}
int Node_ConnectPeer(net_node_t* node, const char* ip, unsigned short port) {
if (!node || !ip) {
return -1;
}
// Never dial ourselves. Without this an echo-back (or a gossiped copy of one of our own
// addresses) can chain into a self-connection per maintenance tick until the slots run out.
struct sockaddr_storage target;
int haveTarget = NetNode_MakeEndpoint(ip, port, &target);
if (haveTarget && node->discovery && NodeDiscovery_IsSelfEndpoint(node->discovery, &target)) {
return -1;
}
// Enforce a single outbound connection per endpoint: if we already have an outbound to this
// (ip, port), do not open a second one. (Inbound from the same endpoint is still allowed - that
// is the peer's own outbound to us.)
if (haveTarget && Node_HasOutboundTo(node, &target)) {
return 0; // already connected outbound to this endpoint
}
for (size_t i = 0; i < MAX_CONS; ++i) {
if (node->outboundClients[i].connection == NULL) {
if (TcpClient_Connect(
&node->outboundClients[i],
ip,
port,
Node_Client_OnConnect,
Node_Client_OnData,
Node_Client_OnDisconnect,
node
) == 0) {
node->outboundCount++;
return 0;
}
return -1;
}
}
return -1;
}
int Node_ConnectStartupPeers(net_node_t* node, const char** ips, const unsigned short* ports, size_t peersCount) {
if (!node || !ips || !ports) {
return -1;
}
int successes = 0;
for (size_t i = 0; i < peersCount; ++i) {
if (Node_ConnectPeer(node, ips[i], ports[i]) == 0) {
successes++;
}
}
return successes;
}
int Node_SendPacket(net_node_t* node, tcp_connection_t* conn, packet_type_t packetType, const void* payload, size_t payloadLen) {
if (!node || !conn || !PacketType_IsValid((uint8_t)packetType) || (!payload && payloadLen > 0)) {
return -1;
}
/*
if (conn->role == TCP_CONNECTION_ROLE_INBOUND && packetType != PACKET_TYPE_RESPONSE) {
return -1;
}
if (conn->role == TCP_CONNECTION_ROLE_OUTBOUND && packetType != PACKET_TYPE_REQUEST) {
return -1;
}
*/
size_t framePayloadLen = payloadLen + 1;
unsigned char* framed = (unsigned char*)malloc(framePayloadLen);
if (!framed) {
return -1;
}
framed[0] = (unsigned char)packetType;
if (payloadLen > 0) {
memcpy(framed + 1, payload, payloadLen);
}
int rc = TcpConnection_SendFramed(conn, framed, framePayloadLen);
free(framed);
return rc;
}
int Node_BroadcastTransaction(net_node_t* node, signed_transaction_t* tx, tcp_connection_t* excludeNode) {
if (!node || !tx) {
return -1;
}
// Serialize transaction into payload
size_t payloadLen = sizeof(signed_transaction_t);
unsigned char* payload = (unsigned char*)malloc(payloadLen);
if (!payload) {
return -1;
}
memcpy(payload, tx, sizeof(signed_transaction_t));
// Broadcast to all outbound peers
pthread_mutex_lock(&node->outboundLock);
for (size_t i = 0; i < MAX_CONS; ++i) {
tcp_connection_t* connection = node->outboundClients[i].connection;
if (connection && connection != excludeNode) {
(void)Node_SendPacket(node, connection, PACKET_TYPE_BROADCAST_TX, payload, payloadLen);
}
}
pthread_mutex_unlock(&node->outboundLock);
free(payload);
return 0;
}
void Node_Server_OnConnect(tcp_connection_t* client) {
net_node_t* node = Node_FromConnection(client);
Node_ForwardConnect(node, client);
printf("Inbound node connected: %u\n", client ? client->connectionId : 0U);
if (echoPeersEnabled && node && client) {
// Attempt to create an outbound connection back to the peer's IP on our configured port.
// We avoid connecting if we already have an outbound to the same IP.
char ipbuf[INET6_ADDRSTRLEN];
if (TcpConnection_GetPeerAddrStr(client, ipbuf, sizeof(ipbuf))) {
// Use the configured port as the target port for the peer's listening service.
unsigned short targetPort = listenPort;
int shouldConnect = 1;
pthread_mutex_lock(&node->outboundLock);
for (size_t i = 0; i < MAX_CONS; ++i) {
if (node->outboundClients[i].connection) {
if (TcpConnection_PeerAddrEqual(node->outboundClients[i].connection, client)) {
shouldConnect = 0;
break;
}
}
}
pthread_mutex_unlock(&node->outboundLock);
if (shouldConnect) {
// Try to connect; ignore failure silently
(void)Node_ConnectPeer(node, ipbuf, targetPort);
}
}
}
}
void Node_Server_OnData(tcp_connection_t* client) {
packet_type_t packetType;
const unsigned char* payload = NULL;
size_t payloadLen = 0;
if (!client || Node_DecodePacket(client, &packetType, &payload, &payloadLen) != 0) {
return;
}
switch (packetType) {
case PACKET_TYPE_HELLO: {
// Decode HELLO
if (payloadLen < sizeof(uint32_t) + sizeof(uint64_t)) {
return;
}
uint32_t protoVersion;
uint64_t blockHeight;
memcpy(&protoVersion, payload, sizeof(protoVersion));
memcpy(&blockHeight, payload + sizeof(protoVersion), sizeof(blockHeight));
// Optional trailing listen port. This inbound peer's source port is ephemeral,
// so we record the port it actually listens on to make it discoverable/reachable.
// Length-guarded so older peers that omit it still work.
if (client && payloadLen >= sizeof(uint32_t) + sizeof(uint64_t) + sizeof(uint16_t)) {
uint16_t peerListenPort;
memcpy(&peerListenPort, payload + sizeof(protoVersion) + sizeof(blockHeight), sizeof(peerListenPort));
client->peerListenPort = peerListenPort;
}
// Optional trailing node identity, same length-guarded deal.
if (client && payloadLen >= sizeof(uint32_t) + sizeof(uint64_t) + sizeof(uint16_t) + sizeof(uint64_t)) {
uint64_t peerNodeId;
memcpy(&peerNodeId, payload + sizeof(protoVersion) + sizeof(blockHeight) + sizeof(uint16_t), sizeof(peerNodeId));
client->peerNodeId = peerNodeId;
}
printf("Received HELLO from node %u: protoVersion=%u, blockHeight=%" PRIu64 ", listenPort=%u, nodeId=%016" PRIx64 "\n",
client ? client->connectionId : 0U, protoVersion, blockHeight,
client ? client->peerListenPort : 0U, client ? client->peerNodeId : 0ULL);
// Craft and send ACK_HELLO (echo protoVersion, our height, our own listen port and our
// identity). This goes out before any decision to drop the connection: the ACK is what
// tells the dialer whose address it just reached, so an endpoint that turns out to be
// another address of a peer it already talks to (or one of its own) is recognised as
// such instead of being redialled forever. shutdown() flushes what is already queued,
// so the peer still receives this even though we close immediately after.
uint8_t ackBuf[100];
uint8_t* ackData = ackBuf;
size_t ackOffset = 0;
memcpy(ackData + ackOffset, &protoVersion, sizeof(protoVersion));
ackOffset += sizeof(protoVersion);
uint64_t currentHeight = Node_GetCurrentBlockHeight();
memcpy(ackData + ackOffset, &currentHeight, sizeof(currentHeight));
ackOffset += sizeof(currentHeight);
uint16_t myListenPort = (uint16_t)listenPort;
memcpy(ackData + ackOffset, &myListenPort, sizeof(myListenPort));
ackOffset += sizeof(myListenPort);
uint64_t myNodeId = localNodeId;
memcpy(ackData + ackOffset, &myNodeId, sizeof(myNodeId));
ackOffset += sizeof(myNodeId);
Node_SendPacket(Node_FromConnection(client), client, PACKET_TYPE_ACK_HELLO, ackData, ackOffset);
// Enforce one connection per node, identified by the advertised nodeId rather than by
// the address it happens to reach us from.
if (client) {
net_node_t* idNode = Node_FromConnection(client);
node_identity_result_t identity = Node_CheckPeerIdentity(idNode, client);
if (identity == NODE_IDENTITY_SELF) {
printf("Rejecting inbound connection %u: it is this node talking to itself\n",
client->connectionId);
TcpConnection_RequestClose(client);
return;
}
if (identity == NODE_IDENTITY_DUPLICATE) {
printf("Rejecting duplicate inbound connection %u (already connected to node %016" PRIx64 ")\n",
client->connectionId, client->peerNodeId);
TcpConnection_RequestClose(client);
return;
}
}
// Endpoint-level fallback for peers that advertise no identity: drop this connection if
// another inbound from the same endpoint already exists (keep the established one). An
// outbound to the same endpoint is unaffected - that is this node's own connection to
// the peer.
if (client && client->peerNodeId == 0 && client->peerListenPort != 0) {
net_node_t* dupNode = Node_FromConnection(client);
struct sockaddr_storage myEp;
if (dupNode && Node_ConnListenEndpoint(client, &myEp) &&
Node_HasOtherInboundFrom(dupNode, client, &myEp)) {
printf("Rejecting duplicate inbound connection %u (already have an inbound from this endpoint)\n",
client->connectionId);
TcpConnection_RequestClose(client);
return;
}
}
break;
}
case PACKET_TYPE_ACK_HELLO: {
// This is illegal
printf("Received unexpected ACK_HELLO packet from node %u\n", client ? client->connectionId : 0U);
// Send the error and kill the connection
const char* msg = "You can't ACK_HELLO me! I'm a server!";
Node_SendPacket(Node_FromConnection(client), client, PACKET_TYPE_ERROR, msg, strlen(msg));
TcpConnection_RequestClose(client);
return;
}
case PACKET_TYPE_FETCH_BLOCK: {
// Decode FETCH_BLOCK - payload is the block height as uint64_t
if (payloadLen != sizeof(uint64_t)) {
return;
}
uint64_t requestedHeight;
memcpy(&requestedHeight, payload, sizeof(requestedHeight));
printf("Received FETCH_BLOCK for height %" PRIu64 " from node %u\n", requestedHeight, client ? client->connectionId : 0U);
if (requestedHeight > Node_GetCurrentBlockHeight()) {
printf("Requested block height %" PRIu64 " is higher than current height, ignoring\n", requestedHeight);
// Error the client, but don't kill
const char* msg = "Requested block height is higher than my current height!";
Node_SendPacket(Node_FromConnection(client), client, PACKET_TYPE_ERROR, msg, strlen(msg));
return;
}
// Find the block (deep-copy it for safe access)
block_t* block = NULL;
bool loadedFromDisk = false;
if (!Chain_GetBlockCopy(currentChain, (size_t)requestedHeight, &block) || !block) {
// Try loading from disk directly
if (!Chain_LoadBlockFromFile(chainDataDir, requestedHeight, true, &block, NULL) || !block) {
printf("Requested block height %" PRIu64 " not found, ignoring\n", requestedHeight);
const char* msg = "Requested block not found!";
Node_SendPacket(Node_FromConnection(client), client, PACKET_TYPE_ERROR, msg, strlen(msg));
return;
}
loadedFromDisk = true;
} else if (!block->transactions) {
// In-memory chain may be compacted to headers only after persistence.
block_t* fullBlock = NULL;
if (Chain_LoadBlockFromFile(chainDataDir, requestedHeight, true, &fullBlock, NULL) && fullBlock) {
Block_Destroy(block);
block = fullBlock;
loadedFromDisk = true;
}
}
if (!block || !block->transactions) {
printf("Requested block height %" PRIu64 " has no transaction data available\n", requestedHeight);
const char* msg = "Requested block missing transactions!";
Node_SendPacket(Node_FromConnection(client), client, PACKET_TYPE_ERROR, msg, strlen(msg));
if (block) {
Block_Destroy(block);
}
return;
}
if (loadedFromDisk) {
printf("Serving block %" PRIu64 " from disk with %zu transaction(s)\n",
requestedHeight,
DynArr_size(block->transactions));
}
// Serialize into a BLOCK_DATA packet [block header][tx count - 8 bytes][transactions...]
size_t txCount = block->transactions ? DynArr_size(block->transactions) : 0;
size_t blockDataSize = sizeof(uint64_t) + sizeof(block_header_t) + sizeof(uint64_t) + (txCount * sizeof(signed_transaction_t));
unsigned char* blockData = (unsigned char*)malloc(blockDataSize);
if (!blockData) {
// Generic error response
printf("Failed to allocate memory for block data response to node %u\n", client ? client->connectionId : 0U);
const char* msg = "Generic error for block data!";
Node_SendPacket(Node_FromConnection(client), client, PACKET_TYPE_ERROR, msg, strlen(msg));
Block_Destroy(block);
return;
}
size_t offset = 0;
// Write height first
uint64_t heightLE = requestedHeight;
memcpy(blockData + offset, &heightLE, sizeof(heightLE));
offset += sizeof(heightLE);
memcpy(blockData + offset, &block->header, sizeof(block_header_t));
offset += sizeof(block_header_t);
uint64_t txCount64 = (uint64_t)txCount;
memcpy(blockData + offset, &txCount64, sizeof(txCount64));
offset += sizeof(txCount64);
if (block->transactions && txCount > 0) {
for (size_t ti = 0; ti < txCount; ++ti) {
signed_transaction_t* tx = (signed_transaction_t*)DynArr_at(block->transactions, ti);
memcpy(blockData + offset, tx, sizeof(signed_transaction_t));
offset += sizeof(signed_transaction_t);
}
}
// Send the block data
Node_SendPacket(Node_FromConnection(client), client, PACKET_TYPE_BLOCK_DATA, blockData, offset);
free(blockData);
Block_Destroy(block);
break;
}
case PACKET_TYPE_BLOCK_DATA: {
// Server can't receive these!
printf("Received unexpected packet type %u from node %u\n", (unsigned int)packetType, client ? client->connectionId : 0U);
// Send the error and kill the connection
const char* msg = "You can't send me BLOCK_DATA! I'm a server!";
Node_SendPacket(Node_FromConnection(client), client, PACKET_TYPE_ERROR, msg, strlen(msg));
TcpConnection_RequestClose(client);
return;
}
case PACKET_TYPE_BROADCAST_BLOCK: {
// Accept broadcast blocks from peers and try to append
if (payloadLen >= sizeof(uint64_t)) {
uint64_t blockHeight = 0;
memcpy(&blockHeight, payload, sizeof(blockHeight));
node_block_accept_result_t result = Node_ParseAndAcceptBlock(payload, payloadLen, true);
if (result == NODE_BLOCK_ACCEPTED) {
printf("Accepted BROADCAST_BLOCK from node %u\n", client ? client->connectionId : 0U);
net_node_t* node = Node_FromConnection(client);
if (node) {
Node_BroadcastChainRange(node, (size_t)blockHeight, client);
}
} else if (result == NODE_BLOCK_ORPHAN_QUEUED) {
printf("Queued orphan BROADCAST_BLOCK from node %u\n", client ? client->connectionId : 0U);
} else if (result == NODE_BLOCK_DUPLICATE) {
// Already on our chain (a peer relayed it to us twice); not an error.
} else {
printf("Rejected BROADCAST_BLOCK from node %u\n", client ? client->connectionId : 0U);
}
}
break;
}
case PACKET_TYPE_ACK_BLOCK:
case PACKET_TYPE_BROADCAST_TX: {
// Decode the block or transaction data inside
if (payloadLen == sizeof(signed_transaction_t)) {
signed_transaction_t tx;
memcpy(&tx, payload, sizeof(tx));
uint8_t txHash[32];
char txHashHex[65];
Transaction_CalculateHash(&tx, txHash);
to_hex(txHash, txHashHex);
printf("Received packet type %u from node %u with transaction sending %llu pebble(s)\n",
(unsigned int)packetType, client ? client->connectionId : 0U, (unsigned long long)tx.transaction.amount1);
if (!Transaction_Verify(&tx)) {
printf("Received invalid transaction from node %u\n", client ? client->connectionId : 0U);
return;
}
// Push to mempool if it's not already present
if (!TxMempool_Lookup(txHash, &tx)) {
if (TxMempool_Insert(tx) >= 0) {
printf("Added transaction %s from node %u to mempool\n", txHashHex, client ? client->connectionId : 0U);
// Broadcast to other peers
net_node_t* node = Node_FromConnection(client);
if (node) {
Node_BroadcastTransaction(node, &tx, client);
}
} else {
printf("Failed to add transaction %s from node %u to mempool\n", txHashHex, client ? client->connectionId : 0U);
}
} else {
printf("Transaction %s from node %u already seen!\n", txHashHex, client ? client->connectionId : 0U);
}
} else {
printf("Received packet type %u from node %u with invalid payload length %zu\n",
(unsigned int)packetType, client ? client->connectionId : 0U, payloadLen);
// TODO: Ignoring for now, might error node later if we want to be strict about malformed messages
}
break;
}
case PACKET_TYPE_ACK_TX:
case PACKET_TYPE_ERROR: {
// Decode the message inside as text
char* text = (char*)malloc(payloadLen + 1);
if (!text) {
return;
}
memcpy(text, payload, payloadLen);
text[payloadLen] = '\0';
printf("Received packet type %u from node %u with message: %s\n",
(unsigned int)packetType, client ? client->connectionId : 0U, text);
free(text);
break;
}
case PACKET_TYPE_GET_PEERS: {
net_node_t* dnode = Node_FromConnection(client);
if (dnode && dnode->discovery) {
NodeDiscovery_OnGetPeers(dnode->discovery, client);
}
break;
}
case PACKET_TYPE_PEERS: {
net_node_t* dnode = Node_FromConnection(client);
if (dnode && dnode->discovery) {
NodeDiscovery_OnPeersReceived(dnode->discovery, client, payload, payloadLen);
}
break;
}
default:
return;
}
net_node_t* node = Node_FromConnection(client);
Node_ForwardData(node, client, payload, payloadLen);
}
void Node_Server_OnDisconnect(tcp_connection_t* client) {
net_node_t* node = Node_FromConnection(client);
Node_ForwardDisconnect(node, client);
printf("Inbound node disconnected: %u\n", client ? client->connectionId : 0U);
Node_HandlePeerDisconnect(node, client);
}
void Node_Client_OnConnect(tcp_connection_t* client) {
net_node_t* node = Node_FromConnection(client);
Node_ForwardConnect(node, client);
printf("Outbound node connected: %u\n", client ? client->connectionId : 0U);
// Construct and send HELLO
if (node) {
uint8_t buf[100];
uint8_t* data = buf;
size_t offset = 0;
uint32_t protoVersion = PROTO_VERSION; // little-endian
uint64_t blockHeight = Node_GetCurrentBlockHeight();
memcpy((unsigned char*)data + offset, &protoVersion, sizeof(protoVersion)); // This is technically "unsafe", but I honestly just don't give a shit at this point
offset += sizeof(protoVersion);
memcpy((unsigned char*)data + offset, &blockHeight, sizeof(blockHeight));
offset += sizeof(blockHeight);
// Advertise the port we listen on so the peer can share us with others (and reach us back)
uint16_t myListenPort = (uint16_t)listenPort;
memcpy((unsigned char*)data + offset, &myListenPort, sizeof(myListenPort));
offset += sizeof(myListenPort);
// ...and who we are, so the peer can tell this connection apart from our other addresses
uint64_t myNodeId = localNodeId;
memcpy((unsigned char*)data + offset, &myNodeId, sizeof(myNodeId));
offset += sizeof(myNodeId);
Node_SendPacket(node, client, PACKET_TYPE_HELLO, data, offset);
}
}
void Node_Client_OnData(tcp_connection_t* client) {
packet_type_t packetType;
const unsigned char* payload = NULL;
size_t payloadLen = 0;
if (!client || Node_DecodePacket(client, &packetType, &payload, &payloadLen) != 0) {
return;
}
switch (packetType) {
case PACKET_TYPE_HELLO: {
// This is illegal
printf("Received unexpected HELLO packet from node %u\n", client ? client->connectionId : 0U);
// Send the error and kill the connection
const char* msg = "You can't HELLO me! I'm a client!";
Node_SendPacket(Node_FromConnection(client), client, PACKET_TYPE_ERROR, msg, strlen(msg));
TcpConnection_RequestClose(client);
return;
}
case PACKET_TYPE_ACK_HELLO: {
// Decode ACK_HELLO
if (payloadLen < sizeof(uint32_t) + sizeof(uint64_t)) {
return;
}
uint32_t protoVersion;
uint64_t blockHeight;
memcpy(&protoVersion, payload, sizeof(protoVersion));
memcpy(&blockHeight, payload + sizeof(protoVersion), sizeof(blockHeight));
// Optional trailing listen port (for outbound peers the dialed port is already the
// listen port, but record the advertised one for consistency). Length-guarded.
if (client && payloadLen >= sizeof(uint32_t) + sizeof(uint64_t) + sizeof(uint16_t)) {
uint16_t peerListenPort;
memcpy(&peerListenPort, payload + sizeof(protoVersion) + sizeof(blockHeight), sizeof(peerListenPort));
client->peerListenPort = peerListenPort;
}
// Optional trailing node identity, same length-guarded deal.
if (client && payloadLen >= sizeof(uint32_t) + sizeof(uint64_t) + sizeof(uint16_t) + sizeof(uint64_t)) {
uint64_t peerNodeId;
memcpy(&peerNodeId, payload + sizeof(protoVersion) + sizeof(blockHeight) + sizeof(uint16_t), sizeof(peerNodeId));
client->peerNodeId = peerNodeId;
}
printf("Received ACK_HELLO from node %u with protoVersion %u, blockHeight %" PRIu64 " and nodeId %016" PRIx64 "\n",
client ? client->connectionId : 0U, protoVersion, blockHeight, client ? client->peerNodeId : 0ULL);
// Store peer-advertised height on matching outbound client
net_node_t* node = Node_FromConnection(client);
// The dialed endpoint may well be one of our own addresses, or another address of a
// peer we already talk to - neither is worth a connection.
if (client) {
node_identity_result_t identity = Node_CheckPeerIdentity(node, client);
if (identity == NODE_IDENTITY_SELF) {
printf("Closing outbound connection %u: it loops back to this node\n", client->connectionId);
TcpConnection_RequestClose(client);
return;
}
if (identity == NODE_IDENTITY_DUPLICATE) {
printf("Closing outbound connection %u: already connected to node %016" PRIx64 " on another address\n",
client->connectionId, client->peerNodeId);
TcpConnection_RequestClose(client);
return;
}
}
if (node) {
pthread_mutex_lock(&node->outboundLock);
for (size_t i = 0; i < MAX_CONS; ++i) {
if (node->outboundClients[i].connection == client) {
node->outboundClients[i].peerBlockHeight = blockHeight;
break;
}
}
pthread_mutex_unlock(&node->outboundLock);
}
break;
}
case PACKET_TYPE_FETCH_BLOCK: {
// A client can't serve a block!
printf("Received unexpected FETCH_BLOCK packet from node %u\n", client ? client->connectionId : 0U);
// Send the error and kill the connection (this might be too aggressive)
const char* msg = "You can't FETCH_BLOCK from me! I'm a client!";
Node_SendPacket(Node_FromConnection(client), client, PACKET_TYPE_ERROR, msg, strlen(msg));
TcpConnection_RequestClose(client);
return;
}
case PACKET_TYPE_BLOCK_DATA: {
if (payloadLen >= sizeof(uint64_t)) {
uint64_t blockHeight = 0;
memcpy(&blockHeight, payload, sizeof(blockHeight));
node_block_accept_result_t result = Node_ParseAndAcceptBlock(payload, payloadLen, true);
if (result == NODE_BLOCK_ACCEPTED) {
printf("Accepted BLOCK_DATA from node %u\n", client ? client->connectionId : 0U);
net_node_t* node = Node_FromConnection(client);
if (node) {
// Update peer advertised height
pthread_mutex_lock(&node->outboundLock);
for (size_t i = 0; i < MAX_CONS; ++i) {
if (node->outboundClients[i].connection == client) {
if (node->outboundClients[i].peerBlockHeight < blockHeight) {
node->outboundClients[i].peerBlockHeight = blockHeight;
}
break;
}
}
pthread_mutex_unlock(&node->outboundLock);
Node_BroadcastChainRange(node, (size_t)blockHeight, client);
}
} else if (result == NODE_BLOCK_ORPHAN_QUEUED) {
printf("Queued orphan BLOCK_DATA from node %u\n", client ? client->connectionId : 0U);
} else if (result == NODE_BLOCK_DUPLICATE) {
// Already on our chain (a peer relayed it to us twice); not an error.
} else {
printf("Rejected BLOCK_DATA from node %u\n", client ? client->connectionId : 0U);
}
}
break;
}
case PACKET_TYPE_BROADCAST_BLOCK: {
if (payloadLen >= sizeof(uint64_t)) {
uint64_t blockHeight = 0;
memcpy(&blockHeight, payload, sizeof(blockHeight));
node_block_accept_result_t result = Node_ParseAndAcceptBlock(payload, payloadLen, true);
if (result == NODE_BLOCK_ACCEPTED) {
printf("Accepted BROADCAST_BLOCK from node %u\n", client ? client->connectionId : 0U);
net_node_t* node = Node_FromConnection(client);
if (node) {
// Update peer advertised height
pthread_mutex_lock(&node->outboundLock);
for (size_t i = 0; i < MAX_CONS; ++i) {
if (node->outboundClients[i].connection == client) {
if (node->outboundClients[i].peerBlockHeight < blockHeight) {
node->outboundClients[i].peerBlockHeight = blockHeight;
}
break;
}
}
pthread_mutex_unlock(&node->outboundLock);
Node_BroadcastChainRange(node, (size_t)blockHeight, client);
}
} else if (result == NODE_BLOCK_ORPHAN_QUEUED) {
printf("Queued orphan BROADCAST_BLOCK from node %u\n", client ? client->connectionId : 0U);
} else if (result == NODE_BLOCK_DUPLICATE) {
// Already on our chain (a peer relayed it to us twice); not an error.
} else {
printf("Rejected BROADCAST_BLOCK from node %u\n", client ? client->connectionId : 0U);
}
}
break;
}
case PACKET_TYPE_ACK_BLOCK:
case PACKET_TYPE_BROADCAST_TX: {
// Client can't receive these!
printf("Received unexpected packet type %u from node %u\n", (unsigned int)packetType, client ? client->connectionId : 0U);
break;
}
case PACKET_TYPE_ACK_TX:
case PACKET_TYPE_ERROR: {
// Decode the message inside as text
char* text = (char*)malloc(payloadLen + 1);
if (!text) {
return;
}
memcpy(text, payload, payloadLen);
text[payloadLen] = '\0';
printf("Received packet type %u from node %u with message: %s\n",
(unsigned int)packetType, client ? client->connectionId : 0U, text);
free(text);
break;
}
case PACKET_TYPE_GET_PEERS: {
net_node_t* dnode = Node_FromConnection(client);
if (dnode && dnode->discovery) {
NodeDiscovery_OnGetPeers(dnode->discovery, client);
}
break;
}
case PACKET_TYPE_PEERS: {
net_node_t* dnode = Node_FromConnection(client);
if (dnode && dnode->discovery) {
NodeDiscovery_OnPeersReceived(dnode->discovery, client, payload, payloadLen);
}
break;
}
default:
return;
}
net_node_t* node = Node_FromConnection(client);
Node_ForwardData(node, client, payload, payloadLen);
}
void Node_Client_OnDisconnect(tcp_connection_t* client) {
net_node_t* node = Node_FromConnection(client);
if (node) {
// Clear peer advertised height for this outbound slot
pthread_mutex_lock(&node->outboundLock);
for (size_t i = 0; i < MAX_CONS; ++i) {
if (node->outboundClients[i].connection == client) {
node->outboundClients[i].peerBlockHeight = 0;
break;
}
}
pthread_mutex_unlock(&node->outboundLock);
if (node->outboundCount > 0) {
node->outboundCount--;
}
}
Node_ForwardDisconnect(node, client);
printf("Outbound node disconnected: %u\n", client ? client->connectionId : 0U);
Node_HandlePeerDisconnect(node, client);
}
int Node_GetBestOutboundPeer(net_node_t* node, tcp_connection_t** outConn, uint64_t* outHeight) {
if (!node || !outConn || !outHeight) return -1;
tcp_connection_t* best = NULL;
uint64_t bestH = 0;
pthread_mutex_lock(&node->outboundLock);
for (size_t i = 0; i < MAX_CONS; ++i) {
tcp_connection_t* c = node->outboundClients[i].connection;
if (!c || TcpConnection_IsDisconnectNotified(c)) continue; // don't hand out a dead peer
if (best == NULL || node->outboundClients[i].peerBlockHeight > bestH) {
best = c;
bestH = node->outboundClients[i].peerBlockHeight;
}
}
// Pin the winner while still holding outboundLock so the reaper cannot free it out from under
// the caller (which uses the raw pointer after this lock is released). Caller must Unpin.
if (best) TcpConnection_Pin(best);
pthread_mutex_unlock(&node->outboundLock);
if (!best) return -1;
*outConn = best;
*outHeight = bestH;
return 0;
}
void Node_BroadcastChainRange(net_node_t* node, size_t startHeightInclusive, tcp_connection_t* sourceConn) {
if (!node || !currentChain) return;
size_t chainSize = Chain_Size(currentChain);
if (startHeightInclusive >= chainSize) return;
for (size_t h = startHeightInclusive; h < chainSize; ++h) {
block_t* blk = NULL;
if (!Chain_GetBlockCopy(currentChain, h, &blk) || !blk) {
if (!Chain_LoadBlockFromFile(chainDataDir, h, true, &blk, NULL) || !blk) {
continue;
}
} else if (!blk->transactions) {
block_t* full = NULL;
if (Chain_LoadBlockFromFile(chainDataDir, h, true, &full, NULL) && full) {
Block_Destroy(blk);
blk = full;
}
}
if (!blk || !blk->transactions) {
if (blk) Block_Destroy(blk);
continue;
}
unsigned char hash[32];
Block_CalculateHash(blk, hash);
// Dedupe using seenBlocks. The hash is only recorded once the block has actually gone out
// to at least one peer: marking it here unconditionally meant that a block relayed while no
// peer was connected (or while every peer was filtered out below) was never offered again.
int seen = 0;
pthread_mutex_lock(&node->seenLock);
if (DynSet_Contains(node->seenBlocks, hash)) {
seen = 1;
}
pthread_mutex_unlock(&node->seenLock);
if (seen) {
Block_Destroy(blk);
continue;
}
// Serialize payload: [uint64_t height][block_header_t][uint64_t txCount][transactions...]
size_t txCount = DynArr_size(blk->transactions);
size_t payloadLen = sizeof(uint64_t) + sizeof(block_header_t) + sizeof(uint64_t) + (txCount * sizeof(signed_transaction_t));
unsigned char* payload = (unsigned char*)malloc(payloadLen);
if (!payload) {
Block_Destroy(blk);
continue;
}
size_t off = 0;
uint64_t h64 = (uint64_t)h;
memcpy(payload + off, &h64, sizeof(h64)); off += sizeof(h64);
memcpy(payload + off, &blk->header, sizeof(block_header_t)); off += sizeof(block_header_t);
uint64_t txCount64 = (uint64_t)txCount;
memcpy(payload + off, &txCount64, sizeof(txCount64)); off += sizeof(txCount64);
for (size_t ti = 0; ti < txCount; ++ti) {
signed_transaction_t* tx = (signed_transaction_t*)DynArr_at(blk->transactions, ti);
memcpy(payload + off, tx, sizeof(signed_transaction_t)); off += sizeof(signed_transaction_t);
}
// Collect one connection per distinct peer, then send with no lock held.
//
// A peer we both dialled and were dialled by occupies two connections (one outbound, one
// inbound). Sending on both delivers every block twice, and the receiver logs the second
// copy as a rejection. Peers are identified by peerNodeId rather than by endpoint, because
// a multi-homed host reaches us from several addresses and an inbound connection carries an
// ephemeral port while the outbound one carries the listen port.
//
// Sends happen outside outboundLock/clientsMutex on purpose: Node_SendPacket writes to a
// socket and can block when the peer is slow to read, and holding the server's clientsMutex
// across that stalls the accept path and every other user of it.
tcp_connection_t* targets[MAX_CONS * 2];
uint64_t targetNodeIds[MAX_CONS * 2];
size_t targetCount = 0;
uint64_t sourceNodeId = sourceConn ? sourceConn->peerNodeId : 0ULL;
// Skip a connection if it is the source, belongs to the source's node, or duplicates a peer
// we have already queued.
#define NODE_RELAY_SHOULD_SKIP(conn) ( \
(conn) == sourceConn || \
((sourceNodeId != 0ULL) && ((conn)->peerNodeId == sourceNodeId)) || \
(sourceConn && (sourceNodeId == 0ULL) && TcpConnection_PeerAddrEqual((conn), sourceConn)))
pthread_mutex_lock(&node->outboundLock);
for (size_t i = 0; i < MAX_CONS && targetCount < (MAX_CONS * 2); ++i) {
tcp_connection_t* conn = node->outboundClients[i].connection;
if (!conn || TcpConnection_IsDisconnectNotified(conn)) continue;
if (NODE_RELAY_SHOULD_SKIP(conn)) continue;
bool duplicate = false;
for (size_t t = 0; t < targetCount; ++t) {
if (conn->peerNodeId != 0ULL && targetNodeIds[t] == conn->peerNodeId) { duplicate = true; break; }
}
if (duplicate) continue;
TcpConnection_Pin(conn);
targetNodeIds[targetCount] = conn->peerNodeId;
targets[targetCount++] = conn;
}
pthread_mutex_unlock(&node->outboundLock);
// Inbound peers too. 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 && targetCount < (MAX_CONS * 2); ++i) {
tcp_connection_t* conn = node->server->clientsArrPtr ? node->server->clientsArrPtr[i] : NULL;
if (!conn || TcpConnection_IsDisconnectNotified(conn)) continue;
if (NODE_RELAY_SHOULD_SKIP(conn)) continue;
bool duplicate = false;
for (size_t t = 0; t < targetCount; ++t) {
if (conn->peerNodeId != 0ULL && targetNodeIds[t] == conn->peerNodeId) { duplicate = true; break; }
}
if (duplicate) continue;
TcpConnection_Pin(conn);
targetNodeIds[targetCount] = conn->peerNodeId;
targets[targetCount++] = conn;
}
pthread_mutex_unlock(&node->server->clientsMutex);
}
#undef NODE_RELAY_SHOULD_SKIP
size_t delivered = 0;
for (size_t t = 0; t < targetCount; ++t) {
if (Node_SendPacket(node, targets[t], PACKET_TYPE_BROADCAST_BLOCK, payload, off) == 0) {
delivered++;
}
TcpConnection_Unpin(targets[t]);
}
if (delivered > 0) {
pthread_mutex_lock(&node->seenLock);
DynSet_Insert(node->seenBlocks, hash);
pthread_mutex_unlock(&node->seenLock);
}
free(payload);
Block_Destroy(blk);
}
}
void Node_GetClientList(net_node_t* node, tcp_connection_t** outClients, size_t* outCount) {
if (!node || !outClients || !outCount) return;
pthread_mutex_lock(&node->outboundLock);
size_t count = 0;
for (size_t i = 0; i < MAX_CONS; ++i) {
tcp_connection_t* c = node->outboundClients[i].connection;
if (c && !TcpConnection_IsDisconnectNotified(c)) { // skip connections that are tearing down
outClients[count++] = c;
}
}
pthread_mutex_unlock(&node->outboundLock);
*outCount = count;
}