Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 75 additions & 63 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,64 +1,76 @@
{
"configurations": [
{
"type": "lldb",
"request": "launch",
"sourceLanguages": [
"swift"
],
"args": [],
"cwd": "${workspaceFolder:boka}/Boka",
"name": "Debug Boka (Boka)",
"program": "${workspaceFolder:boka}/Boka/.build/debug/Boka",
"preLaunchTask": "swift: Build Debug Boka (Boka)"
},
{
"type": "lldb",
"request": "launch",
"sourceLanguages": [
"swift"
],
"args": [],
"cwd": "${workspaceFolder:boka}/Boka",
"name": "Release Boka (Boka)",
"program": "${workspaceFolder:boka}/Boka/.build/release/Boka",
"preLaunchTask": "swift: Build Release Boka (Boka)"
},
{
"type": "lldb",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:boka}/Cli",
"name": "Debug Cli (Cli)",
"program": "${workspaceFolder:boka}/Cli/.build/debug/Cli",
"preLaunchTask": "swift: Build Debug Cli (Cli)"
},
{
"type": "lldb",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:boka}/Cli",
"name": "Release Cli (Cli)",
"program": "${workspaceFolder:boka}/Cli/.build/release/Cli",
"preLaunchTask": "swift: Build Release Cli (Cli)"
},
{
"type": "lldb",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:boka}/Tools",
"name": "Debug Tools (Tools)",
"program": "${workspaceFolder:boka}/Tools/.build/debug/Tools",
"preLaunchTask": "swift: Build Debug Tools (Tools)"
},
{
"type": "lldb",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:boka}/Tools",
"name": "Release Tools (Tools)",
"program": "${workspaceFolder:boka}/Tools/.build/release/Tools",
"preLaunchTask": "swift: Build Release Tools (Tools)"
}
]
}
"configurations": [
{
"type": "swift",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:boka}/Boka",
"name": "Debug Boka (Boka)",
"program": "${workspaceFolder:boka}/Boka/.build/debug/Boka",
"preLaunchTask": "swift: Build Debug Boka (Boka)"
},
{
"type": "swift",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:boka}/Boka",
"name": "Release Boka (Boka)",
"program": "${workspaceFolder:boka}/Boka/.build/release/Boka",
"preLaunchTask": "swift: Build Release Boka (Boka)"
},
{
"type": "lldb",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:boka}/Cli",
"name": "Debug Cli (Cli)",
"program": "${workspaceFolder:boka}/Cli/.build/debug/Cli",
"preLaunchTask": "swift: Build Debug Cli (Cli)"
},
{
"type": "lldb",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:boka}/Cli",
"name": "Release Cli (Cli)",
"program": "${workspaceFolder:boka}/Cli/.build/release/Cli",
"preLaunchTask": "swift: Build Release Cli (Cli)"
},
{
"type": "lldb",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:boka}/Tools",
"name": "Debug Tools (Tools)",
"program": "${workspaceFolder:boka}/Tools/.build/debug/Tools",
"preLaunchTask": "swift: Build Debug Tools (Tools)"
},
{
"type": "lldb",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:boka}/Tools",
"name": "Release Tools (Tools)",
"program": "${workspaceFolder:boka}/Tools/.build/release/Tools",
"preLaunchTask": "swift: Build Release Tools (Tools)"
},
{
"type": "swift",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:boka}/Fuzzing",
"name": "Debug BokaFuzzer (Fuzzing)",
"program": "${workspaceFolder:boka}/Fuzzing/.build/debug/BokaFuzzer",
"preLaunchTask": "swift: Build Debug BokaFuzzer (Fuzzing)"
},
{
"type": "swift",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:boka}/Fuzzing",
"name": "Release BokaFuzzer (Fuzzing)",
"program": "${workspaceFolder:boka}/Fuzzing/.build/release/BokaFuzzer",
"preLaunchTask": "swift: Build Release BokaFuzzer (Fuzzing)"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ extension BlockchainDataProvider {
try await dataProvider.getFinalizedHead()
}

