diff --git a/src/fuzz.zig b/src/fuzz.zig index b0573ed6a..25c3dbac2 100644 --- a/src/fuzz.zig +++ b/src/fuzz.zig @@ -158,6 +158,6 @@ pub fn main() !void { => |run_cmd| try allocators_fuzz.run(gpa, seed, run_cmd), .ledger, - => |run_cmd| try ledger_fuzz.run(seed, run_cmd), + => |run_cmd| try ledger_fuzz.run(gpa, .from(logger), seed, sub_data_dir, run_cmd), } } diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index 92f9bd018..6cf6e65d5 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -5,10 +5,12 @@ const cli = @import("cli"); const ColumnFamily = sig.ledger.database.ColumnFamily; -const allocator = std.heap.c_allocator; - const Data = struct { value: []const u8, + + fn deinit(self: Data, allocator: std.mem.Allocator) void { + allocator.free(self.value); + } }; const cf1 = ColumnFamily{ @@ -17,17 +19,17 @@ const cf1 = ColumnFamily{ .Value = Data, }; -var executed_actions = std.AutoHashMap(Actions, void).init(allocator); - pub const LedgerDB = switch (sig.build_options.ledger_db) { .rocksdb => ledger.database.RocksDB(&.{cf1}), .hashmap => ledger.database.SharedHashMapDB(&.{cf1}), }; +const MIN_ACTION_COUNT = @typeInfo(Action).@"enum".fields.len; + // Note: deleteFilesInRange is not included in the fuzzing as it is not // implemented in the hashmap implementation, and the RocksDB implementation // requires manual flushing of the memtable to disk to make the changes visible. -const Actions = enum { +const Action = enum { put, get, get_bytes, @@ -37,30 +39,23 @@ const Actions = enum { batch, }; -fn getKeys(map: *std.AutoHashMap(u32, Data)) !std.ArrayList(u32) { - var keys = std.ArrayList(u32).init(allocator); - var it = map.iterator(); - while (it.next()) |entry| { - try keys.append(entry.key_ptr.*); +fn getKeys( + allocator: std.mem.Allocator, + map: *const std.AutoArrayHashMapUnmanaged(u32, Data), +) ![]const u32 { + var keys: std.ArrayListUnmanaged(u32) = .empty; + errdefer keys.deinit(allocator); + try keys.ensureTotalCapacityPrecise(allocator, map.count()); + for (map.keys()) |key| { + keys.appendAssumeCapacity(key); } - return keys; + return try keys.toOwnedSlice(allocator); } -fn createLedgerDB() !LedgerDB { - const ledger_path = - try std.fmt.allocPrint(allocator, "{s}/ledger", .{sig.FUZZ_DATA_DIR}); - - // ensure we start with a clean slate. - if (std.fs.cwd().access(ledger_path, .{})) |_| { - try std.fs.cwd().deleteTree(ledger_path); - } else |_| {} - try std.fs.cwd().makePath(ledger_path); - - return try LedgerDB.open( - allocator, - .noop, - ledger_path, - ); +fn createLedgerDB(allocator: std.mem.Allocator, dst_dir: std.fs.Dir) !LedgerDB { + const ledger_path = try dst_dir.realpathAlloc(allocator, "."); + defer allocator.free(ledger_path); + return try LedgerDB.open(allocator, .noop, ledger_path); } pub const RunCmd = struct { @@ -78,146 +73,180 @@ pub const RunCmd = struct { .alias = .m, .default_value = null, .config = {}, - .help = "Maximum number of actions to take before exiting the fuzzer.", + .help = std.fmt.comptimePrint( + \\Maximum number of actions to take before exiting the fuzzer; + \\floored by the minimum number of actions ({d}). + , .{MIN_ACTION_COUNT}), }, }, }; }; +const FuzzLogger = sig.trace.Logger("ledger.fuzz"); + pub fn run( + allocator: std.mem.Allocator, + logger: FuzzLogger, initial_seed: u64, + fuzz_data_dir: std.fs.Dir, run_cmd: RunCmd, ) !void { - try runInner(initial_seed, run_cmd.max_actions, true); + try runInner( + allocator, + logger, + initial_seed, + fuzz_data_dir, + run_cmd.max_actions, + ); } -fn runInner(initial_seed: u64, maybe_max_actions: ?u64, log: bool) !void { - var seed = initial_seed; - - const ledger_path = - try std.fmt.allocPrint(allocator, "{s}/ledger", .{sig.FUZZ_DATA_DIR}); - - // ensure we start with a clean slate. - if (std.fs.cwd().access(ledger_path, .{})) |_| { - try std.fs.cwd().deleteTree(ledger_path); - } else |_| {} - try std.fs.cwd().makePath(ledger_path); - - var db = try createLedgerDB(); - +fn runInner( + allocator: std.mem.Allocator, + logger: FuzzLogger, + initial_seed: u64, + fuzz_data_dir: std.fs.Dir, + maybe_max_actions: ?usize, +) !void { + var db = try createLedgerDB(allocator, fuzz_data_dir); defer db.deinit(); + var missing_actions: std.EnumSet(Action) = .initFull(); + var seed = initial_seed; var count: u64 = 0; - outer: while (true) { - var prng = std.Random.DefaultPrng.init(seed); - const random = prng.random(); + var prng_state: std.Random.DefaultPrng = .init(seed); + const prng = prng_state.random(); + // This is a simpler ledger which is used to make sure // the method calls being fuzzed return expected data. - var data_map = std.AutoHashMap(u32, Data).init(allocator); - defer data_map.deinit(); + var data_map: std.AutoArrayHashMapUnmanaged(u32, Data) = .empty; + defer { + for (data_map.values()) |data| allocator.free(data.value); + data_map.deinit(allocator); + } + for (0..1_000) |_| { if (maybe_max_actions) |max| { - if (count >= max) { - if (log) std.debug.print("reached max actions: {}\n", .{max}); + const actual_max = @max(MIN_ACTION_COUNT, max); + if (count >= actual_max) { + logger.info().logf("reached max actions: {}\n", .{actual_max}); break :outer; } } - const action = random.enumValue(Actions); - + const action = while (true) { + const action = prng.enumValue(Action); + if (missing_actions.count() == 0) break action; + if (missing_actions.contains(action)) break action; + }; + missing_actions.remove(action); switch (action) { - .put => try dbPut(&data_map, &db, random), - .get => try dbGet(&data_map, &db, random), - .get_bytes => try dbGetBytes(&data_map, &db, random), + .put => try dbPut(allocator, &data_map, &db, prng), + .get => try dbGet(allocator, &data_map, &db, prng), + .get_bytes => try dbGetBytes(allocator, &data_map, &db, prng), .count => try dbCount(&data_map, &db), - .contains => try dbContains(&data_map, &db, random), - .delete => try dbDelete(&data_map, &db, random), - .batch => try batchAPI(&data_map, &db, random), + .contains => try dbContains(allocator, &data_map, &db, prng), + .delete => try dbDelete(allocator, &data_map, &db, prng), + .batch => try batchAPI(allocator, &data_map, &db, prng), } count += 1; } seed += 1; - if (log) std.debug.print("using seed: {}\n", .{seed}); + logger.debug().logf("using seed: {}\n", .{seed}); } - inline for (@typeInfo(Actions).@"enum".fields) |field| { - const variant = @field(Actions, field.name); - if (!executed_actions.contains(variant)) { - std.debug.print("Action: '{s}' not executed by the fuzzer", .{@tagName(variant)}); - return error.NonExhaustive; - } + if (missing_actions.count() != 0) { + std.debug.panic("This shouldn't be possible.", .{}); } } -fn dbPut(data_map: *std.AutoHashMap(u32, Data), db: *LedgerDB, random: std.Random) !void { - try executed_actions.put(Actions.put, {}); +fn dbPut( + allocator: std.mem.Allocator, + data_map: *std.AutoArrayHashMapUnmanaged(u32, Data), + db: *LedgerDB, + random: std.Random, +) !void { const key = random.int(u32); - var buffer: [61]u8 = undefined; // Fill the buffer with random bytes - for (0..buffer.len) |i| { - buffer[i] = @intCast(random.int(u8)); - } - - const value: []const u8 = try allocator.dupe(u8, buffer[0..]); - const data = Data{ .value = value }; + var buffer: [61]u8 = undefined; + random.bytes(&buffer); + const data: Data = .{ .value = try allocator.dupe(u8, &buffer) }; + errdefer data.deinit(allocator); try db.put(cf1, key, data); - try data_map.put(key, data); + try data_map.put(allocator, key, data); } -fn dbGet(data_map: *std.AutoHashMap(u32, Data), db: *LedgerDB, random: std.Random) !void { - try executed_actions.put(Actions.get, {}); - const dataKeys = try getKeys(data_map); - if (dataKeys.items.len > 0 and random.boolean()) { - const random_index = random.uintLessThan(usize, dataKeys.items.len); - const key = dataKeys.items[random_index]; - const expected = data_map.get(key) orelse return error.KeyNotFoundError; +fn dbGet( + allocator: std.mem.Allocator, + data_map: *const std.AutoArrayHashMapUnmanaged(u32, Data), + db: *LedgerDB, + random: std.Random, +) !void { + const data_keys = try getKeys(allocator, data_map); + defer allocator.free(data_keys); - const actual = try db.get(allocator, cf1, key) orelse return error.KeyNotFoundError; + if (data_keys.len > 0 and random.boolean()) { + const random_index = random.uintLessThan(usize, data_keys.len); + const key = data_keys[random_index]; + const expected = data_map.get(key) orelse return error.KeyNotFoundError; + const actual: Data = try db.get(allocator, cf1, key) orelse return error.KeyNotFoundError; + defer actual.deinit(allocator); try std.testing.expect(std.mem.eql(u8, expected.value, actual.value)); } else { // If there are no keys, we should get a null value. var key: u32 = random.int(u32); while (data_map.contains(key)) key = random.int(u32); - const actual = try db.get(allocator, cf1, key); + + const actual: ?Data = try db.get(allocator, cf1, key); + defer if (actual) |unwrapped| unwrapped.deinit(allocator); // shouldn't happen, but if it does, nice to avoid a leak in the stacktrace try std.testing.expectEqual(null, actual); } } -fn dbGetBytes(data_map: *std.AutoHashMap(u32, Data), db: *LedgerDB, random: std.Random) !void { - try executed_actions.put(Actions.get_bytes, {}); - const dataKeys = try getKeys(data_map); - if (dataKeys.items.len > 0 and random.boolean()) { - const random_index = random.uintLessThan(usize, dataKeys.items.len); - const key = dataKeys.items[random_index]; +fn dbGetBytes( + allocator: std.mem.Allocator, + data_map: *const std.AutoArrayHashMapUnmanaged(u32, Data), + db: *LedgerDB, + random: std.Random, +) !void { + const data_keys = try getKeys(allocator, data_map); + defer allocator.free(data_keys); + + if (data_keys.len > 0 and random.boolean()) { + const random_index = random.uintLessThan(usize, data_keys.len); + const key = data_keys[random_index]; const expected = data_map.get(key) orelse return error.KeyNotFoundError; - const actualBytes = try db.getBytes(cf1, key) orelse return error.KeyNotFoundError; - const actual = try ledger.database.value_serializer.deserialize( + const actual_bytes = try db.getBytes(cf1, key) orelse return error.KeyNotFoundError; + defer actual_bytes.deinit(); + + const actual: Data = try ledger.database.value_serializer.deserialize( cf1.Value, allocator, - actualBytes.data, + actual_bytes.data, ); + defer actual.deinit(allocator); - try std.testing.expect(std.mem.eql(u8, expected.value, actual.value)); + try std.testing.expectEqualSlices(u8, expected.value, actual.value); } else { // If there are no keys, we should get a null value. var key: u32 = random.int(u32); while (data_map.contains(key)) key = random.int(u32); + const actual = try db.getBytes(cf1, key); + defer if (actual) |unwrapped| unwrapped.deinit(); // shouldn't happen, but if it does, nice to avoid a leak in the stacktrace try std.testing.expectEqual(null, actual); } } fn dbCount( - data_map: *std.AutoHashMap(u32, Data), + data_map: *const std.AutoArrayHashMapUnmanaged(u32, Data), db: *LedgerDB, ) !void { - try executed_actions.put(Actions.count, {}); // TODO Fix why changes are not reflected in count with rocksdb implementation, // but it does with hashmap. if (sig.build_options.ledger_db == .rocksdb) { @@ -230,40 +259,54 @@ fn dbCount( try std.testing.expectEqual(expected, actual); } -fn dbContains(data_map: *std.AutoHashMap(u32, Data), db: *LedgerDB, random: std.Random) !void { - try executed_actions.put(Actions.contains, {}); - const dataKeys = try getKeys(data_map); - if (dataKeys.items.len > 0 and random.boolean()) { - const random_index = random.uintLessThan(usize, dataKeys.items.len); - const key = dataKeys.items[random_index]; +fn dbContains( + allocator: std.mem.Allocator, + data_map: *const std.AutoArrayHashMapUnmanaged(u32, Data), + db: *LedgerDB, + random: std.Random, +) !void { + const data_keys = try getKeys(allocator, data_map); + defer allocator.free(data_keys); - const actual = try db.contains(cf1, key); + if (data_keys.len > 0 and random.boolean()) { + const random_index = random.uintLessThan(usize, data_keys.len); + const key = data_keys[random_index]; + const actual = try db.contains(cf1, key); try std.testing.expect(actual); } else { // If there are no keys, we should get a null value. var key: u32 = random.int(u32); while (data_map.contains(key)) key = random.int(u32); + const actual = try db.contains(cf1, key); try std.testing.expect(!actual); } } -fn dbDelete(data_map: *std.AutoHashMap(u32, Data), db: *LedgerDB, random: std.Random) !void { - try executed_actions.put(Actions.delete, {}); - const dataKeys = try getKeys(data_map); - if (dataKeys.items.len > 0 and random.boolean()) { - const random_index = random.uintLessThan(usize, dataKeys.items.len); - const key = dataKeys.items[random_index]; +fn dbDelete( + allocator: std.mem.Allocator, + data_map: *std.AutoArrayHashMapUnmanaged(u32, Data), + db: *LedgerDB, + random: std.Random, +) !void { + const data_keys = try getKeys(allocator, data_map); + defer allocator.free(data_keys); + + if (data_keys.len > 0 and random.boolean()) { + const random_index = random.uintLessThan(usize, data_keys.len); + const key = data_keys[random_index]; try db.delete(cf1, key); const actual = try db.get(allocator, cf1, key) orelse null; + defer if (actual) |unwrapped| unwrapped.deinit(allocator); // shouldn't happen, but if it does, nice to avoid a leak in the stacktrace try std.testing.expectEqual(null, actual); - // Remove the keys from the global map. - _ = data_map.remove(key); + + // Remove the keys from the map. + const data = data_map.fetchSwapRemove(key).?.value; // if this panics, something is very wrong + defer data.deinit(allocator); } else { - // If there are no keys, we should get a null value. var key: u32 = random.int(u32); while (data_map.contains(key)) key = random.int(u32); try db.delete(cf1, key); @@ -271,94 +314,95 @@ fn dbDelete(data_map: *std.AutoHashMap(u32, Data), db: *LedgerDB, random: std.Ra } // Batch API -fn batchAPI(data_map: *std.AutoHashMap(u32, Data), db: *LedgerDB, random: std.Random) !void { - try executed_actions.put(Actions.batch, {}); +fn batchAPI( + allocator: std.mem.Allocator, + data_map: *std.AutoArrayHashMapUnmanaged(u32, Data), + db: *LedgerDB, + random: std.Random, +) !void { // Batch put { - const startKey = random.int(u32); - const endKey = startKey +| random.int(u8); - var buffer: [61]u8 = undefined; var batch = try db.initWriteBatch(); defer batch.deinit(); - for (startKey..endKey) |key| { + + const start_key = random.int(u32); + const end_key = start_key +| random.int(u8); + for (start_key..end_key) |key| { // Fill the buffer with random bytes for each key. - for (0..buffer.len) |i| { - buffer[i] = @intCast(random.int(u8)); - } + var buffer: [61]u8 = undefined; + random.bytes(&buffer); - const value: []const u8 = try allocator.dupe(u8, buffer[0..]); - const data = Data{ .value = value }; + const data: Data = .{ .value = try allocator.dupe(u8, &buffer) }; + errdefer data.deinit(allocator); try batch.put(cf1, key, data); - try data_map.put(@as(u32, @intCast(key)), data); + try data_map.put(allocator, @as(u32, @intCast(key)), data); } + // Commit batch put. // Note: Returns void so no confirmation needed. try db.commit(&batch); - var it = data_map.iterator(); - while (it.next()) |entry| { - const entryKey = entry.key_ptr.*; - const expected = entry.value_ptr.*; - const actual = try db.get( + + for (data_map.keys(), data_map.values()) |entry_key, expected| { + const actual: Data = try db.get( allocator, cf1, - entryKey, + entry_key, ) orelse return error.KeyNotFoundError; - try std.testing.expect(std.mem.eql(u8, expected.value, actual.value)); + defer actual.deinit(allocator); + try std.testing.expectEqualSlices(u8, expected.value, actual.value); } } // Batch delete. { - const startKey = random.int(u32); - const endKey = startKey +| random.int(u8); - var buffer: [61]u8 = undefined; var batch = try db.initWriteBatch(); defer batch.deinit(); - for (startKey..endKey) |key| { - // Fill the buffer with random bytes for each key. - for (0..buffer.len) |i| { - buffer[i] = @intCast(random.int(u8)); - } - const value: []const u8 = try allocator.dupe(u8, buffer[0..]); - const data = Data{ .value = value }; + const start_key = random.int(u32); + const end_key = start_key +| random.int(u8); + for (start_key..end_key) |key| { + // Fill the buffer with random bytes for each key. + var buffer: [61]u8 = undefined; + random.bytes(&buffer); + const data: Data = .{ .value = &buffer }; try batch.put(cf1, key, data); try batch.delete(cf1, key); } + // Commit batch put and delete. // Note: Returns void so no confirmation needed. try db.commit(&batch); - for (startKey..endKey) |key| { - const actual = try db.get(allocator, cf1, @as(u32, @intCast(key))); + + for (start_key..end_key) |key| { + const actual: ?Data = try db.get(allocator, cf1, @as(u32, @intCast(key))); + defer if (actual) |unwrapped| unwrapped.deinit(allocator); try std.testing.expectEqual(null, actual); } } // Batch delete range. { - const startKey = random.int(u32); - const endKey = startKey +| random.int(u8); - var buffer: [61]u8 = undefined; + const start_key = random.int(u32); + const end_key = start_key +| random.int(u8); var batch = try db.initWriteBatch(); defer batch.deinit(); - for (startKey..endKey) |key| { + for (start_key..end_key) |key| { // Fill the buffer with random bytes for each key. - for (0..buffer.len) |i| { - buffer[i] = @intCast(random.int(u8)); - } - - const value: []const u8 = try allocator.dupe(u8, buffer[0..]); - const data = Data{ .value = value }; + var buffer: [61]u8 = undefined; + random.bytes(&buffer); + const data: Data = .{ .value = &buffer }; try batch.put(cf1, key, data); } - try batch.deleteRange(cf1, startKey, endKey); + + try batch.deleteRange(cf1, start_key, end_key); + // Commit batch put and delete range. // Note: Returns void so no confirmation needed. try db.commit(&batch); - for (startKey..endKey) |key| { + for (start_key..end_key) |key| { const actual = try db.get(allocator, cf1, @as(u32, @intCast(key))); try std.testing.expectEqual(null, actual); } @@ -366,7 +410,7 @@ fn batchAPI(data_map: *std.AutoHashMap(u32, Data), db: *LedgerDB, random: std.Ra } test run { - var args = std.process.ArgIterator.init(); - while (args.next()) |_| {} - try runInner(0, 100, false); + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + try runInner(std.testing.allocator, .noop, 0, tmp_dir.dir, 100); }