This commit is contained in:
2026-03-30 09:06:17 +02:00
parent 50e357d8a2
commit c358115af4
6 changed files with 272 additions and 111 deletions

View File

@@ -1 +1,2 @@
Eventually move the Proof of Work to RandomX. Keep the SHA-256 for Block IDs, TX IDs, etc. But nonce and mining should be RandomX. This will make it more resistant to ASICs and more accessible for CPU mining. Move to a GPU algo. RandomX is a good candidate, but CPU mining is not that attractive to anyone but people who actually want to support the project.
It won't incentivize people who want to profit, which let's be fair, is the majority of miners.

View File

@@ -8,6 +8,7 @@
#include <stdbool.h> #include <stdbool.h>
#include <string.h> #include <string.h>
#include <randomx/librx_wrapper.h> #include <randomx/librx_wrapper.h>
#include <stdlib.h>
typedef struct { typedef struct {
uint64_t blockNumber; uint64_t blockNumber;
@@ -27,11 +28,13 @@ typedef struct {
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_CalculateRandomXHash(const block_t* block, uint8_t* outHash); void Block_CalculateRandomXHash(const block_t* block, uint8_t* outHash);
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); bool Block_HasValidProofOfWork(const block_t* block);
bool Block_AllTransactionsValid(const block_t* block); bool Block_AllTransactionsValid(const block_t* block);
void Block_Destroy(block_t* block); void Block_Destroy(block_t* block);
void Block_Print(const block_t* block);
#endif #endif

View File

@@ -6,7 +6,10 @@
#include <stdbool.h> #include <stdbool.h>
#define DECIMALS 1000000000000ULL #define DECIMALS 1000000000000ULL
#define EMISSION_SPEED_FACTOR 20 #define DIFFICULTY_ADJUSTMENT_INTERVAL 960 // Every 960 blocks (roughly every 24 hours with a 90 second block time)
// Max adjustment per is x2. So if blocks are coming in too fast, the difficulty will at most double every 24 hours, and vice versa if they're coming in too slow.
#define RANDOMX_KEY_ROTATION_INTERVAL 6720 // 1 week at 90s block time
#define TARGET_BLOCK_TIME 90 // Target block time in seconds
static const uint64_t M_CAP = 18446744073709551615ULL; // Max uint64 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 = DECIMALS; // Emission floor is 1.0 coins per block
// No max supply. Instead of halving, it'll follow a more gradual, Monero-like emission curve. // No max supply. Instead of halving, it'll follow a more gradual, Monero-like emission curve.
@@ -26,8 +29,11 @@ static inline uint64_t CalculateBlockReward(uint256_t currentSupply, uint64_t he
uint64_t supply_64 = currentSupply.limbs[0]; uint64_t supply_64 = currentSupply.limbs[0];
// Formula: (M - Supply) >> 2^k - lifted from Monero's codebase (thanks guys!) // Formula: ((M - Supply) >> 20) * 181 / 256
uint64_t reward = (M_CAP - supply_64) >> EMISSION_SPEED_FACTOR; // 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
// Check if the calculated reward has fallen below the floor // Check if the calculated reward has fallen below the floor
if (reward < TAIL_EMISSION) { if (reward < TAIL_EMISSION) {

View File

@@ -16,40 +16,113 @@ block_t* Block_Create() {
} }
void Block_CalculateHash(const block_t* block, uint8_t* outHash) { void Block_CalculateHash(const block_t* block, uint8_t* outHash) {
if (!block || !outHash || !block->transactions || DynArr_size(block->transactions) <= 0) { if (!block || !outHash) {
return; return;
} }
// Merkle root TODO // Canonical block hash commits to header fields, including merkleRoot.
SHA256((const unsigned char*)&block->header, sizeof(block_header_t), outHash);
// Flatten the block header and transactions into a single buffer for hashing (assume that txs are verified - usually on receive)
uint8_t buffer[sizeof(block_header_t) + (DynArr_size(block->transactions) * DynArr_elemSize(block->transactions))];
memcpy(buffer, &block->header, sizeof(block_header_t));
for (size_t i = 0; i < DynArr_size(block->transactions); i++) {
void* txPtr = (char*)DynArr_at(block->transactions, i);
memcpy(buffer + sizeof(block_header_t) + (i * DynArr_elemSize(block->transactions)), txPtr, DynArr_elemSize(block->transactions));
}
SHA256((const unsigned char*)buffer, sizeof(buffer), outHash);
SHA256(outHash, 32, outHash); // Double-Hash SHA256(outHash, 32, outHash); // Double-Hash
} }
void Block_CalculateRandomXHash(const block_t* block, uint8_t* outHash) { void Block_CalculateMerkleRoot(const block_t* block, uint8_t* outHash) {
if (!block || !outHash || !block->transactions || DynArr_size(block->transactions) <= 0) { if (!block || !block->transactions || !outHash) {
return; return;
} }
// Merkle root TODO const size_t txCount = DynArr_size(block->transactions);
if (txCount == 0) {
// Flatten the block header and transactions into a single buffer for hashing (assume that txs are verified - usually on receive) memset(outHash, 0, 32);
uint8_t buffer[sizeof(block_header_t) + (DynArr_size(block->transactions) * DynArr_elemSize(block->transactions))]; return;
memcpy(buffer, &block->header, sizeof(block_header_t)); }
for (size_t i = 0; i < DynArr_size(block->transactions); i++) { if (txCount == 1) {
void* txPtr = (char*)DynArr_at(block->transactions, i); signed_transaction_t* tx = (signed_transaction_t*)DynArr_at(block->transactions, 0);
memcpy(buffer + sizeof(block_header_t) + (i * DynArr_elemSize(block->transactions)), txPtr, DynArr_elemSize(block->transactions)); Transaction_CalculateHash(tx, outHash);
return;
} }
RandomX_CalculateHash(buffer, sizeof(buffer), outHash); // TODO: Make this not shit
DynArr* hashes1 = DynArr_create(sizeof(uint8_t) * 32, 1);
DynArr* hashes2 = DynArr_create(sizeof(uint8_t) * 32, 1);
if (!hashes1 || !hashes2) {
if (hashes1) DynArr_destroy(hashes1);
if (hashes2) DynArr_destroy(hashes2);
return;
}
// Handle the transactions
for (size_t i = 0; i < txCount - 1; i++) {
signed_transaction_t* tx = (signed_transaction_t*)DynArr_at(block->transactions, i);
signed_transaction_t* txNext = (signed_transaction_t*)DynArr_at(block->transactions, i + 1);
uint8_t buf1[32] = {0}; uint8_t buf2[32] = {0}; // Zeroed out
// Unless if by some miracle the hash just so happens to be all zeros,
// I think we can safely assume that a 1 : 2^256 chance will NEVER be hit
Transaction_CalculateHash(tx, buf1);
Transaction_CalculateHash(txNext, buf2);
// Concat the two hashes
uint8_t dataInBuffer[64] = {0};
uint8_t* nextStart = dataInBuffer;
nextStart += 32;
memcpy(dataInBuffer, buf1, 32);
if (txNext) { memcpy(nextStart, buf2, 32); }
// Double hash that tx set
uint8_t outHash[32];
SHA256((const unsigned char*)dataInBuffer, 64, outHash);
SHA256(outHash, 32, outHash);
// Copy to the hashes dynarr
DynArr_push_back(hashes1, outHash);
}
// Move to hashing the existing ones until only one remains
do {
for (size_t i = 0; i < DynArr_size(hashes1) - 1; i++) {
uint8_t* hash1 = (uint8_t*)DynArr_at(hashes1, i); uint8_t* hash2 = (uint8_t*)DynArr_at(hashes1, i + 1);
// Concat the two hashes
uint8_t dataInBuffer[64] = {0};
uint8_t* nextStart = dataInBuffer;
nextStart += 32;
memcpy(dataInBuffer, hash1, 32);
memcpy(nextStart, hash2, 32);
// Double hash that tx set
uint8_t outHash[32];
SHA256((const unsigned char*)dataInBuffer, 64, outHash);
SHA256(outHash, 32, outHash);
DynArr_push_back(hashes2, outHash);
}
DynArr_erase(hashes1);
for (size_t i = 0; i < DynArr_size(hashes2); i++) {
DynArr_push_back(hashes1, (uint8_t*)DynArr_at(hashes2, i));
}
DynArr_erase(hashes2);
} while (DynArr_size(hashes1) > 1);
// Final Merkle
uint8_t* merkle = (uint8_t*)DynArr_at(hashes1, 0);
if (merkle) {
memcpy(outHash, merkle, 32);
} else {
memset(outHash, 0, 32);
}
DynArr_destroy(hashes1);
DynArr_destroy(hashes2);
}
void Block_CalculateRandomXHash(const block_t* block, uint8_t* outHash) {
if (!block || !outHash) {
return;
}
// PoW hash is also computed from the header only.
RandomX_CalculateHash((const uint8_t*)&block->header, sizeof(block_header_t), outHash);
} }
void Block_AddTransaction(block_t* block, signed_transaction_t* tx) { void Block_AddTransaction(block_t* block, signed_transaction_t* tx) {
@@ -162,3 +235,34 @@ void Block_Destroy(block_t* block) {
DynArr_destroy(block->transactions); DynArr_destroy(block->transactions);
free(block); free(block);
} }
void Block_Print(const block_t* block) {
if (!block) return;
printf("Block #%llu\n", block->header.blockNumber);
printf("Timestamp: %llu\n", block->header.timestamp);
printf("Nonce: %llu\n", block->header.nonce);
printf("Difficulty Target: 0x%08x\n", block->header.difficultyTarget);
printf("Version: %u\n", block->header.version);
printf("Previous Hash: ");
for (size_t i = 0; i < 32; i++) {
printf("%02x", block->header.prevHash[i]);
}
printf("\n");
printf("Merkle Root: ");
for (size_t i = 0; i < 32; i++) {
printf("%02x", block->header.merkleRoot[i]);
}
printf("\n");
if (block->transactions) {
printf("Transactions (%zu):\n", DynArr_size(block->transactions));
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);
}
}
} else {
printf("No transactions (or none loaded)\n");
}
}

View File

@@ -32,6 +32,23 @@ static bool BuildPath(char* out, size_t outSize, const char* dirpath, const char
return written > 0 && (size_t)written < outSize; return written > 0 && (size_t)written < outSize;
} }
static void Chain_ClearBlocks(blockchain_t* chain) {
if (!chain || !chain->blocks) {
return;
}
for (size_t i = 0; i < DynArr_size(chain->blocks); i++) {
block_t* blk = (block_t*)DynArr_at(chain->blocks, i);
if (blk && blk->transactions) {
DynArr_destroy(blk->transactions);
blk->transactions = NULL;
}
}
DynArr_erase(chain->blocks);
chain->size = 0;
}
blockchain_t* Chain_Create() { blockchain_t* Chain_Create() {
blockchain_t* ptr = (blockchain_t*)malloc(sizeof(blockchain_t)); blockchain_t* ptr = (blockchain_t*)malloc(sizeof(blockchain_t));
if (!ptr) { if (!ptr) {
@@ -47,6 +64,7 @@ blockchain_t* Chain_Create() {
void Chain_Destroy(blockchain_t* chain) { void Chain_Destroy(blockchain_t* chain) {
if (chain) { if (chain) {
if (chain->blocks) { if (chain->blocks) {
Chain_ClearBlocks(chain);
DynArr_destroy(chain->blocks); DynArr_destroy(chain->blocks);
} }
free(chain); free(chain);
@@ -113,10 +131,7 @@ bool Chain_IsValid(blockchain_t* chain) {
} }
void Chain_Wipe(blockchain_t* chain) { void Chain_Wipe(blockchain_t* chain) {
if (chain && chain->blocks) { Chain_ClearBlocks(chain);
DynArr_erase(chain->blocks);
chain->size = 0;
}
} }
bool Chain_SaveToFile(blockchain_t* chain, const char* dirpath, uint256_t currentSupply) { bool Chain_SaveToFile(blockchain_t* chain, const char* dirpath, uint256_t currentSupply) {
@@ -247,7 +262,7 @@ bool Chain_LoadFromFile(blockchain_t* chain, const char* dirpath, uint256_t* out
fclose(metaFile); fclose(metaFile);
// TODO: Might add a flag to allow reading from a point onward, but just rewrite for now // TODO: Might add a flag to allow reading from a point onward, but just rewrite for now
DynArr_erase(chain->blocks); // Clear current chain blocks, but keep allocated memory for efficiency, since we will likely be loading a similar number of blocks as currently in memory. Chain_ClearBlocks(chain); // Clear current chain blocks and free owned transaction buffers before reload.
// Load blocks // Load blocks
for (size_t i = 0; i < savedSize; i++) { for (size_t i = 0; i < savedSize; i++) {
@@ -280,7 +295,7 @@ bool Chain_LoadFromFile(blockchain_t* chain, const char* dirpath, uint256_t* out
return false; return false;
} }
for (size_t j = 0; j < txSize; j++) { /*for (size_t j = 0; j < txSize; j++) {
signed_transaction_t tx; signed_transaction_t tx;
if (fread(&tx, sizeof(signed_transaction_t), 1, blockFile) != 1) { if (fread(&tx, sizeof(signed_transaction_t), 1, blockFile) != 1) {
fclose(blockFile); fclose(blockFile);
@@ -288,10 +303,16 @@ bool Chain_LoadFromFile(blockchain_t* chain, const char* dirpath, uint256_t* out
return false; return false;
} }
Block_AddTransaction(blk, &tx); Block_AddTransaction(blk, &tx);
} }*/ // Transactions are not read, we use the merkle root for validity
fclose(blockFile);
fclose(blockFile);
Chain_AddBlock(chain, blk); Chain_AddBlock(chain, blk);
if (blk->transactions) {
DynArr_destroy(blk->transactions);
blk->transactions = NULL;
}
free(blk); // chain stores block headers/fields by value
} }
chain->size = savedSize; chain->size = savedSize;

View File

@@ -87,12 +87,12 @@ static bool MineBlock(block_t* block) {
} }
} }
int main(void) { int main(int argc, char* argv[]) {
signal(SIGINT, handle_sigint); signal(SIGINT, handle_sigint);
const char* chainDataDir = CHAIN_DATA_DIR; const char* chainDataDir = CHAIN_DATA_DIR;
const uint64_t blocksToMine = 10; const uint64_t blocksToMine = 10;
const double targetSeconds = 90.0; const double targetSeconds = TARGET_BLOCK_TIME;
uint256_t currentSupply = uint256_from_u64(0); uint256_t currentSupply = uint256_from_u64(0);
@@ -134,91 +134,117 @@ int main(void) {
} }
} }
const double hps = MeasureRandomXHashrate(); // Get flag from argv "-mine" to mine blocks
const double expectedHashes = (hps > 0.0) ? (hps * targetSeconds) : 65536.0; if (argc > 1 && strcmp(argv[1], "-mine") == 0) {
const uint32_t calibratedBits = CompactTargetForExpectedHashes(expectedHashes); printf("Mining %llu blocks with target time %.0fs...\n", (unsigned long long)blocksToMine, targetSeconds);
printf("RandomX benchmark: %.2f H/s, target %.0fs, nBits=0x%08x\n", const double hps = MeasureRandomXHashrate();
hps, const double expectedHashes = (hps > 0.0) ? (hps * targetSeconds) : 65536.0;
targetSeconds, const uint32_t calibratedBits = CompactTargetForExpectedHashes(expectedHashes);
calibratedBits); //const uint32_t calibratedBits = 0xffffffff; // Absurdly low diff for testing
uint8_t minerAddress[32]; printf("RandomX benchmark: %.2f H/s, target %.0fs, nBits=0x%08x, diff=%.2f\n",
SHA256((const unsigned char*)"minicoin-miner-1", strlen("minicoin-miner-1"), minerAddress); hps,
targetSeconds,
calibratedBits,
(double)(1.f / calibratedBits) * 4294967296.0); // Basic representation as a big number for now
for (uint64_t mined = 0; mined < blocksToMine; ++mined) { uint8_t minerAddress[32];
block_t* block = Block_Create(); SHA256((const unsigned char*)"minicoin-miner-1", strlen("minicoin-miner-1"), minerAddress);
if (!block) {
fprintf(stderr, "failed to create block\n");
Chain_Destroy(chain);
RandomX_Destroy();
return 1;
}
block->header.version = 1; for (uint64_t mined = 0; mined < blocksToMine; ++mined) {
block->header.blockNumber = (uint64_t)Chain_Size(chain); block_t* block = Block_Create();
if (Chain_Size(chain) > 0) { if (!block) {
block_t* lastBlock = Chain_GetBlock(chain, Chain_Size(chain) - 1); fprintf(stderr, "failed to create block\n");
if (lastBlock) { Chain_Destroy(chain);
Block_CalculateHash(lastBlock, block->header.prevHash); RandomX_Destroy();
return 1;
}
block->header.version = 1;
block->header.blockNumber = (uint64_t)Chain_Size(chain);
if (Chain_Size(chain) > 0) {
block_t* lastBlock = Chain_GetBlock(chain, Chain_Size(chain) - 1);
if (lastBlock) {
Block_CalculateHash(lastBlock, block->header.prevHash);
} else {
memset(block->header.prevHash, 0, sizeof(block->header.prevHash));
}
} else { } else {
memset(block->header.prevHash, 0, sizeof(block->header.prevHash)); memset(block->header.prevHash, 0, sizeof(block->header.prevHash));
} }
block->header.timestamp = (uint64_t)time(NULL);
block->header.difficultyTarget = calibratedBits;
block->header.nonce = 0;
signed_transaction_t coinbaseTx;
memset(&coinbaseTx, 0, sizeof(coinbaseTx));
coinbaseTx.transaction.version = 1;
coinbaseTx.transaction.amount = CalculateBlockReward(currentSupply, block->header.blockNumber);
coinbaseTx.transaction.fee = 0;
memcpy(coinbaseTx.transaction.recipientAddress, minerAddress, sizeof(minerAddress));
memset(coinbaseTx.transaction.compressedPublicKey, 0, sizeof(coinbaseTx.transaction.compressedPublicKey));
memset(coinbaseTx.transaction.senderAddress, 0xFF, sizeof(coinbaseTx.transaction.senderAddress));
Block_AddTransaction(block, &coinbaseTx);
uint8_t merkleRoot[32];
Block_CalculateMerkleRoot(block, merkleRoot);
memcpy(block->header.merkleRoot, merkleRoot, sizeof(block->header.merkleRoot));
if (!MineBlock(block)) {
fprintf(stderr, "failed to mine block within nonce range\n");
Block_Destroy(block);
Chain_Destroy(chain);
RandomX_Destroy();
return 1;
}
if (!Chain_AddBlock(chain, block)) {
fprintf(stderr, "failed to append block to chain\n");
Block_Destroy(block);
Chain_Destroy(chain);
RandomX_Destroy();
return 1;
}
(void)uint256_add_u64(&currentSupply, coinbaseTx.transaction.amount);
uint8_t blockHash[32];
Block_CalculateHash(block, blockHash);
printf("Mined block %llu/%llu (height=%llu) nonce=%llu reward=%llu supply=%llu merkle=%02x%02x%02x%02x... hash=%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.amount,
(unsigned long long)currentSupply.limbs[0],
block->header.merkleRoot[0], block->header.merkleRoot[1], block->header.merkleRoot[2], block->header.merkleRoot[3],
blockHash[0], blockHash[1], blockHash[2], blockHash[3]);
free(block); // chain stores blocks by value; transactions are owned by chain copy
// Save chain after each mined block
Chain_SaveToFile(chain, chainDataDir, currentSupply);
}
if (!Chain_SaveToFile(chain, chainDataDir, currentSupply)) {
fprintf(stderr, "failed to save chain to %s\n", chainDataDir);
} else { } else {
memset(block->header.prevHash, 0, sizeof(block->header.prevHash)); printf("Saved chain with %zu blocks to %s (supply=%llu)\n",
Chain_Size(chain),
chainDataDir,
(unsigned long long)currentSupply.limbs[0]);
} }
memset(block->header.merkleRoot, 0, sizeof(block->header.merkleRoot)); } else {
block->header.timestamp = (uint64_t)time(NULL); printf("Current chain has %zu blocks, total supply %llu\n", Chain_Size(chain), (unsigned long long)currentSupply.limbs[0]);
block->header.difficultyTarget = calibratedBits;
block->header.nonce = 0;
signed_transaction_t coinbaseTx;
memset(&coinbaseTx, 0, sizeof(coinbaseTx));
coinbaseTx.transaction.version = 1;
coinbaseTx.transaction.amount = CalculateBlockReward(currentSupply, block->header.blockNumber);
coinbaseTx.transaction.fee = 0;
memcpy(coinbaseTx.transaction.recipientAddress, minerAddress, sizeof(minerAddress));
memset(coinbaseTx.transaction.compressedPublicKey, 0, sizeof(coinbaseTx.transaction.compressedPublicKey));
memset(coinbaseTx.transaction.senderAddress, 0xFF, sizeof(coinbaseTx.transaction.senderAddress));
Block_AddTransaction(block, &coinbaseTx);
if (!MineBlock(block)) {
fprintf(stderr, "failed to mine block within nonce range\n");
Block_Destroy(block);
Chain_Destroy(chain);
RandomX_Destroy();
return 1;
}
if (!Chain_AddBlock(chain, block)) {
fprintf(stderr, "failed to append block to chain\n");
Block_Destroy(block);
Chain_Destroy(chain);
RandomX_Destroy();
return 1;
}
(void)uint256_add_u64(&currentSupply, coinbaseTx.transaction.amount);
uint8_t blockHash[32];
Block_CalculateHash(block, blockHash);
printf("Mined block %llu/%llu (height=%llu) nonce=%llu reward=%llu supply=%llu hash=%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.amount,
(unsigned long long)currentSupply.limbs[0],
blockHash[0], blockHash[1], blockHash[2], blockHash[3]);
} }
if (!Chain_SaveToFile(chain, chainDataDir, currentSupply)) { // Print chain
fprintf(stderr, "failed to save chain to %s\n", chainDataDir); for (size_t i = 0; i < Chain_Size(chain); i++) {
} else { block_t* blk = Chain_GetBlock(chain, i);
printf("Saved chain with %zu blocks to %s (supply=%llu)\n", if (blk) {
Chain_Size(chain), Block_Print(blk);
chainDataDir, }
(unsigned long long)currentSupply.limbs[0]);
} }
Chain_Destroy(chain); Chain_Destroy(chain);