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.
This commit is contained in:
@@ -32,7 +32,9 @@ bool Autolykos2_Hash(
|
|||||||
uint8_t outHash[32]
|
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(
|
bool Autolykos2_LightHashAtHeight(
|
||||||
const uint8_t seed32[32],
|
const uint8_t seed32[32],
|
||||||
const uint8_t* message,
|
const uint8_t* message,
|
||||||
|
|||||||
+40
-5
@@ -18,7 +18,10 @@ typedef struct {
|
|||||||
uint8_t merkleRoot[32];
|
uint8_t merkleRoot[32];
|
||||||
uint32_t difficultyTarget; // Encoding: [1 byte exponent][3 byte coefficient]; Target = coefficient * 256^(exponent-3)
|
uint32_t difficultyTarget; // Encoding: [1 byte exponent][3 byte coefficient]; Target = coefficient * 256^(exponent-3)
|
||||||
uint8_t version;
|
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;
|
} block_header_t;
|
||||||
#pragma pack(pop)
|
#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.
|
DynArr* transactions; // Array of signed_transaction_t, NOTE: Potentially move to a hashmap at some point for quick lookups.
|
||||||
} block_t;
|
} 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();
|
block_t* Block_Create();
|
||||||
void Block_CalculateHash(const block_t* block, uint8_t* outHash);
|
void Block_CalculateHash(const block_t* block, uint8_t* outHash);
|
||||||
void Block_CalculateMerkleRoot(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_AddTransaction(block_t* block, signed_transaction_t* tx);
|
||||||
void Block_RemoveTransaction(block_t* block, uint8_t* txHash);
|
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_AllTransactionsValid(const block_t* block);
|
||||||
bool Block_ValidateCoinbaseAndFees(const block_t* block, uint64_t expectedCoinbaseAmount, uint64_t* outTotalFees);
|
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_ShutdownPowContext(void);
|
||||||
void Block_Destroy(block_t* block);
|
void Block_Destroy(block_t* block);
|
||||||
void Block_Print(const block_t* block);
|
void Block_Print(const block_t* block);
|
||||||
|
|||||||
@@ -7,13 +7,41 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <pthread.h>
|
||||||
#include <uint256.h>
|
#include <uint256.h>
|
||||||
#include <storage/block_table.h>
|
#include <storage/block_table.h>
|
||||||
#include <balance_sheet.h>
|
#include <balance_sheet.h>
|
||||||
|
|
||||||
|
// One entry of the memoised DAG size recurrence, one per epoch. See Chain_DagParamsForHeight.
|
||||||
typedef struct {
|
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;
|
DynArr* blocks;
|
||||||
size_t size;
|
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;
|
||||||
|
|
||||||
blockchain_t* Chain_Create();
|
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`.
|
// Call after any change to the tip. Must NOT be called while holding `chainLock`.
|
||||||
void Chain_OnTipAdvanced(blockchain_t* chain);
|
void Chain_OnTipAdvanced(blockchain_t* chain);
|
||||||
|
|
||||||
|
// 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
|
// Work
|
||||||
// Expected number of hashes to satisfy `difficultyTargetBits`, i.e. 2^256 / (target + 1).
|
// Expected number of hashes to satisfy `difficultyTargetBits`, i.e. 2^256 / (target + 1).
|
||||||
bool Chain_ComputeBlockWork(uint32_t difficultyTargetBits, uint256_t* outWork);
|
bool Chain_ComputeBlockWork(uint32_t difficultyTargetBits, uint256_t* outWork);
|
||||||
|
|||||||
+59
-89
@@ -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
|
// 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.
|
// 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
|
// Expressed as integer rationals on purpose: this feeds fork choice, so it must evaluate
|
||||||
// identically on every node. Floating point is not acceptable here.
|
// 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 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
|
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
|
// 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;
|
static const uint64_t REORG_PENALTY_MAX_DEPTH = 1000ULL;
|
||||||
|
|
||||||
// Upper bound on pooled orphan blocks. Orphans are accepted before the chain-derived difficulty
|
// 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.
|
// Keep this at 20 to match the canonical curve shape against a 2^64 atomic supply cap.
|
||||||
#define MONERO_EMISSION_SPEED_FACTOR 20U
|
#define MONERO_EMISSION_SPEED_FACTOR 20U
|
||||||
|
|
||||||
// Future Autolykos2 constants:
|
// Autolykos2 epoch / DAG constants.
|
||||||
#define EPOCH_LENGTH 350000 // ~1 year at 90s
|
#define EPOCH_LENGTH 350000 // ~1 year at 90s
|
||||||
#define DAG_BASE_GROWTH (1ULL << 30) // 1 GB per epoch, adjusted by acceleration
|
#define DAG_GENESIS_SEED 0x00 // Epoch 0's seed is all zeroes; epoch k's seed is the hash of the last
|
||||||
//#define DAG_BASE_SIZE (6ULL << 30) // 6 GB, adjusted per cycle based off DAG_BASE_GROWTH
|
// block of epoch k-1, so it is unpredictable until that block is mined.
|
||||||
#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
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Each epoch has 2 phases, connected logarithmically:
|
* DAG size band and the miner signal that moves within it.
|
||||||
* - 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)
|
* 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 M_CAP = 18446744073709551615ULL; // Max uint64
|
||||||
static const uint64_t TAIL_EMISSION = 750000000000ULL; // 0.75 coins per block floor
|
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));
|
return CalculateBlockRewardAtHeight(currentSupply, (uint64_t)Chain_Size(chain));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hashing DAG
|
// Hashing DAG: see Chain_DagParamsForHeight in block/chain.h. Both the size and the epoch seed are
|
||||||
static inline size_t CalculateTargetDAGSize(blockchain_t* chain) {
|
// derived from the chain by that one function, so the mining and verification paths cannot drift
|
||||||
// Base size plus (base growth * difficulty factor), adjusted by acceleration
|
// apart. The previous CalculateTargetDAGSize/GetNextDAGSeed pair lived here, took chainLock
|
||||||
if (!chain || !chain->blocks) { return 0; } // Invalid
|
// internally, was not epoch-aligned, and disagreed with the verifier's own copy in main.c.
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -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(
|
bool Autolykos2_LightHashAtHeight(
|
||||||
const uint8_t seed32[32],
|
const uint8_t seed32[32],
|
||||||
const uint8_t* message,
|
const uint8_t* message,
|
||||||
|
|||||||
+125
-37
@@ -1,45 +1,118 @@
|
|||||||
#include <block/block.h>
|
#include <block/block.h>
|
||||||
|
#include <block/chain.h>
|
||||||
#include <autolykos2/autolykos2.h>
|
#include <autolykos2/autolykos2.h>
|
||||||
#include <utils.h>
|
#include <utils.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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) {
|
if (!g_autolykos2Ctx) {
|
||||||
g_autolykos2Ctx = Autolykos2_Create();
|
g_autolykos2Ctx = Autolykos2_Create();
|
||||||
if (!g_autolykos2Ctx) {
|
if (!g_autolykos2Ctx) {
|
||||||
fprintf(stderr, "Failed to create Autolykos2 context\n");
|
fprintf(stderr, "Failed to create Autolykos2 context\n");
|
||||||
exit(1);
|
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;
|
return g_autolykos2Ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Block_ShutdownPowContext(void) {
|
void Block_ShutdownPowContext(void) {
|
||||||
|
pthread_mutex_lock(&g_powCtxLock);
|
||||||
if (g_autolykos2Ctx) {
|
if (g_autolykos2Ctx) {
|
||||||
Autolykos2_Destroy(g_autolykos2Ctx);
|
Autolykos2_Destroy(g_autolykos2Ctx);
|
||||||
g_autolykos2Ctx = NULL;
|
g_autolykos2Ctx = NULL;
|
||||||
}
|
}
|
||||||
|
g_dagReady = false;
|
||||||
|
pthread_mutex_unlock(&g_powCtxLock);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Block_RebuildAutolykos2Dag(size_t dagBytes, const uint8_t seed32[32]) {
|
bool Block_EnsureAutolykos2Dag(uint64_t epochIndex, size_t dagBytes, const uint8_t seed32[32]) {
|
||||||
if (!seed32 || dagBytes == 0) {
|
if (!seed32 || dagBytes < 32u || (dagBytes % 32u) != 0u) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Autolykos2Context* ctx = GetAutolykos2Ctx();
|
pthread_mutex_lock(&g_powCtxLock);
|
||||||
if (!ctx) {
|
|
||||||
return false;
|
// 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);
|
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 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() {
|
block_t* Block_Create() {
|
||||||
@@ -133,30 +206,6 @@ void Block_CalculateMerkleRoot(const block_t* block, uint8_t* outHash) {
|
|||||||
free(next);
|
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) {
|
void Block_AddTransaction(block_t* block, signed_transaction_t* tx) {
|
||||||
if (!block || !tx || !block->transactions) {
|
if (!block || !tx || !block->transactions) {
|
||||||
return;
|
return;
|
||||||
@@ -189,7 +238,8 @@ static int Uint256_CompareBE(const uint8_t a[32], const uint8_t b[32]) {
|
|||||||
return 0;
|
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) {
|
if (!block) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -199,12 +249,49 @@ bool Block_HasValidProofOfWork(const block_t* block) {
|
|||||||
return false;
|
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];
|
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;
|
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) {
|
bool Block_AllTransactionsValid(const block_t* block) {
|
||||||
if (!block || !block->transactions) {
|
if (!block || !block->transactions) {
|
||||||
return false;
|
return false;
|
||||||
@@ -296,7 +383,7 @@ bool Block_ValidateCoinbaseAndFees(const block_t* block, uint64_t expectedCoinba
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Block_IsFullyValid(const block_t* block) {
|
bool Block_IsFullyValid(const block_t* block, blockchain_t* chain) {
|
||||||
bool merkleValid = false;
|
bool merkleValid = false;
|
||||||
uint8_t calculatedMerkleRoot[32];
|
uint8_t calculatedMerkleRoot[32];
|
||||||
if (block && block->transactions) {
|
if (block && block->transactions) {
|
||||||
@@ -304,7 +391,8 @@ bool Block_IsFullyValid(const block_t* block) {
|
|||||||
merkleValid = (memcmp(calculatedMerkleRoot, block->header.merkleRoot, 32) == 0);
|
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) {
|
void Block_Destroy(block_t* block) {
|
||||||
|
|||||||
+230
-5
@@ -181,6 +181,27 @@ bool Chain_RecomputeRuntimeState(blockchain_t* chain) {
|
|||||||
return true;
|
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) {
|
static void Chain_ClearBlocks(blockchain_t* chain) {
|
||||||
if (!chain || !chain->blocks) {
|
if (!chain || !chain->blocks) {
|
||||||
return;
|
return;
|
||||||
@@ -196,6 +217,7 @@ static void Chain_ClearBlocks(blockchain_t* chain) {
|
|||||||
|
|
||||||
DynArr_erase(chain->blocks);
|
DynArr_erase(chain->blocks);
|
||||||
chain->size = 0;
|
chain->size = 0;
|
||||||
|
Chain_InvalidateDagEpochs(chain);
|
||||||
}
|
}
|
||||||
|
|
||||||
blockchain_t* Chain_Create() {
|
blockchain_t* Chain_Create() {
|
||||||
@@ -207,6 +229,15 @@ blockchain_t* Chain_Create() {
|
|||||||
ptr->blocks = DYNARR_CREATE(block_t, 1);
|
ptr->blocks = DYNARR_CREATE(block_t, 1);
|
||||||
ptr->size = 0;
|
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;
|
return ptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +247,8 @@ void Chain_Destroy(blockchain_t* chain) {
|
|||||||
Chain_ClearBlocks(chain);
|
Chain_ClearBlocks(chain);
|
||||||
DynArr_destroy(chain->blocks);
|
DynArr_destroy(chain->blocks);
|
||||||
}
|
}
|
||||||
|
free(chain->dagEpochs);
|
||||||
|
pthread_mutex_destroy(&chain->dagCacheLock);
|
||||||
free(chain);
|
free(chain);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -557,6 +590,9 @@ static bool Chain_RollbackToHeightLocked(blockchain_t* chain, size_t height) {
|
|||||||
chain->size = DynArr_size(chain->blocks);
|
chain->size = DynArr_size(chain->blocks);
|
||||||
currentBlockHeight = chain->size ? (uint64_t)(chain->size - 1) : 0ULL;
|
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
|
// Rebuild balance sheet from scratch up to current chain size
|
||||||
BalanceSheet_Destroy();
|
BalanceSheet_Destroy();
|
||||||
BalanceSheet_Init();
|
BalanceSheet_Init();
|
||||||
@@ -1678,6 +1714,195 @@ bool Chain_ComputeBranchWork(block_t** blocks, size_t count, uint256_t* outWork)
|
|||||||
return true;
|
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) {
|
void Chain_OnTipAdvanced(blockchain_t* chain) {
|
||||||
if (!chain || !chain->blocks) {
|
if (!chain || !chain->blocks) {
|
||||||
return;
|
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.
|
// (mining, P2P accept, sync, orphan attach, reorg) stays on the same difficulty as its peers.
|
||||||
difficultyTarget = Chain_GetTargetForHeight(chain, (uint64_t)chainSize);
|
difficultyTarget = Chain_GetTargetForHeight(chain, (uint64_t)chainSize);
|
||||||
|
|
||||||
if (chainSize % EPOCH_LENGTH == 0 && chainSize > 0) {
|
// The epoch DAG is deliberately NOT rebuilt here. It is a mining accelerator only -- validation
|
||||||
uint8_t dagSeed[32];
|
// derives its lanes from the epoch seed, so a node that does not mine never allocates one --
|
||||||
GetNextDAGSeed(chain, dagSeed);
|
// and MineBlock builds it on demand for the height it is working on. Keeping generation off
|
||||||
(void)Block_RebuildAutolykos2Dag(CalculateTargetDAGSize(chain), dagSeed);
|
// 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.
|
||||||
}
|
}
|
||||||
|
|||||||
+91
-96
@@ -77,14 +77,33 @@ static void ApplyRuntimeConfigFromEnv(void) {
|
|||||||
|
|
||||||
uint32_t difficultyTarget = INITIAL_DIFFICULTY;
|
uint32_t difficultyTarget = INITIAL_DIFFICULTY;
|
||||||
|
|
||||||
static bool MineBlock(block_t* block) {
|
static bool MineBlock(blockchain_t* chain, block_t* block) {
|
||||||
if (!block) {
|
if (!chain || !block) {
|
||||||
return false;
|
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) {
|
for (uint64_t nonce = 0;; ++nonce) {
|
||||||
block->header.nonce = nonce;
|
block->header.nonce = nonce;
|
||||||
if (Block_HasValidProofOfWork(block)) {
|
if (Block_HasValidProofOfWorkWithParams(block, epochIndex, dagBytes, seed)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,12 +130,22 @@ static bool FlushChainAndSheet(blockchain_t* chain,
|
|||||||
return chainSaved && sheetSaved;
|
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) {
|
static block_t* BuildNextBlock(blockchain_t* chain, uint32_t difficultyTarget) {
|
||||||
block_t* block = Block_Create();
|
block_t* block = Block_Create();
|
||||||
if (!block) {
|
if (!block) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
block->header.reserved[0] = g_dagVote;
|
||||||
|
block->header.reserved[1] = 0;
|
||||||
|
block->header.reserved[2] = 0;
|
||||||
block->header.version = 1;
|
block->header.version = 1;
|
||||||
block->header.blockNumber = (uint64_t)Chain_Size(chain);
|
block->header.blockNumber = (uint64_t)Chain_Size(chain);
|
||||||
if (Chain_Size(chain) > 0) {
|
if (Chain_Size(chain) > 0) {
|
||||||
@@ -238,96 +267,17 @@ static void PrintBlockDetail(const block_t* block, size_t txCount, const uint8_t
|
|||||||
printf("\n");
|
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]) {
|
static bool ComputeHistoricalAutolykosHashFromChain(const blockchain_t* chain, const block_t* block, uint64_t blockHeight, uint8_t outHash[32]) {
|
||||||
if (!chain || !block || !outHash) {
|
if (!chain || !block || !outHash) {
|
||||||
return false;
|
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];
|
uint8_t seed[32];
|
||||||
size_t dagBytes = 0;
|
size_t dagBytes = 0;
|
||||||
if (!ComputeEpochSeedForHeightFromChain(chain, blockHeight, seed)) {
|
if (!Chain_DagParamsForHeight((blockchain_t*)chain, blockHeight, &dagBytes, seed)) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!ComputeEpochDagBytesForHeightFromChain(chain, blockHeight, &dagBytes)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,7 +395,7 @@ static bool MineAndAppendBlock(blockchain_t* chain,
|
|||||||
Block_CalculateMerkleRoot(block, merkleRoot);
|
Block_CalculateMerkleRoot(block, merkleRoot);
|
||||||
memcpy(block->header.merkleRoot, merkleRoot, sizeof(block->header.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");
|
fprintf(stderr, "failed to mine block within nonce range\n");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -477,7 +427,16 @@ static bool MineAndAppendBlock(blockchain_t* chain,
|
|||||||
uint8_t canonicalHash[32];
|
uint8_t canonicalHash[32];
|
||||||
uint8_t powHash[32];
|
uint8_t powHash[32];
|
||||||
Block_CalculateHash(block, canonicalHash);
|
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];
|
char supplyStr[80];
|
||||||
Uint256ToDecimal(currentSupply, supplyStr, sizeof(supplyStr));
|
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];
|
uint8_t dagSeed[32];
|
||||||
GetNextDAGSeed(chain, dagSeed);
|
const uint64_t nextHeight = (uint64_t)Chain_Size(chain);
|
||||||
(void)Block_RebuildAutolykos2Dag(CalculateTargetDAGSize(chain), dagSeed);
|
if (Chain_DagParamsForHeight(chain, nextHeight, &dagBytes, dagSeed)) {
|
||||||
printf("Built initial DAG with seed %02x%02x%02x%02x... and size %zu bytes\n",
|
printf("Epoch %llu DAG: seed %02x%02x%02x%02x... size %zu bytes\n",
|
||||||
dagSeed[0], dagSeed[1], dagSeed[2], dagSeed[3],
|
(unsigned long long)(nextHeight / (uint64_t)EPOCH_LENGTH),
|
||||||
CalculateTargetDAGSize(chain));
|
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) {
|
if (Chain_Size(chain) > 0) {
|
||||||
@@ -895,6 +865,32 @@ int main(int argc, char* argv[]) {
|
|||||||
continue;
|
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 <grow|hold|down>\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 <grow|hold|down>\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) {
|
if (strcmp(cmd, "mine") == 0) {
|
||||||
char* blocksStr = strtok(NULL, " \t");
|
char* blocksStr = strtok(NULL, " \t");
|
||||||
if (!blocksStr) {
|
if (!blocksStr) {
|
||||||
@@ -1547,9 +1543,8 @@ int main(int argc, char* argv[]) {
|
|||||||
difficultyTarget = INITIAL_DIFFICULTY;
|
difficultyTarget = INITIAL_DIFFICULTY;
|
||||||
currentReward = CalculateBlockReward(currentSupply, chain);
|
currentReward = CalculateBlockReward(currentSupply, chain);
|
||||||
|
|
||||||
uint8_t dagSeed[32];
|
// No DAG rebuild needed: Chain_Wipe drops the memoised epoch table, and MineBlock
|
||||||
memset(dagSeed, DAG_GENESIS_SEED, sizeof(dagSeed));
|
// rebuilds the DAG on demand for whatever epoch it next mines in.
|
||||||
(void)Block_RebuildAutolykos2Dag(DAG_BASE_SIZE, dagSeed);
|
|
||||||
|
|
||||||
printf("chain data wiped\n");
|
printf("chain data wiped\n");
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -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
|
// 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))
|
// step: penalty = ceil(raised * FACTOR_NUM * REF_BLOCK_TIME / (FACTOR_DEN * TARGET_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;
|
// 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) {
|
if (denominator == 0ULL) {
|
||||||
return 0ULL;
|
return 0ULL;
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-5
@@ -463,16 +463,18 @@ static node_block_accept_result_t Node_ParseAndAcceptBlock(const unsigned char*
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate block
|
// The chain check has to come first now: PoW validity is chain-relative (the epoch DAG size and
|
||||||
if (!Block_IsFullyValid(blk)) {
|
// seed are derived from it), so there is nothing to validate against without a chain.
|
||||||
printf("Rejected BLOCK_DATA at height %" PRIu64 " during validation\n", blockHeight);
|
if (!currentChain) {
|
||||||
|
printf("Rejected BLOCK_DATA at height %" PRIu64 ": no active chain\n", blockHeight);
|
||||||
DynArr_destroy(blk->transactions);
|
DynArr_destroy(blk->transactions);
|
||||||
free(blk);
|
free(blk);
|
||||||
return NODE_BLOCK_REJECTED;
|
return NODE_BLOCK_REJECTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentChain) {
|
// Validate block
|
||||||
printf("Rejected BLOCK_DATA at height %" PRIu64 ": no active chain\n", blockHeight);
|
if (!Block_IsFullyValid(blk, currentChain)) {
|
||||||
|
printf("Rejected BLOCK_DATA at height %" PRIu64 " during validation\n", blockHeight);
|
||||||
DynArr_destroy(blk->transactions);
|
DynArr_destroy(blk->transactions);
|
||||||
free(blk);
|
free(blk);
|
||||||
return NODE_BLOCK_REJECTED;
|
return NODE_BLOCK_REJECTED;
|
||||||
|
|||||||
Reference in New Issue
Block a user