diff --git a/server/gradle.properties b/server/gradle.properties index ff0ed32..795c5e1 100644 --- a/server/gradle.properties +++ b/server/gradle.properties @@ -19,6 +19,7 @@ kotlin.code.style=official org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 org.gradle.caching=true org.gradle.parallel=true +org.gradle.configuration-cache=true # Application ccoin.version=1.0.0 diff --git a/server/src/main/kotlin/org/ccoin/models/Block.kt b/server/src/main/kotlin/org/ccoin/models/Block.kt index a1b8966..9c2e53f 100644 --- a/server/src/main/kotlin/org/ccoin/models/Block.kt +++ b/server/src/main/kotlin/org/ccoin/models/Block.kt @@ -25,7 +25,7 @@ data class BlockResponse( val timestamp: Long, val difficulty: Int, val nonce: Long, - val minerAddress: String + val minerAddress: String, val reward: Double, val height: Int, val transactionCount: Int, @@ -51,3 +51,11 @@ data class MiningStatsResponse( val lastBlockMined: Long?, val currentDifficulty: Int ) + +@Serializable +data class BlockRangeResponse( + val blocks: List, + val totalCount: Int, + val fromHeight: Int, + val toHeight: Int +) diff --git a/server/src/main/kotlin/org/ccoin/services/BlockService.kt b/server/src/main/kotlin/org/ccoin/services/BlockService.kt index e69de29..26c8352 100644 --- a/server/src/main/kotlin/org/ccoin/services/BlockService.kt +++ b/server/src/main/kotlin/org/ccoin/services/BlockService.kt @@ -0,0 +1,218 @@ +package org.ccoin.services + +import org.ccoin.database.Tables +import org.ccoin.models.BlockResponse +import org.ccoin.models.BlockRangeResponse +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.transaction + +object BlockService { + /** Gets block by hash */ + fun getBlock(hash: String): BlockResponse? = transaction { + Tables.Blocks.selectAll().where { Tables.Blocks.hash eq hash } + .map { + BlockResponse( + it[Tables.Blocks.hash], + it[Tables.Blocks.previousHash], + it[Tables.Blocks.merkleRoot], + it[Tables.Blocks.timestamp], + it[Tables.Blocks.difficulty], + it[Tables.Blocks.nonce], + it[Tables.Blocks.minerAddress], + it[Tables.Blocks.reward].toDouble(), + it[Tables.Blocks.height], + it[Tables.Blocks.transactionCount], + it[Tables.Blocks.confirmations] + ) + }.singleOrNull() + } + + /** Gets block by height */ + fun getBlockByHeight(height: Int): BlockResponse? = transaction { + Tables.Blocks.selectAll().where { Tables.Blocks.height eq height } + .map { + BlockResponse( + it[Tables.Blocks.hash], + it[Tables.Blocks.previousHash], + it[Tables.Blocks.merkleRoot], + it[Tables.Blocks.timestamp], + it[Tables.Blocks.difficulty], + it[Tables.Blocks.nonce], + it[Tables.Blocks.minerAddress], + it[Tables.Blocks.reward].toDouble(), + it[Tables.Blocks.height], + it[Tables.Blocks.transactionCount], + it[Tables.Blocks.confirmations] + ) + }.singleOrNull() + } + + /** Gets latest blocks */ + fun getLatestBlocks(limit: Int = 10): List = transaction { + Tables.Blocks.selectAll() + .orderBy(Tables.Blocks.height, SortOrder.DESC) + .limit(limit) + .map { + BlockResponse( + it[Tables.Blocks.hash], + it[Tables.Blocks.previousHash], + it[Tables.Blocks.merkleRoot], + it[Tables.Blocks.timestamp], + it[Tables.Blocks.difficulty], + it[Tables.Blocks.nonce], + it[Tables.Blocks.minerAddress], + it[Tables.Blocks.reward].toDouble(), + it[Tables.Blocks.height], + it[Tables.Blocks.transactionCount], + it[Tables.Blocks.confirmations] + ) + } + } + + /** Gets blocks in height range */ + fun getBlocksInRange(fromHeight: Int, toHeight: Int): BlockRangeResponse = transaction { + val blocks = Tables.Blocks.selectAll() + .where { Tables.Blocks.height.between(fromHeight, toHeight) } + .orderBy(Tables.Blocks.height, SortOrder.ASC) + .map { + BlockResponse( + it[Tables.Blocks.hash], + it[Tables.Blocks.previousHash], + it[Tables.Blocks.merkleRoot], + it[Tables.Blocks.timestamp], + it[Tables.Blocks.difficulty], + it[Tables.Blocks.nonce], + it[Tables.Blocks.minerAddress], + it[Tables.Blocks.reward].toDouble(), + it[Tables.Blocks.height], + it[Tables.Blocks.transactionCount], + it[Tables.Blocks.confirmations] + ) + } + + BlockRangeResponse(blocks, blocks.size, fromHeight, toHeight) + } + + /** Gets blocks mined by specific address */ + fun getBlocksByMiner(minerAddress: String, limit: Int = 50, offset: Int = 0): List = transaction { + Tables.Blocks.selectAll() + .where { Tables.Blocks.minerAddress eq minerAddress } + .orderBy(Tables.Blocks.height, SortOrder.DESC) + .limit(limit) + .offset(offset.toLong()) + .map { + BlockResponse( + it[Tables.Blocks.hash], + it[Tables.Blocks.previousHash], + it[Tables.Blocks.merkleRoot], + it[Tables.Blocks.timestamp], + it[Tables.Blocks.difficulty], + it[Tables.Blocks.nonce], + it[Tables.Blocks.minerAddress], + it[Tables.Blocks.reward].toDouble(), + it[Tables.Blocks.height], + it[Tables.Blocks.transactionCount], + it[Tables.Blocks.confirmations] + ) + } + } + + /** Gets total block count */ + fun getTotalBlockCount(): Long = transaction { + Tables.Blocks.selectAll().count() + } + + /** Gets latest block height */ + fun getLatestBlockHeight(): Int = transaction { + Tables.Blocks.selectAll() + .orderBy(Tables.Blocks.height, SortOrder.DESC) + .limit(1) + .map { it[Tables.Blocks.height] } + .singleOrNull() ?: 0 + } + + /** Gets latest block hash */ + fun getLatestBlockHash(): String = transaction { + Tables.Blocks.selectAll() + .orderBy(Tables.Blocks.height, SortOrder.DESC) + .limit(1) + .map { it[Tables.Blocks.hash] } + .singleOrNull() ?: "0".repeat(64) + } + + /** Checks if block exists */ + fun blockExists(hash: String): Boolean = transaction { + Tables.Blocks.selectAll().where { Tables.Blocks.hash eq hash }.count() > 0 + } + + /** Gets blocks by timestamp range */ + fun getBlocksByTimeRange(fromTime: Long, toTime: Long): List = transaction { + Tables.Blocks.selectAll() + .where { Tables.Blocks.timestamp.between(fromTime, toTime) } + .orderBy(Tables.Blocks.timestamp, SortOrder.ASC) + .map { + BlockResponse( + it[Tables.Blocks.hash], + it[Tables.Blocks.previousHash], + it[Tables.Blocks.merkleRoot], + it[Tables.Blocks.timestamp], + it[Tables.Blocks.difficulty], + it[Tables.Blocks.nonce], + it[Tables.Blocks.minerAddress], + it[Tables.Blocks.reward].toDouble(), + it[Tables.Blocks.height], + it[Tables.Blocks.transactionCount], + it[Tables.Blocks.confirmations] + ) + } + } + + /** Updates block confirmations */ + fun updateBlockConfirmations(hash: String, confirmations: Int): Boolean = transaction { + val updated = Tables.Blocks.update({ Tables.Blocks.hash eq hash }) { + it[Tables.Blocks.confirmations] = confirmations + } + updated > 0 + } + + /** Gets average block time over last N blocks */ + fun getAverageBlockTime(blockCount: Int = 100): Long = transaction { + val blocks = Tables.Blocks.selectAll() + .orderBy(Tables.Blocks.timestamp, SortOrder.DESC) + .limit(blockCount) + .map { it[Tables.Blocks.timestamp] } + + if (blocks.size < 2) return@transaction 0L + + val timeDiffs = blocks.zipWithNext { newer, older -> newer - older } + timeDiffs.average().toLong() + } + + /** Gets total rewards distributed */ + fun getTotalRewardsDistributed(): Double = transaction { + Tables.Blocks.select(Tables.Blocks.reward.sum()) + .single()[Tables.Blocks.reward.sum()]?.toDouble() ?: 0.0 + } + + /** Gets blocks with specific difficulty */ + fun getBlocksByDifficulty(difficulty: Int): List = transaction { + Tables.Blocks.selectAll() + .where { Tables.Blocks.difficulty eq difficulty } + .orderBy(Tables.Blocks.height, SortOrder.DESC) + .map { + BlockResponse( + it[Tables.Blocks.hash], + it[Tables.Blocks.previousHash], + it[Tables.Blocks.merkleRoot], + it[Tables.Blocks.timestamp], + it[Tables.Blocks.difficulty], + it[Tables.Blocks.nonce], + it[Tables.Blocks.minerAddress], + it[Tables.Blocks.reward].toDouble(), + it[Tables.Blocks.height], + it[Tables.Blocks.transactionCount], + it[Tables.Blocks.confirmations] + ) + } + } +} diff --git a/server/src/main/kotlin/org/ccoin/services/ValidationService.kt b/server/src/main/kotlin/org/ccoin/services/ValidationService.kt index e69de29..f6a2384 100644 --- a/server/src/main/kotlin/org/ccoin/services/ValidationService.kt +++ b/server/src/main/kotlin/org/ccoin/services/ValidationService.kt @@ -0,0 +1,200 @@ +package org.ccoin.services + +import org.ccoin.config.ServerConfig +import org.ccoin.exceptions.InvalidTransactionException +import org.ccoin.utils.CryptoUtils +import org.ccoin.utils.HashUtils + +object ValidationService { + /** Validates wallet address format */ + fun validateWalletAddress(address: String): Boolean = CryptoUtils.isValidAddress(address) + + /** Validates transaction amount */ + fun validateTransactionAmount(amount: Double): Boolean = amount > 0 && amount <= Double.MAX_VALUE && !amount.isNaN() && !amount.isInfinite() + + /** Validates transaction fee */ + fun validateTransactionFee(fee: Double): Boolean = fee >= 0 && fee <= Double.MAX_VALUE && !fee.isNaN() && !fee.isInfinite() + + /** Validates memo length */ + fun validateMemo(memo: String?): Boolean = memo == null || memo.length <= ServerConfig.maxMemoLength + + /** Validates transaction hash format */ + fun validateTransactionHash(hash: String): Boolean = HashUtils.isValidSha256Hash(hash) + + /** Validates block hash format */ + fun validateBlockHash(hash: String): Boolean = HashUtils.isValidSha256Hash(hash) + + /** Validates mining difficulty */ + fun validateMiningDifficulty(difficulty: Int): Boolean = difficulty in 1..32 + + /** Validates mining nonce */ + fun validateMiningNonce(nonce: Long): Boolean = nonce >= 0 + + /** Validates timestamp */ + fun validateTimestamp(timestamp: Long): Boolean { + val now = System.currentTimeMillis() / 1000 + val oneHourAgo = now - 3600 + val oneHourFromNow = now + 3600 + return timestamp in oneHourAgo..oneHourFromNow + } + + /** Validates wallet label */ + fun validateWalletLabel(label: String?): Boolean = label == null || (label.isNotBlank() && label.length <= 255) + + /** Validates page number for pagination */ + fun validatePageNumber(page: Int): Boolean = page >= 1 + + /** Validates page size for pagination */ + fun validatePageSize(pageSize: Int): Boolean = pageSize in 1..ServerConfig.maxPageSize + + /** Validates mining hash meets difficulty requirement */ + fun validateMiningHash(hash: String, difficulty: Int): Boolean = validateBlockHash(hash) && CryptoUtils.isValidHash(hash, difficulty) + + /** Validates complete transaction data */ + fun validateTransaction( + fromAddress: String?, + toAddress: String, + amount: Double, + fee: Double, + memo: String? + ): ValidationResult { + val errors = mutableListOf() + + // Validate addresses + if (fromAddress != null && !validateWalletAddress(fromAddress)) { + errors.add("Invalid from address format") + } + + if (!validateWalletAddress(toAddress)) { + errors.add("Invalid to address format") + } + + if (fromAddress == toAddress) { + errors.add("Cannot send to same address") + } + + // Validate amounts + if (!validateTransactionAmount(amount)) { + errors.add("Invalid transaction amount") + } + + if (!validateTransactionFee(fee)) { + errors.add("Invalid transaction fee") + } + + // Validate memo + if (!validateMemo(memo)) { + errors.add("Memo too long (max ${ServerConfig.maxMemoLength} characters)") + } + + return ValidationResult(errors.isEmpty(), errors) + } + + /** Validates complete block data */ + fun validateBlock( + hash: String, + previousHash: String?, + merkleRoot: String, + timestamp: Long, + difficulty: Int, + nonce: Long, + minerAddress: String + ): ValidationResult { + val errors = mutableListOf() + + // Validate hash + if (!validateBlockHash(hash)) { + errors.add("Invalid block hash format") + } + + // Validate previous hash + if (previousHash != null && !validateBlockHash(previousHash)) { + errors.add("Invalid previous hash format") + } + + // Validate merkle root + if (!validateBlockHash(merkleRoot)) { + errors.add("Invalid merkle root format") + } + + // Validate timestamp + if (!validateTimestamp(timestamp)) { + errors.add("Invalid timestamp (must be within 1 hour of current time)") + } + + // Validate difficulty + if (!validateMiningDifficulty(difficulty)) { + errors.add("Invalid mining difficulty (must be between 1 and 32)") + } + + // Validate nonce + if (!validateMiningNonce(nonce)) { + errors.add("Invalid nonce (must be non-negative)") + } + + // Validate miner address + if (!validateWalletAddress(minerAddress)) { + errors.add("Invalid miner address format") + } + + // Validate hash meets difficulty + if (!validateMiningHash(hash, difficulty)) { + errors.add("Hash does not meet difficulty requirements") + } + + return ValidationResult(errors.isEmpty(), errors) + } + + /** Validates wallet creation data */ + fun validateWalletCreation(label: String?): ValidationResult { + val errors = mutableListOf() + + if (!validateWalletLabel(label)) { + errors.add("Invalid wallet label") + } + + return ValidationResult(errors.isEmpty(), errors) + } + + /** Validates pagination parameters */ + fun validatePagination(page: Int, pageSize: Int): ValidationResult { + val errors = mutableListOf() + + if (!validatePageNumber(page)) { + errors.add("Page number must be >= 1") + } + + if (!validatePageSize(pageSize)) { + errors.add("Page size must be between 1 and ${ServerConfig.maxPageSize}") + } + + return ValidationResult(errors.isEmpty(), errors) + } + + /** Sanitizes user input */ + fun sanitizeInput(input: String): String { + return input.trim() + .replace(Regex("[\\r\\n\\t]"), " ") + .replace(Regex("\\s+"), " ") + } + + /** Validates hex string */ + fun validateHexString(hex: String, expectedLength: Int? = null): Boolean { + if (!HashUtils.isValidHex(hex)) return false + return expectedLength == null || hex.length == expectedLength + } +} + +/** Result of validation with success status and error messages */ +data class ValidationResult( + val isValid: Boolean, + val errors: List = emptyList() +) { + fun getErrorMessage(): String = errors.joinToString(", ") + + fun throwIfInvalid() { + if (!isValid) { + throw InvalidTransactionException(getErrorMessage()) + } + } +}