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
14 changes: 13 additions & 1 deletion Blockchain/Sources/Blockchain/State/State.swift
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,19 @@ public struct State: Sendable {

public var stateRoot: Data32 {
get async {
await backend.rootHash
// TODO: should use backend.rootHash after StateTrie is fixed
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do {
let allKeys = try await backend.getKeys(nil, nil, nil)
var kv: [Data31: Data] = [:]
for (key, value) in allKeys {
if let key31 = Data31(key) {
kv[key31] = value
}
}
return try stateMerklize(kv: kv)
} catch {
return await backend.rootHash
}
}
}

Expand Down
11 changes: 7 additions & 4 deletions Boka/Sources/Fuzz.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,14 @@ extension Boka.Fuzz {
@Option(help: "JAM Protocol configuration preset.")
var config: JamConfig = .tiny

@Option(name: .long, help: "Random seed for deterministic testing. Default is random")
@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
var blocks: Int = 200

@Option(name: .long, help: "Directory containing traces test vectors to run.")
var tracesDir: String?

func run() async throws {
let env = ProcessInfo.processInfo.environment
Expand All @@ -76,9 +79,9 @@ extension Boka.Fuzz {
socketPath: socketPath,
config: config.rawValue,
seed: seed,
blockCount: blocks
blockCount: blocks,
tracesDir: tracesDir
)

try await fuzzer.run()
}
}
Expand Down
2 changes: 2 additions & 0 deletions Fuzzing/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ let package = Package(
.package(path: "../Codec"),
.package(path: "../TracingUtils"),
.package(path: "../Utils"),
.package(path: "../JAMTests"),
.package(url: "https://github.com/apple/swift-testing.git", branch: "6.0.0"),
],
targets: [
Expand All @@ -28,6 +29,7 @@ let package = Package(
"Codec",
"TracingUtils",
"Utils",
"JAMTests",
],
swiftSettings: [
.interoperabilityMode(.Cxx),
Expand Down
15 changes: 12 additions & 3 deletions Fuzzing/Sources/Fuzzing/FuzzGenerator/FuzzGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,21 @@ import Utils

/// Protocol for generating fuzzing test data
public protocol FuzzGenerator {
/// Generate initial state for fuzzing
/// Generate initial pre-state for fuzzing
/// - Parameters:
/// - timeslot: The timeslot for which to generate the state
/// - config: Protocol configuration
/// - Returns: Array of fuzz key-value pairs representing the state
func generateState(timeslot: TimeslotIndex, config: ProtocolConfigRef) async throws -> [FuzzKeyValue]
/// - Returns: Tuple containing state root and array of fuzz key-value pairs representing the pre-state
func generatePreState(timeslot: TimeslotIndex, config: ProtocolConfigRef) async throws
-> (stateRoot: Data32, keyValues: [FuzzKeyValue])

/// Generate expected post-state after block execution
/// - Parameters:
/// - timeslot: The timeslot for which to generate the state
/// - config: Protocol configuration
/// - Returns: Tuple containing state root and array of fuzz key-value pairs representing the expected post-state
func generatePostState(timeslot: TimeslotIndex, config: ProtocolConfigRef) async throws
-> (stateRoot: Data32, keyValues: [FuzzKeyValue])

/// Generate a block for the given timeslot and state
/// - Parameters:
Expand Down
15 changes: 13 additions & 2 deletions Fuzzing/Sources/Fuzzing/FuzzGenerator/FuzzGeneratorRandom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,19 @@ public class FuzzGeneratorRandom: FuzzGenerator {
dataProvider = nil
}

// TODO: Implement state generation logic
public func generateState(timeslot _: TimeslotIndex, config _: ProtocolConfigRef) async throws -> [FuzzKeyValue] {
// TODO: Implement pre-state generation logic
public func generatePreState(
timeslot _: TimeslotIndex,
config _: ProtocolConfigRef
) async throws -> (stateRoot: Data32, keyValues: [FuzzKeyValue]) {
fatalError("not implemented")
}

// TODO: Implement post-state generation logic
public func generatePostState(
timeslot _: TimeslotIndex,
config _: ProtocolConfigRef
) async throws -> (stateRoot: Data32, keyValues: [FuzzKeyValue]) {
fatalError("not implemented")
}

Expand Down
138 changes: 138 additions & 0 deletions Fuzzing/Sources/Fuzzing/FuzzGenerator/FuzzGeneratorTraces.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import Blockchain
import Foundation
import JAMTests
import TracingUtils
import Utils

private let logger = Logger(label: "FuzzGeneratorTraces")

/// A fuzz generator that loads test cases from JAM conformance fuzz traces
public class FuzzGeneratorTraces: FuzzGenerator {
private let testCases: [JamTestnetTestcase]

public init(tracesDir: String) throws {
logger.info("Loading test vectors from directory: \(tracesDir)")

testCases = try Self.loadTestCases(from: tracesDir)

logger.info("Loaded \(testCases.count) test cases")

if testCases.isEmpty {
throw FuzzGeneratorError.invalidTestData("No test cases found in directory: \(tracesDir)")
}
}

public func generatePreState(
timeslot: TimeslotIndex,
config _: ProtocolConfigRef
) async throws -> (stateRoot: Data32, keyValues: [FuzzKeyValue]) {
let testIndex = Int(timeslot)
guard testIndex >= 0, testIndex < testCases.count else {
throw FuzzGeneratorError.stateGenerationFailed("No test case available for timeslot \(timeslot) (index \(testIndex))")
}

let testCase = testCases[testIndex]

logger.debug("Generating pre-state for timeslot \(timeslot) (test case \(testIndex + 1)/\(testCases.count))")

let keyValues = testCase.preState.keyvals.map { keyval in
FuzzKeyValue(key: keyval.key, value: keyval.value)
}

return (stateRoot: testCase.preState.root, keyValues: keyValues)
}

public func generatePostState(
timeslot: TimeslotIndex,
config _: ProtocolConfigRef
) async throws -> (stateRoot: Data32, keyValues: [FuzzKeyValue]) {
let testIndex = Int(timeslot)
guard testIndex >= 0, testIndex < testCases.count else {
throw FuzzGeneratorError.stateGenerationFailed("No test case available for timeslot \(timeslot) (index \(testIndex))")
}

let testCase = testCases[testIndex]

logger.debug("Generating expected post-state for timeslot \(timeslot) (test case \(testIndex + 1)/\(testCases.count))")

let keyValues = testCase.postState.keyvals.map { keyval in
FuzzKeyValue(key: keyval.key, value: keyval.value)
}

return (stateRoot: testCase.postState.root, keyValues: keyValues)
}

public func generateBlock(timeslot: UInt32, currentStateRef _: StateRef, config _: ProtocolConfigRef) async throws -> BlockRef {
let testIndex = Int(timeslot)
guard testIndex >= 0, testIndex < testCases.count else {
throw FuzzGeneratorError.blockGenerationFailed("No test case available for timeslot \(timeslot) (index \(testIndex))")
}

let testCase = testCases[testIndex]

logger.debug("Generating block for timeslot \(timeslot) (test case \(testIndex + 1)/\(testCases.count))")

let block = testCase.block

return block.asRef()
}

private static func loadTestCases(from directory: String) throws -> [JamTestnetTestcase] {
logger.info("Loading test cases from directory: \(directory)")

let basePath = directory

guard FileManager.default.fileExists(atPath: basePath) else {
throw FuzzGeneratorError.invalidTestData("Directory does not exist: \(basePath)")
}

var allDecodedTestCases: [JamTestnetTestcase] = []

// Find all .bin files with depth of 2
func findBinFiles(in path: String, currentDepth: Int = 0) throws -> [String] {
guard currentDepth <= 2 else { return [] }

var binFiles: [String] = []
let contents = try FileManager.default.contentsOfDirectory(atPath: path)

for item in contents {
guard !item.starts(with: ".") else { continue }

let itemPath = "\(path)/\(item)"
var isDirectory: ObjCBool = false

guard FileManager.default.fileExists(atPath: itemPath, isDirectory: &isDirectory) else {
continue
}

if isDirectory.boolValue, currentDepth < 2 {
// Recurse into subdirectory
binFiles += try findBinFiles(in: itemPath, currentDepth: currentDepth + 1)
} else if !isDirectory.boolValue, item.hasSuffix(".bin") {
// Found a .bin file
binFiles.append(itemPath)
}
}

return binFiles.sorted()
}

let testFiles = try findBinFiles(in: basePath)

for testFilePath in testFiles {
do {
let testData = try Data(contentsOf: URL(fileURLWithPath: testFilePath))
let rawTestCase = Testcase(description: URL(fileURLWithPath: testFilePath).lastPathComponent, data: testData)

let decoded = try JamTestnet.decodeTestcase(rawTestCase)
allDecodedTestCases.append(decoded)
logger.debug("Successfully loaded test case: \(testFilePath)")
} catch {
logger.warning("Failed to decode test case \(testFilePath): \(error)")
}
}

logger.info("Successfully loaded \(allDecodedTestCases.count) test cases")
return allDecodedTestCases
}
}
Loading
Loading