Skip to content

Commit

Permalink
fix(ledger): fuzz testing (#452)
Browse files Browse the repository at this point in the history
* Added fuzz for rockdb implementation for ledger

* Fix build

* Have all imports at top of file

* Zig fmt

* Remove explicit type annotation

* Remove concurrency

* No need to divide up the provided max action

* Assert result of ledger calls

* Pass random as value

* Added dbDeleteFilesInRange

* Use method syntax

* Skip count for rocksdb impl

* Rename batchOps to BatchAPI

* Fix style

* Use if as expression

* Remove explicit reference to rocksdb

* Randomize method calls

* Renamed variable and added documentation

* Added comments explaining why deleteFilesInRange is not included

* Comment

* Use the defined enum

* Use just {} to represent the void value

* test(ledger): discrete fuzzer test cases

* use std.Random not std.rand

* test(ledger): key should sometimes found, sometimes not

---------

Co-authored-by: Drew Nutter <[email protected]>
  • Loading branch information
dadepo and dnut authored Jan 15, 2025
1 parent ca29acd commit fbc554e
Show file tree
Hide file tree
Showing 4 changed files with 353 additions and 0 deletions.
5 changes: 5 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@ pub fn build(b: *Build) void {
fuzz_exe.root_module.addImport("zig-network", zig_network_module);
fuzz_exe.root_module.addImport("httpz", httpz_mod);
fuzz_exe.root_module.addImport("zstd", zstd_mod);
fuzz_exe.root_module.addOptions("build-options", build_options);
switch (blockstore_db) {
.rocksdb => fuzz_exe.root_module.addImport("rocksdb", rocksdb_mod),
.hashmap => {},
}
fuzz_exe.linkLibC();

const fuzz_exe_run = b.addRunArtifact(fuzz_exe);
Expand Down
3 changes: 3 additions & 0 deletions src/fuzz.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const accountsdb_fuzz = sig.accounts_db.fuzz;
const gossip_fuzz_service = sig.gossip.fuzz_service;
const gossip_fuzz_table = sig.gossip.fuzz_table;
const accountsdb_snapshot_fuzz = sig.accounts_db.fuzz_snapshot;
const ledger_fuzz = sig.ledger.fuzz_ledger;
const StandardErrLogger = sig.trace.ChannelPrintLogger;
const Level = sig.trace.Level;

Expand All @@ -22,6 +23,7 @@ pub const FuzzFilter = enum {
gossip_service,
gossip_table,
allocators,
ledger,
};

pub fn main() !void {
Expand Down Expand Up @@ -86,6 +88,7 @@ pub fn main() !void {
.snapshot => try accountsdb_snapshot_fuzz.run(&cli_args),
.gossip_service => try gossip_fuzz_service.run(seed, &cli_args),
.gossip_table => try gossip_fuzz_table.run(seed, &cli_args),
.ledger => try ledger_fuzz.run(seed, &cli_args),
.allocators => try sig.utils.allocators.runFuzzer(seed, &cli_args),
}
}
Expand Down
344 changes: 344 additions & 0 deletions src/ledger/fuzz.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,344 @@
const std = @import("std");
const sig = @import("../sig.zig");
const build_options = @import("build-options");
const ledger = @import("lib.zig");

const ColumnFamily = sig.ledger.database.ColumnFamily;

const allocator = std.heap.c_allocator;

const Data = struct {
value: []const u8,
};

const cf1 = ColumnFamily{
.name = "data",
.Key = u64,
.Value = Data,
};

var executed_actions = std.AutoHashMap(Actions, void).init(allocator);

pub const BlockstoreDB = switch (build_options.blockstore_db) {
.rocksdb => ledger.database.RocksDB(&.{cf1}),
.hashmap => ledger.database.SharedHashMapDB(&.{cf1}),
};

// 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 {
put,
get,
get_bytes,
count,
contains,
delete,
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.*);
}
return keys;
}

fn createBlockstoreDB() !BlockstoreDB {
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 BlockstoreDB.open(
allocator,
.noop,
ledger_path,
);
}

pub fn run(initial_seed: u64, args: *std.process.ArgIterator) !void {
var seed = initial_seed;
const maybe_max_actions_string = args.next();

const maybe_max_actions = if (maybe_max_actions_string) |max_actions_str|
try std.fmt.parseInt(usize, max_actions_str, 10)
else
null;

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 createBlockstoreDB();

defer db.deinit();

var count: u64 = 0;

while (true) outer: {
var prng = std.Random.DefaultPrng.init(seed);
const random = prng.random();
// This is a simpler blockstore 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();
for (0..1_000) |_| {
if (maybe_max_actions) |max| {
if (count >= max) {
std.debug.print("{s} reached max actions: {}\n", .{ "action_name", max });
break :outer;
}
}

const action = random.enumValue(Actions);

switch (action) {
.put => try dbPut(&data_map, &db, random),
.get => try dbGet(&data_map, &db, random),
.get_bytes => try dbGetBytes(&data_map, &db, random),
.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),
}

count += 1;
}
seed += 1;
std.debug.print("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;
}
}
}

fn dbPut(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.Random) !void {
try executed_actions.put(Actions.put, {});
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 };

try db.put(cf1, key, data);
try data_map.put(key, data);
}

fn dbGet(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, 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;

const actual = try db.get(allocator, cf1, key) orelse return error.KeyNotFoundError;

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);
try std.testing.expectEqual(null, actual);
}
}

fn dbGetBytes(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, 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];
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(
cf1.Value,
allocator,
actualBytes.data,
);

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.getBytes(cf1, key);
try std.testing.expectEqual(null, actual);
}
}

fn dbCount(
data_map: *std.AutoHashMap(u32, Data),
db: *BlockstoreDB,
) !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 (build_options.blockstore_db == .rocksdb) {
return;
}

const expected = data_map.count();
const actual = try db.count(cf1);

try std.testing.expectEqual(expected, actual);
}

fn dbContains(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, 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];

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: *BlockstoreDB, 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];

try db.delete(cf1, key);

const actual = try db.get(allocator, cf1, key) orelse null;
try std.testing.expectEqual(null, actual);
// Remove the keys from the global map.
_ = data_map.remove(key);
} 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);
}
}

// Batch API
fn batchAPI(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.Random) !void {
try executed_actions.put(Actions.batch, {});
// 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| {
// 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 };

try batch.put(cf1, key, data);
try data_map.put(@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(
allocator,
cf1,
entryKey,
) orelse return error.KeyNotFoundError;
try std.testing.expect(std.mem.eql(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 };

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)));
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;
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 };

try batch.put(cf1, key, data);
}
try batch.deleteRange(cf1, startKey, endKey);
// Commit batch put and delete range.
// 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)));
try std.testing.expectEqual(null, actual);
}
}
}
1 change: 1 addition & 0 deletions src/ledger/lib.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub const shred = @import("shred.zig");
pub const shredder = @import("shredder.zig");
pub const transaction_status = @import("transaction_status.zig");
pub const tests = @import("tests.zig");
pub const fuzz_ledger = @import("fuzz.zig");

pub const BlockstoreDB = blockstore.BlockstoreDB;
pub const ShredInserter = shred_inserter.ShredInserter;
Expand Down

0 comments on commit fbc554e

Please sign in to comment.