diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 2f186543..63c0c821 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -2,6 +2,8 @@ name: docker on: push: + branches: + - master tags: - '*' diff --git a/Blockchain/Sources/Blockchain/BlockchainDataProvider/BlockchainDataProvider.swift b/Blockchain/Sources/Blockchain/BlockchainDataProvider/BlockchainDataProvider.swift index 3da0693b..f186abd1 100644 --- a/Blockchain/Sources/Blockchain/BlockchainDataProvider/BlockchainDataProvider.swift +++ b/Blockchain/Sources/Blockchain/BlockchainDataProvider/BlockchainDataProvider.swift @@ -98,6 +98,18 @@ extension BlockchainDataProvider { try await dataProvider.getBlockHash(byNumber: number) } + public func getFinalizedHead() async throws -> Data32? { + try await dataProvider.getFinalizedHead() + } + + public func getKeys(prefix: Data32, count: UInt32, startKey: Data32?, blockHash: Data32?) async throws -> [String] { + try await dataProvider.getKeys(prefix: prefix, count: count, startKey: startKey, blockHash: blockHash) + } + + public func getStorage(key: Data32, blockHash: Data32?) async throws -> [String] { + try await dataProvider.getStorage(key: key, blockHash: blockHash) + } + // add forks of finalized head is not allowed public func add(block: BlockRef) async throws { logger.debug("adding block: \(block.hash)") diff --git a/Blockchain/Sources/Blockchain/BlockchainDataProvider/BlockchainDataProviderProtocol.swift b/Blockchain/Sources/Blockchain/BlockchainDataProvider/BlockchainDataProviderProtocol.swift index f59e3e08..8c8ac352 100644 --- a/Blockchain/Sources/Blockchain/BlockchainDataProvider/BlockchainDataProviderProtocol.swift +++ b/Blockchain/Sources/Blockchain/BlockchainDataProvider/BlockchainDataProviderProtocol.swift @@ -14,8 +14,13 @@ public protocol BlockchainDataProviderProtocol: Sendable { func getState(hash: Data32) async throws -> StateRef? func getFinalizedHead() async throws -> Data32? + func getHeads() async throws -> Set + func getKeys(prefix: Data32, count: UInt32, startKey: Data32?, blockHash: Data32?) async throws -> [String] + + func getStorage(key: Data32, blockHash: Data32?) async throws -> [String] + /// return empty set if not found func getBlockHash(byTimeslot timeslot: TimeslotIndex) async throws -> Set /// return empty set if not found diff --git a/Blockchain/Sources/Blockchain/BlockchainDataProvider/InMemoryDataProvider.swift b/Blockchain/Sources/Blockchain/BlockchainDataProvider/InMemoryDataProvider.swift index 93d66c7c..3f9c1b57 100644 --- a/Blockchain/Sources/Blockchain/BlockchainDataProvider/InMemoryDataProvider.swift +++ b/Blockchain/Sources/Blockchain/BlockchainDataProvider/InMemoryDataProvider.swift @@ -22,6 +22,25 @@ public actor InMemoryDataProvider { } extension InMemoryDataProvider: BlockchainDataProviderProtocol { + public func getKeys(prefix: Data32, count: UInt32, startKey: Data32?, blockHash: Data32?) async throws -> [String] { + guard let stateRef = try getState(hash: blockHash ?? genesisBlockHash) else { + return [] + } + + return try await stateRef.value.backend.getKeys(prefix, startKey, count).map { $0.key.toHexString() } + } + + public func getStorage(key: Data32, blockHash: Data32?) async throws -> [String] { + guard let stateRef = try getState(hash: blockHash ?? genesisBlockHash) else { + return [] + } + + guard let value = try await stateRef.value.backend.readRaw(key) else { + throw StateBackendError.missingState(key: key) + } + return [value.toHexString()] + } + public func hasBlock(hash: Data32) -> Bool { blockByHash[hash] != nil } diff --git a/Blockchain/Sources/Blockchain/Config/ProtocolConfig+Preset.swift b/Blockchain/Sources/Blockchain/Config/ProtocolConfig+Preset.swift index b06e4b76..1bcde81e 100644 --- a/Blockchain/Sources/Blockchain/Config/ProtocolConfig+Preset.swift +++ b/Blockchain/Sources/Blockchain/Config/ProtocolConfig+Preset.swift @@ -1,5 +1,18 @@ import Utils +extension ProtocolConfig { + private static let presetMapping: [String: ProtocolConfig] = [ + "minimal": ProtocolConfigRef.minimal.value, + "dev": ProtocolConfigRef.dev.value, + "tiny": ProtocolConfigRef.tiny.value, + "mainnet": ProtocolConfigRef.mainnet.value, + ] + + public func presetName() -> String? { + ProtocolConfig.presetMapping.first(where: { $0.value == self })?.key + } +} + extension Ref where T == ProtocolConfig { // TODO: pick some good numbers for dev env public static let minimal = Ref(ProtocolConfig( diff --git a/Blockchain/Sources/Blockchain/State/StateBackend.swift b/Blockchain/Sources/Blockchain/State/StateBackend.swift index a34feeee..552b69fc 100644 --- a/Blockchain/Sources/Blockchain/State/StateBackend.swift +++ b/Blockchain/Sources/Blockchain/State/StateBackend.swift @@ -38,6 +38,10 @@ public final class StateBackend: Sendable { throw StateBackendError.missingState(key: key) } + public func getKeys(_ prefix: Data32, _ startKey: Data32?, _ limit: UInt32?) async throws -> [(key: Data, value: Data)] { + try await impl.readAll(prefix: prefix.data, startKey: startKey?.data, limit: limit) + } + public func batchRead(_ keys: [any StateKey]) async throws -> [(key: any StateKey, value: (Codable & Sendable)?)] { var ret = [(key: any StateKey, value: (Codable & Sendable)?)]() ret.reserveCapacity(keys.count) diff --git a/Database/Sources/Database/RocksDBBackend.swift b/Database/Sources/Database/RocksDBBackend.swift index a56dddbd..172cd4a5 100644 --- a/Database/Sources/Database/RocksDBBackend.swift +++ b/Database/Sources/Database/RocksDBBackend.swift @@ -72,6 +72,31 @@ public final class RocksDBBackend: Sendable { } extension RocksDBBackend: BlockchainDataProviderProtocol { + public func getKeys(prefix: Data32, count: UInt32, startKey: Data32?, blockHash: Data32?) async throws -> [String] { + logger.trace(""" + getKeys() prefix: \(prefix), count: \(count), + startKey: \(String(describing: startKey)), blockHash: \(String(describing: blockHash)) + """) + + guard let stateRef = try await getState(hash: blockHash ?? genesisBlockHash) else { + return [] + } + return try await stateRef.value.backend.getKeys(prefix, startKey, count).map { $0.key.toHexString() } + } + + public func getStorage(key: Data32, blockHash: Data32?) async throws -> [String] { + logger.trace("getStorage() key: \(key), blockHash: \(String(describing: blockHash))") + + guard let stateRef = try await getState(hash: blockHash ?? genesisBlockHash) else { + return [] + } + + guard let value = try await stateRef.value.backend.readRaw(key) else { + throw StateBackendError.missingState(key: key) + } + return [value.toHexString()] + } + public func hasBlock(hash: Data32) async throws -> Bool { try blocks.exists(key: hash) } diff --git a/Makefile b/Makefile index 0d07c7d9..8342a9b9 100644 --- a/Makefile +++ b/Makefile @@ -81,7 +81,7 @@ format-clang: .PHONY: run run: githooks - swift run --package-path Boka Boka --validator + SWIFT_BACKTRACE=enable=yes swift run --package-path Boka Boka --validator .PHONY: devnet devnet: diff --git a/Node/Sources/Node/NetworkingProtocol/Network.swift b/Node/Sources/Node/NetworkingProtocol/Network.swift index ee3880fc..149da7e0 100644 --- a/Node/Sources/Node/NetworkingProtocol/Network.swift +++ b/Node/Sources/Node/NetworkingProtocol/Network.swift @@ -92,6 +92,10 @@ public final class Network: Sendable { public var peersCount: Int { peer.peersCount } + + public var networkKey: String { + peer.publicKey.description + } } struct HandlerDef: StreamHandler { diff --git a/Node/Sources/Node/NetworkingProtocol/SyncManager.swift b/Node/Sources/Node/NetworkingProtocol/SyncManager.swift index 305fa38b..30a9fd78 100644 --- a/Node/Sources/Node/NetworkingProtocol/SyncManager.swift +++ b/Node/Sources/Node/NetworkingProtocol/SyncManager.swift @@ -27,7 +27,12 @@ public actor SyncManager { private let subscriptions: EventSubscriptions - private var status = SyncStatus.discovering + private var status = SyncStatus.discovering { + didSet { + logger.trace("status changed", metadata: ["status": "\(status)"]) + } + } + private var syncContinuation: [CheckedContinuation] = [] private var networkBest: HashAndSlot? @@ -61,6 +66,15 @@ public actor SyncManager { private func on(peerUpdated info: PeerInfo, newBlockHeader: HeaderRef?) async { // TODO: improve this to handle the case misbehaved peers seding us the wrong best + logger.trace( + "on peer updated", + metadata: [ + "peer": "\(info.id)", + "best": "\(String(describing: info.best))", + "finalized": "\(info.finalized)", + "newBlockHeader": "\(String(describing: newBlockHeader))", + ] + ) if let networkBest { if let peerBest = info.best, peerBest.timeslot > networkBest.timeslot { self.networkBest = peerBest @@ -78,7 +92,7 @@ public actor SyncManager { } let currentHead = await blockchain.dataProvider.bestHead - if currentHead.timeslot >= networkBest!.timeslot { + if let networkBest, currentHead.timeslot >= networkBest.timeslot { syncCompleted() return } diff --git a/Node/Sources/Node/NodeDataSource.swift b/Node/Sources/Node/NodeDataSource.swift index ebcecf53..b42c835c 100644 --- a/Node/Sources/Node/NodeDataSource.swift +++ b/Node/Sources/Node/NodeDataSource.swift @@ -22,9 +22,46 @@ public final class NodeDataSource: Sendable { } } -extension NodeDataSource: SystemDataSource {} +extension NodeDataSource: SystemDataSource { + public func getProperties() async throws -> JSON { + // TODO: Get a custom set of properties as a JSON object, defined in the chain spec + JSON.array([]) + } + + public func getChainName() async throws -> String { + blockchain.config.value.presetName() ?? "" + } + + public func getNodeRoles() async throws -> [String] { + // TODO: Returns the roles the node is running as. + [] + } + + public func getVersion() async throws -> String { + // TODO: From spec or config + "0.0.1" + } + + public func getHealth() async throws -> Bool { + // TODO: Check health status + true + } + + public func getImplementation() async throws -> String { + name + } +} extension NodeDataSource: ChainDataSource { + public func getKeys(prefix: Data32, count: UInt32, startKey: Data32?, blockHash: Data32?) async throws -> [String] { + // TODO: + try await chainDataProvider.getKeys(prefix: prefix, count: count, startKey: startKey, blockHash: blockHash) + } + + public func getStorage(key: Data32, blockHash: Utils.Data32?) async throws -> [String] { + try await chainDataProvider.getStorage(key: key, blockHash: blockHash) + } + public func getBestBlock() async throws -> BlockRef { try await chainDataProvider.getBlock(hash: chainDataProvider.bestHead.hash) } @@ -37,6 +74,18 @@ extension NodeDataSource: ChainDataSource { let state = try await chainDataProvider.getState(hash: blockHash) return try await state.value.read(key: key) } + + public func getBlockHash(byTimeslot timeslot: TimeslotIndex) async throws -> Set { + try await chainDataProvider.getBlockHash(byTimeslot: timeslot) + } + + public func getHeader(hash: Data32) async throws -> HeaderRef? { + try await chainDataProvider.getHeader(hash: hash) + } + + public func getFinalizedHead() async throws -> Data32? { + try await chainDataProvider.getFinalizedHead() + } } extension NodeDataSource: TelemetryDataSource { @@ -47,4 +96,8 @@ extension NodeDataSource: TelemetryDataSource { public func getPeersCount() async throws -> Int { networkManager.peersCount } + + public func getNetworkKey() async throws -> String { + networkManager.network.networkKey + } } diff --git a/Node/Tests/NodeTests/NodeTests.swift b/Node/Tests/NodeTests/NodeTests.swift index ae048af8..005540d9 100644 --- a/Node/Tests/NodeTests/NodeTests.swift +++ b/Node/Tests/NodeTests/NodeTests.swift @@ -1,4 +1,5 @@ import Blockchain +import Database import Foundation import Testing import Utils @@ -79,6 +80,10 @@ final class NodeTests { // Verify block was produced #expect(newTimeslot > initialTimeslot) #expect(try await validatorNode.blockchain.dataProvider.hasBlock(hash: newBestHead.hash)) + #expect(try await validatorNode.blockchain.dataProvider.getKeys(prefix: Data32(), count: 0, startKey: nil, blockHash: nil).isEmpty) + await #expect(throws: StateBackendError.self) { + _ = try await validatorNode.blockchain.dataProvider.getStorage(key: Data32.random(), blockHash: nil) + } } @Test func sync() async throws { diff --git a/Node/Tests/NodeTests/Topology.swift b/Node/Tests/NodeTests/Topology.swift index 7b42fb14..503a8826 100644 --- a/Node/Tests/NodeTests/Topology.swift +++ b/Node/Tests/NodeTests/Topology.swift @@ -25,8 +25,6 @@ struct Topology { } func build(genesis: Genesis) async throws -> ([(Node, StoreMiddleware)], MockScheduler) { - // setupTestLogger() - let timeProvider = MockTimeProvider(time: 1000) let scheduler = MockScheduler(timeProvider: timeProvider) var ret: [(Node, StoreMiddleware)] = [] diff --git a/RPC/Sources/RPC/DataSource/DataSource.swift b/RPC/Sources/RPC/DataSource/DataSource.swift index adffa63b..41d39957 100644 --- a/RPC/Sources/RPC/DataSource/DataSource.swift +++ b/RPC/Sources/RPC/DataSource/DataSource.swift @@ -2,17 +2,30 @@ import Blockchain import Foundation import Utils -public protocol SystemDataSource: Sendable {} +public protocol SystemDataSource: Sendable { + func getNodeRoles() async throws -> [String] + func getVersion() async throws -> String + func getHealth() async throws -> Bool + func getImplementation() async throws -> String + func getProperties() async throws -> JSON + func getChainName() async throws -> String +} public protocol ChainDataSource: Sendable { func getBestBlock() async throws -> BlockRef func getBlock(hash: Data32) async throws -> BlockRef? func getState(blockHash: Data32, key: Data32) async throws -> Data? + func getBlockHash(byTimeslot timeslot: TimeslotIndex) async throws -> Set + func getHeader(hash: Data32) async throws -> HeaderRef? + func getFinalizedHead() async throws -> Data32? + func getKeys(prefix: Data32, count: UInt32, startKey: Data32?, blockHash: Data32?) async throws -> [String] + func getStorage(key: Data32, blockHash: Data32?) async throws -> [String] } public protocol TelemetryDataSource: Sendable { func name() async throws -> String func getPeersCount() async throws -> Int + func getNetworkKey() async throws -> String } public typealias DataSource = ChainDataSource & SystemDataSource & TelemetryDataSource diff --git a/RPC/Sources/RPC/Handlers/ChainHandlers.swift b/RPC/Sources/RPC/Handlers/ChainHandlers.swift index 5733f8ec..9abb8f8f 100644 --- a/RPC/Sources/RPC/Handlers/ChainHandlers.swift +++ b/RPC/Sources/RPC/Handlers/ChainHandlers.swift @@ -58,9 +58,12 @@ public enum ChainHandlers { self.source = source } - public func handle(request _: Request) async throws -> Response? { - // TODO: implement - nil + public func handle(request: Request) async throws -> Response? { + if let timeslot = request.value { + let blocks = try await source.getBlockHash(byTimeslot: timeslot) + return blocks.first?.data + } + return nil } } @@ -78,8 +81,7 @@ public enum ChainHandlers { } public func handle(request _: Request) async throws -> Response? { - // TODO: implement - nil + try await source.getFinalizedHead() } } @@ -97,9 +99,13 @@ public enum ChainHandlers { self.source = source } - public func handle(request _: Request) async throws -> Response? { - // TODO: implement - nil + public func handle(request: Request) async throws -> Response? { + let header = if let hash = request.value { + try await source.getHeader(hash: hash)?.value + } else { + try await source.getBestBlock().header + } + return try header.map { try JamEncoder.encode($0) } } } } diff --git a/RPC/Sources/RPC/Handlers/StateHandlers.swift b/RPC/Sources/RPC/Handlers/StateHandlers.swift index 1bc2fecc..7b22247a 100644 --- a/RPC/Sources/RPC/Handlers/StateHandlers.swift +++ b/RPC/Sources/RPC/Handlers/StateHandlers.swift @@ -29,9 +29,8 @@ public enum StateHandlers { self.source = source } - public func handle(request _: Request) async throws -> Response? { - // TODO: implement - [] + public func handle(request: Request) async throws -> Response? { + try await source.getKeys(prefix: request.value.0, count: request.value.1, startKey: request.value.2, blockHash: request.value.3) } } @@ -49,9 +48,8 @@ public enum StateHandlers { self.source = source } - public func handle(request _: Request) async throws -> Response? { - // TODO: implement - [] + public func handle(request: Request) async throws -> Response? { + try await source.getStorage(key: request.value.0, blockHash: request.value.1) } } } diff --git a/RPC/Sources/RPC/Handlers/SystemHandlers.swift b/RPC/Sources/RPC/Handlers/SystemHandlers.swift index 5d33d060..65826161 100644 --- a/RPC/Sources/RPC/Handlers/SystemHandlers.swift +++ b/RPC/Sources/RPC/Handlers/SystemHandlers.swift @@ -12,8 +12,8 @@ public enum SystemHandlers { public static func getHandlers(source: SystemDataSource) -> [any RPCHandler] { [ - Health(), - Implementation(), + Health(source: source), + Implementation(source: source), Version(source: source), Properties(source: source), NodeRoles(source: source), @@ -28,8 +28,14 @@ public enum SystemHandlers { public static var method: String { "system_health" } public static var summary: String? { "Returns true if the node is healthy." } + private let source: SystemDataSource + + init(source: SystemDataSource) { + self.source = source + } + public func handle(request _: Request) async throws -> Response? { - true + try await source.getHealth() } } @@ -40,8 +46,14 @@ public enum SystemHandlers { public static var method: String { "system_implementation" } public static var summary: String? { "Returns the implementation name of the node." } + private let source: SystemDataSource + + init(source: SystemDataSource) { + self.source = source + } + public func handle(request _: Request) async throws -> Response? { - "Boka" + try await source.getImplementation() } } @@ -59,8 +71,7 @@ public enum SystemHandlers { } public func handle(request _: Request) async throws -> Response? { - // TODO: read it from somewhere - "0.0.1" + try await source.getVersion() } } @@ -78,8 +89,7 @@ public enum SystemHandlers { } public func handle(request _: Request) async throws -> Response? { - // TODO: implement - JSON.array([]) + try await source.getProperties() } } @@ -97,8 +107,7 @@ public enum SystemHandlers { } public func handle(request _: Request) async throws -> Response? { - // TODO: implement - [] + try await source.getNodeRoles() } } @@ -116,8 +125,7 @@ public enum SystemHandlers { } public func handle(request _: Request) async throws -> Response? { - // TODO: implement - "dev" + try await source.getChainName() } } } diff --git a/RPC/Sources/RPC/Handlers/TelemetryHandlers.swift b/RPC/Sources/RPC/Handlers/TelemetryHandlers.swift index 9f031cc2..c51d38b5 100644 --- a/RPC/Sources/RPC/Handlers/TelemetryHandlers.swift +++ b/RPC/Sources/RPC/Handlers/TelemetryHandlers.swift @@ -67,8 +67,7 @@ public enum TelemetryHandlers { } public func handle(request _: Request) async throws -> Response? { - // TODO: implement - nil + try await source.getNetworkKey() } } } diff --git a/RPC/Sources/RPC/JSONRPC/JSONRPCController.swift b/RPC/Sources/RPC/JSONRPC/JSONRPCController.swift index 875435b6..05cecfc3 100644 --- a/RPC/Sources/RPC/JSONRPC/JSONRPCController.swift +++ b/RPC/Sources/RPC/JSONRPC/JSONRPCController.swift @@ -63,7 +63,7 @@ final class JSONRPCController: RouteCollection, Sendable { let responseData = try encoder.encode(jsonResponse) try await ws.send(raw: responseData, opcode: .text) } catch { - logger.debug("Failed to decode JSON request: \(error)") + logger.error("Failed to decode JSON request: \(error)") let rpcError = JSONError(code: -32600, message: "Invalid Request") let rpcResponse = JSONResponse(id: nil, error: rpcError) diff --git a/RPC/Sources/RPC/JSONRPC/RequestParameter.swift b/RPC/Sources/RPC/JSONRPC/RequestParameter.swift index fd45a459..90cb439b 100644 --- a/RPC/Sources/RPC/JSONRPC/RequestParameter.swift +++ b/RPC/Sources/RPC/JSONRPC/RequestParameter.swift @@ -39,7 +39,7 @@ public struct Request1: RequestParameter { public struct Request2: RequestParameter { public static var types: [Any.Type] { [T1.self, T2.self] } - public let valuu: (T1, T2) + public let value: (T1, T2) public init(from json: JSON?) throws { guard let json else { @@ -51,7 +51,7 @@ public struct Request2: RequestParameter { guard arr.count <= 2 else { throw RequestError.unexpectedLength } - valuu = try (T1(from: arr[safe: 0]), T2(from: arr[safe: 1])) + value = try (T1(from: arr[safe: 0]), T2(from: arr[safe: 1])) } } diff --git a/RPC/Tests/RPCTests/ChainHandlesTests.swift b/RPC/Tests/RPCTests/ChainHandlesTests.swift new file mode 100644 index 00000000..85824b5b --- /dev/null +++ b/RPC/Tests/RPCTests/ChainHandlesTests.swift @@ -0,0 +1,125 @@ +import Blockchain +@testable import RPC +import Testing +import TracingUtils +@testable import Utils +import Vapor +import XCTVapor + +public final class DummyNodeDataSource: Sendable { + public let chainDataProvider: BlockchainDataProvider + public init( + chainDataProvider: BlockchainDataProvider + ) { + self.chainDataProvider = chainDataProvider + } +} + +extension DummyNodeDataSource: ChainDataSource { + public func getKeys(prefix _: Data32, count _: UInt32, startKey _: Data32?, blockHash _: Data32?) async throws + -> [String] + { + ["key1", "key2", "key3"] + } + + public func getStorage(key _: Data32, blockHash _: Data32?) async throws -> [String] { + ["value1", "value2"] + } + + public func getFinalizedHead() async throws -> Data32? { + try await chainDataProvider.getFinalizedHead() + } + + public func getBestBlock() async throws -> BlockRef { + try await chainDataProvider.getBlock(hash: chainDataProvider.bestHead.hash) + } + + public func getBlock(hash: Data32) async throws -> BlockRef? { + try await chainDataProvider.getBlock(hash: hash) + } + + public func getState(blockHash: Data32, key: Data32) async throws -> Data? { + let state = try await chainDataProvider.getState(hash: blockHash) + return try await state.value.read(key: key) + } + + public func getBlockHash(byTimeslot timeslot: TimeslotIndex) async throws -> Set { + try await chainDataProvider.getBlockHash(byTimeslot: timeslot) + } + + public func getHeader(hash: Data32) async throws -> HeaderRef? { + try await chainDataProvider.getHeader(hash: hash) + } +} + +final class ChainRPCControllerTests { + var app: Application! + var dataProvider: BlockchainDataProvider! + + func setUp() async throws { + app = try await Application.make(.testing) + let (genesisState, genesisBlock) = try! State.devGenesis(config: .minimal) + dataProvider = try! await BlockchainDataProvider(InMemoryDataProvider(genesisState: genesisState, genesisBlock: genesisBlock)) + let rpcController = JSONRPCController(handlers: ChainHandlers + .getHandlers(source: DummyNodeDataSource(chainDataProvider: dataProvider))) + try app.register(collection: rpcController) + } + + @Test func getBlock() async throws { + try await setUp() + let hashHex = await dataProvider.bestHead.hash.toHexString() + let params = JSON.array([.string(hashHex)]) + let req = JSONRequest(jsonrpc: "2.0", method: "chain_getBlock", params: params, id: 1) + var buffer = ByteBuffer() + try buffer.writeJSONEncodable(req) + try await app.test(.POST, "/", headers: ["Content-Type": "application/json"], body: buffer) { res async in + #expect(res.status == .ok) + let resp = try! res.content.decode(JSONResponse.self, using: JSONDecoder()) + #expect(resp.result!.value != nil) + } + try await app.asyncShutdown() + } + + @Test func getBlockHash() async throws { + try await setUp() + let timeslot = await dataProvider.bestHead.timeslot + let params = JSON.array([JSON(integerLiteral: Int32(timeslot))]) + let req = JSONRequest(jsonrpc: "2.0", method: "chain_getBlockHash", params: params, id: 2) + var buffer = ByteBuffer() + try buffer.writeJSONEncodable(req) + try await app.test(.POST, "/", headers: ["Content-Type": "application/json"], body: buffer) { res async in + #expect(res.status == .ok) + let resp = try! res.content.decode(JSONResponse.self, using: JSONDecoder()) + #expect(resp.result!.value != nil) + } + try await app.asyncShutdown() + } + + @Test func getFinalizedHead() async throws { + try await setUp() + let req = JSONRequest(jsonrpc: "2.0", method: "chain_getFinalizedHead", params: nil, id: 3) + var buffer = ByteBuffer() + try buffer.writeJSONEncodable(req) + try await app.test(.POST, "/", headers: ["Content-Type": "application/json"], body: buffer) { res async in + #expect(res.status == .ok) + let resp = try! res.content.decode(JSONResponse.self, using: JSONDecoder()) + #expect(resp.result!.value != nil) + } + try await app.asyncShutdown() + } + + @Test func getHeader() async throws { + try await setUp() + let hashHex = await dataProvider.bestHead.hash.toHexString() + let params = JSON.array([.string(hashHex)]) + let req = JSONRequest(jsonrpc: "2.0", method: "chain_getHeader", params: params, id: 4) + var buffer = ByteBuffer() + try buffer.writeJSONEncodable(req) + try await app.test(.POST, "/", headers: ["Content-Type": "application/json"], body: buffer) { res async in + #expect(res.status == .ok) + let resp = try! res.content.decode(JSONResponse.self, using: JSONDecoder()) + #expect(resp.result!.value != nil) + } + try await app.asyncShutdown() + } +} diff --git a/RPC/Tests/RPCTests/JSONRPCControllerTests.swift b/RPC/Tests/RPCTests/JSONRPCControllerTests.swift deleted file mode 100644 index ee41ca15..00000000 --- a/RPC/Tests/RPCTests/JSONRPCControllerTests.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Blockchain -@testable import RPC -import Testing -import TracingUtils -import Vapor -import XCTVapor - -struct DummySource: SystemDataSource {} - -final class JSONRPCControllerTests { - var app: Application - - init() throws { - app = Application(.testing) - - let rpcController = JSONRPCController(handlers: SystemHandlers.getHandlers(source: DummySource())) - try app.register(collection: rpcController) - } - - deinit { - app.shutdown() - } - - @Test func health() throws { - let req = JSONRequest(jsonrpc: "2.0", method: "system_health", params: nil, id: 1) - var buffer = ByteBuffer() - try buffer.writeJSONEncodable(req) - try app.test(.POST, "/", headers: ["Content-Type": "application/json"], body: buffer) { res in - #expect(res.status == .ok) - let resp = try res.content.decode(JSONResponse.self, using: JSONDecoder()) - #expect(resp.result?.value != nil) - } - } -} diff --git a/RPC/Tests/RPCTests/StateHandlersTests.swift b/RPC/Tests/RPCTests/StateHandlersTests.swift new file mode 100644 index 00000000..64c20ed5 --- /dev/null +++ b/RPC/Tests/RPCTests/StateHandlersTests.swift @@ -0,0 +1,59 @@ +import Blockchain +import Testing +import TracingUtils +import Vapor +import XCTVapor + +@testable import RPC +@testable import Utils + +final class StateHandlersTests { + var app: Application! + var dataProvider: BlockchainDataProvider! + + func setUp() async throws { + app = try await Application.make(.testing) + let (genesisState, genesisBlock) = try! State.devGenesis(config: .minimal) + dataProvider = try! await BlockchainDataProvider(InMemoryDataProvider(genesisState: genesisState, genesisBlock: genesisBlock)) + let rpcController = JSONRPCController(handlers: StateHandlers + .getHandlers(source: DummyNodeDataSource(chainDataProvider: dataProvider))) + try app.register(collection: rpcController) + } + + @Test func getKeys() async throws { + try await setUp() + let hashHex = await dataProvider.bestHead.hash.toHexString() + let params = JSON.array( + [ + .string(hashHex), + .init(integerLiteral: 10), + .null, + .null, + ] + ) + let req = JSONRequest(jsonrpc: "2.0", method: "state_getKeys", params: params, id: 1) + var buffer = ByteBuffer() + try buffer.writeJSONEncodable(req) + try await app.test(.POST, "/", headers: ["Content-Type": "application/json"], body: buffer) { res async in + #expect(res.status == .ok) + let resp = try! res.content.decode(JSONResponse.self, using: JSONDecoder()) + #expect(resp.result!.value != nil) + } + try await app.asyncShutdown() + } + + @Test func getStorage() async throws { + try await setUp() + let hashHex = await dataProvider.bestHead.hash.toHexString() + let params = JSON.array([.string(hashHex)]) + let req = JSONRequest(jsonrpc: "2.0", method: "state_getStorage", params: params, id: 2) + var buffer = ByteBuffer() + try buffer.writeJSONEncodable(req) + try await app.test(.POST, "/", headers: ["Content-Type": "application/json"], body: buffer) { res async in + #expect(res.status == .ok) + let resp = try! res.content.decode(JSONResponse.self, using: JSONDecoder()) + #expect(resp.result!.value != nil) + } + try await app.asyncShutdown() + } +} diff --git a/RPC/Tests/RPCTests/SystemHandlersTests.swift b/RPC/Tests/RPCTests/SystemHandlersTests.swift new file mode 100644 index 00000000..14baa069 --- /dev/null +++ b/RPC/Tests/RPCTests/SystemHandlersTests.swift @@ -0,0 +1,117 @@ +import Blockchain +import Testing +import TracingUtils +import Vapor +import XCTVapor + +@testable import RPC +@testable import Utils + +struct DummySource: SystemDataSource { + func getProperties() async throws -> JSON { + JSON.array([]) + } + + func getChainName() async throws -> String { + "dev" + } + + func getNodeRoles() async throws -> [String] { + [] + } + + func getVersion() async throws -> String { + "0.0.1" + } + + func getHealth() async throws -> Bool { + true + } + + func getImplementation() async throws -> String { + "Boka" + } +} + +final class SystemHandlersTests { + var app: Application + + init() throws { + app = Application(.testing) + + let rpcController = JSONRPCController( + handlers: SystemHandlers.getHandlers(source: DummySource()) + ) + try app.register(collection: rpcController) + } + + deinit { + app.shutdown() + } + + @Test func health() throws { + let req = JSONRequest(jsonrpc: "2.0", method: "system_health", params: nil, id: 1) + var buffer = ByteBuffer() + try buffer.writeJSONEncodable(req) + try app.test(.POST, "/", headers: ["Content-Type": "application/json"], body: buffer) { res in + #expect(res.status == .ok) + let resp = try res.content.decode(JSONResponse.self, using: JSONDecoder()) + #expect((resp.result!.value as! Utils.JSON).bool == true) + } + } + + @Test func implementation() throws { + let req = JSONRequest(jsonrpc: "2.0", method: "system_implementation", params: nil, id: 2) + var buffer = ByteBuffer() + try buffer.writeJSONEncodable(req) + try app.test(.POST, "/", headers: ["Content-Type": "application/json"], body: buffer) { res in + #expect(res.status == .ok) + let resp = try res.content.decode(JSONResponse.self, using: JSONDecoder()) + #expect((resp.result!.value as! Utils.JSON).string == "Boka") + } + } + + @Test func version() throws { + let req = JSONRequest(jsonrpc: "2.0", method: "system_version", params: nil, id: 3) + var buffer = ByteBuffer() + try buffer.writeJSONEncodable(req) + try app.test(.POST, "/", headers: ["Content-Type": "application/json"], body: buffer) { res in + #expect(res.status == .ok) + let resp = try res.content.decode(JSONResponse.self, using: JSONDecoder()) + #expect((resp.result!.value as! Utils.JSON).string == "0.0.1") + } + } + + @Test func properties() throws { + let req = JSONRequest(jsonrpc: "2.0", method: "system_properties", params: nil, id: 4) + var buffer = ByteBuffer() + try buffer.writeJSONEncodable(req) + try app.test(.POST, "/", headers: ["Content-Type": "application/json"], body: buffer) { res in + #expect(res.status == .ok) + let resp = try res.content.decode(JSONResponse.self, using: JSONDecoder()) + #expect(resp.result?.value != nil) + } + } + + @Test func nodeRoles() throws { + let req = JSONRequest(jsonrpc: "2.0", method: "system_nodeRoles", params: nil, id: 5) + var buffer = ByteBuffer() + try buffer.writeJSONEncodable(req) + try app.test(.POST, "/", headers: ["Content-Type": "application/json"], body: buffer) { res in + #expect(res.status == .ok) + let resp = try res.content.decode(JSONResponse.self, using: JSONDecoder()) + #expect((resp.result!.value as! Utils.JSON).array == []) + } + } + + @Test func chain() throws { + let req = JSONRequest(jsonrpc: "2.0", method: "system_chain", params: nil, id: 6) + var buffer = ByteBuffer() + try buffer.writeJSONEncodable(req) + try app.test(.POST, "/", headers: ["Content-Type": "application/json"], body: buffer) { res in + #expect(res.status == .ok) + let resp = try res.content.decode(JSONResponse.self, using: JSONDecoder()) + #expect((resp.result!.value as! Utils.JSON).string == "dev") + } + } +} diff --git a/RPC/Tests/RPCTests/TelemetryHandlersTests.swift b/RPC/Tests/RPCTests/TelemetryHandlersTests.swift new file mode 100644 index 00000000..195bae74 --- /dev/null +++ b/RPC/Tests/RPCTests/TelemetryHandlersTests.swift @@ -0,0 +1,72 @@ +import Blockchain +import Testing +import TracingUtils +import Vapor +import XCTVapor + +@testable import RPC +@testable import Utils + +struct TelemetryDummySource: TelemetryDataSource { + func name() async throws -> String { + "TestNode" + } + + func getPeersCount() async throws -> Int { + 42 + } + + func getNetworkKey() async throws -> String { + "Ed25519:TestKey" + } +} + +final class TelemetryHandlersTests { + var app: Application + + init() throws { + app = Application(.testing) + + let rpcController = JSONRPCController( + handlers: TelemetryHandlers.getHandlers(source: TelemetryDummySource()) + ) + try app.register(collection: rpcController) + } + + deinit { + app.shutdown() + } + + @Test func name() throws { + let req = JSONRequest(jsonrpc: "2.0", method: "telemetry_name", params: nil, id: 1) + var buffer = ByteBuffer() + try buffer.writeJSONEncodable(req) + try app.test(.POST, "/", headers: ["Content-Type": "application/json"], body: buffer) { res in + #expect(res.status == .ok) + let resp = try res.content.decode(JSONResponse.self, using: JSONDecoder()) + #expect((resp.result!.value as! Utils.JSON).string == "TestNode") + } + } + + @Test func peersCount() throws { + let req = JSONRequest(jsonrpc: "2.0", method: "telemetry_peersCount", params: nil, id: 2) + var buffer = ByteBuffer() + try buffer.writeJSONEncodable(req) + try app.test(.POST, "/", headers: ["Content-Type": "application/json"], body: buffer) { res in + #expect(res.status == .ok) + let resp = try res.content.decode(JSONResponse.self, using: JSONDecoder()) + #expect((resp.result!.value as! Utils.JSON).number == 42) + } + } + + @Test func networkKey() throws { + let req = JSONRequest(jsonrpc: "2.0", method: "telemetry_networkKey", params: nil, id: 3) + var buffer = ByteBuffer() + try buffer.writeJSONEncodable(req) + try app.test(.POST, "/", headers: ["Content-Type": "application/json"], body: buffer) { res in + #expect(res.status == .ok) + let resp = try res.content.decode(JSONResponse.self, using: JSONDecoder()) + #expect((resp.result!.value as! Utils.JSON).string == "Ed25519:TestKey") + } + } +} diff --git a/scripts/dev.Dockerfile b/scripts/dev.Dockerfile index f94e89e1..f5e7635b 100644 --- a/scripts/dev.Dockerfile +++ b/scripts/dev.Dockerfile @@ -35,6 +35,7 @@ RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true && ap libcurl4 \ libxml2 \ tzdata \ + librocksdb-dev \ && rm -r /var/lib/apt/lists/* # Everything up to here should cache nicely between Swift versions, assuming dev dependencies change little diff --git a/scripts/release.Dockerfile b/scripts/release.Dockerfile index ecb7782f..0abd9843 100644 --- a/scripts/release.Dockerfile +++ b/scripts/release.Dockerfile @@ -36,6 +36,7 @@ RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true && ap libcurl4 \ libxml2 \ tzdata \ + librocksdb-dev \ && rm -r /var/lib/apt/lists/* # Everything up to here should cache nicely between Swift versions, assuming dev dependencies change little