diff --git a/server/src/main/kotlin/org/ccoin/services/MiningService.kt b/server/src/main/kotlin/org/ccoin/services/MiningService.kt index e69de29..925fb19 100644 --- a/server/src/main/kotlin/org/ccoin/services/MiningService.kt +++ b/server/src/main/kotlin/org/ccoin/services/MiningService.kt @@ -0,0 +1,185 @@ +package org.ccoin.services + +import org.ccoin.config.ServerConfig +import org.ccoin.database.Tables +import org.ccoin.exceptions.InvalidTransactionException +import org.ccoin.models.BlockResponse +import org.ccoin.models.MiningJobResponse +import org.ccoin.models.MiningStatsResponse +import org.ccoin.utils.CryptoUtils +import org.ccoin.utils.HashUtils +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.transaction +import java.math.BigDecimal +import java.time.Instant + +object MiningService { + + /** Starts a mining job for a miner */ + fun startMining(minerAddress: String, difficulty: Int? = null): MiningJobResponse { + val jobId = CryptoUtils.generateJobId() + val currentDifficulty = difficulty ?: getCurrentDifficulty() + val previousHash = getLatestBlockHash() + val height = getNextBlockHeight() + val timestamp = Instant.now().epochSecond + val expiresAt = timestamp + 300 // 5 minutes + val target = "0".repeat(currentDifficulty) + + return MiningJobResponse( + jobId, target, currentDifficulty, previousHash, height, timestamp, expiresAt + ) + } + + /** Submits mining result and creates block if valid */ + fun submitMiningResult( + minerAddress: String, + nonce: Long, + hash: String, + previousHash: String, + timestamp: Long = Instant.now().epochSecond + ): BlockResponse { + val currentDifficulty = getCurrentDifficulty() + + // Validate hash meets difficulty requirements + if (!CryptoUtils.isValidHash(hash, currentDifficulty)) { + throw InvalidTransactionException("Hash does not meet difficulty requirements") + } + + // Validate previous hash matches latest block + val latestHash = getLatestBlockHash() + if (previousHash != latestHash) { + throw InvalidTransactionException("Previous hash does not match latest block") + } + + val height = getNextBlockHeight() + val reward = ServerConfig.miningReward + val merkleRoot = calculateMerkleRoot(emptyList()) // No transactions for now + + return transaction { + // Create block + Tables.Blocks.insert { + it[Tables.Blocks.hash] = hash + it[Tables.Blocks.previousHash] = if (previousHash == "0".repeat(64)) null else previousHash + it[Tables.Blocks.merkleRoot] = merkleRoot + it[Tables.Blocks.timestamp] = timestamp + it[Tables.Blocks.difficulty] = currentDifficulty + it[Tables.Blocks.nonce] = nonce + it[Tables.Blocks.minerAddress] = minerAddress + it[Tables.Blocks.reward] = BigDecimal.valueOf(reward) + it[transactionCount] = 0 + } + + // Reward miner with genesis transaction + TransactionService.createGenesisTransaction(minerAddress, reward) + + BlockResponse( + hash, if (previousHash == "0".repeat(64)) null else previousHash, merkleRoot, + timestamp, currentDifficulty, nonce, minerAddress, reward, height, 0, 0 + ) + } + } + + /** Gets current mining difficulty */ + fun getCurrentDifficulty(): Int = ServerConfig.miningDifficulty + + /** Gets mining statistics for a miner */ + fun getMinerStats(minerAddress: String): MiningStatsResponse = transaction { + val blocks = Tables.Blocks.selectAll() + .where { Tables.Blocks.minerAddress eq minerAddress } + .orderBy(Tables.Blocks.timestamp, SortOrder.DESC) + + val totalBlocks = blocks.count().toInt() + val totalReward = blocks.sumOf { it[Tables.Blocks.reward] }.toDouble() + val lastBlockMined = blocks.firstOrNull()?.get(Tables.Blocks.timestamp)?.toLong() + + MiningStatsResponse( + minerAddress, totalBlocks, totalReward, lastBlockMined, getCurrentDifficulty() + ) + } + + /** Gets latest block hash */ + private fun getLatestBlockHash(): String = transaction { + Tables.Blocks.selectAll() + .orderBy(Tables.Blocks.height, SortOrder.DESC) + .limit(1) + .map { it[Tables.Blocks.hash] } + .singleOrNull() ?: "0".repeat(64) // Genesis hash + } + + /** Gets next block height */ + private fun getNextBlockHeight(): Int = transaction { + val latestHeight = Tables.Blocks.selectAll() + .orderBy(Tables.Blocks.height, SortOrder.DESC) + .limit(1) + .map { it[Tables.Blocks.height] } + .singleOrNull() ?: 0 + + latestHeight + 1 + } + + /** Calculates merkle root for transactions */ + private fun calculateMerkleRoot(transactionHashes: List): String { + return if (transactionHashes.isEmpty()) { + HashUtils.sha256Hex("empty_block") + } else { + CryptoUtils.calculateMerkleRoot(transactionHashes) + } + } + + /** Gets network hash rate estimate */ + fun getNetworkHashRate(): Double = transaction { + val recentBlocks = Tables.Blocks.selectAll() + .orderBy(Tables.Blocks.timestamp, SortOrder.DESC) + .limit(100) + + if (recentBlocks.count() < 2) return@transaction 0.0 + + val blocks = recentBlocks.toList() + val timeSpan = blocks.first()[Tables.Blocks.timestamp] - blocks.last()[Tables.Blocks.timestamp] + val difficulty = getCurrentDifficulty() + + if (timeSpan <= 0) return@transaction 0.0 + + // Rough estimate: (blocks * 2^difficulty) / time_span + (blocks.size * Math.pow(2.0, difficulty.toDouble())) / timeSpan + } + + /** Gets average block time */ + fun getAverageBlockTime(): Long = transaction { + val recentBlocks = Tables.Blocks.selectAll() + .orderBy(Tables.Blocks.timestamp, SortOrder.DESC) + .limit(100) + .map { it[Tables.Blocks.timestamp] } + + if (recentBlocks.size < 2) return@transaction 0L + + val timeDiffs = recentBlocks.zipWithNext { a, b -> a - b } + timeDiffs.average().toLong() + } + + /** Gets total number of active miners */ + fun getActiveMinersCount(): Int = transaction { + val oneDayAgo = Instant.now().epochSecond - 86400 + + Tables.Blocks.selectAll() + .where { Tables.Blocks.timestamp greater oneDayAgo } + .groupBy(Tables.Blocks.minerAddress) + .count().toInt() + } + + /** Validates mining job */ + fun validateMiningJob(jobId: String, hash: String, nonce: Long): Boolean { + // Simple validation - in production you'd store job details + return CryptoUtils.isValidHash(hash, getCurrentDifficulty()) + } + + /** Gets pending transactions for mining */ + fun getPendingTransactionsForMining(limit: Int = 100): List = transaction { + Tables.Transactions.selectAll() + .where { Tables.Transactions.status eq "pending" } + .orderBy(Tables.Transactions.timestamp, SortOrder.ASC) + .limit(limit) + .map { it[Tables.Transactions.hash] } + } +} +