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 <string.h>
#include <randomx/librx_wrapper.h>
#include <stdlib.h>
typedef struct {
uint64_t blockNumber;
@@ -27,11 +28,13 @@ typedef struct {
block_t* Block_Create();
void Block_CalculateHash(const block_t* block, uint8_t* outHash);
void Block_CalculateMerkleRoot(const block_t* block, uint8_t* outHash);
void Block_CalculateRandomXHash(const block_t* block, uint8_t* outHash);
void Block_AddTransaction(block_t* block, signed_transaction_t* tx);
void Block_RemoveTransaction(block_t* block, uint8_t* txHash);
bool Block_HasValidProofOfWork(const block_t* block);
bool Block_AllTransactionsValid(const block_t* block);
void Block_Destroy(block_t* block);
void Block_Print(const block_t* block);
#endif

View File

@@ -6,7 +6,10 @@
#include <stdbool.h>
#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 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.
@@ -26,8 +29,11 @@ static inline uint64_t CalculateBlockReward(uint256_t currentSupply, uint64_t he
uint64_t supply_64 = currentSupply.limbs[0];
// Formula: (M - Supply) >> 2^k - lifted from Monero's codebase (thanks guys!)
uint64_t reward = (M_CAP - supply_64) >> EMISSION_SPEED_FACTOR;
// 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
// Check if the calculated reward has fallen below the floor
if (reward < TAIL_EMISSION) {

View File

@@ -16,40 +16,113 @@ block_t* Block_Create() {
}
void Block_CalculateHash(const block_t* block, uint8_t* outHash) {
if (!block || !outHash || !block->transactions || DynArr_size(block->transactions) <= 0) {
if (!block || !outHash) {
return;
}
// Merkle root TODO
// 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);
// Canonical block hash commits to header fields, including merkleRoot.
SHA256((const unsigned char*)&block->header, sizeof(block_header_t), outHash);
SHA256(outHash, 32, outHash); // Double-Hash
}
void Block_CalculateRandomXHash(const block_t* block, uint8_t* outHash) {
if (!block || !outHash || !block->transactions || DynArr_size(block->transactions) <= 0) {
void Block_CalculateMerkleRoot(const block_t* block, uint8_t* outHash) {
if (!block || !block->transactions || !outHash) {
return;
}
// Merkle root TODO
// 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));
const size_t txCount = DynArr_size(block->transactions);
if (txCount == 0) {
memset(outHash, 0, 32);
return;
}
if (txCount == 1) {
signed_transaction_t* tx = (signed_transaction_t*)DynArr_at(block->transactions, 0);
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) {
@@ -162,3 +235,34 @@ void Block_Destroy(block_t* block) {
DynArr_destroy(block->transactions);
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;
}
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* ptr = (blockchain_t*)malloc(sizeof(blockchain_t));
if (!ptr) {
@@ -47,6 +64,7 @@ blockchain_t* Chain_Create() {
void Chain_Destroy(blockchain_t* chain) {
if (chain) {
if (chain->blocks) {
Chain_ClearBlocks(chain);
DynArr_destroy(chain->blocks);
}
free(chain);
@@ -113,10 +131,7 @@ bool Chain_IsValid(blockchain_t* chain) {
}
void Chain_Wipe(blockchain_t* chain) {
if (chain && chain->blocks) {
DynArr_erase(chain->blocks);
chain->size = 0;
}
Chain_ClearBlocks(chain);
}
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);
// 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
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;
}
for (size_t j = 0; j < txSize; j++) {
/*for (size_t j = 0; j < txSize; j++) {
signed_transaction_t tx;
if (fread(&tx, sizeof(signed_transaction_t), 1, blockFile) != 1) {
fclose(blockFile);
@@ -288,10 +303,16 @@ bool Chain_LoadFromFile(blockchain_t* chain, const char* dirpath, uint256_t* out
return false;
}
Block_AddTransaction(blk, &tx);
}
fclose(blockFile);
}*/ // Transactions are not read, we use the merkle root for validity
fclose(blockFile);
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;

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);
const char* chainDataDir = CHAIN_DATA_DIR;
const uint64_t blocksToMine = 10;
const double targetSeconds = 90.0;
const double targetSeconds = TARGET_BLOCK_TIME;
uint256_t currentSupply = uint256_from_u64(0);
@@ -134,91 +134,117 @@ int main(void) {
}
}
const double hps = MeasureRandomXHashrate();
const double expectedHashes = (hps > 0.0) ? (hps * targetSeconds) : 65536.0;
const uint32_t calibratedBits = CompactTargetForExpectedHashes(expectedHashes);
// Get flag from argv "-mine" to mine blocks
if (argc > 1 && strcmp(argv[1], "-mine") == 0) {
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",
hps,
targetSeconds,
calibratedBits);
const double hps = MeasureRandomXHashrate();
const double expectedHashes = (hps > 0.0) ? (hps * targetSeconds) : 65536.0;
const uint32_t calibratedBits = CompactTargetForExpectedHashes(expectedHashes);
//const uint32_t calibratedBits = 0xffffffff; // Absurdly low diff for testing
uint8_t minerAddress[32];
SHA256((const unsigned char*)"minicoin-miner-1", strlen("minicoin-miner-1"), minerAddress);
printf("RandomX benchmark: %.2f H/s, target %.0fs, nBits=0x%08x, diff=%.2f\n",
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) {
block_t* block = Block_Create();
if (!block) {
fprintf(stderr, "failed to create block\n");
Chain_Destroy(chain);
RandomX_Destroy();
return 1;
}
uint8_t minerAddress[32];
SHA256((const unsigned char*)"minicoin-miner-1", strlen("minicoin-miner-1"), minerAddress);
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);
for (uint64_t mined = 0; mined < blocksToMine; ++mined) {
block_t* block = Block_Create();
if (!block) {
fprintf(stderr, "failed to create block\n");
Chain_Destroy(chain);
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 {
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 {
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));
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);
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]);
} else {
printf("Current chain has %zu blocks, total supply %llu\n", Chain_Size(chain), (unsigned long long)currentSupply.limbs[0]);
}
if (!Chain_SaveToFile(chain, chainDataDir, currentSupply)) {
fprintf(stderr, "failed to save chain to %s\n", chainDataDir);
} else {
printf("Saved chain with %zu blocks to %s (supply=%llu)\n",
Chain_Size(chain),
chainDataDir,
(unsigned long long)currentSupply.limbs[0]);
// Print chain
for (size_t i = 0; i < Chain_Size(chain); i++) {
block_t* blk = Chain_GetBlock(chain, i);
if (blk) {
Block_Print(blk);
}
}
Chain_Destroy(chain);