diff --git a/include/autolykos2/autolykos2.h b/include/autolykos2/autolykos2.h index f73f3a7..9bb77c2 100644 --- a/include/autolykos2/autolykos2.h +++ b/include/autolykos2/autolykos2.h @@ -32,7 +32,9 @@ bool Autolykos2_Hash( uint8_t outHash[32] ); -bool Autolykos2_LightHash(const uint8_t* seed, blockchain_t* chain, uint64_t nonce, uint8_t* out); +// Derives the DAG lanes it needs straight from the epoch seed, so it needs no DAG allocation and +// stays correct for any height regardless of which epoch a DAG happens to be built for. Produces +// exactly the same hash as Autolykos2_Hash against a DAG generated from the same seed and size. bool Autolykos2_LightHashAtHeight( const uint8_t seed32[32], const uint8_t* message, diff --git a/include/block/block.h b/include/block/block.h index e37a029..a58117d 100644 --- a/include/block/block.h +++ b/include/block/block.h @@ -18,7 +18,10 @@ typedef struct { uint8_t merkleRoot[32]; uint32_t difficultyTarget; // Encoding: [1 byte exponent][3 byte coefficient]; Target = coefficient * 256^(exponent-3) uint8_t version; - uint8_t reserved[3]; // 3 bytes (Explicit padding for 8-byte alignment) + // reserved[0] carries the miner's DAG-size vote (DAG_VOTE_* in constants.h); reserved[1..2] must + // be zero. All three are inside the hashed header, so a vote is committed to by both the + // canonical hash and the PoW hash and cannot be altered after the block is mined. + uint8_t reserved[3]; } block_header_t; #pragma pack(pop) @@ -27,17 +30,49 @@ typedef struct { DynArr* transactions; // Array of signed_transaction_t, NOTE: Potentially move to a hashmap at some point for quick lookups. } block_t; +// PoW validity is chain-relative: it needs the epoch DAG size and seed. chain.h includes this +// header, so the tag declared there is forward-declared here to break the cycle. +typedef struct blockchain blockchain_t; + block_t* Block_Create(); void Block_CalculateHash(const block_t* block, uint8_t* outHash); void Block_CalculateMerkleRoot(const block_t* block, uint8_t* outHash); -void Block_CalculateAutolykos2Hash(const block_t* block, uint8_t* outHash); -bool Block_RebuildAutolykos2Dag(size_t dagBytes, const uint8_t seed32[32]); void Block_AddTransaction(block_t* block, signed_transaction_t* tx); void Block_RemoveTransaction(block_t* block, uint8_t* txHash); -bool Block_HasValidProofOfWork(const block_t* block); + +/** + * Autolykos2 PoW hashing. + * + * The heavy variant reads its lanes from the process-global DAG and is a MINING accelerator only; + * the light variant derives the same lanes from the epoch seed on demand. They are bit-for-bit + * equivalent by construction -- Autolykos2_DagGenerate fills lane i with exactly what + * ReadDagLaneFromSeed recomputes for lane i -- so a block mined through either verifies through + * either. Validation always uses the light path: it needs no allocation, which is what keeps the + * DAG a miner requirement rather than a full-node memory requirement, and it stays correct for + * blocks from earlier epochs (the heavy path can only ever answer for whichever epoch the global + * DAG was last built for). +**/ +bool Block_EnsureAutolykos2Dag(uint64_t epochIndex, size_t dagBytes, const uint8_t seed32[32]); +// Fails rather than answering from a DAG built for a different epoch or size, so it can never +// silently hash against the wrong lanes. +bool Block_PowHashHeavy(const block_t* block, uint64_t epochIndex, size_t dagBytes, uint8_t outHash[32]); +bool Block_PowHashLight(const block_t* block, size_t dagBytes, const uint8_t seed32[32], uint8_t outHash[32]); + +// PoW check against explicitly supplied epoch parameters, for callers that resolve them once and +// then iterate (the miner). Returns false if the hash cannot be computed -- never treat an +// uncomputable proof as valid. +bool Block_HasValidProofOfWorkWithParams(const block_t* block, uint64_t epochIndex, + size_t dagBytes, const uint8_t seed32[32]); + +// PoW check that resolves the epoch parameters for the block's own height from `chain`. +bool Block_HasValidProofOfWork(const block_t* block, blockchain_t* chain); + +// Header vote field is a recognised value and the unused reserved bytes are zero. +bool Block_HasValidVote(const block_t* block); + bool Block_AllTransactionsValid(const block_t* block); bool Block_ValidateCoinbaseAndFees(const block_t* block, uint64_t expectedCoinbaseAmount, uint64_t* outTotalFees); -bool Block_IsFullyValid(const block_t* block); +bool Block_IsFullyValid(const block_t* block, blockchain_t* chain); void Block_ShutdownPowContext(void); void Block_Destroy(block_t* block); void Block_Print(const block_t* block); diff --git a/include/block/chain.h b/include/block/chain.h index 6606182..31ef719 100644 --- a/include/block/chain.h +++ b/include/block/chain.h @@ -7,13 +7,41 @@ #include #include #include +#include #include #include #include +// One entry of the memoised DAG size recurrence, one per epoch. See Chain_DagParamsForHeight. typedef struct { + uint64_t sizeBytes; // DAG size used by every block whose height falls in this epoch + bool downQualified; // this epoch's own votes met the down supermajority +} dag_epoch_state_t; + +// Tagged so block.h can forward-declare it: PoW validity depends on the chain (it needs the epoch +// seed), but chain.h includes block.h, so the tag is what breaks the cycle. +typedef struct blockchain { DynArr* blocks; size_t size; + + /** + * Memoised DAG size recurrence: a pure cache of a function of the block headers, extended + * lazily and dropped whenever anything at or below the tip changes (every epoch's size depends + * on the votes of every epoch before it). It lives on the chain rather than in a global because + * a second, header-only blockchain_t is built to re-verify historical PoW, and the two must not + * share a cache. + * + * `dagEpochsComputed` counts valid `sizeBytes` entries. `downQualified` is only filled in for + * an epoch once the *following* entry has been computed, so it is valid on + * [0, dagEpochsComputed - 1). + * + * Guarded by `dagCacheLock`, which is always taken AFTER `chainLock` and is never held across a + * call back into chain.c. + **/ + dag_epoch_state_t* dagEpochs; + size_t dagEpochsComputed; + size_t dagEpochsCapacity; + pthread_mutex_t dagCacheLock; } blockchain_t; blockchain_t* Chain_Create(); @@ -80,6 +108,25 @@ uint32_t Chain_GetTargetForHeight(blockchain_t* chain, uint64_t height); // Call after any change to the tip. Must NOT be called while holding `chainLock`. void Chain_OnTipAdvanced(blockchain_t* chain); +// DAG +/** + * The Autolykos2 DAG size and epoch seed that the block at `blockHeight` must be hashed against. + * + * This is the single source of truth for both, so the mining path and the verification path cannot + * drift apart. Size follows the default-grow recurrence gated by the miner votes in + * `header.reserved[0]` (see the DAG band in constants.h); the seed is epoch-aligned -- epoch 0 uses + * the genesis seed, epoch k uses the hash of the last block of epoch k-1 -- so it is constant for + * the whole epoch rather than changing every block. + * + * Requires the chain to hold every block below the start of `blockHeight`'s epoch, which is always + * true when validating or mining a block at that height. Returns false if it cannot produce both + * values; callers MUST treat that as an invalid proof rather than falling back to a default. + * + * Takes `chainLock` for reading internally. Must NOT be called while holding it. +**/ +bool Chain_DagParamsForHeight(blockchain_t* chain, uint64_t blockHeight, + size_t* outDagBytes, uint8_t outSeed[32]); + // Work // Expected number of hashes to satisfy `difficultyTargetBits`, i.e. 2^256 / (target + 1). bool Chain_ComputeBlockWork(uint32_t difficultyTargetBits, uint256_t* outWork); diff --git a/include/constants.h b/include/constants.h index 2960bf1..d25b923 100644 --- a/include/constants.h +++ b/include/constants.h @@ -55,7 +55,11 @@ static const int MAX_FORK_PROBE_ROUNDS = 3; // 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) +// penalty(B) = ceil(FACTOR_NUM/FACTOR_DEN * B^EXPONENT * REF_BLOCK_TIME / TARGET_BLOCK_TIME) +// +// The block-time ratio is REF/TARGET, not TARGET/REF. penalty() counts BLOCKS, so the wall-clock +// protection is penalty(B) * TARGET_BLOCK_TIME ~= B^EXPONENT * REF_BLOCK_TIME: TARGET_BLOCK_TIME +// cancels and the protection is block-time-independent. See fetch_scheduler.c. // // Expressed as integer rationals on purpose: this feeds fork choice, so it must evaluate // identically on every node. Floating point is not acceptable here. @@ -65,7 +69,8 @@ static const uint64_t REORG_PENALTY_FACTOR_DEN = 1ULL; // base scaling factor (t 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. +// ~1.67M blocks (~4.75 years at a 90s block time), so this only exists to keep the arithmetic away +// from overflow rather than to bound the penalty in any meaningful sense. static const uint64_t REORG_PENALTY_MAX_DEPTH = 1000ULL; // Upper bound on pooled orphan blocks. Orphans are accepted before the chain-derived difficulty @@ -95,26 +100,58 @@ static const size_t MEDIAN_TIME_SPAN = 11U; // Keep this at 20 to match the canonical curve shape against a 2^64 atomic supply cap. #define MONERO_EMISSION_SPEED_FACTOR 20U -// Future Autolykos2 constants: +// Autolykos2 epoch / DAG constants. #define EPOCH_LENGTH 350000 // ~1 year at 90s -#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 (1ULL << 30) // TEMPORARY FOR TESTING -// Swings - calculated as MIN(percentage, absolute GB) to prevent absurd swings from low hashrate or very large DAG growth. -// Percentages are integer numerator/denominator pairs, never float literals: DAG size feeds PoW -// 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_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 // Epoch 0's seed is all zeroes; epoch k's seed is the hash of the last + // block of epoch k-1, so it is unpredictable until that block is mined. /** - * Each epoch has 2 phases, connected logarithmically: - * - Phase 1: Aggressive DAG growth (target is ~75% of the max cap) to kick out any ASICs, 30k blocks (roughly 1 month) - * - Phase 2: Stable DAG growth (target is the max cap) to provide a stable environment for GPU miners, 320k blocks (roughly 11 months) + * DAG size band and the miner signal that moves within it. + * + * Growth is the DEFAULT: the size walks up by DAG_EPOCH_STEP every epoch unless miners actively + * brake it. There is deliberately no "grow faster" vote -- every signal a miner can express only + * slows the walk or reverses it. That is what makes the scheme safe against pool capture: under + * stratum-style pooled mining the pool builds the header, so it controls its share of the vote, and + * a pool that wanted a larger DAG to price smaller miners out simply has no lever to pull. The + * entire upward trajectory is set by DAG_EPOCH_STEP and DAG_MAX_SIZE, i.e. by release, not by vote. + * + * DAG_MIN_SIZE is the ASIC-resistance floor: it must stay above the on-die SRAM an ASIC could + * economically carry, because *this constant*, not the vote, is what secures the property. No vote + * outcome can go below it. DAG_MAX_SIZE is the intended destination rather than an emergency bound, + * since the DAG reaches it on its own -- pick it as the largest DAG miners should ever hold. + * + * NOTE: these three sizes are economic judgements, not derivations. Sanity-check them before + * launch. DAG_BASE_SIZE was previously commented as an intended 6 GiB; it now has to sit inside + * the band (see the static_assert below). Lowering the DAG for a test run means lowering + * DAG_MIN_SIZE too, not just DAG_BASE_SIZE. **/ +#define DAG_MIN_SIZE (2ULL << 30) // 2 GiB -- ASIC-resistance floor +#define DAG_BASE_SIZE (2ULL << 30) // epoch 0 size +#define DAG_MAX_SIZE (8ULL << 30) // 8 GiB -- intended destination, ~6 unbraked years from base +#define DAG_EPOCH_STEP (1ULL << 30) // 1 GiB drift per epoch, in either direction + +// Vote thresholds as integer numerator/denominator pairs, never float literals: this feeds PoW +// verification, so every node must reach the same verdict. The tests cross-multiply rather than +// divide, so there is no rounding to disagree on. +#define DAG_BRAKE_NUM 1ULL +#define DAG_BRAKE_DEN 2ULL // brake growth when hold+down votes exceed 1/2 of the epoch +#define DAG_DOWN_NUM 7ULL +#define DAG_DOWN_DEN 8ULL // shrink when down votes exceed 7/8 of the epoch, two epochs running + +// reserved[0] of the block header carries the vote. 0 must mean GROW: the point of this shape is +// that inaction produces growth, so a miner that knows nothing about the vote contributes to the +// intended default instead of silently freezing the schedule. +#define DAG_VOTE_GROW 0u // default -- let the schedule run +#define DAG_VOTE_HOLD 1u // brake: stop growing +#define DAG_VOTE_DOWN 2u // reverse: shrink (needs a sustained supermajority to take effect) +#define DAG_VOTE_MAX DAG_VOTE_DOWN + +static_assert(DAG_MIN_SIZE <= DAG_BASE_SIZE && DAG_BASE_SIZE <= DAG_MAX_SIZE, + "DAG_BASE_SIZE must start inside [DAG_MIN_SIZE, DAG_MAX_SIZE]"); +static_assert(DAG_MIN_SIZE % 32ULL == 0ULL && DAG_MAX_SIZE % 32ULL == 0ULL && + DAG_BASE_SIZE % 32ULL == 0ULL && DAG_EPOCH_STEP % 32ULL == 0ULL, + "Autolykos2 lane addressing requires every DAG size to be a multiple of 32"); +static_assert(DAG_EPOCH_STEP > 0ULL, "DAG_EPOCH_STEP must be positive or the DAG can never move"); static const uint64_t M_CAP = 18446744073709551615ULL; // Max uint64 static const uint64_t TAIL_EMISSION = 750000000000ULL; // 0.75 coins per block floor @@ -226,76 +263,9 @@ static inline uint64_t CalculateBlockReward(uint256_t currentSupply, blockchain_ return CalculateBlockRewardAtHeight(currentSupply, (uint64_t)Chain_Size(chain)); } -// Hashing DAG -static inline size_t CalculateTargetDAGSize(blockchain_t* chain) { - // Base size plus (base growth * difficulty factor), adjusted by acceleration - if (!chain || !chain->blocks) { return 0; } // Invalid - uint64_t height = (uint64_t)Chain_Size(chain); - - if (height < EPOCH_LENGTH) { - return DAG_BASE_SIZE; - } - - // Get the height - EPOCH_LENGTH block and the last block; - block_t* lastBlock = NULL; - block_t* epochStartBlock = NULL; - if (!Chain_GetBlockCopy(chain, Chain_Size(chain) - 1, &lastBlock) || !lastBlock) { - if (lastBlock) Block_Destroy(lastBlock); - return 0; - } - if (!Chain_GetBlockCopy(chain, (size_t)(Chain_Size(chain) - 1 - EPOCH_LENGTH), &epochStartBlock) || !epochStartBlock) { - Block_Destroy(lastBlock); - if (epochStartBlock) Block_Destroy(epochStartBlock); - return 0; - } - - int64_t difficultyDelta = (int64_t)epochStartBlock->header.difficultyTarget - (int64_t)lastBlock->header.difficultyTarget; - int64_t growth = (DAG_BASE_GROWTH * difficultyDelta); // Can be negative if difficulty has decreased, which is why we use int64_t - - // Clamp - if (growth > 0) { - // Difficulty increased -> Clamp the UPWARD swing - 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 > (int64_t)DAG_MAX_UP_SWING_GB) growth = DAG_MAX_UP_SWING_GB; - } else { - // Difficulty decreased -> Clamp the DOWNWARD swing - 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 > (int64_t)DAG_MAX_DOWN_SWING_GB) growth = -(int64_t)DAG_MAX_DOWN_SWING_GB; - } - - int64_t targetSize = (int64_t)DAG_BASE_SIZE + growth; - if (targetSize <= 0) { - Block_Destroy(lastBlock); - Block_Destroy(epochStartBlock); - return 0; - } - - size_t out = (size_t)targetSize; - Block_Destroy(lastBlock); - Block_Destroy(epochStartBlock); - return out; -} - -static inline void GetNextDAGSeed(blockchain_t* chain, uint8_t outSeed[32]) { - if (!chain || !chain->blocks || !outSeed) { return; } // Invalid - uint64_t height = (uint64_t)Chain_Size(chain); - - if (height < EPOCH_LENGTH) { - memset(outSeed, DAG_GENESIS_SEED, 32); - return; - } - - block_t* prevBlock = NULL; - if (!Chain_GetBlockCopy(chain, Chain_Size(chain) - 1, &prevBlock) || !prevBlock) { - memset(outSeed, 0x00, 32); // Fallback to zeroes if we can't get the previous block for some reason; The caller should treat this as an error if height >= EPOCH_LENGTH - if (prevBlock) Block_Destroy(prevBlock); - return; - } - - Block_CalculateHash(prevBlock, outSeed); - Block_Destroy(prevBlock); -} +// Hashing DAG: see Chain_DagParamsForHeight in block/chain.h. Both the size and the epoch seed are +// derived from the chain by that one function, so the mining and verification paths cannot drift +// apart. The previous CalculateTargetDAGSize/GetNextDAGSeed pair lived here, took chainLock +// internally, was not epoch-aligned, and disagreed with the verifier's own copy in main.c. #endif diff --git a/src/autolykos2/autolykos2.c b/src/autolykos2/autolykos2.c index 179def3..c518b45 100644 --- a/src/autolykos2/autolykos2.c +++ b/src/autolykos2/autolykos2.c @@ -409,37 +409,6 @@ bool Autolykos2_Hash( ); } -bool Autolykos2_LightHash(const uint8_t* seed, blockchain_t* chain, uint64_t nonce, uint8_t* out) { - if (!seed || !chain || !out) { - return false; - } - - const uint64_t height = (uint64_t)Chain_Size(chain); - const size_t dagBytes = CalculateTargetDAGSize(chain); - if (dagBytes < 32 || (dagBytes % 32) != 0) { - return false; - } - - const size_t laneCount64 = dagBytes / 32u; - if (laneCount64 == 0 || laneCount64 > UINT32_MAX) { - return false; - } - - // Light path derives the needed DAG lanes from seed on-demand, no large DAG allocation required. - return Autolykos2_HashCore( - seed, - seed, - seed, - 32, - nonce, - height, - (uint32_t)laneCount64, - NULL, - false, - out - ); -} - bool Autolykos2_LightHashAtHeight( const uint8_t seed32[32], const uint8_t* message, diff --git a/src/block/block.c b/src/block/block.c index 8f13fff..be57a90 100644 --- a/src/block/block.c +++ b/src/block/block.c @@ -1,45 +1,118 @@ #include +#include #include #include #include +#include +/** + * The process-global mining DAG. + * + * Guarded by `g_powCtxLock` because generation frees and reallocates the buffer that hashing reads + * from: without the lock, an epoch rollover would pull the DAG out from under a miner mid-hash. + * Only the miner ever builds or reads this -- validation goes through the light path -- so the lock + * is essentially uncontended, and a node that does not mine never allocates a DAG at all. +**/ static Autolykos2Context* g_autolykos2Ctx = NULL; +static pthread_mutex_t g_powCtxLock = PTHREAD_MUTEX_INITIALIZER; +static uint64_t g_dagEpoch = 0; +static bool g_dagReady = false; -static Autolykos2Context* GetAutolykos2Ctx(void) { +// Caller must hold `g_powCtxLock`. +static Autolykos2Context* GetAutolykos2CtxLocked(void) { if (!g_autolykos2Ctx) { g_autolykos2Ctx = Autolykos2_Create(); if (!g_autolykos2Ctx) { fprintf(stderr, "Failed to create Autolykos2 context\n"); exit(1); } - Autolykos2_DagAllocate(g_autolykos2Ctx, DAG_BASE_SIZE); + // Deliberately no DagAllocate here. Allocating without generating leaves dag.len == 0, so + // every heavy hash fails -- which used to be indistinguishable from a valid proof, because + // the failure path handed back a zeroed hash that compares below every target. } return g_autolykos2Ctx; } void Block_ShutdownPowContext(void) { + pthread_mutex_lock(&g_powCtxLock); if (g_autolykos2Ctx) { Autolykos2_Destroy(g_autolykos2Ctx); g_autolykos2Ctx = NULL; } + g_dagReady = false; + pthread_mutex_unlock(&g_powCtxLock); } -bool Block_RebuildAutolykos2Dag(size_t dagBytes, const uint8_t seed32[32]) { - if (!seed32 || dagBytes == 0) { +bool Block_EnsureAutolykos2Dag(uint64_t epochIndex, size_t dagBytes, const uint8_t seed32[32]) { + if (!seed32 || dagBytes < 32u || (dagBytes % 32u) != 0u) { return false; } - Autolykos2Context* ctx = GetAutolykos2Ctx(); - if (!ctx) { - return false; + pthread_mutex_lock(&g_powCtxLock); + + // Already built for this epoch at this size: generation is seconds of work, so never redo it. + if (g_dagReady && g_autolykos2Ctx && g_dagEpoch == epochIndex && + Autolykos2_DagSize(g_autolykos2Ctx) == dagBytes) { + pthread_mutex_unlock(&g_powCtxLock); + return true; } + Autolykos2Context* ctx = GetAutolykos2CtxLocked(); + g_dagReady = false; // the buffer is about to be invalid; no heavy hash may run against it + + // Generation is one Blake2b per 64 bytes, single-threaded, so a multi-GiB DAG is tens of + // seconds. Say so rather than leaving the miner looking hung. + printf("Generating the epoch %llu mining DAG (%zu MiB), this takes a moment...\n", + (unsigned long long)epochIndex, dagBytes >> 20); + fflush(stdout); + Autolykos2_DagClear(ctx); - if (!Autolykos2_DagAllocate(ctx, dagBytes)) { + const bool ok = Autolykos2_DagAllocate(ctx, dagBytes) && Autolykos2_DagGenerate(ctx, seed32); + if (ok) { + g_dagEpoch = epochIndex; + g_dagReady = true; + } + + pthread_mutex_unlock(&g_powCtxLock); + return ok; +} + +bool Block_PowHashHeavy(const block_t* block, uint64_t epochIndex, size_t dagBytes, uint8_t outHash[32]) { + if (!block || !outHash) { return false; } - return Autolykos2_DagGenerate(ctx, seed32); + pthread_mutex_lock(&g_powCtxLock); + // Checking the epoch and size here, rather than trusting the caller to have built the right + // DAG, is what makes this impossible to misuse: an unbuilt or stale DAG yields false and the + // caller falls back to deriving the lanes from the seed. + const bool usable = g_dagReady && g_autolykos2Ctx && g_dagEpoch == epochIndex && + Autolykos2_DagSize(g_autolykos2Ctx) == dagBytes; + const bool ok = usable && + Autolykos2_Hash( + g_autolykos2Ctx, + (const uint8_t*)&block->header, + sizeof(block_header_t), + block->header.nonce, + block->header.blockNumber, // full 64-bit width; the light path takes uint64 + outHash); + pthread_mutex_unlock(&g_powCtxLock); + return ok; +} + +bool Block_PowHashLight(const block_t* block, size_t dagBytes, const uint8_t seed32[32], uint8_t outHash[32]) { + if (!block || !seed32 || !outHash) { + return false; + } + + return Autolykos2_LightHashAtHeight( + seed32, + (const uint8_t*)&block->header, + sizeof(block_header_t), + block->header.nonce, + block->header.blockNumber, + dagBytes, + outHash); } block_t* Block_Create() { @@ -133,30 +206,6 @@ void Block_CalculateMerkleRoot(const block_t* block, uint8_t* outHash) { free(next); } -void Block_CalculateAutolykos2Hash(const block_t* block, uint8_t* outHash) { - if (!block || !outHash) { - return; - } - - // PoW hash is computed from the block header, while canonical block hash remains SHA256. - Autolykos2Context* ctx = GetAutolykos2Ctx(); - if (!ctx) { - memset(outHash, 0, 32); - return; - } - - if (!Autolykos2_Hash( - ctx, - (const uint8_t*)&block->header, - sizeof(block_header_t), - block->header.nonce, - (uint32_t)block->header.blockNumber, - outHash - )) { - memset(outHash, 0, 32); - } -} - void Block_AddTransaction(block_t* block, signed_transaction_t* tx) { if (!block || !tx || !block->transactions) { return; @@ -189,7 +238,8 @@ static int Uint256_CompareBE(const uint8_t a[32], const uint8_t b[32]) { return 0; } -bool Block_HasValidProofOfWork(const block_t* block) { +bool Block_HasValidProofOfWorkWithParams(const block_t* block, uint64_t epochIndex, + size_t dagBytes, const uint8_t seed32[32]) { if (!block) { return false; } @@ -199,12 +249,49 @@ bool Block_HasValidProofOfWork(const block_t* block) { return false; } + // Prefer the prebuilt DAG when it is provably the one for this block's epoch and size -- the + // miner keeps it warm, and reading a lane beats recomputing it -- otherwise derive the lanes + // from the epoch seed. The two produce identical hashes, so which one runs is invisible to + // consensus; only speed differs. uint8_t hash[32]; - Block_CalculateAutolykos2Hash(block, hash); + if (!Block_PowHashHeavy(block, epochIndex, dagBytes, hash) && + !Block_PowHashLight(block, dagBytes, seed32, hash)) { + // Fail CLOSED. This used to hand back a zeroed hash on any failure and compare that to the + // target -- and zero is below every target, so a DAG that was missing, mis-sized or failed + // to build made the PoW check pass for every block instead of rejecting them. + return false; + } return Uint256_CompareBE(hash, target) <= 0; } +bool Block_HasValidProofOfWork(const block_t* block, blockchain_t* chain) { + if (!block || !chain) { + return false; + } + + size_t dagBytes = 0; + uint8_t seed[32]; + if (!Chain_DagParamsForHeight(chain, block->header.blockNumber, &dagBytes, seed)) { + return false; + } + + const uint64_t epochIndex = block->header.blockNumber / (uint64_t)EPOCH_LENGTH; + return Block_HasValidProofOfWorkWithParams(block, epochIndex, dagBytes, seed); +} + +bool Block_HasValidVote(const block_t* block) { + if (!block) { + return false; + } + + // Unrecognised vote values and non-zero spare bytes are rejected rather than ignored, so the + // header has no bits whose meaning is undefined and nothing to grind for extra nonce space. + return block->header.reserved[0] <= (uint8_t)DAG_VOTE_MAX && + block->header.reserved[1] == 0u && + block->header.reserved[2] == 0u; +} + bool Block_AllTransactionsValid(const block_t* block) { if (!block || !block->transactions) { return false; @@ -296,7 +383,7 @@ bool Block_ValidateCoinbaseAndFees(const block_t* block, uint64_t expectedCoinba return true; } -bool Block_IsFullyValid(const block_t* block) { +bool Block_IsFullyValid(const block_t* block, blockchain_t* chain) { bool merkleValid = false; uint8_t calculatedMerkleRoot[32]; if (block && block->transactions) { @@ -304,7 +391,8 @@ bool Block_IsFullyValid(const block_t* block) { merkleValid = (memcmp(calculatedMerkleRoot, block->header.merkleRoot, 32) == 0); } - return Block_HasValidProofOfWork(block) && Block_AllTransactionsValid(block) && DynArr_size(block->transactions) > 0 && merkleValid; + return Block_HasValidVote(block) && Block_HasValidProofOfWork(block, chain) && + Block_AllTransactionsValid(block) && DynArr_size(block->transactions) > 0 && merkleValid; } void Block_Destroy(block_t* block) { diff --git a/src/block/chain.c b/src/block/chain.c index 4c812f5..2edae25 100644 --- a/src/block/chain.c +++ b/src/block/chain.c @@ -181,6 +181,27 @@ bool Chain_RecomputeRuntimeState(blockchain_t* chain) { return true; } +/** + * Drop the memoised DAG size recurrence. + * + * Every epoch's size is folded from the votes of every epoch before it, so any change at or below + * the tip invalidates the whole table. It is only a cache -- rebuilding it costs one pass over the + * headers reading a single byte each -- so blowing it away wholesale is both correct and cheap, + * and far safer than trying to work out which suffix a reorg actually disturbed. + * + * Takes `dagCacheLock`. Safe to call while holding `chainLock` (that is the required lock order); + * must NOT be called while already holding `dagCacheLock`. +**/ +static void Chain_InvalidateDagEpochs(blockchain_t* chain) { + if (!chain) { + return; + } + + pthread_mutex_lock(&chain->dagCacheLock); + chain->dagEpochsComputed = 0; + pthread_mutex_unlock(&chain->dagCacheLock); +} + static void Chain_ClearBlocks(blockchain_t* chain) { if (!chain || !chain->blocks) { return; @@ -196,6 +217,7 @@ static void Chain_ClearBlocks(blockchain_t* chain) { DynArr_erase(chain->blocks); chain->size = 0; + Chain_InvalidateDagEpochs(chain); } blockchain_t* Chain_Create() { @@ -207,6 +229,15 @@ blockchain_t* Chain_Create() { ptr->blocks = DYNARR_CREATE(block_t, 1); ptr->size = 0; + ptr->dagEpochs = NULL; + ptr->dagEpochsComputed = 0; + ptr->dagEpochsCapacity = 0; + if (pthread_mutex_init(&ptr->dagCacheLock, NULL) != 0) { + DynArr_destroy(ptr->blocks); + free(ptr); + return NULL; + } + return ptr; } @@ -216,6 +247,8 @@ void Chain_Destroy(blockchain_t* chain) { Chain_ClearBlocks(chain); DynArr_destroy(chain->blocks); } + free(chain->dagEpochs); + pthread_mutex_destroy(&chain->dagCacheLock); free(chain); } } @@ -557,6 +590,9 @@ static bool Chain_RollbackToHeightLocked(blockchain_t* chain, size_t height) { chain->size = DynArr_size(chain->blocks); currentBlockHeight = chain->size ? (uint64_t)(chain->size - 1) : 0ULL; + // Blocks below the old tip are gone, so the DAG recurrence folded from their votes is stale. + Chain_InvalidateDagEpochs(chain); + // Rebuild balance sheet from scratch up to current chain size BalanceSheet_Destroy(); BalanceSheet_Init(); @@ -1678,6 +1714,195 @@ bool Chain_ComputeBranchWork(block_t** blocks, size_t count, uint256_t* outWork) return true; } +/** + * Grow the memoised epoch table to hold at least `needed` entries. + * Caller must hold `chain->dagCacheLock`. +**/ +static bool Chain_ReserveDagEpochsLocked(blockchain_t* chain, size_t needed) { + if (needed <= chain->dagEpochsCapacity) { + return true; + } + + size_t capacity = chain->dagEpochsCapacity ? chain->dagEpochsCapacity : 8u; + while (capacity < needed) { + if (capacity > SIZE_MAX / 2u) { + return false; + } + capacity *= 2u; + } + + dag_epoch_state_t* grown = + (dag_epoch_state_t*)realloc(chain->dagEpochs, capacity * sizeof(dag_epoch_state_t)); + if (!grown) { + return false; + } + + chain->dagEpochs = grown; + chain->dagEpochsCapacity = capacity; + return true; +} + +/** + * Fold the DAG size recurrence forward until entry `epochIndex` is valid. + * + * Entry k's size comes from the votes cast during epoch k-1, so extending to k requires blocks + * [0, k * EPOCH_LENGTH) to all be present. Growth is the default: the size only stays put or falls + * when miners actively say so, and there is no vote that makes it climb faster. + * + * Caller must hold `chainLock` (read) and `chain->dagCacheLock`. +**/ +static bool Chain_ExtendDagEpochsLocked(blockchain_t* chain, size_t epochIndex) { + if (!chain || !chain->blocks) { + return false; + } + + if (!Chain_ReserveDagEpochsLocked(chain, epochIndex + 1u)) { + return false; + } + + if (chain->dagEpochsComputed == 0) { + chain->dagEpochs[0].sizeBytes = DAG_BASE_SIZE; + chain->dagEpochs[0].downQualified = false; + chain->dagEpochsComputed = 1; + } + + const size_t chainSize = DynArr_size(chain->blocks); + const size_t epochLength = (size_t)EPOCH_LENGTH; + + for (size_t k = chain->dagEpochsComputed; k <= epochIndex; ++k) { + const size_t prev = k - 1u; // the epoch whose votes decide entry k + const size_t from = prev * epochLength; + const size_t to = from + epochLength; // exclusive + + if (to > chainSize) { + // The epoch that would decide this entry has not been fully mined yet. + return false; + } + + uint64_t holdVotes = 0; + uint64_t downVotes = 0; + for (size_t i = from; i < to; ++i) { + const block_t* blk = (const block_t*)DynArr_at(chain->blocks, i); + if (!blk) { + return false; + } + + // Anything other than HOLD or DOWN counts as GROW. The accept path rejects values + // above DAG_VOTE_MAX, so in practice the only other value that reaches here is GROW. + const uint8_t vote = blk->header.reserved[0]; + if (vote == DAG_VOTE_HOLD) { + holdVotes++; + } else if (vote == DAG_VOTE_DOWN) { + downVotes++; + } + } + + // Cross-multiplied rather than divided, so there is no rounding for nodes to disagree on. + // The denominator is the constant epoch length, not the number of blocks looked at, so a + // partial epoch can never be read as a stronger signal than it is. + const uint64_t epochBlocks = (uint64_t)EPOCH_LENGTH; + const bool brake = (holdVotes + downVotes) * DAG_BRAKE_DEN > epochBlocks * DAG_BRAKE_NUM; + const bool downQualified = downVotes * DAG_DOWN_DEN > epochBlocks * DAG_DOWN_NUM; + + chain->dagEpochs[prev].downQualified = downQualified; + const bool priorDownQualified = (prev >= 1u) ? chain->dagEpochs[prev - 1u].downQualified : false; + + const uint64_t prevSize = chain->dagEpochs[prev].sizeBytes; + uint64_t next; + + // Order matters: a qualifying down vote also satisfies the brake condition (down > 7/8 + // implies hold+down > 1/2), so testing the brake first would make shrinking impossible. + if (downQualified && priorDownQualified) { + // Shrinking needs the supermajority sustained across two consecutive epochs. That gates + // the *onset* only -- it is deliberately not reset afterwards, so miners who are + // genuinely being squeezed keep getting relief every epoch rather than every other one. + next = (prevSize > DAG_MIN_SIZE + DAG_EPOCH_STEP) ? (prevSize - DAG_EPOCH_STEP) + : (uint64_t)DAG_MIN_SIZE; + } else if (brake) { + next = prevSize; + } else { + next = (prevSize + DAG_EPOCH_STEP < DAG_MAX_SIZE) ? (prevSize + DAG_EPOCH_STEP) + : (uint64_t)DAG_MAX_SIZE; + } + + chain->dagEpochs[k].sizeBytes = next; + chain->dagEpochs[k].downQualified = false; // filled in when entry k+1 is folded + chain->dagEpochsComputed = k + 1u; + } + + return true; +} + +/** + * Epoch-aligned DAG seed for `blockHeight`: the genesis seed in epoch 0, otherwise the hash of the + * last block of the previous epoch. Constant for a whole epoch, which is what lets the DAG be + * generated once per epoch instead of once per block. + * + * Caller must hold `chainLock`. +**/ +static bool Chain_EpochDagSeedForHeightLocked(blockchain_t* chain, uint64_t blockHeight, uint8_t outSeed[32]) { + const uint64_t epochIndex = blockHeight / (uint64_t)EPOCH_LENGTH; + if (epochIndex == 0) { + memset(outSeed, DAG_GENESIS_SEED, 32); + return true; + } + + const uint64_t seedBlockNumber = (epochIndex * (uint64_t)EPOCH_LENGTH) - 1ULL; + if (seedBlockNumber >= (uint64_t)DynArr_size(chain->blocks)) { + return false; + } + + const block_t* seedBlock = (const block_t*)DynArr_at(chain->blocks, (size_t)seedBlockNumber); + if (!seedBlock) { + return false; + } + + Block_CalculateHash(seedBlock, outSeed); + return true; +} + +bool Chain_DagParamsForHeight(blockchain_t* chain, uint64_t blockHeight, + size_t* outDagBytes, uint8_t outSeed[32]) { + if (!chain || !chain->blocks || !outDagBytes || !outSeed) { + return false; + } + + const size_t epochIndex = (size_t)(blockHeight / (uint64_t)EPOCH_LENGTH); + + uint64_t bytes = 0; + bool ok = false; + + pthread_rwlock_rdlock(&chainLock); + + // Lock order is chainLock -> dagCacheLock, everywhere. Nothing under dagCacheLock calls back + // into chain.c, so this pair cannot deadlock. + pthread_mutex_lock(&chain->dagCacheLock); + if (Chain_ExtendDagEpochsLocked(chain, epochIndex)) { + bytes = chain->dagEpochs[epochIndex].sizeBytes; + ok = true; + } + pthread_mutex_unlock(&chain->dagCacheLock); + + if (ok) { + ok = Chain_EpochDagSeedForHeightLocked(chain, blockHeight, outSeed); + } + + pthread_rwlock_unlock(&chainLock); + + if (!ok) { + return false; + } + + // Autolykos2 addresses the DAG in 32-byte lanes. A size that is zero or not a multiple of 32 + // would make hashing fail, and a proof that cannot be computed must never read as valid. + if (bytes < 32ULL || (bytes % 32ULL) != 0ULL) { + return false; + } + + *outDagBytes = (size_t)bytes; + return true; +} + void Chain_OnTipAdvanced(blockchain_t* chain) { if (!chain || !chain->blocks) { return; @@ -1689,9 +1914,9 @@ void Chain_OnTipAdvanced(blockchain_t* chain) { // (mining, P2P accept, sync, orphan attach, reorg) stays on the same difficulty as its peers. difficultyTarget = Chain_GetTargetForHeight(chain, (uint64_t)chainSize); - if (chainSize % EPOCH_LENGTH == 0 && chainSize > 0) { - uint8_t dagSeed[32]; - GetNextDAGSeed(chain, dagSeed); - (void)Block_RebuildAutolykos2Dag(CalculateTargetDAGSize(chain), dagSeed); - } + // The epoch DAG is deliberately NOT rebuilt here. It is a mining accelerator only -- validation + // derives its lanes from the epoch seed, so a node that does not mine never allocates one -- + // and MineBlock builds it on demand for the height it is working on. Keeping generation off + // this path matters twice over: it is seconds of work per epoch, and it used to run from + // whichever thread happened to advance the tip, freeing the buffer under any miner mid-hash. } diff --git a/src/main.c b/src/main.c index 019626d..4bb801d 100644 --- a/src/main.c +++ b/src/main.c @@ -77,14 +77,33 @@ static void ApplyRuntimeConfigFromEnv(void) { uint32_t difficultyTarget = INITIAL_DIFFICULTY; -static bool MineBlock(block_t* block) { - if (!block) { +static bool MineBlock(blockchain_t* chain, block_t* block) { + if (!chain || !block) { return false; } + // Resolve the epoch parameters ONCE. Doing it per nonce would take chainLock millions of times + // per block and contend with every network thread. + size_t dagBytes = 0; + uint8_t seed[32]; + if (!Chain_DagParamsForHeight(chain, block->header.blockNumber, &dagBytes, seed)) { + fprintf(stderr, "failed to resolve epoch DAG parameters for height %llu\n", + (unsigned long long)block->header.blockNumber); + return false; + } + + // Build the DAG for this block's epoch up front. It is only a speedup -- the check falls back + // to deriving the same lanes from the seed -- so a machine that cannot allocate one still + // mines, just slower. + const uint64_t epochIndex = block->header.blockNumber / (uint64_t)EPOCH_LENGTH; + if (!Block_EnsureAutolykos2Dag(epochIndex, dagBytes, seed)) { + fprintf(stderr, "could not build the epoch %llu DAG (%zu bytes); mining via the slow path\n", + (unsigned long long)epochIndex, dagBytes); + } + for (uint64_t nonce = 0;; ++nonce) { block->header.nonce = nonce; - if (Block_HasValidProofOfWork(block)) { + if (Block_HasValidProofOfWorkWithParams(block, epochIndex, dagBytes, seed)) { return true; } @@ -111,12 +130,22 @@ static bool FlushChainAndSheet(blockchain_t* chain, return chainSaved && sheetSaved; } +/** + * This node's DAG-size vote, stamped into every block it mines. DAG_VOTE_GROW is the default and + * means "let the schedule run" -- there is deliberately no vote that makes the DAG grow faster, so + * the only thing a miner can express is to brake or reverse it. Settable at runtime via `dagvote`. +**/ +static uint8_t g_dagVote = (uint8_t)DAG_VOTE_GROW; + static block_t* BuildNextBlock(blockchain_t* chain, uint32_t difficultyTarget) { block_t* block = Block_Create(); if (!block) { return NULL; } + block->header.reserved[0] = g_dagVote; + block->header.reserved[1] = 0; + block->header.reserved[2] = 0; block->header.version = 1; block->header.blockNumber = (uint64_t)Chain_Size(chain); if (Chain_Size(chain) > 0) { @@ -238,96 +267,17 @@ static void PrintBlockDetail(const block_t* block, size_t txCount, const uint8_t printf("\n"); } -static bool ComputeEpochSeedForHeightFromChain(const blockchain_t* chain, uint64_t blockHeight, uint8_t outSeed[32]) { - if (!chain || !outSeed) { - return false; - } - - const uint64_t epochIndex = blockHeight / EPOCH_LENGTH; - if (epochIndex == 0) { - memset(outSeed, DAG_GENESIS_SEED, 32); - return true; - } - - const uint64_t seedBlockNumber = (epochIndex * EPOCH_LENGTH) - 1ULL; - if (seedBlockNumber >= Chain_Size((blockchain_t*)chain)) { - return false; - } - - block_t* seedBlock = NULL; - if (!Chain_GetBlockCopy((blockchain_t*)chain, (size_t)seedBlockNumber, &seedBlock)) { - return false; - } - - Block_CalculateHash(seedBlock, outSeed); - Block_Destroy(seedBlock); - return true; -} - -static bool ComputeEpochDagBytesForHeightFromChain(const blockchain_t* chain, uint64_t blockHeight, size_t* outDagBytes) { - if (!chain || !outDagBytes) { - return false; - } - - if (blockHeight <= EPOCH_LENGTH) { - *outDagBytes = DAG_BASE_SIZE; - return true; - } - - const uint64_t lastBlockNumber = blockHeight - 1ULL; - const uint64_t epochStartBlockNumber = lastBlockNumber - EPOCH_LENGTH; - if (lastBlockNumber >= Chain_Size((blockchain_t*)chain) || epochStartBlockNumber >= Chain_Size((blockchain_t*)chain)) { - return false; - } - - block_t* lastBlock = NULL; - block_t* epochStartBlock = NULL; - if (!Chain_GetBlockCopy((blockchain_t*)chain, (size_t)lastBlockNumber, &lastBlock)) { return false; } - if (!Chain_GetBlockCopy((blockchain_t*)chain, (size_t)epochStartBlockNumber, &epochStartBlock)) { Block_Destroy(lastBlock); return false; } - - int64_t difficultyDelta = (int64_t)epochStartBlock->header.difficultyTarget - (int64_t)lastBlock->header.difficultyTarget; - int64_t growth = (int64_t)((int64_t)DAG_BASE_GROWTH * difficultyDelta); - - if (growth > 0) { - 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 > (int64_t)DAG_MAX_UP_SWING_GB) { - growth = (int64_t)DAG_MAX_UP_SWING_GB; - } - } else { - 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 > (int64_t)DAG_MAX_DOWN_SWING_GB) { - growth = -(int64_t)DAG_MAX_DOWN_SWING_GB; - } - } - - const int64_t targetSize = (int64_t)DAG_BASE_SIZE + growth; - if (targetSize <= 0) { - return false; - } - - *outDagBytes = (size_t)targetSize; - Block_Destroy(lastBlock); - Block_Destroy(epochStartBlock); - return true; -} - static bool ComputeHistoricalAutolykosHashFromChain(const blockchain_t* chain, const block_t* block, uint64_t blockHeight, uint8_t outHash[32]) { if (!chain || !block || !outHash) { return false; } + // Same single source of truth the accept path uses, so a block that verifies here verifies + // there. This used to be a second, independent implementation of the epoch size and seed rules + // that disagreed with the one in constants.h -- notably at exactly height EPOCH_LENGTH. uint8_t seed[32]; size_t dagBytes = 0; - if (!ComputeEpochSeedForHeightFromChain(chain, blockHeight, seed)) { - return false; - } - if (!ComputeEpochDagBytesForHeightFromChain(chain, blockHeight, &dagBytes)) { + if (!Chain_DagParamsForHeight((blockchain_t*)chain, blockHeight, &dagBytes, seed)) { return false; } @@ -445,7 +395,7 @@ static bool MineAndAppendBlock(blockchain_t* chain, Block_CalculateMerkleRoot(block, merkleRoot); memcpy(block->header.merkleRoot, merkleRoot, sizeof(block->header.merkleRoot)); - if (!MineBlock(block)) { + if (!MineBlock(chain, block)) { fprintf(stderr, "failed to mine block within nonce range\n"); return false; } @@ -477,7 +427,16 @@ static bool MineAndAppendBlock(blockchain_t* chain, uint8_t canonicalHash[32]; uint8_t powHash[32]; Block_CalculateHash(block, canonicalHash); - Block_CalculateAutolykos2Hash(block, powHash); + memset(powHash, 0, sizeof(powHash)); + { + // For the log line only. Resolved fresh rather than carried out of MineBlock because the + // block is on the chain by now, so this also confirms it hashes the same from the tip. + size_t dagBytes = 0; + uint8_t seed[32]; + if (Chain_DagParamsForHeight(chain, block->header.blockNumber, &dagBytes, seed)) { + (void)Block_PowHashLight(block, dagBytes, seed, powHash); + } + } char supplyStr[80]; Uint256ToDecimal(currentSupply, supplyStr, sizeof(supplyStr)); @@ -761,12 +720,23 @@ int main(int argc, char* argv[]) { } { + // Report the epoch parameters the next block will use. The DAG itself is NOT generated + // here: it is a mining accelerator, so MineBlock builds it on demand and a node that never + // mines never pays for it. This used to build one from a seed derived from the tip block + // rather than the epoch boundary, which meant a node restarted mid-epoch mined against a + // different DAG than one that had run straight through the boundary. + size_t dagBytes = 0; uint8_t dagSeed[32]; - GetNextDAGSeed(chain, dagSeed); - (void)Block_RebuildAutolykos2Dag(CalculateTargetDAGSize(chain), dagSeed); - printf("Built initial DAG with seed %02x%02x%02x%02x... and size %zu bytes\n", - dagSeed[0], dagSeed[1], dagSeed[2], dagSeed[3], - CalculateTargetDAGSize(chain)); + const uint64_t nextHeight = (uint64_t)Chain_Size(chain); + if (Chain_DagParamsForHeight(chain, nextHeight, &dagBytes, dagSeed)) { + printf("Epoch %llu DAG: seed %02x%02x%02x%02x... size %zu bytes\n", + (unsigned long long)(nextHeight / (uint64_t)EPOCH_LENGTH), + dagSeed[0], dagSeed[1], dagSeed[2], dagSeed[3], + dagBytes); + } else { + fprintf(stderr, "Failed to resolve epoch DAG parameters for height %llu\n", + (unsigned long long)nextHeight); + } } if (Chain_Size(chain) > 0) { @@ -895,6 +865,32 @@ int main(int argc, char* argv[]) { continue; } + if (strcmp(cmd, "dagvote") == 0) { + char* voteStr = strtok(NULL, " \t"); + if (!voteStr) { + printf("dag vote is %u (%s)\n", (unsigned)g_dagVote, + g_dagVote == DAG_VOTE_HOLD ? "hold" : (g_dagVote == DAG_VOTE_DOWN ? "down" : "grow")); + printf("usage: dagvote \n"); + continue; + } + + if (strcmp(voteStr, "grow") == 0) { + g_dagVote = (uint8_t)DAG_VOTE_GROW; + } else if (strcmp(voteStr, "hold") == 0) { + g_dagVote = (uint8_t)DAG_VOTE_HOLD; + } else if (strcmp(voteStr, "down") == 0) { + g_dagVote = (uint8_t)DAG_VOTE_DOWN; + } else { + printf("usage: dagvote \n"); + continue; + } + + // Only affects blocks this node mines from here on; it cannot change how already-mined + // blocks are counted, since the vote is committed to inside the hashed header. + printf("dag vote set to %s\n", voteStr); + continue; + } + if (strcmp(cmd, "mine") == 0) { char* blocksStr = strtok(NULL, " \t"); if (!blocksStr) { @@ -1547,9 +1543,8 @@ int main(int argc, char* argv[]) { difficultyTarget = INITIAL_DIFFICULTY; currentReward = CalculateBlockReward(currentSupply, chain); - uint8_t dagSeed[32]; - memset(dagSeed, DAG_GENESIS_SEED, sizeof(dagSeed)); - (void)Block_RebuildAutolykos2Dag(DAG_BASE_SIZE, dagSeed); + // No DAG rebuild needed: Chain_Wipe drops the memoised epoch table, and MineBlock + // rebuilds the DAG on demand for whatever epoch it next mines in. printf("chain data wiped\n"); continue; diff --git a/src/nets/fetch_scheduler.c b/src/nets/fetch_scheduler.c index 65d27ec..1a11eff 100644 --- a/src/nets/fetch_scheduler.c +++ b/src/nets/fetch_scheduler.c @@ -24,9 +24,16 @@ uint64_t FetchScheduler_ComputeReorgPenaltyBlocks(uint64_t delayBlocks) { } // 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; + // step: penalty = ceil(raised * FACTOR_NUM * REF_BLOCK_TIME / (FACTOR_DEN * TARGET_BLOCK_TIME)) + // + // REF_BLOCK_TIME is the NUMERATOR and TARGET_BLOCK_TIME the DENOMINATOR, not the other way + // round. The result is a count of BLOCKS, so the wall-clock protection it buys is + // penalty(d) * TARGET_BLOCK_TIME ~= d^p * REF_BLOCK_TIME -- TARGET_BLOCK_TIME cancels, and the + // protection is the same number of seconds whatever the block time is. Inverting these two + // makes wall-clock protection scale as TARGET_BLOCK_TIME^2, so shortening the block time + // silently weakens reorg protection. Do not "simplify" this back. + const uint64_t numeratorScale = REORG_PENALTY_FACTOR_NUM * REORG_PENALTY_REF_BLOCK_TIME; + const uint64_t denominator = REORG_PENALTY_FACTOR_DEN * (uint64_t)TARGET_BLOCK_TIME; if (denominator == 0ULL) { return 0ULL; } diff --git a/src/nets/net_node.c b/src/nets/net_node.c index 3104c7b..bea4982 100644 --- a/src/nets/net_node.c +++ b/src/nets/net_node.c @@ -463,16 +463,18 @@ static node_block_accept_result_t Node_ParseAndAcceptBlock(const unsigned char* } } - // Validate block - if (!Block_IsFullyValid(blk)) { - printf("Rejected BLOCK_DATA at height %" PRIu64 " during validation\n", blockHeight); + // 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; } - if (!currentChain) { - printf("Rejected BLOCK_DATA at height %" PRIu64 ": no active chain\n", blockHeight); + // 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;