From 06e6f02b860593197cc97f4ecd6b299548d9b92b Mon Sep 17 00:00:00 2001 From: DcruBro Date: Wed, 1 Apr 2026 11:56:09 +0200 Subject: [PATCH] Figured out the reward scheme --- include/block/transaction.h | 15 +++-- include/constants.h | 115 ++++++++++++++++++++++++------------ include/uint256.h | 12 ++++ src/block/block.c | 3 +- src/block/transaction.c | 14 ++++- src/main.c | 69 ++++++++++++++++++++-- 6 files changed, 172 insertions(+), 56 deletions(-) diff --git a/include/block/transaction.h b/include/block/transaction.h index 646cc02..19243e1 100644 --- a/include/block/transaction.h +++ b/include/block/transaction.h @@ -20,26 +20,25 @@ static inline bool Address_IsCoinbase(const uint8_t address[32]) { return true; } -// 178 bytes total for v1 +// 160 bytes total for v1 typedef struct { - uint8_t version; + uint64_t fee; // Rewarded to the miner; can be zero, but the miner may choose to ignore transactions with very low fees + uint64_t amount1; + uint64_t amount2; // Only one "input" sender address uint8_t senderAddress[32]; - // The "main" recepient address and amount. This is the only required output, and is used for calculating the transaction hash and signature. uint8_t recipientAddress1[32]; - uint64_t amount1; // The "extra" recepient address and amount. This can safely be NULL/0 if not used and has multiple uses: // - Sending zero: parital spend, sender keeps coins on the same address // - Sending to a different address: normal spend, sender's coins move to a new address, e.g. change address // - Private Transactions: Can nullify the whole original stealth address input (sender) and send change to a new stealth address (recipient 2) to obfuscate the transaction graph. // Note that coinbase will have this as NULL/0 (for now, but we could have multiple payouts in the future) uint8_t recipientAddress2[32]; - uint64_t amount2; - - uint64_t fee; // Rewarded to the miner; can be zero, but the miner may choose to ignore transactions with very low fees - uint8_t compressedPublicKey[33]; // Timestamp is dictated by the block + uint8_t compressedPublicKey[33]; + uint8_t version; + uint8_t reserved[6]; // 6 bytes (Explicit padding for 8-byte alignment) } transaction_t; typedef struct { diff --git a/include/constants.h b/include/constants.h index b0d525f..1f445dc 100644 --- a/include/constants.h +++ b/include/constants.h @@ -14,7 +14,18 @@ #define INITIAL_DIFFICULTY 0x1f0c1422 // Default compact target used by Autolykos2 PoW (This is ridiculously low) //#define INITIAL_DIFFICULTY 0x1d1b7c51 // This takes 90s on my machine with a single thread, good for testing -#define INFLATION_PERCENTAGE_PER_EPOCH 15 // 1.5% +// Reward schedule acceleration: 1 means normal-speed progression. +#define EMISSION_ACCELERATION_FACTOR 1ULL + +// Phase-one target horizon: emit ~2^64-1 atomic units by this many blocks at x1. +#define PHASE1_TARGET_BLOCKS 3000000ULL + +// Inflation is expressed in tenths of a percent to preserve integer math. +#define INFLATION_PERCENTAGE_PER_EPOCH_TENTHS 15ULL // 1.5% + +// Monero-style main emission: reward = (MONEY_SUPPLY - generated) >> speed factor. +// 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: #define EPOCH_LENGTH 350000 // ~1 year at 90s @@ -35,32 +46,36 @@ * - Phase 2: Stable DAG growth (target is the max cap) to provide a stable environment for GPU miners, 320k blocks (roughly 11 months) **/ -static uint64_t currentReward = 0; // Global variable to track current block reward; updated with each block mined static const uint64_t M_CAP = 18446744073709551615ULL; // Max uint64 -static const uint64_t TAIL_EMISSION = DECIMALS; // Emission floor is 1.0 coins per block +static const uint64_t TAIL_EMISSION = 750000000000ULL; // 0.75 coins per block floor +static uint64_t currentReward = 750000000000ULL; // Epoch reward cache for phase 3 // No max supply. Instead of halving, it'll follow a more gradual, Monero-like emission curve. static uint256_t currentSupply = {{0, 0, 0, 0}}; // Global variable to track total supply; updated with each block mined -// Call every epoch +// Phase 3: update once per effective epoch and keep a fixed per-block reward for that epoch. static inline uint64_t GetInflationRateReward(uint256_t currentSupply, blockchain_t* chain) { if (!chain || !chain->blocks) { return 0x00; } // Invalid size_t height = Chain_Size(chain); - - block_t* blk = (block_t*)Chain_GetBlock(chain, height - 1); // Last block - if (!blk) { return 0x00; } // Invalid - - if (height % EPOCH_LENGTH == 0) { - // Calculate the new block reward (using all integer math to avoid floating point issues) + const uint64_t effectiveEpochLength = + (EPOCH_LENGTH / EMISSION_ACCELERATION_FACTOR) > 0 + ? (EPOCH_LENGTH / EMISSION_ACCELERATION_FACTOR) + : 1; - // 1. Multiply supply by 3 - uint256_t multiplied = currentSupply; - uint256_t temp = currentSupply; - uint256_add(&multiplied, &temp); // currentSupply * 2 - uint256_add(&multiplied, &temp); // currentSupply * 3 + if (height == 0) { + currentReward = TAIL_EMISSION; + return currentReward; + } + + if (height % effectiveEpochLength == 0) { + // inflationPerBlock = currentSupply * 1.5% / effectiveEpochLength + // = currentSupply * 15 / (1000 * effectiveEpochLength) + uint256_t multiplied = uint256_from_u64(0); + for (uint64_t i = 0; i < INFLATION_PERCENTAGE_PER_EPOCH_TENTHS; ++i) { + uint256_add(&multiplied, ¤tSupply); + } - // 2. Divide by 70,000,000 using scalar short division - uint64_t divisor = 70000000ULL; + uint64_t divisor = 1000ULL * effectiveEpochLength; uint256_t quotient = {{0, 0, 0, 0}}; unsigned __int128 remainder = 0; @@ -71,43 +86,67 @@ static inline uint64_t GetInflationRateReward(uint256_t currentSupply, blockchai remainder = current % divisor; } - currentReward = quotient.limbs[0]; // Update the global reward variable with the new calculated reward for this epoch - return quotient.limbs[0]; // Return the least significant limb as the reward (the rest should be 0 for reasonable supply levels) + uint64_t inflationPerBlock = quotient.limbs[0]; + currentReward = (inflationPerBlock > TAIL_EMISSION) ? inflationPerBlock : TAIL_EMISSION; + return currentReward; } - return currentReward; + return (currentReward > TAIL_EMISSION) ? currentReward : TAIL_EMISSION; } static inline uint64_t CalculateBlockReward(uint256_t currentSupply, blockchain_t* chain) { if (!chain || !chain->blocks) { return 0x00; } // Invalid - uint64_t height = Chain_Size(chain); + const uint64_t effectivePhase1Blocks = + (PHASE1_TARGET_BLOCKS / EMISSION_ACCELERATION_FACTOR) > 0 + ? (PHASE1_TARGET_BLOCKS / EMISSION_ACCELERATION_FACTOR) + : 1; + const uint64_t height = (uint64_t)Chain_Size(chain); + + // After the phase-one target horizon, only floor/inflation schedule applies. + if (height >= effectivePhase1Blocks) { + return GetInflationRateReward(currentSupply, chain); + } if (currentSupply.limbs[1] > 0 || currentSupply.limbs[2] > 0 || currentSupply.limbs[3] > 0 || - currentSupply.limbs[0] >= M_CAP) { - return TAIL_EMISSION; + currentSupply.limbs[0] >= M_CAP) + { + // Post-Monero phase with unlimited supply: floor/inflation schedule only. + return GetInflationRateReward(currentSupply, chain); } - uint64_t supply_64 = currentSupply.limbs[0]; - - // Formula: ((M - Supply) >> 20) * 181 / 256 - // Use 128-bit intermediate to avoid overflow while preserving integer math. - __uint128_t rewardWide = (((__uint128_t)(M_CAP - supply_64) >> 20) * 181u) >> 8; - uint64_t reward = (rewardWide > UINT64_MAX) ? UINT64_MAX : (uint64_t)rewardWide; - // At a block time of ~90s and a floor of 1.0 coins, this will make a curve of ~8.5 years + const uint64_t generated = currentSupply.limbs[0]; + const uint64_t remaining = M_CAP - generated; - // Check if the calculated reward has fallen below the floor - if (reward < TAIL_EMISSION) { - if (height < EPOCH_LENGTH * 10) { // Transitionary period to inflation of 1.5% per epoch - return TAIL_EMISSION; - } else { - return GetInflationRateReward(currentSupply, chain); // After the transitionary period, switch to the inflation-based reward - } + // Monero-style base curve against ~2^64 atomic-unit terminal supply. + uint64_t reward = remaining >> MONERO_EMISSION_SPEED_FACTOR; + + // Acceleration preserves curve shape while reaching the floor sooner in block-height terms. + if (EMISSION_ACCELERATION_FACTOR > 1ULL && reward > 0ULL) { + __uint128_t accelerated = (__uint128_t)reward * (__uint128_t)EMISSION_ACCELERATION_FACTOR; + reward = (accelerated > (__uint128_t)remaining) ? remaining : (uint64_t)accelerated; } - return reward; + // Retarget phase one to finish by PHASE1_TARGET_BLOCKS (x1), while keeping + // Monero-style behavior as the preferred curve when it is already sufficient. + const uint64_t blocksLeft = effectivePhase1Blocks - height; + const uint64_t minRewardToFinish = (remaining + blocksLeft - 1ULL) / blocksLeft; // ceil(remaining / blocksLeft) + if (reward < minRewardToFinish) { + reward = minRewardToFinish; + } + if (reward > remaining) { + reward = remaining; + } + + // Phase 1 until Monero reward goes below the floor. + if (reward > TAIL_EMISSION) { + return reward; + } + + // Phase 2 + 3: floor and epoch inflation updates. + return GetInflationRateReward(currentSupply, chain); } #endif diff --git a/include/uint256.h b/include/uint256.h index 6aa8c92..93ab19f 100644 --- a/include/uint256.h +++ b/include/uint256.h @@ -55,4 +55,16 @@ static inline bool uint256_add(uint256_t* a, const uint256_t* b) { return carry > 0; } +/** + * Compares two uint256_t values in a greater-than manner. + * Returns [-1, 0, 1] if a > b, a < b, or a == b respectively. +**/ +static inline int uint256_cmp(const uint256_t* a, const uint256_t* b) { + for (int i = 3; i >= 0; i--) { + if (a->limbs[i] > b->limbs[i]) return 1; + if (a->limbs[i] < b->limbs[i]) return -1; + } + return 0; +} + #endif diff --git a/src/block/block.c b/src/block/block.c index 7a718b2..449681a 100644 --- a/src/block/block.c +++ b/src/block/block.c @@ -291,7 +291,8 @@ void Block_Print(const block_t* block) { for (size_t i = 0; i < DynArr_size(block->transactions); i++) { signed_transaction_t* tx = (signed_transaction_t*)DynArr_at(block->transactions, i); if (tx) { - printf(" Tx #%zu: %llu -> %llu, fee %llu\n", i, tx->transaction.amount, tx->transaction.fee, tx->transaction.amount + tx->transaction.fee); + printf(" Tx #%zu: 1: %llu -> %02x%02x...%02x%02x, fee %llu\n 2: %llu -> %02x%02x...%02x%02x, fee %llu\n", + i, tx->transaction.amount1, tx->transaction.recipientAddress1[0], tx->transaction.recipientAddress1[1], tx->transaction.recipientAddress1[30], tx->transaction.recipientAddress1[31], tx->transaction.fee, tx->transaction.amount2, tx->transaction.recipientAddress2[0], tx->transaction.recipientAddress2[1], tx->transaction.recipientAddress2[30], tx->transaction.recipientAddress2[31], tx->transaction.fee); } } } else { diff --git a/src/block/transaction.c b/src/block/transaction.c index 2894ff4..ec70c6a 100644 --- a/src/block/transaction.c +++ b/src/block/transaction.c @@ -43,11 +43,11 @@ bool Transaction_Verify(const signed_transaction_t* tx) { return false; // Sender address does not match public key } - if (tx->transaction.amount == 0) { + if (tx->transaction.amount1 == 0) { return false; // Zero-amount transactions are not valid } - if (tx->transaction.fee > tx->transaction.amount) { + if (tx->transaction.fee > tx->transaction.amount1) { return false; // Fee cannot exceed amount } @@ -55,10 +55,18 @@ bool Transaction_Verify(const signed_transaction_t* tx) { return false; // Unsupported version } - if (Address_IsCoinbase(tx->transaction.recipientAddress)) { + if (Address_IsCoinbase(tx->transaction.recipientAddress1) || Address_IsCoinbase(tx->transaction.recipientAddress2)) { return false; // Cannot send to coinbase address } + if (tx->transaction.amount2 == 0) { + // If amount2 is zero, address2 must be all zeros + uint8_t zeroAddress[32] = {0}; + if (memcmp(tx->transaction.recipientAddress2, zeroAddress, 32) != 0) { + return false; // amount2 is zero but address2 is not zeroed + } + } + uint8_t txHash[32]; Transaction_CalculateHash(tx, txHash); diff --git a/src/main.c b/src/main.c index 7a806e9..91075f7 100644 --- a/src/main.c +++ b/src/main.c @@ -27,6 +27,53 @@ uint32_t difficultyTarget = INITIAL_DIFFICULTY; // extern the currentReward from constants.h so we can update it as we mine blocks and save it to disk extern uint64_t currentReward; +static void Uint256ToDecimal(const uint256_t* value, char* out, size_t outSize) { + if (!value || !out || outSize == 0) { + return; + } + + uint64_t tmp[4] = { + value->limbs[0], + value->limbs[1], + value->limbs[2], + value->limbs[3] + }; + + if (tmp[0] == 0 && tmp[1] == 0 && tmp[2] == 0 && tmp[3] == 0) { + if (outSize >= 2) { + out[0] = '0'; + out[1] = '\0'; + } else { + out[0] = '\0'; + } + return; + } + + char digits[80]; + size_t digitCount = 0; + + while (tmp[0] != 0 || tmp[1] != 0 || tmp[2] != 0 || tmp[3] != 0) { + uint64_t remainder = 0; + for (int i = 3; i >= 0; --i) { + __uint128_t cur = ((__uint128_t)remainder << 64) | tmp[i]; + tmp[i] = (uint64_t)(cur / 10u); + remainder = (uint64_t)(cur % 10u); + } + + if (digitCount < sizeof(digits) - 1) { + digits[digitCount++] = (char)('0' + remainder); + } else { + break; + } + } + + size_t writeLen = (digitCount < (outSize - 1)) ? digitCount : (outSize - 1); + for (size_t i = 0; i < writeLen; ++i) { + out[i] = digits[digitCount - 1 - i]; + } + out[writeLen] = '\0'; +} + static bool MineBlock(block_t* block) { if (!block) { return false; @@ -154,18 +201,20 @@ int main(int argc, char* argv[]) { } (void)uint256_add_u64(¤tSupply, coinbaseTx.transaction.amount1); + char supplyStr[80]; + Uint256ToDecimal(¤tSupply, supplyStr, sizeof(supplyStr)); uint8_t canonicalHash[32]; uint8_t powHash[32]; Block_CalculateHash(block, canonicalHash); Block_CalculateAutolykos2Hash(block, powHash); - printf("Mined block %llu/%llu (height=%llu) nonce=%llu reward=%llu supply=%llu diff=%#x merkle=%02x%02x%02x%02x... pow=%02x%02x%02x%02x... canonical=%02x%02x%02x%02x...\n", + printf("Mined block %llu/%llu (height=%llu) nonce=%llu reward=%llu supply=%s diff=%#x merkle=%02x%02x%02x%02x... pow=%02x%02x%02x%02x... canonical=%02x%02x%02x%02x...\n", (unsigned long long)(mined + 1), (unsigned long long)blocksToMine, (unsigned long long)block->header.blockNumber, (unsigned long long)block->header.nonce, (unsigned long long)coinbaseTx.transaction.amount1, - (unsigned long long)currentSupply.limbs[0], + supplyStr, (unsigned int)block->header.difficultyTarget, block->header.merkleRoot[0], block->header.merkleRoot[1], block->header.merkleRoot[2], block->header.merkleRoot[3], powHash[0], powHash[1], powHash[2], powHash[3], @@ -174,7 +223,11 @@ int main(int argc, char* argv[]) { free(block); // chain stores blocks by value; transactions are owned by chain copy // Save chain after each mined block; NOTE: In reality, blocks will appear every ~90s, so this won't be a realistic bottleneck on the mainnet - Chain_SaveToFile(chain, chainDataDir, currentSupply, currentReward); + // TEMP + if (Chain_Size(chain) % 100 == 0) { + printf("Saving chain at height %zu...\n", Chain_Size(chain)); + Chain_SaveToFile(chain, chainDataDir, currentSupply, currentReward); + } currentReward = CalculateBlockReward(currentSupply, chain); // Update the global currentReward for the next block @@ -187,13 +240,17 @@ int main(int argc, char* argv[]) { if (!Chain_SaveToFile(chain, chainDataDir, currentSupply, currentReward)) { fprintf(stderr, "failed to save chain to %s\n", chainDataDir); } else { - printf("Saved chain with %zu blocks to %s (supply=%llu)\n", + char supplyStr[80]; + Uint256ToDecimal(¤tSupply, supplyStr, sizeof(supplyStr)); + printf("Saved chain with %zu blocks to %s (supply=%s)\n", Chain_Size(chain), chainDataDir, - (unsigned long long)currentSupply.limbs[0]); + supplyStr); } } else { - printf("Current chain has %zu blocks, total supply %llu\n", Chain_Size(chain), (unsigned long long)currentSupply.limbs[0]); + char supplyStr[80]; + Uint256ToDecimal(¤tSupply, supplyStr, sizeof(supplyStr)); + printf("Current chain has %zu blocks, total supply %s\n", Chain_Size(chain), supplyStr); } // Print chain