494 lines
17 KiB
C
494 lines
17 KiB
C
#include <block/block.h>
|
|
#include <block/chain.h>
|
|
#include <autolykos2/autolykos2.h>
|
|
#include <utils.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 pthread_mutex_t g_powCtxLock = PTHREAD_MUTEX_INITIALIZER;
|
|
static uint64_t g_dagEpoch = 0;
|
|
// The seed the current DAG was generated from. Matching on epoch index and size is NOT enough: a
|
|
// reorg replaces the block an epoch's seed is derived from while leaving the epoch index and size
|
|
// unchanged, so a stale DAG would still look current and silently hash against the wrong lanes.
|
|
static uint8_t g_dagSeed[32];
|
|
static bool g_dagReady = false;
|
|
|
|
// Caller must hold `g_powCtxLock`.
|
|
static Autolykos2Context* GetAutolykos2CtxLocked(void) {
|
|
if (!g_autolykos2Ctx) {
|
|
g_autolykos2Ctx = Autolykos2_Create();
|
|
if (!g_autolykos2Ctx) {
|
|
fprintf(stderr, "Failed to create Autolykos2 context\n");
|
|
exit(1);
|
|
}
|
|
// Deliberately no DagAllocate here. Allocating without generating leaves dag.len == 0, so
|
|
// every heavy hash fails -- which used to be indistinguishable from a valid proof, because
|
|
// the failure path handed back a zeroed hash that compares below every target.
|
|
}
|
|
return g_autolykos2Ctx;
|
|
}
|
|
|
|
void Block_ShutdownPowContext(void) {
|
|
pthread_mutex_lock(&g_powCtxLock);
|
|
if (g_autolykos2Ctx) {
|
|
Autolykos2_Destroy(g_autolykos2Ctx);
|
|
g_autolykos2Ctx = NULL;
|
|
}
|
|
g_dagReady = false;
|
|
pthread_mutex_unlock(&g_powCtxLock);
|
|
}
|
|
|
|
bool Block_EnsureAutolykos2Dag(uint64_t epochIndex, size_t dagBytes, const uint8_t seed32[32]) {
|
|
if (!seed32 || dagBytes < 32u || (dagBytes % 32u) != 0u) {
|
|
return false;
|
|
}
|
|
|
|
pthread_mutex_lock(&g_powCtxLock);
|
|
|
|
// Already built from exactly this seed at this size: generation is seconds of work, never redo
|
|
// it. The seed has to be part of the test -- see g_dagSeed.
|
|
if (g_dagReady && g_autolykos2Ctx && g_dagEpoch == epochIndex &&
|
|
Autolykos2_DagSize(g_autolykos2Ctx) == dagBytes &&
|
|
memcmp(g_dagSeed, seed32, 32) == 0) {
|
|
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);
|
|
const bool ok = Autolykos2_DagAllocate(ctx, dagBytes) && Autolykos2_DagGenerate(ctx, seed32);
|
|
if (ok) {
|
|
g_dagEpoch = epochIndex;
|
|
memcpy(g_dagSeed, seed32, 32);
|
|
g_dagReady = true;
|
|
}
|
|
|
|
pthread_mutex_unlock(&g_powCtxLock);
|
|
return ok;
|
|
}
|
|
|
|
bool Block_PowHashHeavy(const block_t* block, uint64_t epochIndex, size_t dagBytes,
|
|
const uint8_t seed32[32], uint8_t outHash[32]) {
|
|
if (!block || !seed32 || !outHash) {
|
|
return false;
|
|
}
|
|
|
|
pthread_mutex_lock(&g_powCtxLock);
|
|
// Verifying the SEED here, not just the epoch and size, is what makes this impossible to
|
|
// misuse. A reorg changes the block an epoch's seed is derived from while the epoch index and
|
|
// size stay put, so an epoch+size check alone happily accepts a DAG built from the pre-reorg
|
|
// seed and returns a hash for the wrong lanes -- which shows up as a valid block failing PoW
|
|
// while a branch is being applied. A mismatch yields false and the caller derives the lanes
|
|
// from the seed instead.
|
|
const bool usable = g_dagReady && g_autolykos2Ctx && g_dagEpoch == epochIndex &&
|
|
Autolykos2_DagSize(g_autolykos2Ctx) == dagBytes &&
|
|
memcmp(g_dagSeed, seed32, 32) == 0;
|
|
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 = (block_t*)malloc(sizeof(block_t));
|
|
if (!block) {
|
|
return NULL;
|
|
}
|
|
memset(&block->header, 0, sizeof(block_header_t));
|
|
block->transactions = vector_create(sizeof(signed_transaction_t));
|
|
if (!block->transactions) {
|
|
free(block);
|
|
return NULL;
|
|
}
|
|
|
|
// Zero out padding
|
|
memset(block->header.reserved, 0, sizeof(block->header.reserved));
|
|
|
|
return block;
|
|
}
|
|
|
|
void Block_CalculateHash(const block_t* block, uint8_t* outHash) {
|
|
if (!block || !outHash) {
|
|
return;
|
|
}
|
|
|
|
// 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_CalculateMerkleRoot(const block_t* block, uint8_t* outHash) {
|
|
if (!block || !block->transactions || !outHash) {
|
|
return;
|
|
}
|
|
|
|
const size_t txCount = vector_size(block->transactions);
|
|
if (txCount == 0) {
|
|
memset(outHash, 0, 32);
|
|
return;
|
|
}
|
|
if (txCount == 1) {
|
|
signed_transaction_t* tx = (signed_transaction_t*)vector_get(block->transactions, 0);
|
|
Transaction_CalculateHash(tx, outHash);
|
|
return;
|
|
}
|
|
|
|
uint8_t* current = (uint8_t*)malloc(txCount * 32u);
|
|
uint8_t* next = (uint8_t*)malloc(txCount * 32u);
|
|
if (!current || !next) {
|
|
free(current);
|
|
free(next);
|
|
memset(outHash, 0, 32);
|
|
return;
|
|
}
|
|
|
|
for (size_t i = 0; i < txCount; ++i) {
|
|
signed_transaction_t* tx = (signed_transaction_t*)vector_get(block->transactions, i);
|
|
if (!tx) {
|
|
free(current);
|
|
free(next);
|
|
memset(outHash, 0, 32);
|
|
return;
|
|
}
|
|
Transaction_CalculateHash(tx, current + (i * 32u));
|
|
}
|
|
|
|
size_t levelCount = txCount;
|
|
while (levelCount > 1) {
|
|
size_t nextCount = 0;
|
|
for (size_t i = 0; i < levelCount; i += 2) {
|
|
const uint8_t* left = current + (i * 32u);
|
|
const uint8_t* right = (i + 1 < levelCount) ? current + ((i + 1) * 32u) : left;
|
|
|
|
uint8_t dataInBuffer[64];
|
|
memcpy(dataInBuffer, left, 32);
|
|
memcpy(dataInBuffer + 32, right, 32);
|
|
|
|
SHA256((const unsigned char*)dataInBuffer, 64, next + (nextCount * 32u));
|
|
SHA256(next + (nextCount * 32u), 32, next + (nextCount * 32u));
|
|
++nextCount;
|
|
}
|
|
|
|
uint8_t* swap = current;
|
|
current = next;
|
|
next = swap;
|
|
levelCount = nextCount;
|
|
}
|
|
|
|
memcpy(outHash, current, 32);
|
|
free(current);
|
|
free(next);
|
|
}
|
|
|
|
void Block_AddTransaction(block_t* block, signed_transaction_t* tx) {
|
|
if (!block || !tx || !block->transactions) {
|
|
return;
|
|
}
|
|
|
|
vector_push_back(block->transactions, tx);
|
|
}
|
|
|
|
void Block_RemoveTransaction(block_t* block, uint8_t* txHash) {
|
|
if (!block || !txHash || !block->transactions) {
|
|
return;
|
|
}
|
|
|
|
for (size_t i = 0; i < vector_size(block->transactions); i++) {
|
|
signed_transaction_t* currentTx = (signed_transaction_t*)vector_get(block->transactions, i);
|
|
uint8_t currentTxHash[32];
|
|
Transaction_CalculateHash(currentTx, currentTxHash);
|
|
if (memcmp(currentTxHash, txHash, 32) == 0) {
|
|
vector_pop_at(block->transactions, i);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static int Uint256_CompareBE(const uint8_t a[32], const uint8_t b[32]) {
|
|
for (int i = 0; i < 32; ++i) {
|
|
if (a[i] < b[i]) return -1;
|
|
if (a[i] > b[i]) return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
bool Block_HasValidProofOfWorkWithParams(const block_t* block, uint64_t epochIndex,
|
|
size_t dagBytes, const uint8_t seed32[32]) {
|
|
if (!block) {
|
|
return false;
|
|
}
|
|
|
|
uint8_t target[32];
|
|
if (!DecodeCompactTarget(block->header.difficultyTarget, target)) {
|
|
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];
|
|
if (!Block_PowHashHeavy(block, epochIndex, dagBytes, seed32, hash) &&
|
|
!Block_PowHashLight(block, dagBytes, seed32, hash)) {
|
|
// Fail CLOSED. This used to hand back a zeroed hash on any failure and compare that to the
|
|
// target -- and zero is below every target, so a DAG that was missing, mis-sized or failed
|
|
// to build made the PoW check pass for every block instead of rejecting them.
|
|
return false;
|
|
}
|
|
|
|
return Uint256_CompareBE(hash, target) <= 0;
|
|
}
|
|
|
|
bool Block_HasValidProofOfWork(const block_t* block, blockchain_t* chain) {
|
|
if (!block || !chain) {
|
|
return false;
|
|
}
|
|
|
|
size_t dagBytes = 0;
|
|
uint8_t seed[32];
|
|
if (!Chain_DagParamsForHeight(chain, block->header.blockNumber, &dagBytes, seed)) {
|
|
return false;
|
|
}
|
|
|
|
const uint64_t epochIndex = block->header.blockNumber / (uint64_t)EPOCH_LENGTH;
|
|
return Block_HasValidProofOfWorkWithParams(block, epochIndex, dagBytes, seed);
|
|
}
|
|
|
|
bool Block_HasValidVote(const block_t* block) {
|
|
if (!block) {
|
|
return false;
|
|
}
|
|
|
|
// Unrecognised vote values and non-zero spare bytes are rejected rather than ignored, so the
|
|
// header has no bits whose meaning is undefined and nothing to grind for extra nonce space.
|
|
return block->header.reserved[0] <= (uint8_t)DAG_VOTE_MAX &&
|
|
block->header.reserved[1] == 0u &&
|
|
block->header.reserved[2] == 0u;
|
|
}
|
|
|
|
bool Block_AllTransactionsValid(const block_t* block) {
|
|
if (!block || !block->transactions) {
|
|
return false;
|
|
}
|
|
|
|
bool hasCoinbase = false;
|
|
|
|
for (size_t i = 0; i < vector_size(block->transactions); i++) {
|
|
signed_transaction_t* tx = (signed_transaction_t*)vector_get(block->transactions, i);
|
|
if (!Transaction_Verify(tx)) {
|
|
return false;
|
|
}
|
|
|
|
if (tx && Address_IsCoinbase(tx->transaction.senderAddress)) {
|
|
if (hasCoinbase) {
|
|
return false;
|
|
}
|
|
|
|
hasCoinbase = true;
|
|
}
|
|
}
|
|
|
|
return true && hasCoinbase && vector_size(block->transactions) > 0; // Every block must have at least one transaction (the coinbase)
|
|
}
|
|
|
|
bool Block_ValidateCoinbaseAndFees(const block_t* block, uint64_t expectedCoinbaseAmount, uint64_t* outTotalFees) {
|
|
if (!block || !block->transactions) {
|
|
return false;
|
|
}
|
|
|
|
bool hasCoinbase = false;
|
|
uint64_t totalFees = 0;
|
|
uint8_t zeroAddress[32] = {0};
|
|
|
|
for (size_t i = 0; i < vector_size(block->transactions); ++i) {
|
|
signed_transaction_t* tx = (signed_transaction_t*)vector_get(block->transactions, i);
|
|
if (!tx) {
|
|
return false;
|
|
}
|
|
|
|
if (Address_IsCoinbase(tx->transaction.senderAddress)) {
|
|
if (hasCoinbase) {
|
|
return false;
|
|
}
|
|
|
|
hasCoinbase = true;
|
|
|
|
if (!Transaction_Verify(tx)) {
|
|
return false;
|
|
}
|
|
|
|
if (tx->transaction.fee != 0 || tx->transaction.amount2 != 0) {
|
|
return false;
|
|
}
|
|
|
|
if (tx->transaction.amount1 != expectedCoinbaseAmount) {
|
|
return false;
|
|
}
|
|
|
|
if (Address_IsCoinbase(tx->transaction.recipientAddress1)) {
|
|
return false;
|
|
}
|
|
|
|
if (memcmp(tx->transaction.recipientAddress2, zeroAddress, sizeof(zeroAddress)) != 0) {
|
|
return false;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (!Transaction_Verify(tx)) {
|
|
return false;
|
|
}
|
|
|
|
if (UINT64_MAX - totalFees < tx->transaction.fee) {
|
|
return false;
|
|
}
|
|
totalFees += tx->transaction.fee;
|
|
}
|
|
|
|
if (!hasCoinbase) {
|
|
return false;
|
|
}
|
|
|
|
if (outTotalFees) {
|
|
*outTotalFees = totalFees;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Block_HasValidStructure(const block_t* block) {
|
|
if (!block || !block->transactions) {
|
|
return false;
|
|
}
|
|
|
|
uint8_t calculatedMerkleRoot[32];
|
|
Block_CalculateMerkleRoot(block, calculatedMerkleRoot);
|
|
if (memcmp(calculatedMerkleRoot, block->header.merkleRoot, 32) != 0) {
|
|
return false;
|
|
}
|
|
|
|
return Block_HasValidVote(block) &&
|
|
Block_AllTransactionsValid(block) &&
|
|
vector_size(block->transactions) > 0;
|
|
}
|
|
|
|
bool Block_IsFullyValid(const block_t* block, blockchain_t* chain) {
|
|
return Block_HasValidStructure(block) && Block_HasValidProofOfWork(block, chain);
|
|
}
|
|
|
|
void Block_Destroy(block_t* block) {
|
|
if (!block) return;
|
|
vector_destroy(&block->transactions);
|
|
free(block);
|
|
}
|
|
|
|
void Block_Print(const block_t* block) {
|
|
if (!block) return;
|
|
|
|
printf("Block #%llu\n", (unsigned long long)block->header.blockNumber);
|
|
printf("Timestamp: %llu\n", (unsigned long long)block->header.timestamp);
|
|
printf("Nonce: %llu\n", (unsigned long long)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", vector_size(block->transactions));
|
|
for (size_t i = 0; i < vector_size(block->transactions); i++) {
|
|
signed_transaction_t* tx = (signed_transaction_t*)vector_get(block->transactions, i);
|
|
if (tx) {
|
|
printf(" Tx #%zu: 1: %llu -> %02x%02x...%02x%02x, fee %llu\n 2: %llu -> %02x%02x...%02x%02x, fee %llu\n",
|
|
i,
|
|
(unsigned long long)tx->transaction.amount1,
|
|
tx->transaction.recipientAddress1[0], tx->transaction.recipientAddress1[1], tx->transaction.recipientAddress1[30], tx->transaction.recipientAddress1[31],
|
|
(unsigned long long)tx->transaction.fee,
|
|
(unsigned long long)tx->transaction.amount2,
|
|
tx->transaction.recipientAddress2[0], tx->transaction.recipientAddress2[1], tx->transaction.recipientAddress2[30], tx->transaction.recipientAddress2[31],
|
|
(unsigned long long)tx->transaction.fee);
|
|
}
|
|
}
|
|
} else {
|
|
printf("No transactions (or none loaded)\n");
|
|
}
|
|
}
|
|
|
|
void Block_ShortPrint(const block_t* block) {
|
|
if (!block) return;
|
|
|
|
printf("Block #%llu: Timestamp %llu, Nonce %llu, DiffTarget 0x%08x, Version %u, PrevHash %02x%02x...%02x%02x, MerkleRoot %02x%02x...%02x%02x, TxCount %zu\n",
|
|
(unsigned long long)block->header.blockNumber,
|
|
(unsigned long long)block->header.timestamp,
|
|
(unsigned long long)block->header.nonce,
|
|
block->header.difficultyTarget,
|
|
block->header.version,
|
|
block->header.prevHash[0], block->header.prevHash[1], block->header.prevHash[30], block->header.prevHash[31],
|
|
block->header.merkleRoot[0], block->header.merkleRoot[1], block->header.merkleRoot[30], block->header.merkleRoot[31],
|
|
vector_size(block->transactions));
|
|
}
|
|
|
|
block_t* Block_Copy(const block_t* src) {
|
|
if (!src) return NULL;
|
|
block_t* dst = (block_t*)malloc(sizeof(block_t));
|
|
if (!dst) return NULL;
|
|
dst->header = src->header;
|
|
if (src->transactions) {
|
|
// Transactions own no memory of their own, so the vector has no destructor and a byte-wise
|
|
// deep copy is exactly an independent copy of the list.
|
|
dst->transactions = vector_deep_copy(src->transactions);
|
|
if (!dst->transactions) {
|
|
free(dst);
|
|
return NULL;
|
|
}
|
|
} else {
|
|
dst->transactions = NULL;
|
|
}
|
|
return dst;
|
|
}
|