diff --git a/Database/Sources/Database/JamCoder.swift b/Database/Sources/Database/JamCoder.swift new file mode 100644 index 00000000..cbaef623 --- /dev/null +++ b/Database/Sources/Database/JamCoder.swift @@ -0,0 +1,48 @@ +import Blockchain +import Codec +import Foundation +import RocksDBSwift + +struct JamCoder: StoreCoder { + typealias Key = Key + typealias Value = Value + + private let config: ProtocolConfigRef + private let prefix: Data + + init(config: ProtocolConfigRef, prefix: Data = Data()) { + self.config = config + self.prefix = prefix + } + + func encode(key: Key) throws -> Data { + let encoder = JamEncoder(prefix) + try encoder.encode(key) + return encoder.data + } + + func encode(value: Value) throws -> Data { + try JamEncoder.encode(value) + } + + func decode(data: Data) throws -> Value { + try JamDecoder.decode(Value.self, from: data, withConfig: config) + } +} + +struct NoopCoder: StoreCoder { + typealias Key = Data + typealias Value = Data + + func encode(key: Key) throws -> Data { + key + } + + func encode(value: Value) throws -> Data { + value + } + + func decode(data: Data) throws -> Value { + data + } +} diff --git a/Database/Sources/Database/RocksDBBackend.swift b/Database/Sources/Database/RocksDBBackend.swift index b75d1881..469d94bc 100644 --- a/Database/Sources/Database/RocksDBBackend.swift +++ b/Database/Sources/Database/RocksDBBackend.swift @@ -3,28 +3,103 @@ import Foundation import RocksDBSwift import Utils -enum StoreId: UInt8, ColumnFamilyKey { - // metadata and configurations - case meta - // blocks - // blockHash => blockBody - case blocks - // timeslot => blockHash - // blockNumber => blockHash - case blockIndexes - // state trie - // hash => trie node - // value hash => state value - case state - // ref count - // node hash => ref count - // value hash => ref count - case stateRefs +public enum RocksDBBackendError: Error { + case genesisHashMismatch(expected: Data32, actual: Data) } -public final class RocksDBBackend: StateBackendProtocol { - public init() {} +public final class RocksDBBackend { + private let db: RocksDB + private let meta: Store + private let blocks: Store> + public let genesisBlockHash: Data32 + + public init(path: URL, config: ProtocolConfigRef, genesisBlockHash: Data32) throws { + db = try RocksDB(path: path) + meta = Store(db: db, column: .meta, coder: NoopCoder()) + blocks = Store(db: db, column: .blocks, coder: JamCoder(config: config)) + + self.genesisBlockHash = genesisBlockHash + + let genesis = try meta.get(key: MetaKey.genesisHash.key) + if let genesis { + guard genesis == genesisBlockHash.data else { + throw RocksDBBackendError.genesisHashMismatch(expected: genesisBlockHash, actual: genesis) + } + } else { + // must be a new db + try meta.put(key: MetaKey.genesisHash.key, value: genesisBlockHash.data) + } + } +} + +extension RocksDBBackend: BlockchainDataProviderProtocol { + public func hasBlock(hash _: Data32) async throws -> Bool { + fatalError("unimplemented") + } + + public func hasState(hash _: Data32) async throws -> Bool { + fatalError("unimplemented") + } + + public func isHead(hash _: Data32) async throws -> Bool { + fatalError("unimplemented") + } + + public func getBlockNumber(hash _: Data32) async throws -> UInt32 { + fatalError("unimplemented") + } + + public func getHeader(hash _: Data32) async throws -> HeaderRef { + fatalError("unimplemented") + } + + public func getBlock(hash _: Data32) async throws -> BlockRef { + fatalError("unimplemented") + } + + public func getState(hash _: Data32) async throws -> StateRef { + fatalError("unimplemented") + } + + public func getFinalizedHead() async throws -> Data32 { + fatalError("unimplemented") + } + + public func getHeads() async throws -> Set { + fatalError("unimplemented") + } + + public func getBlockHash(byTimeslot _: TimeslotIndex) async throws -> Set { + fatalError("unimplemented") + } + + public func getBlockHash(byNumber _: UInt32) async throws -> Set { + fatalError("unimplemented") + } + + public func add(block _: BlockRef) async throws { + fatalError("unimplemented") + } + + public func add(state _: StateRef) async throws { + fatalError("unimplemented") + } + + public func setFinalizedHead(hash _: Data32) async throws { + fatalError("unimplemented") + } + + public func updateHead(hash _: Data32, parent _: Data32) async throws { + fatalError("unimplemented") + } + + public func remove(hash _: Data32) async throws { + fatalError("unimplemented") + } +} + +extension RocksDBBackend: StateBackendProtocol { public func read(key _: Data) async throws -> Data? { fatalError("unimplemented") } diff --git a/Database/Sources/Database/Stores.swift b/Database/Sources/Database/Stores.swift new file mode 100644 index 00000000..f0556e76 --- /dev/null +++ b/Database/Sources/Database/Stores.swift @@ -0,0 +1,33 @@ +import Blockchain +import Foundation +import RocksDBSwift +import Utils + +enum StoreId: UInt8, ColumnFamilyKey { + // metadata and configurations + case meta = 0 + // blocks + // blockHash => blockBody + case blocks = 1 + // timeslot => blockHash + // blockNumber => blockHash + case blockIndexes = 2 + // state trie + // hash => trie node + // value hash => state value + case state = 3 + // ref count + // node hash => ref count + // value hash => ref count + case stateRefs = 4 +} + +enum MetaKey: UInt8 { + case genesisHash = 0 // Data32 + case heads = 1 // Set + case finalizedHead = 2 // Data32 + + var key: Data { + Data([rawValue]) + } +} diff --git a/Database/Sources/RocksDBSwift/RocksDB.swift b/Database/Sources/RocksDBSwift/RocksDB.swift index 23878026..3f27f794 100644 --- a/Database/Sources/RocksDBSwift/RocksDB.swift +++ b/Database/Sources/RocksDBSwift/RocksDB.swift @@ -4,12 +4,12 @@ import Utils public protocol ColumnFamilyKey: Sendable, CaseIterable, Hashable, RawRepresentable {} -public final class RocksDB: Sendable { - public enum BatchOperation { - case delete(column: CFKey, key: Data) - case put(column: CFKey, key: Data, value: Data) - } +public enum BatchOperation { + case delete(column: UInt8, key: Data) + case put(column: UInt8, key: Data, value: Data) +} +public final class RocksDB: Sendable { public enum Error: Swift.Error { case openFailed(message: String) case putFailed(message: String) @@ -17,6 +17,7 @@ public final class RocksDB: Sendable { case deleteFailed(message: String) case batchFailed(message: String) case noData + case invalidColumn(UInt8) } private let writeOptions: WriteOptions @@ -146,6 +147,10 @@ extension RocksDB { private func getHandle(column: CFKey) -> OpaquePointer { cfHandles[Int(column.rawValue)].value } + + private func getHandle(column: UInt8) -> OpaquePointer? { + cfHandles[safe: Int(column)]?.value + } } // MARK: - public methods @@ -203,14 +208,14 @@ extension RocksDB { for operation in operations { switch operation { case let .delete(column, key): - let handle = getHandle(column: column) + let handle = try getHandle(column: column).unwrap(orError: Error.invalidColumn(column)) try Self.call(key) { ptrs in let key = ptrs[0] rocksdb_writebatch_delete_cf(writeBatch, handle, key.ptr, key.count) } case let .put(column, key, value): - let handle = getHandle(column: column) + let handle = try getHandle(column: column).unwrap(orError: Error.invalidColumn(column)) try Self.call(key, value) { ptrs in let key = ptrs[0] let value = ptrs[1] diff --git a/Database/Sources/RocksDBSwift/Store.swift b/Database/Sources/RocksDBSwift/Store.swift index dd52f062..ee1f43a8 100644 --- a/Database/Sources/RocksDBSwift/Store.swift +++ b/Database/Sources/RocksDBSwift/Store.swift @@ -5,6 +5,7 @@ public protocol StoreCoder: Sendable { associatedtype Value: Decodable func encode(key: Key) throws -> Data + func encode(value: Value) throws -> Data func decode(data: Data) throws -> Value } @@ -26,4 +27,33 @@ public final class Store: Sendable { return try data.map { try coder.decode(data: $0) } } + + public func put(key: Coder.Key, value: Coder.Value) throws { + let encodedKey = try coder.encode(key: key) + let encodedValue = try coder.encode(value: value) + + try db.put(column: column, key: encodedKey, value: encodedValue) + } + + public func delete(key: Coder.Key) throws { + let encodedKey = try coder.encode(key: key) + try db.delete(column: column, key: encodedKey) + } + + public func exists(key: Coder.Key) throws -> Bool { + let encodedKey = try coder.encode(key: key) + // it seems like + return try db.get(column: column, key: encodedKey) != nil + } + + public func putOperation(key: Coder.Key, value: Coder.Value) throws -> BatchOperation { + let encodedKey = try coder.encode(key: key) + let encodedValue = try coder.encode(value: value) + return .put(column: column.rawValue, key: encodedKey, value: encodedValue) + } + + public func deleteOperation(key: Coder.Key) throws -> BatchOperation { + let encodedKey = try coder.encode(key: key) + return .delete(column: column.rawValue, key: encodedKey) + } }