pkbfi.zig 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. const std = @import("std");
  2. const engine_mod = @import("engine.zig");
  3. const pkvdb = @import("pkvdb.zig");
  4. pub const header_size = 32;
  5. pub const max_frame_size = pkvdb.max_key_size + pkvdb.max_value_size + 1024;
  6. pub const Opcode = enum(u16) {
  7. ping = 1,
  8. status = 2,
  9. get = 3,
  10. put = 4,
  11. delete = 5,
  12. exists = 6,
  13. multi_get = 7,
  14. batch_write = 8,
  15. scan_open = 9,
  16. scan_next = 10,
  17. scan_close = 11,
  18. compare_batch_write = 12,
  19. };
  20. pub const Frame = struct {
  21. opcode: Opcode,
  22. flags: u16,
  23. request_id: u64,
  24. payload: []const u8,
  25. consumed: usize,
  26. };
  27. const Scan = struct {
  28. id: u64,
  29. prefix: []u8,
  30. cursor: []u8,
  31. include_values: bool,
  32. limit: u32,
  33. };
  34. pub const Session = struct {
  35. allocator: std.mem.Allocator,
  36. scans: std.ArrayListUnmanaged(Scan) = .{},
  37. next_scan_id: u64 = 1,
  38. pub fn init(allocator: std.mem.Allocator) Session {
  39. return .{ .allocator = allocator };
  40. }
  41. pub fn deinit(self: *Session) void {
  42. for (self.scans.items) |scan| {
  43. self.allocator.free(scan.prefix);
  44. self.allocator.free(scan.cursor);
  45. }
  46. self.scans.deinit(self.allocator);
  47. self.* = undefined;
  48. }
  49. fn findScan(self: *Session, id: u64) ?usize {
  50. for (self.scans.items, 0..) |scan, index| if (scan.id == id) return index;
  51. return null;
  52. }
  53. pub fn execute(self: *Session, engine: *engine_mod.Engine, frame: Frame) ![]u8 {
  54. var body = std.ArrayListUnmanaged(u8){};
  55. defer body.deinit(self.allocator);
  56. self.executeBody(engine, frame, &body) catch |err| {
  57. body.clearRetainingCapacity();
  58. try appendInt(u16, &body, self.allocator, 2);
  59. try body.appendSlice(self.allocator, @errorName(err));
  60. };
  61. return encode(self.allocator, @intFromEnum(frame.opcode) | 0x8000, 1, frame.request_id, body.items);
  62. }
  63. fn executeBody(self: *Session, engine: *engine_mod.Engine, frame: Frame, body: *std.ArrayListUnmanaged(u8)) !void {
  64. try appendInt(u16, body, self.allocator, 0);
  65. switch (frame.opcode) {
  66. .ping => try body.appendSlice(self.allocator, frame.payload),
  67. .status => {
  68. if (frame.payload.len != 0) return error.InvalidPayload;
  69. const status = engine.status();
  70. try body.appendSlice(self.allocator, &status.uuid);
  71. inline for (.{ status.file_bytes, status.latest_lsn, status.oldest_lsn, status.checkpoint_lsn, status.journal_bytes_since_checkpoint, status.live_keys, status.keydir_bytes, status.ordered_index_bytes, status.bytes_written, status.checksum_failures, status.partial_tails, status.recovery_ns, status.checkpoint_ns, status.connection_bytes, status.active_requests, status.commit_groups, status.committed_transactions, status.largest_commit_group }) |value| try appendInt(u64, body, self.allocator, value);
  72. },
  73. .get => {
  74. const key = try oneKey(frame.payload);
  75. if (try engine.get(self.allocator, key)) |value| {
  76. defer self.allocator.free(value.bytes);
  77. try appendInt(u64, body, self.allocator, value.lsn);
  78. try appendInt(u32, body, self.allocator, @intCast(value.bytes.len));
  79. try body.appendSlice(self.allocator, value.bytes);
  80. } else {
  81. body.clearRetainingCapacity();
  82. try appendInt(u16, body, self.allocator, 1);
  83. }
  84. },
  85. .put => {
  86. const operation = try putOperation(frame);
  87. const lsn = try engine.put(operation.key, operation.value);
  88. try appendInt(u64, body, self.allocator, lsn);
  89. },
  90. .delete => {
  91. const deleted = try engine.delete(try oneKey(frame.payload));
  92. try body.append(self.allocator, @intFromBool(deleted));
  93. },
  94. .exists => {
  95. const present = try engine.exists(try oneKey(frame.payload));
  96. try body.append(self.allocator, @intFromBool(present));
  97. },
  98. .multi_get => try self.multiGet(engine, frame.payload, body),
  99. .batch_write => try self.batchWrite(engine, frame.payload, body),
  100. .compare_batch_write => try self.compareBatchWrite(engine, frame.payload, body),
  101. .scan_open => try self.scanOpen(frame.payload, body),
  102. .scan_next => try self.scanNext(engine, frame.payload, body),
  103. .scan_close => try self.scanClose(frame.payload, body),
  104. }
  105. }
  106. fn multiGet(self: *Session, engine: *engine_mod.Engine, payload: []const u8, body: *std.ArrayListUnmanaged(u8)) !void {
  107. if (payload.len < 4) return error.InvalidPayload;
  108. const count = readInt(u32, payload, 0);
  109. if (count > pkvdb.max_operations) return error.InvalidPayload;
  110. const keys = try self.allocator.alloc([]const u8, count);
  111. defer self.allocator.free(keys);
  112. var position: usize = 4;
  113. for (keys) |*key| {
  114. if (position > payload.len or payload.len - position < 4) return error.InvalidPayload;
  115. const length = readInt(u32, payload, position);
  116. position = try std.math.add(usize, position, 4);
  117. const end = try std.math.add(usize, position, length);
  118. if (end > payload.len or length > pkvdb.max_key_size) return error.InvalidPayload;
  119. key.* = payload[position..end];
  120. position = end;
  121. }
  122. if (position != payload.len) return error.InvalidPayload;
  123. const values = try engine.multiGet(self.allocator, keys);
  124. defer {
  125. for (values) |value| if (value) |present| self.allocator.free(present.bytes);
  126. self.allocator.free(values);
  127. }
  128. try appendInt(u32, body, self.allocator, count);
  129. for (values) |value| if (value) |present| {
  130. try body.append(self.allocator, 1);
  131. try body.appendNTimes(self.allocator, 0, 3);
  132. try appendInt(u32, body, self.allocator, @intCast(present.bytes.len));
  133. try appendInt(u64, body, self.allocator, present.lsn);
  134. try body.appendSlice(self.allocator, present.bytes);
  135. } else {
  136. try body.appendNTimes(self.allocator, 0, 16);
  137. };
  138. }
  139. fn batchWrite(self: *Session, engine: *engine_mod.Engine, payload: []const u8, body: *std.ArrayListUnmanaged(u8)) !void {
  140. if (payload.len < 8) return error.InvalidPayload;
  141. const count = readInt(u32, payload, 0);
  142. const metadata_length = readInt(u32, payload, 4);
  143. if (count == 0 or count > pkvdb.max_operations or metadata_length > payload.len - 8) return error.InvalidPayload;
  144. const metadata_end = 8 + metadata_length;
  145. const metadata = payload[8..metadata_end];
  146. const operations = try self.allocator.alloc(engine_mod.Operation, count);
  147. defer self.allocator.free(operations);
  148. var position: usize = metadata_end;
  149. for (operations) |*operation| {
  150. if (position > payload.len or payload.len - position < 12) return error.InvalidPayload;
  151. const opcode: pkvdb.Opcode = std.meta.intToEnum(pkvdb.Opcode, payload[position]) catch return error.InvalidPayload;
  152. const key_length = readInt(u32, payload, position + 4);
  153. const value_length = readInt(u32, payload, position + 8);
  154. position += 12;
  155. const key_end = try std.math.add(usize, position, key_length);
  156. const value_end = try std.math.add(usize, key_end, value_length);
  157. if (value_end > payload.len or key_length > pkvdb.max_key_size or value_length > pkvdb.max_value_size or (opcode == .delete and value_length != 0)) return error.InvalidPayload;
  158. operation.* = .{ .opcode = opcode, .key = payload[position..key_end], .value = payload[key_end..value_end] };
  159. position = value_end;
  160. }
  161. if (position != payload.len) return error.InvalidPayload;
  162. try appendInt(u64, body, self.allocator, try engine.batchWrite(operations, metadata));
  163. }
  164. fn compareBatchWrite(self: *Session, engine: *engine_mod.Engine, payload: []const u8, body: *std.ArrayListUnmanaged(u8)) !void {
  165. if (payload.len < 16) return error.InvalidPayload;
  166. const check_count = readInt(u32, payload, 0);
  167. const op_count = readInt(u32, payload, 4);
  168. const metadata_length = readInt(u32, payload, 8);
  169. if (op_count == 0 or op_count > pkvdb.max_operations or check_count > pkvdb.max_operations) return error.InvalidPayload;
  170. const checks = try self.allocator.alloc(engine_mod.CompareCheck, check_count);
  171. defer self.allocator.free(checks);
  172. var position: usize = 16;
  173. for (checks) |*check| {
  174. if (position > payload.len or payload.len - position < 16) return error.InvalidPayload;
  175. const key_length = readInt(u32, payload, position);
  176. position += 4;
  177. position += 4;
  178. const expected_lsn = readInt(u64, payload, position);
  179. position += 8;
  180. const end = try std.math.add(usize, position, key_length);
  181. if (end > payload.len or key_length > pkvdb.max_key_size) return error.InvalidPayload;
  182. check.* = .{ .key = payload[position..end], .expected_lsn = expected_lsn };
  183. position = end;
  184. }
  185. if (position > payload.len or payload.len - position < metadata_length) return error.InvalidPayload;
  186. const metadata_end = position + metadata_length;
  187. const metadata = payload[position..metadata_end];
  188. const operations = try self.allocator.alloc(engine_mod.Operation, op_count);
  189. defer self.allocator.free(operations);
  190. position = metadata_end;
  191. for (operations) |*operation| {
  192. if (position > payload.len or payload.len - position < 12) return error.InvalidPayload;
  193. const opcode: pkvdb.Opcode = std.meta.intToEnum(pkvdb.Opcode, payload[position]) catch return error.InvalidPayload;
  194. const key_length = readInt(u32, payload, position + 4);
  195. const value_length = readInt(u32, payload, position + 8);
  196. position += 12;
  197. const key_end = try std.math.add(usize, position, key_length);
  198. const value_end = try std.math.add(usize, key_end, value_length);
  199. if (value_end > payload.len or key_length > pkvdb.max_key_size or value_length > pkvdb.max_value_size or (opcode == .delete and value_length != 0)) return error.InvalidPayload;
  200. operation.* = .{ .opcode = opcode, .key = payload[position..key_end], .value = payload[key_end..value_end] };
  201. position = value_end;
  202. }
  203. if (position != payload.len) return error.InvalidPayload;
  204. const result = try engine.compareBatchWrite(checks, operations, metadata);
  205. try body.append(self.allocator, @intFromBool(result.committed));
  206. try body.appendNTimes(self.allocator, 0, 7);
  207. try appendInt(u64, body, self.allocator, result.lsn);
  208. }
  209. fn scanOpen(self: *Session, payload: []const u8, body: *std.ArrayListUnmanaged(u8)) !void {
  210. if (payload.len < 12 or self.scans.items.len >= 64) return error.InvalidPayload;
  211. const include_values = payload[0] != 0;
  212. const limit = readInt(u32, payload, 4);
  213. const prefix_length = readInt(u32, payload, 8);
  214. if (limit == 0 or limit > 4096 or prefix_length > pkvdb.max_key_size or 12 + prefix_length != payload.len) return error.InvalidPayload;
  215. const prefix = try self.allocator.dupe(u8, payload[12..]);
  216. errdefer self.allocator.free(prefix);
  217. const cursor = try self.allocator.alloc(u8, 0);
  218. errdefer self.allocator.free(cursor);
  219. const id = self.next_scan_id;
  220. self.next_scan_id +%= 1;
  221. if (self.next_scan_id == 0) self.next_scan_id = 1;
  222. try self.scans.append(self.allocator, .{ .id = id, .prefix = prefix, .cursor = cursor, .include_values = include_values, .limit = limit });
  223. try appendInt(u64, body, self.allocator, id);
  224. }
  225. fn scanNext(self: *Session, engine: *engine_mod.Engine, payload: []const u8, body: *std.ArrayListUnmanaged(u8)) !void {
  226. if (payload.len != 12) return error.InvalidPayload;
  227. const index = self.findScan(readInt(u64, payload, 0)) orelse return error.ScanNotFound;
  228. const requested = readInt(u32, payload, 8);
  229. const scan = &self.scans.items[index];
  230. const limit = if (requested == 0) scan.limit else @min(requested, scan.limit);
  231. var batch = try engine.scan(self.allocator, scan.prefix, scan.cursor, limit, scan.include_values, max_frame_size - 10);
  232. defer batch.deinit(self.allocator);
  233. try body.append(self.allocator, @intFromBool(batch.done));
  234. try body.appendNTimes(self.allocator, 0, 3);
  235. try appendInt(u32, body, self.allocator, @intCast(batch.entries.len));
  236. for (batch.entries) |entry| {
  237. try appendInt(u32, body, self.allocator, @intCast(entry.key.len));
  238. try appendInt(u32, body, self.allocator, @intCast(if (entry.value) |value| value.len else 0));
  239. try appendInt(u64, body, self.allocator, entry.lsn);
  240. try body.appendSlice(self.allocator, entry.key);
  241. if (entry.value) |value| try body.appendSlice(self.allocator, value);
  242. }
  243. const cursor = try self.allocator.dupe(u8, batch.next_cursor);
  244. self.allocator.free(scan.cursor);
  245. scan.cursor = cursor;
  246. }
  247. fn scanClose(self: *Session, payload: []const u8, body: *std.ArrayListUnmanaged(u8)) !void {
  248. if (payload.len != 8) return error.InvalidPayload;
  249. const index = self.findScan(readInt(u64, payload, 0)) orelse return error.ScanNotFound;
  250. const scan = self.scans.orderedRemove(index);
  251. self.allocator.free(scan.prefix);
  252. self.allocator.free(scan.cursor);
  253. try body.append(self.allocator, 1);
  254. }
  255. };
  256. pub fn putOperation(frame: Frame) !engine_mod.Operation {
  257. if (frame.opcode != .put or frame.payload.len < 8) return error.InvalidPayload;
  258. const key_length = readInt(u32, frame.payload, 0);
  259. const value_length = readInt(u32, frame.payload, 4);
  260. const total = try std.math.add(usize, 8, try std.math.add(usize, key_length, value_length));
  261. if (total != frame.payload.len or key_length > pkvdb.max_key_size or value_length > pkvdb.max_value_size) return error.InvalidPayload;
  262. return .{ .opcode = .put, .key = frame.payload[8 .. 8 + key_length], .value = frame.payload[8 + key_length ..] };
  263. }
  264. fn oneKey(payload: []const u8) ![]const u8 {
  265. if (payload.len < 4) return error.InvalidPayload;
  266. const length = readInt(u32, payload, 0);
  267. if (length > pkvdb.max_key_size or 4 + length != payload.len) return error.InvalidPayload;
  268. return payload[4..];
  269. }
  270. pub fn parse(bytes: []const u8) !Frame {
  271. if (bytes.len < header_size) return error.Incomplete;
  272. if (!std.mem.eql(u8, bytes[0..4], "PKBF")) return error.InvalidMagic;
  273. if (readInt(u16, bytes, 4) != 1) return error.IncompatibleVersion;
  274. const payload_length = readInt(u32, bytes, 20);
  275. if (payload_length > max_frame_size) return error.FrameTooLarge;
  276. const total = try std.math.add(usize, header_size, payload_length);
  277. if (total > bytes.len) return error.Incomplete;
  278. var header: [32]u8 = undefined;
  279. @memcpy(&header, bytes[0..32]);
  280. const header_crc = readInt(u32, &header, 28);
  281. writeInt(u32, &header, 28, 0);
  282. if (pkvdb.crc32c(&header) != header_crc) return error.ChecksumMismatch;
  283. const payload = bytes[header_size..total];
  284. if (pkvdb.crc32c(payload) != readInt(u32, bytes, 24)) return error.ChecksumMismatch;
  285. const opcode = std.meta.intToEnum(Opcode, readInt(u16, bytes, 8) & 0x7fff) catch return error.UnknownOpcode;
  286. return .{ .opcode = opcode, .flags = readInt(u16, bytes, 10), .request_id = readInt(u64, bytes, 12), .payload = payload, .consumed = total };
  287. }
  288. pub fn encode(allocator: std.mem.Allocator, opcode: u16, flags: u16, request_id: u64, payload: []const u8) ![]u8 {
  289. if (payload.len > max_frame_size) return error.FrameTooLarge;
  290. const output = try allocator.alloc(u8, header_size + payload.len);
  291. errdefer allocator.free(output);
  292. @memset(output[0..header_size], 0);
  293. @memcpy(output[0..4], "PKBF");
  294. writeInt(u16, output, 4, 1);
  295. writeInt(u16, output, 6, 0);
  296. writeInt(u16, output, 8, opcode);
  297. writeInt(u16, output, 10, flags);
  298. writeInt(u64, output, 12, request_id);
  299. writeInt(u32, output, 20, @intCast(payload.len));
  300. writeInt(u32, output, 24, pkvdb.crc32c(payload));
  301. writeInt(u32, output, 28, 0);
  302. writeInt(u32, output, 28, pkvdb.crc32c(output[0..header_size]));
  303. @memcpy(output[header_size..], payload);
  304. return output;
  305. }
  306. fn appendInt(comptime T: type, list: *std.ArrayListUnmanaged(u8), allocator: std.mem.Allocator, value: T) !void {
  307. var bytes: [@sizeOf(T)]u8 = undefined;
  308. std.mem.writeInt(T, &bytes, value, .little);
  309. try list.appendSlice(allocator, &bytes);
  310. }
  311. fn readInt(comptime T: type, bytes: []const u8, offset: usize) T {
  312. return std.mem.readInt(T, bytes[offset..][0..@sizeOf(T)], .little);
  313. }
  314. fn writeInt(comptime T: type, bytes: []u8, offset: usize, value: T) void {
  315. std.mem.writeInt(T, bytes[offset..][0..@sizeOf(T)], value, .little);
  316. }
  317. test "PKBFI frame round trip and malicious length" {
  318. const bytes = try encode(std.testing.allocator, @intFromEnum(Opcode.ping), 0, 42, "a\x00b");
  319. defer std.testing.allocator.free(bytes);
  320. const frame = try parse(bytes);
  321. try std.testing.expectEqual(@as(u64, 42), frame.request_id);
  322. try std.testing.expectEqualSlices(u8, "a\x00b", frame.payload);
  323. var bad = [_]u8{0} ** header_size;
  324. @memcpy(bad[0..4], "PKBF");
  325. writeInt(u16, &bad, 4, 1);
  326. writeInt(u32, &bad, 20, max_frame_size + 1);
  327. try std.testing.expectError(error.FrameTooLarge, parse(&bad));
  328. }
  329. test "PKBFI binary point batch and streaming scan" {
  330. var tmp = std.testing.tmpDir(.{});
  331. defer tmp.cleanup();
  332. var path_buffer: [std.fs.max_path_bytes]u8 = undefined;
  333. const directory = try tmp.dir.realpath(".", &path_buffer);
  334. const path = try std.fmt.allocPrint(std.testing.allocator, "{s}/pkbfi.pkvdb", .{directory});
  335. defer std.testing.allocator.free(path);
  336. var engine = try engine_mod.Engine.open(std.testing.allocator, path);
  337. defer engine.close();
  338. var session = Session.init(std.testing.allocator);
  339. defer session.deinit();
  340. var put_payload = [_]u8{0} ** 14;
  341. writeInt(u32, &put_payload, 0, 2);
  342. writeInt(u32, &put_payload, 4, 4);
  343. @memcpy(put_payload[8..10], "k\x00");
  344. @memcpy(put_payload[10..14], "v\x00x\n");
  345. const put_frame = Frame{ .opcode = .put, .flags = 0, .request_id = 1, .payload = &put_payload, .consumed = 0 };
  346. const response = try session.execute(&engine, put_frame);
  347. defer std.testing.allocator.free(response);
  348. const parsed = try parse(response);
  349. try std.testing.expectEqual(@as(u16, 0), readInt(u16, parsed.payload, 0));
  350. var open_payload = [_]u8{0} ** 12;
  351. open_payload[0] = 1;
  352. writeInt(u32, &open_payload, 4, 1);
  353. writeInt(u32, &open_payload, 8, 0);
  354. const open_response = try session.execute(&engine, .{ .opcode = .scan_open, .flags = 0, .request_id = 2, .payload = &open_payload, .consumed = 0 });
  355. defer std.testing.allocator.free(open_response);
  356. const open_frame = try parse(open_response);
  357. const scan_id = readInt(u64, open_frame.payload, 2);
  358. var next_payload: [12]u8 = undefined;
  359. writeInt(u64, &next_payload, 0, scan_id);
  360. writeInt(u32, &next_payload, 8, 1);
  361. const next_response = try session.execute(&engine, .{ .opcode = .scan_next, .flags = 0, .request_id = 3, .payload = &next_payload, .consumed = 0 });
  362. defer std.testing.allocator.free(next_response);
  363. const next_frame = try parse(next_response);
  364. try std.testing.expectEqual(@as(u32, 1), readInt(u32, next_frame.payload, 6));
  365. }
  366. test "PKBFI compare batch write commit and conflict" {
  367. var tmp = std.testing.tmpDir(.{});
  368. defer tmp.cleanup();
  369. var path_buffer: [std.fs.max_path_bytes]u8 = undefined;
  370. const directory = try tmp.dir.realpath(".", &path_buffer);
  371. const path = try std.fmt.allocPrint(std.testing.allocator, "{s}/pkbfi-compare.pkvdb", .{directory});
  372. defer std.testing.allocator.free(path);
  373. var engine = try engine_mod.Engine.open(std.testing.allocator, path);
  374. defer engine.close();
  375. var session = Session.init(std.testing.allocator);
  376. defer session.deinit();
  377. var payload = [_]u8{0} ** 47;
  378. writeInt(u32, &payload, 0, 1);
  379. writeInt(u32, &payload, 4, 1);
  380. writeInt(u32, &payload, 8, 0);
  381. var position: usize = 16;
  382. writeInt(u32, &payload, position, 1);
  383. writeInt(u64, &payload, position + 8, 0);
  384. position += 16;
  385. payload[position] = 'k';
  386. position += 1;
  387. payload[position] = @intFromEnum(pkvdb.Opcode.put);
  388. writeInt(u32, &payload, position + 4, 1);
  389. writeInt(u32, &payload, position + 8, 1);
  390. position += 12;
  391. payload[position] = 'k';
  392. position += 1;
  393. payload[position] = 'v';
  394. const response = try session.execute(&engine, .{ .opcode = .compare_batch_write, .flags = 0, .request_id = 1, .payload = &payload, .consumed = 0 });
  395. defer std.testing.allocator.free(response);
  396. const frame = try parse(response);
  397. try std.testing.expectEqual(@as(u16, 0), readInt(u16, frame.payload, 0));
  398. try std.testing.expectEqual(@as(u8, 1), frame.payload[2]);
  399. try std.testing.expectEqual(@as(u64, 1), readInt(u64, frame.payload, 10));
  400. const conflict_response = try session.execute(&engine, .{ .opcode = .compare_batch_write, .flags = 0, .request_id = 2, .payload = &payload, .consumed = 0 });
  401. defer std.testing.allocator.free(conflict_response);
  402. const conflict_frame = try parse(conflict_response);
  403. try std.testing.expectEqual(@as(u16, 0), readInt(u16, conflict_frame.payload, 0));
  404. try std.testing.expectEqual(@as(u8, 0), conflict_frame.payload[2]);
  405. try std.testing.expectEqual(@as(u64, 0), readInt(u64, conflict_frame.payload, 10));
  406. }