feat: added mining service
This commit is contained in:
@@ -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>): 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<String> = transaction {
|
||||||
|
Tables.Transactions.selectAll()
|
||||||
|
.where { Tables.Transactions.status eq "pending" }
|
||||||
|
.orderBy(Tables.Transactions.timestamp, SortOrder.ASC)
|
||||||
|
.limit(limit)
|
||||||
|
.map { it[Tables.Transactions.hash] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user