public func getKeys(prefix: Data31, count: UInt32, startKey: Data31?, blockHash: Data32?) async throws -> [String] {
public func getKeys(prefix: Data, count: UInt32, startKey: Data31?, blockHash: Data32?) async throws -> [String] {
try await dataProvider.getKeys(prefix: prefix, count: count, startKey: startKey, blockHash: blockHash)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public protocol BlockchainDataProviderProtocol: Sendable {

func getHeads() async throws -> Set<Data32>

func getKeys(prefix: Data31, count: UInt32, startKey: Data31?, blockHash: Data32?) async throws -> [String]
func getKeys(prefix: Data, count: UInt32, startKey: Data31?, blockHash: Data32?) async throws -> [String]

func getStorage(key: Data31, blockHash: Data32?) async throws -> [String]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ extension InMemoryDataProvider: BlockchainDataProviderProtocol {
guaranteedWorkReports[guaranteedWorkReport.value.workReport.hash()] = guaranteedWorkReport
}

public func getKeys(prefix: Data31, count: UInt32, startKey: Data31?, blockHash: Data32?) async throws -> [String] {
public func getKeys(prefix: Data, count: UInt32, startKey: Data31?, blockHash: Data32?) async throws -> [String] {
guard let stateRef = try getState(hash: blockHash ?? genesisBlockHash) else {
return []
}
Expand Down
28 changes: 28 additions & 0 deletions Blockchain/Sources/Blockchain/State/InMemoryBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public actor InMemoryBackend: StateBackendProtocol {

public func readAll(prefix: Data, startKey: Data?, limit: UInt32?) async throws -> [(key: Data, value: Data)] {
var resp = [(key: Data, value: Data)]()

if let limit {
resp.reserveCapacity(Int(limit))
}

let startKey = startKey ?? prefix
let startIndex = store.insertIndex(KVPair(key: startKey, value: Data()))
for i in startIndex ..< store.array.count {
Expand Down Expand Up @@ -104,4 +109,27 @@ public actor InMemoryBackend: StateBackendProtocol {
logger.info("ref count: \(refCount)")
}
}

public func createIterator(prefix: Data, startKey: Data?) async throws -> StateBackendIterator {
InMemoryStateIterator(store: store, prefix: prefix, startKey: startKey)
}
}

public final class InMemoryStateIterator: StateBackendIterator, @unchecked Sendable {
private var iterator: Array<(key: Data, value: Data)>.Iterator

init(store: SortedArray<InMemoryBackend.KVPair>, prefix: Data, startKey: Data?) {
let searchKey = startKey ?? prefix
let startIndex = store.insertIndex(InMemoryBackend.KVPair(key: searchKey, value: Data()))

let matchingItems = Array(store.array[startIndex...].prefix { item in
item.key.starts(with: prefix)
}.map { (key: $0.key, value: $0.value) })

iterator = matchingItems.makeIterator()
}

public func next() async throws -> (key: Data, value: Data)? {
iterator.next()
}
}
46 changes: 44 additions & 2 deletions Blockchain/Sources/Blockchain/State/StateBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,50 @@ public final class StateBackend: Sendable {
throw StateBackendError.missingState(key: key)
}

public func getKeys(_ prefix: Data31, _ startKey: Data31?, _ limit: UInt32?) async throws -> [(key: Data, value: Data)] {
try await impl.readAll(prefix: prefix.data, startKey: startKey?.data, limit: limit)
public func getKeys(_ prefix: Data?, _ startKey: Data31?, _ limit: UInt32?) async throws -> [(key: Data, value: Data)] {
let prefixData = prefix ?? Data()
let startKeyData = startKey?.data

let iterator = try await impl.createIterator(prefix: Data(), startKey: startKeyData)

var stateKeyValues: [(key: Data, value: Data)] = []

if let limit {
stateKeyValues.reserveCapacity(Int(limit))
}

while let (_, trieNodeData) = try await iterator.next() {
if let limit, stateKeyValues.count >= limit {
break
}

guard trieNodeData.count == 64 else {
continue
}

let firstByte = trieNodeData[relative: 0]
let isLeaf = (firstByte & 0b1100_0000) == 0b1000_0000 || (firstByte & 0b1100_0000) == 0b1100_0000

guard isLeaf else {
continue
}

let stateKey = Data(trieNodeData[relative: 1 ..< 32])

if !prefixData.isEmpty, !stateKey.starts(with: prefixData) {
continue
}

if let startKeyData, stateKey.lexicographicallyPrecedes(startKeyData) {
continue
}

if let value = try await trie.read(key: Data31(stateKey)!) {
stateKeyValues.append((key: stateKey, value: value))
}
}

return stateKeyValues
}

public func batchRead(_ keys: [any StateKey]) async throws -> [(key: any StateKey, value: (Codable & Sendable)?)] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ public enum StateBackendOperation: Sendable {
case refDecrement(key: Data)
}

public protocol StateBackendIterator: Sendable {
func next() async throws -> (key: Data, value: Data)?
}

/// key: trie node hash (31 bytes)
/// value: trie node data (64 bytes)
/// ref counting requirements:
Expand All @@ -20,6 +24,7 @@ public enum StateBackendOperation: Sendable {
public protocol StateBackendProtocol: Sendable {
func read(key: Data) async throws -> Data?
func readAll(prefix: Data, startKey: Data?, limit: UInt32?) async throws -> [(key: Data, value: Data)]
func createIterator(prefix: Data, startKey: Data?) async throws -> StateBackendIterator
func batchUpdate(_ ops: [StateBackendOperation]) async throws

// hash is the blake2b256 hash of the value
Expand Down
2 changes: 2 additions & 0 deletions Boka/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ let package = Package(
dependencies: [
.package(path: "../Node"),
.package(path: "../TracingUtils"),
.package(path: "../Fuzzing"),
.package(url: "https://github.com/slashmo/swift-otel.git", from: "0.9.0"),
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.6.0"),
.package(url: "https://github.com/vapor/console-kit.git", from: "4.15.0"),
Expand All @@ -25,6 +26,7 @@ let package = Package(
dependencies: [
"Node",
"TracingUtils",
"Fuzzing",
.product(name: "ServiceLifecycle", package: "swift-service-lifecycle"),
.product(name: "OTel", package: "swift-otel"),
.product(name: "OTLPGRPC", package: "swift-otel"),
Expand Down
2 changes: 1 addition & 1 deletion Boka/Sources/Boka.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ struct Boka: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "JAM built with Swift",
version: "0.0.1",
subcommands: [Generate.self]
subcommands: [Generate.self, Fuzz.self]
)

@Option(name: .shortAndLong, help: "Base path to database files.")
Expand Down
85 changes: 85 additions & 0 deletions Boka/Sources/Fuzz.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import ArgumentParser
import Foundation
import Fuzzing
import Logging

extension Boka {
struct Fuzz: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "fuzz",
abstract: "JAM Conformance Protocol",
subcommands: [Target.self, Fuzzer.self]
)
}
}

extension Boka.Fuzz {
enum JamConfig: String, CaseIterable, ExpressibleByArgument {
case tiny
case full
}

struct Target: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Run fuzzing target - waits for fuzzer connections"
)

@Option(help: "Unix socket path for fuzzing protocol")
var socketPath: String = "/tmp/jam_conformance.sock"

@Option(help: "JAM Protocol configuration preset")
var config: JamConfig = .tiny

func run() async throws {
let env = ProcessInfo.processInfo.environment
LoggingSystem.bootstrap { label in
var handler = StreamLogHandler.standardOutput(label: label)
handler.logLevel = parseLevel(env["LOG_LEVEL"] ?? "") ?? .info
return handler
}

let fuzzTarget = try FuzzingTarget(
socketPath: socketPath,
config: config.rawValue
)

try await fuzzTarget.run()
}
}

struct Fuzzer: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Run fuzzing fuzzer - connects to targets"
)

@Option(help: "Unix socket path for fuzzing protocol.")
var socketPath: String = "/tmp/jam_conformance.sock"

@Option(help: "JAM Protocol configuration preset.")
var config: JamConfig = .tiny

@Option(name: .long, help: "Random seed for deterministic testing. Default is random")
var seed: UInt64 = .random(in: 0 ... UInt64.max)

@Option(name: .long, help: "Number of blocks to process.")
var blocks: Int = 100

func run() async throws {
let env = ProcessInfo.processInfo.environment
LoggingSystem.bootstrap { label in
var handler = StreamLogHandler.standardOutput(label: label)
handler.logLevel = parseLevel(env["LOG_LEVEL"] ?? "") ?? .info
return handler
}

let fuzzer = try FuzzingClient(
socketPath: socketPath,
config: config.rawValue,
seed: seed,
blockCount: blocks
)

try await fuzzer.run()
}
}
}
2 changes: 1 addition & 1 deletion Boka/Sources/Tracing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public func parse(from: String) -> (
)
}

private func parseLevel(_ level: String) -> Logger.Level? {
public func parseLevel(_ level: String) -> Logger.Level? {
switch level.lowercased().trimmingCharacters(in: .whitespaces) {
case "trace": .trace
case "debug": .debug
Expand Down
Loading